-
Notifications
You must be signed in to change notification settings - Fork 84
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
OETH withdrawal queue #2062
OETH withdrawal queue #2062
Conversation
* 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
* 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 * Added Defender Relayer for validator registrator Added ssv utils to get cluster data Added native staking fork tests * Removed now redundant IWETH9 import * moved more logic into native staking fixture * Removed unused imports * fix native staking unit tests * Fail accounting if activeDepositedValidators < fullyWithdrawnValidators Changed Harvester to transfer WETH to dripper Added more mainnet fork tests for native staking * Updated the OETH value flows * Added governable Hardhat tasks Created a resolveContract util * deconstruct params for Hardhat tasks * WIP Hardhat tasks for validator registration * Added depositSSV HH task * Updated OETH contract dependency diagram * Update to diagrams * mini fixes * fix bug and minor test improvement * update yarn fulie * unify the holesky and the mainnet fork tests * prettier * re-deploy holesky native staking strategy (#2046) * test updates * also re-deploy the harvester * upgrade harvester as well * fix test * fix upgrade script and correct the bug in deploy actions * Deployed new Native Staking strategy including the proxy * Added Hardhat tasks for generic strategy functions * remove nativeStakingSSVStrategyProxy from js addresses file --------- Co-authored-by: Domen Grabec <[email protected]>
… failing the withdrawAll. Flux is no longer used so skipping
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OETH Async Withdraws
Requirements
We wish to allow both the public and the OETH ARM to make asynchronous, perfectly 1:1 withdraws.
We wish to avoid the theft of yield attacks that would normally happen with a rebasing token, and we wish to avoid flash loan mint/redeem attacks in imprecisions.
Easy Checks
Authentication
- Never use tx.origin
- Every external/public function is supposed to be externally accessible
- -
addWithdrawLiquidty
should be restricted to being called by the vault only, since it's only external for code savings reasons.- (Nope, turns out it needs to be called from outside our system)
- -
- Every external/public function has the correct authentication
Ethereum
- Contract does not send or receive Ethereum.
- Contract has no payable methods.
- Contract is not vulnerable to being sent self destruct ETH
Cryptographic code
no crypto
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. If an implementation contract were to call delegatecall under attacker control, it could call selfdestruct the implementation contract, leading to calls through the proxy silently succeeding, even though they were failing. - 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
- All state changing functions emit events
Medium Checks
Rounding
- Contract rounds in the protocols favor
- 🟣 Redeem fee calculations round in favor of users against protocol. We can leave this for now since the rounding error is not multiplied out anywhere and will remain small.
- Casts
- 🟠 Need some safecasts on casts (done)
- 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
- 🟣 _checkBalance() does not validate the address of the token used. This is acceptable because it is currently only reachable externally via checkBalance(), which does not update state. The only call outwards is via a staticcall, which precludes reentrancy. However someone could make a coin that could checkBalance cause checkBalance to return silly amounts when called by the vault. Having check balance return zero on non-valid tokens would mean that it would ignore non-valid tokens just as the vault totals do. This is not the PR for that change though.
- No unsafe external calls
- All external calls are to trusted addresses
- 🟢 Dripper calls are carefully positioned so that a compromised dripper could not reentrancy attack, even without reentrancy checks. Nice.
- Reentrancy guards on all state changing functions
- 🟣 addWithdrawalQueueLiquidity does not have reentrancy guards. However the maximum damage it could be used for, if there was a reentrancy issue elsewhere in the code, would be to allow someone to exit the queue out of order.
- Low level call() must require success.
- No slippage attacks (we need to validate expected tokens received)
- Oracles, one of:
- 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
- Each publicly callable method has a test
Deploy
- Deployer permissions are removed after deploy
Thinking
Logic
- If's correct
- For's correct
Internal State
Invariant queued >= claimable >= claimed
on withdrawalQueueMetadata
Each is of these have their own single place in code that sets them. And each only goes up.
queued
is increased by the amount of a withdraw request.
claimable
can only move up by the maximum of difference between it and queued, keeping the invariant.
claimed
can only move up by withdraw requests as they are paid. This keeps the invariant with queued >= claimed
. claimed
can only be increased when queue.claimable >= request.queued
, which keeps the claimable >= claimed
invariant.
Invariant
More invariants
- ✅ queue.claimable == max(request.queued)
- ✅ queue.nextWithdrawIndex == max(request.withdrawIndex-1)
- ✅ `(queue.queued - queue.claimed) = sum(request.amount where amount unclaimed)
More invariants
- ✅ User should not be able to request a withdraw that would end with the protocol overly insolvent.
- ✅ User should not be able to claim a withdraw that would end with the protocol overly insolvent.
- ✅ User should not be able to do a sync redeem that would end with the protocol overly insolvent.
Attack
This PR affects the most of important code in the vault. This how money moves out of the vault to users and this adds two more exit paths to current redeem path. It also alters the calculation of total value, which is again as critical of code as we have. And it adds code to the mint function (again critical code).
If total value over calculated how much we have, this would lead to rebases. This should be correct because the strategy part is unchanged, the vault value part is correct, and because the reserved calculations are only on the amount that has been burned but not paid out. ✅
An attacker could attempt to withdraw to much via the redeem path. The redeem path will never return more WETH than it burned WETH. ✅
An attacker could attempt to mint too much OETH for WETH. The mint path will never mint more OETH than it takes WETH. ✅
✅ Request withdraw burns the exact amount of OETH that it stores in a request.
✅ claimWithdraw and claimWithdraws both send users the exact amount in requests, and these requests cannot be used twice.
The OETH token has occasional rounding errors. However these are only one off, and against the user as far as we know. These rounding errors do not affect the protocol during minting, redeeming, and total valuing. ✅
A concern is that the odds of the vault reading a total value of zero have just gone up from roughly impossible, to something the code now moves values to. This should not open any attacks because rebasing will disable when the the vault total value is zero, and nothing else relies on it being non-zero. ✅
Flavor
We've done many flavor improvements, and it's looking fairly good.
Remaining
- 🟠
OETHVaultAdmin#_wethAvailable
does not match code with the vault core one.
@DanielVF I've fixed |
WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; | ||
// Need to remove WETH that is reserved for the withdrawal queue | ||
if (balance + queue.claimed >= queue.queued) { | ||
return balance + queue.claimed - queue.queued; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: return
inside conditional statements, Isn't this something that we just agreed not to do to make sure that the code is more readable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are correct. From what we discussed at the last contract sync, this would be rewritten to
function _checkBalance(address _asset)
internal
view
override
returns (uint256 balance)
{
balance = super._checkBalance(_asset);
if (_asset != weth) {
return 0;
}
WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata;
// Need to remove WETH that is reserved for the withdrawal queue
if (balance + queue.claimed < queue.queued) {
return 0;
}
return balance + queue.claimed - queue.queued;
}
But I'm not going to change it at this late stage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DanielVF @shahthepro
In hind sight I should have done this late change as _checkBalance
has the same bug as what _wethAvailable
had. I'm going to change to the above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the PR to fix this issue on master
#2162
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Requirements
Allow user to make async withdrawals. Should avoid flash loan and other attacks by incorporating a claim delay.
Easy Checks
Authentication
- Never use tx.origin
- Every external/public function is supposed to be externally accessible
- Every external/public function has the correct authentication
Ethereum
- Contract does not send or receive Ethereum.
- Contract has no payable methods.
- Contract is not vulnerable to being sent self destruct ETH
Cryptographic code
- This contract code does not roll it's own crypto.
- No signature checks without reverting on a 0x00 result.
- No signed data could be used in a replay attack, on our contract or others.
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. If an implementation contract were to call delegatecall under attacker control, it could call selfdestruct the implementation contract, leading to calls through the proxy silently succeeding, even though they were failing. - 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
- All state changing functions emit events
Medium Checks
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 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:
- 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
- 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
Remove this section if the code being reviewed is not a strategy.
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()
- 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
Downstream
- We have monitoring on all backend protocol's governances
- We have monitoring on a pauses in all downstream systems
Thinking
Logic
Are there bugs in the logic?
- Correct usage of global & local variables. -> they might differentiate only by an underscore that can be overlooked (e.g. address vs _address).
Deployment Considerations
Are there things that must be done on deploy, or in the wider ecosystem for this code to work. Are they done?
Internal State
Leaving this out since DVF already did a great job at explaining the internal state and invariants
Attack
- The code assumes that OETH is pegged to WETH 1:1. So, that it doesn't use any oracles that will leave the contract vulnerable.
- Claim delay would prevent any sort of flash loan attacks of mints & redeems
- There're solvency checks wherever it's needed
Flavor
Code looks simple and good. There's one minor nitpick in checkBalance
where return
is used inside the conditional statement. This is something that we decided not to do. Would be nice to get rid of it. Also, could move the asset == weth
check that could moved up like if (asset == weth) return 0
that should address the checkBalance
not checking the asset address concern that Daniel raised.
Contract Changes
requestWithdrawal
,claimWithdrawal
andaddWithdrawalQueueLiquidity
.allocate
,mint
,redeem
,depositToStrategy
,withdrawFromStrategy
,withdrawAllFromStrategy
,withdrawAllFromStrategies
andswapCollateral
.redeem
will only work if there is enough WETH liquidity in the Vault to satisfy all outstanding withdrawal requests.allocate
changed to use the OETH total supply rather than calculating the vault's total assets when calculating the vault buffer.rebaseThreshold
(1 OETH) will call theDripper
to collect any streamed rewards to that point. The minter's OETH is calculated after the rewards are collected. This prevents someone minting with a large amount and then withdrawing 10 minutes later collecting most of the yield from the Dripper.Dependencies
Interfaces
Test Chains
The OETH Vault on Holesky has been upgraded to include the withdrawal queue 0x19d2bAaBA949eFfa163bFB9efB53ed8701aA5dD9
Tenderly testnet OETH ARM is an older fork of mainnet.
Tenderly testnet OETH ARM 2 is a newer fork of mainnet with the last Vault code.
Processes
requestWithdrawal
claimWithdrawal
Deployment
Holesky
The Holesky deploy script to upgrade the OETHVault is
contracts/deploy/holesky/017_upgrade_vault.js
Mainnet
The deploy script for the OETH Withdrawal Queue is
contracts/deploy/mainnet/103_oeth_withdraw_queue.js
.Testing
Unit Tests
The OETH Vault unit tests are in
contracts/test/vault/oeth-vault.js
.Fork Tests
The mainnet OETH Vault fork tests are in
contracts/test/vault/oeth-vault.mainnet.fork-test.js
Holesky using Hardhat
Security
If you made a contract change, make sure to complete the checklist below before merging it in master.
Refer to our documentation for more details about contract security best practices.
Contract change checklist: