Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSV Native staking #2015

Merged
merged 159 commits into from
Jun 8, 2024
Merged

SSV Native staking #2015

merged 159 commits into from
Jun 8, 2024

Conversation

sparrowDom
Copy link
Member

@sparrowDom sparrowDom commented Apr 4, 2024

Contracts

Pull Requests to this branch

Related pull requests

Changes

  • Added new NativeStakingSSVStrategy that takes WETH from the Vault and stakes ETH into SSV validators.
  • Added FeeAccumulator to receive ETH execution rewards.
  • Changes BaseHarvester so it can collect WETH and send it straight to the Dripper

oethContracts

Diagrams

NativeStakingSSVStrategy

NativeStakingSSVStrategyHierarchy

NativeStakingSSVStrategySquashed

FeeAccumulator

FeeAccumulatorHierarchy

FeeAccumulatorSquashed

Processes

Value Flows

The flow of native currency (ETH) and tokens between the wallets and contracts.

oethValueFlows-native-staking

Validator Registration and Staking

oethProcesses-register

Validator withdrawals

oethProcesses-withdraw

Validator rewards

oethProcesses-rewards

Native Staking Admin

oethProcesses-admin

Testing

Unit Tests

Unit tests are in contracts/test/strategies/nativeSSVStaking.js

yarn run test

Holesky fork tests

Holesky fork tests are in contracts/test/strategies/nativeSsvStaking.holesky.fork-test.js

yarn run node:hol
yarn run test:hol-fork

Mainnet fork tests

Mainnet fork tests are in contracts/test/strategies/nativeSsvStaking.fork-test.js

yarn run node
yarn run test:fork

Holesky end-to-end testing using Hardhat tasks

export DEBUG=origin*

# uncomment DEFENDER_API_KEY and DEFENDER_API_SECRET in the .env file
npx hardhat depositWETH --amount 64 --network holesky
npx hardhat mint --asset WETH --amount 64 --network holesky

# Uncomment DEPLOYER_PK in .env file to private key of 0x1b94CA50D3Ad9f8368851F8526132272d1a5028C
npx hardhat depositToStrategy --assets WETH --amounts 64 --strategy NativeStakingSSVStrategyProxy --network holesky

# uncomment DEFENDER_API_KEY and DEFENDER_API_SECRET in the .env file
npx hardhat resetStakeETHTally --network holesky
npx hardhat registerValidators --days 2 --network holesky

# give it 10 minutes before running
npx hardhat stakeValidators --network holesky

# Claim SSV from faucet https://faucet.ssv.network/
# transfer claimed SSV to Native Staking Strategy proxy
# Uncomment DEPLOYER_PK in .env file to private key of 0x1b94CA50D3Ad9f8368851F8526132272d1a5028C
npx hardhat depositSSV --amount 2 --operatorids 563,564,565,566 --network holesky

# uncomment DEFENDER_API_KEY and DEFENDER_API_SECRET in the .env file
npx hardhat exitValidator --operatorids 563,564,565,566 --pubkey 0x95883A44A9629017441D318E51D80F89B5A13E98CAC6EC2481167319753C14F4C9C694462B057D98B7AD1CEACA9441E3 --network holesky

npx hardhat removeValidator --operatorids 563,564,565,566 --pubkey 0x95883A44A9629017441D318E51D80F89B5A13E98CAC6EC2481167319753C14F4C9C694462B057D98B7AD1CEACA9441E3 --network holesky

npx hardhat doAccounting --network holesky

# transfer some ETH to the FeeAccumulator 0x65a289f4BF934c964C942eFF6E6F83b6481BE550
npx hardhat harvest --strategy NativeStakingSSVStrategyProxy --network holesky

# Uncomment DEPLOYER_PK in .env file to private key of 0x1b94CA50D3Ad9f8368851F8526132272d1a5028C
npx hardhat pauseStaking --network holesky

npx hardhat fixAccounting --network holesky

Deployment

The deploy script for the Native Staking Strategy is contracts/deploy/mainnet/097_native_ssv_staking.js.

Operations

Hardhat tasks

@sparrowDom sparrowDom self-assigned this Apr 4, 2024
Copy link

github-actions bot commented Apr 4, 2024

Warnings
⚠️ 👀 This PR needs at least 2 reviewers

Generated by 🚫 dangerJS against af77be4

Copy link

codecov bot commented Apr 4, 2024

Codecov Report

Attention: Patch coverage is 75.32895% with 75 lines in your changes missing coverage. Please review.

Project coverage is 61.86%. Comparing base (a52ee8e) to head (af77be4).

Files Patch % Lines
...ts/contracts/strategies/LidoWithdrawalStrategy.sol 30.43% 48 Missing ⚠️
.../strategies/NativeStaking/ValidatorRegistrator.sol 75.32% 19 Missing ⚠️
contracts/contracts/oracle/OETHFixedOracle.sol 0.00% 4 Missing ⚠️
contracts/contracts/harvest/BaseHarvester.sol 57.14% 3 Missing ⚠️
...ategies/NativeStaking/NativeStakingSSVStrategy.sol 98.36% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2015      +/-   ##
==========================================
+ Coverage   60.54%   61.86%   +1.31%     
==========================================
  Files          59       66       +7     
  Lines        3021     3322     +301     
  Branches      779      649     -130     
==========================================
+ Hits         1829     2055     +226     
- Misses       1189     1264      +75     
  Partials        3        3              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.


balance = activeDepositedValidators * 32 ether;
balance += beaconChainRewardWETH;
balance += FEE_ACCUMULATOR_ADDRESS.balance;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to include the MEV rewards in the FeeAccumulator if those are going to be harvested and sent to through the dripper.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks this is a nice catch. I see I haven't given it enough though as to how (W)ETH should be handled.
Lets continue discussion here so we have it all in 1 place: https://www.notion.so/originprotocol/How-should-accounting-function-handle-ETH-after-accounting-4ed3a9352c0f4fc8aa6bb866022a25cb

naddison36 and others added 10 commits April 22, 2024 17:35
* Update Natspec

* Generated docs for native eth strategy

* Prettier and linter
Fixed spelling of ValidatorAccountant events
Implemented depositSSV

* Updated Natspec
Moved MAX_STAKE on ValidatorAccountant to a constant

* Removed strategist from strategy as its already maintained in the Vault

* Fix compilation error

* Fix unit tests

* fix linter
* Added OETH process diagram with functions calls for native staking

* Native Staking Strategy now hold consensus rewards at ETH
FeeAccumulator now holds execution rewards as ETH
Removed WETH immutable from FeeAccumulator
Converted custom errors back to require with string
collect rewards now converts ETH to WETH at harvest
checkBalance is now validators * 32 plus WETH balance from deposits
Renamed beaconChainRewardsWETH to consensusRewards
Fixed bug in stakeETH that was converting all WETH to ETH
* Fixed native staking deployment since the strategist is got from the vault

* Refactor of some Native Staking events
Refactor of Native Staking unit tests

* Renamed AccountingBeaconChainRewards to AccountingConsensusRewards
Accounting updated to handle zero ETH from the beacon chain

* fixed bug not accounting for previous consensus rewards
Blow fuse if ETH balance < previous consensus rewards

* Pause collectRewardTokens and doAccounting on accounting failure.

Validated asset on deposit to Native Staking Strategy.

Moved depositSSV from NativeStakingSSVStrategy to ValidatorRegistrator

moved onlyStrategist modified and VAULT_ADDRESS immutable from ValidatorAccountant to ValidatorRegistrator

manuallyFixAccounting changed to use whenPaused modifier

made fuseIntervalEnd inclusive

Natspec updates

refactoring of native staking unit tests
* add basic steps to deploy OETH to holesky

* prettier

* minor change

* holesky deployment ifles
holesky deployment files

* add holesky deployment files

* minor fix

* minor fixes

* make the fork tests run on Holesky

* add some more tests

* testing SSV staking on Holesky

* refactor where deployment files are located

* more progress on deployment

* add deposit to validator deployment files

* remove log

* prettier

* lint

* move file

* SSV cluster info (#2036)

* add ability to fetch SSV cluster information

* prettier
* manuallyFixAccounting now uses delta values and only callable by the strategist
manuallyFixAccounting calls doAccounting to check the fuse is still not blown
Removed accountingGovernor

* Added pauseOnFail param to internal _doAccounting
Increased the allowed delta values of manuallyFixAccounting

* ran prettier
@DanielVF
Copy link
Collaborator

DanielVF commented Jun 7, 2024

Posting my mostly complete review

Code Review: Native Staking

Requirements

OETH should be able to deposit and withdraw ETH from SSV controlled native staking as a yield earning strategy. It is planned that this will be the primary yield for OETH and that OETH will become an LST.

This is a difficult strategy to build because validators operate cross chain, and the execution chain only has very poor information about the actual status of validators. What information can be gathered far to gas intensive to be effective.

Because Ethereum has horrible support for LSTs, there are essentially two approaches to building an LST. The first is YOLO trust of an address that updates the execution layer about what is going on the validator layer. The second is building a massive offchain oracle infrastructure and operating it.

We've chosen a third route:

  1. If everything goes smoothly, the accounting operates off onchain data.
  2. A limited-trust address interacts with P2P/SSV for validator provisioning. The amount of damage this address can do is limited to a maximum amount.
  3. In the event of things getting out of sync enough to cause accounting errors, the strategist can make limited corrections at a certain rate over time.
  4. In the event of things being very out of sync, governance can act to correct.

The overall concept is contract enforced damage mitigation.

We want to transition to fully onchain accounting once that is possible from future Ethereum updates.

Authentication

  • Never use tx.origin
  • Every external/public function is supposed to be externally accessible
  • Every external/public function has the correct authentication

Ethereum

_Unlike almost all of contracts, we do send and receive ETH from this contract. However, we only send ETH to two trusted addresss.

🟠 There are three methods to prevent reentrancy: only calling trusted contracts, reentrancy guards, and writing functions in order such that reentrancy is not an issue. Usually we try to hit all three. This code only does the first.

  • Contract does not send or receive Ethereum.
  • Contract has no payable methods.
  • Contract is not vulnerable to being sent self destruct ETH

Cryptographic code

The only crypto used is hashing the validator public keys in order to look them up in a mapping. I don't think that can be abused to get different public keys to map to the same hash.

Otherwise the contract passes through signature data, but does not use it itself.

Gas problems

  • Contracts with for loops must have either:
    • A way to remove items
    • Can be upgraded to get unstuck
    • Size can only controlled by admins
  • Contracts with for loops must not allow end users to add unlimited items to a loop that is used by others or admins.

Black magic

  • Does not contain selfdestruct
  • Does not use delegatecall outside of proxying.
  • Address.isContract should be treated as if could return anything at any time, because that's reality.

Overflow

  • Code is solidity version >= 0.8.0
  • All for loops use uint256

Proxy

  • No storage variable initialized at definition when contract used as a proxy implementation.

Events

🟣 Deposit SSV does not emit directly, but that's okay. We could monitor the SSV contract for this if we wished.

Rounding

  • Contract rounds in the protocols favor
  • Contract does not have bugs from loosing rounding precision
  • Code correctly multiplies before division
  • Contract does not have bugs from zero or near zero amounts

Dependencies

  • Review any new contract dependencies thoroughly (e.g. OpenZeppelin imports) when new dependencies are added or version of dependencies changes.
  • If OpenZeppelin ACL roles are use review & enumerate all of them.
  • Check OpenZeppelin security vulnerabilities and see if any apply to current PR considering the version of OpenZeppelin contract used.

External calls

  • Contract addresses passed in are validated
    • 🔴 No validation in withdraw (made inline note, fixed).
  • No unsafe external calls
  • Reentrancy guards on all state changing functions
    • Still doesn't protect against external contracts changing the state of the world if they are called.
  • No malicious behaviors
  • Low level call() must require success.
  • No slippage attacks (we need to validate expected tokens received)
  • Oracles, one of:
    • It's complicated
    • No oracles
    • Oracles can't be bent
    • If oracle can be bent, it won't hurt us.
  • Do not call balanceOf for external contracts to determine what they will do when they use internal accounting

Tests

not checked yet

  • Each publicly callable method has a test
  • Each logical branch has a test
  • Each require() has a test
  • Edge conditions are tested
  • If tests interact with AMM make sure enough edge cases (pool tilts) are tested. Ideally with fuzzing.

Deploy

  • Deployer permissions are removed after deploy

Strategy Specific

Strategy checks

  • Check balance cannot be manipulated up AND down by an attacker
  • No read only reentrancy on downstream protocols during checkBalance
  • All reward tokens are collected
  • The harvester can sell all reward tokens
  • No funds are left in the contract that should not be as a result of depositing or withdrawing
  • All funds can be recovered from the strategy by some combination of depositAll, withdraw, or withdrawAll()
    • Way more complicated, but we can get funds out.
  • WithdrawAll() can always withdraw an amount equal to or larger than checkBalances report, even in spite of attacker manipulation.
  • WithdrawAll() cannot be MEV'd
  • Strategist cannot steal funds
    • Limited amounts though

Accounting Invariants

  • depWeth should go up whenever WETH is added via the vault
  • depWeth should go down whenever WETH is staked
  • depWeth should go down whenever WETH is manually withdrawn to the vault
  • depWETH should never be greater than the standing WETH balance (not checked yet)
  • depWETH should never be less than the standing WETH balance unless someone donates
  • ETH only arrives from off chain + mev rewards (outside donations)
  • Sitting ETH is only ever sent to WETH
  • All funds sent upwards to the vault and harvester are done in WETH
  • ActVal * 32 should always be less than the sum of WETH sent to the contract
    • not counting manual adjustments
  • ETH balance at rest should always be greater than or equal to consensus rewards
    • manual adjustments can set this below, but it will revert at the end of the manual set when it calls do accounting
  • After a successful doAccounting, eth balance should equal consensus rewards
    • unless accounting fails and the contract is paused

What happens if:

  • We exit all validators? - should be fairly sane. No more rewards than we would normally get.
  • Lots of validators get slashed? - accounting will undercount how many validators have withdrawn. We must pause() the strategy accounting when this happens on the beacon chain, before the slashed validators eventually get withdrawn.
  • All validators go down for a week - accounting thinks we have an extra validator, and would need manual adjustment.
  • We miss a rewards beat - we blow the fuse and stop updating.
  • gas prices spike for a week - no real effect
  • Validator max sizes go up, the number of global active validators goes down by 20x, resulting in a 20x withdraw cycle speedup - okay if we call do accounting 20x more often. Otherwise we need to migrate to larger stakes and/or switch to using the better accounting which is probably gas possible now.

Logic

Are there bugs in the logic?

Still not specifically reviewed yet

  • Correct usage of global & local variables. -> they might differentiate only by an underscore that can be overlooked (e.g. address vs _address).

Deployment Considerations

  • SSV strategy proxy must be deployed by the relayer account
  • Before non testing amounts of funds are in, we must have monitoring for slashings and missed validations

Internal State

See above in "Accounting Invariants" section

Attack

This code is inherently dangerous. We focus on damage mitigation rather than the usual perfection.

First, there is no way for a smart contract to ensure that a staking deposit will end up with correct withdrawal address in the beacon chain. We are essentially shipping out gold bars via packing labels created on an EOA.

To mitigate this risk, we limit how many deposits can be made before requiring a 5 of 8 multisig to verify that the previous deposits were correctly made. This caps our max losses at about two million per strategy contract.

We could reduce the deposit risk by a magnitude and remove all humans from the loop by doing the following:

  • Deposit 1 ETH into a validator, increment, mark validator as pending
  • When this validator is active, post back to the contract with a proof that the validator withdraw address is recorded correctly. This deposit 31 ETH and marks the validator as registered.

By keeping a limited number of deposits in the pending state, we reduce our risk. 1 ETH instead of 32 ETH to test out a validator deposit is also a fairly big win.

Secondly, it would take approximately 50% of all yield to be spent in gas to be mostly sure about what each validator is doin on the beacon chain. Instead we take a guess based on the amount of funds we see coming in. This can be wrong and need manual correction. We also throttle the amount of manual correction to avoid malicious actions.

Outside the scope of this contract, failures or vulnerabilities with SSV or P2P could result in mass slashings of our validators.

Withdrawing funds is also ultimately outside the contract's control. We can ask the SSV contract to emit an event which hopefully is picked up. There are other off-chain ways to kick off withdraws.

SSV rewards are also outside our contracts control, since they go only to the EOA that deployed the contracts.

ETH rewards are not distinguished from withdraws, both go to the same place, via the same mechanism. We use heuristics to discriminate between them.

Our strategy contracts have four jobs:

  1. Deposit money
  2. Withdraw money
  3. Collect yield + rewards
  4. Know the balance

Four out of four of these cannot be guaranteed by the code.

Monitoring is critical here.

Other code:

We might be in trouble if the SSV deposit made callbacks to somewhere that an attacker could hook into. A quick glance at the SSV code shows that not only is it dynamic, but extremely dynamic in its dynamicness.

🟠 Let's add nonReentrant to stakeETH, doAccounting and manuallyFixAccounting so something weird happening with SSV can't mess anything up on our side.

Flavor

The strategy is broken into three different files in an inheritance hierarchy. In general I'm not a fan of this, as it makes it more difficult to see the big picture, but the cutpoints here are very good, and each has clear ownership of difference concerns, and small interaction with other layers. Works here.

Otherwise, the flavor is now good.

/// The `ValidatorStakeData` struct contains the pubkey, signature and depositDataRoot.
/// Only the registrator can call this function.
// slither-disable-start reentrancy-eth
function stakeEth(ValidatorStakeData[] calldata validators)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add nonReentrant to stakeETH, doAccounting and manuallyFixAccounting so something weird happening with SSV can't mess anything up on our side.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added nonReentrant to stakeETH, doAccounting and manuallyFixAccounting

naddison36 added 12 commits June 8, 2024 08:20
* Changed registerSsvValidator to registerSsvValidators so it can register multiple validators

* Added length check on publicKeys and sharesData in registerSsvValidators
* Added test to bulk register validators

* Updated process diagrams

* deployed new NativeStakingSSVStrategy to Holesky

* Changes to bulk register validators

* Fixed stakeValidators

* Added snapStaking hardhat task

* Add optional validators to registerValidators Hardhat task

* stakeValidators validator logic moved

* fixes to registerValidators

* Saved Gnosis Safe file into contracts folder for now

* New deployment to Holesky with multi validator registrations
* Added LidoWithdrawalStrategy

* Added deployment of Lido withdrawal strategy

* Completed Lido Withdrawal Strategy fork tests

* Updated Natspec

* Renamed fraxEth to stEth in LidoWithdrawalStrategy

* Moved _abstractSetPToken to bottom of the LidoWithdrawalStrategy
made _abstractSetPToken pure

* renamed numWithdrawals to withdrawalLength

* outstandingWithdrawals now accounts for stETH dust left behind

* don't format Defender Action code in dist folder
Copy link
Collaborator

@DanielVF DanielVF left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DanielVF
DanielVF previously approved these changes Jun 8, 2024
Copy link
Collaborator

@DanielVF DanielVF left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NativeSSVStrategy contract is approved for use with test amounts of funds.

I have not yer reviewed the rest of this PR.

Copy link
Collaborator

@naddison36 naddison36 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All pre deployment checks have bene done

@naddison36 naddison36 merged commit 0535236 into master Jun 8, 2024
16 checks passed
@naddison36 naddison36 deleted the sparrowDom/nativeStaking branch June 8, 2024 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contracts Works related to contracts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants