-
Notifications
You must be signed in to change notification settings - Fork 85
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
WOETH: Donation attack prevention #2106
base: master
Are you sure you want to change the base?
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #2106 +/- ##
==========================================
+ Coverage 53.26% 53.85% +0.58%
==========================================
Files 79 79
Lines 4098 4120 +22
Branches 1079 1081 +2
==========================================
+ Hits 2183 2219 +36
+ Misses 1912 1898 -14
Partials 3 3 ☔ View full report in Codecov by Sentry. |
RequirementsWhat is the PR trying to do? Is this the right thing? Are there bugs in the requirements? Easy ChecksAuthentication
Ethereum
Cryptographic code
Gas problems
Black magic
Overflow
Proxy
Events
Medium ChecksRounding
Dependencies
External calls
Tests
Deploy
ThinkingLogicAre there bugs in the logic?
Deployment ConsiderationsAre there things that must be done on deploy, or in the wider ecosystem for this code to work. Are they done? Internal State
For all 3 questions above it is important that: The internal credits stored in WOETH and stored in OETH (for WOETH contract) should always match unless someone sends extra OETH to the WOETH contract manually. Does this code do that? AttackWhat could the impacts of code failure in this code be. What conditions could cause this code to fail if they were not true. Does this code successfully block all attacks. FlavorCould this code be simpler? |
The core attack we are trying to stop is someone sending the OETH to the wOETH contract, causing the value of wOETH in OETH terms to go suddenly up. It looks like totalAssets uses the amount of OETH held by the contract as one of two multipliers. totalAssets is in turn used to calculate the exchange ratio. If someone donates to the contract, one of these two multipliers goes up, and the donation has perfectly succeeded in increasing the value of each wOETH. This attack does not appear to be blocked at all? Or am I missing something? |
It also feels really scary that were are minting and burning using old ratios. That doesn't cause rektness? |
@pandadefi yes the donation attack on OETH with donating WETH to the Vault and calling |
* Add wOETH donation fork tests * test: add fork test for deposit/mint/withdraw/redeem. * test: add test for redeem after rebase. --------- Co-authored-by: clement-ux <[email protected]>
contracts/contracts/token/WOETH.sol
Outdated
//@dev TODO: we could implement a feature where if anyone sends OETH direclty to | ||
// the contract, that we can let the governor transfer the excess of the token. |
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: perhaps we could just treat any donation as "yield"?
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.
While we shouldn't treat donations as instant yield (that's what this contract is trying to get away from), I do think we should build in a separate governor method to collect donated funds that are in excess of the backing funds.
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.
We should probably merge this PR in? #2119
contracts/contracts/token/WOETH.sol
Outdated
@@ -31,11 +43,40 @@ contract WOETH is ERC4626, Governable, Initializable { | |||
OETH(address(asset())).rebaseOptIn(); | |||
} | |||
|
|||
function name() public view virtual override returns (string memory) { | |||
function initialize2() external onlyGovernor { |
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.
Perhaps we should have initialize()
call initialize2()
. This way new contract deploys don't need to call both, and we are less likely to make the bad mistake of not calling initialize2()
.
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.
Great idea thanks: 5a7192d
contracts/contracts/token/WOETH.sol
Outdated
uint256 woethAmount, | ||
address receiver, | ||
address owner | ||
) public virtual override returns (uint256 oethAmount) { |
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.
This contract does not currently compile. This should be fixed.
Also, although this is not the actual compile error, I'm wondering if these virtual
s in these methods are wrong, since I think we want these functions callable without needing to be overridden by a child class.
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.
Yes you are right these virtual keywords are not needed. We can add it in the future if need be: 5ff5c5d
contracts/contracts/token/WOETH.sol
Outdated
* @return amount of OETH credits the OETH amount corresponds to | ||
*/ | ||
function _oethToCredits(uint256 oethAmount) internal returns (uint256) { | ||
(, uint256 creditsPerTokenHighres, ) = OETH(asset()) |
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.
Both _oethToCredits()
and totalAssets()
call oeth.creditsBalanceOfHighres()
to get the creditsPerToken
, discarding the other values that function returns.
I think it makes more sense to call the simpler oeth.rebasingCreditsPerTokenHighres()
instead.
There's two scenarios here:
-
This wrapped token is correctly marked as rebasing. In this case,
oeth.rebasingCreditsPerTokenHighres()
will return the same value as what we are doing now, but be simpler, return only what we need, and cost less gas. -
Governance messes up the world in a bad way and turns off yield to the contract. The current call will immediately return 1e18 instead of 1e27ish, making for a really really wrong totalAssets, and thus really really wrong conversion rate, which would roughly speaking destroy the wrapped token and anything else using it. However, if
oeth.rebasingCreditsPerTokenHighres()
is used we'll only get a gradual drift off the correct value as expected yield does not come in.
In both cases the behavior of oeth.rebasingCreditsPerTokenHighres()
seems better.
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.
Great comment and great points thanks: 3ef2219
contracts/contracts/token/WOETH.sol
Outdated
address owner | ||
) public virtual override returns (uint256 oethAmount) { | ||
oethAmount = super.redeem(woethAmount, receiver, owner); | ||
oethCreditsHighres -= _oethToCredits(oethAmount); |
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.
This is just a mathematical nit, and perhaps the code is okay without this, but in general, if you are depositing/plus-ing and withdrawing/minus-ing using the same conversion function, you are almost certainly rounding the wrong direction in one of them. It's possible we need two _oethToCredits
, one that round up, and one that rounds down.
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.
good point I need to sleep on this one....
This reverts commit 637cd3c.
} | ||
|
||
/** @dev See {IERC4262-totalAssets} */ | ||
function totalAssets() public view override returns (uint256) { |
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.
🔴 rebasingCreditsPerToken
has the property that the smaller it is, the more money a user has.
Default solidity division results in rounding down. Therefore this totalAssets()
method is rounding up the amount of money in contract. Therefore it is very easy for the contract to be a rounding error on the wrong side of solvent.
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.
That said, we should be careful to fuzz test the behavior differences. There's no perfect answer here, and so we want to make sure that erroring on the solvent side is correct.
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.
Overview
This PR prevents an attacker to manipulate the exchange rate between WOETH & OETH by donating OETH to the contract .
Code Change Checklist
To be completed before internal review begins:
Internal review: