-
Notifications
You must be signed in to change notification settings - Fork 85
WOETH: Donation attack prevention #2106
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
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? |
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.
* option 1 to combat the rounding error * naming * simplify * add comment
🔴 RequirementsWe want to make our non-rebasing wrapper contracts able to be safe to be borrowed on lending platforms, not just used as collateral. There’s a little known attack in DeFi, where a wrapper token that can be donated to can be used in an attack. The details of this attack we will skip here, but it requires:
My concern is that other coins who try to stop this attack, do so by slowly dripping out yield on their wrapped tokens. This means that flash loans are completely ineffective and an alternative slow approach allows a lending platforms to profitably liquidate anyone trying it. This PR works by completely blocking donations to the wrapper contract, but donations to vault are passed through instantly. This still dilutes an attack, but only by the difference between the vault and wrapper token. This PR still increases the attack cost 2x-3x on our current coins, instead of the 7,000x cost increase with the yield drip approach. Now a 2x-3x difficulty increase is certainly nice, and we get to a safe TVL size sooner, but it means that we still need to do the math on any particular lending platform to see if okay to be borrowed.
I’m quite concerned that without the time drip, we only have a small band-aid, rather than a strong block. |
Some additional info on the viability of inflation/donation attack. (for reference see xSushi Aave vulnerability incident report) How the attack worksBefore we are able to evaluate how much security a 2-3x increase in difficulty provides we should understand how the attack works:
Steps 4 & 5 are repeated as long as the Loan To Value amount on the lending platform allows it. Say it was possible to loop steps so many times that Account A owes 500m in WOETH and Account B has supplied all WOETH to the lending platform. To sum up: Account A:
Account B:
Observations
|
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: