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

WOETH: Donation attack prevention #2106

Open
wants to merge 34 commits into
base: master
Choose a base branch
from

Conversation

sparrowDom
Copy link
Member

@sparrowDom sparrowDom commented Jun 21, 2024

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:

  • The contract code is complete
  • Executable deployment file
  • Fork tests that test after the deployment file runs (done with a brownie script to confirm that existing WOETH balances are not affected)
  • Unit tests *if needed
  • The owner has done a full checklist review of the code + tests

Internal review:

  • Two approvals by internal reviewers

Copy link

github-actions bot commented Jun 21, 2024

Warnings
⚠️ 👀 This PR needs at least 2 reviewers

Generated by 🚫 dangerJS against 0a988d5

Copy link

codecov bot commented Jun 21, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 53.85%. Comparing base (fa077cd) to head (f0580bc).

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.
📢 Have feedback on the report? Share it here.

@sparrowDom sparrowDom marked this pull request as ready for review June 26, 2024 08:44
@sparrowDom
Copy link
Member Author

sparrowDom commented Jun 26, 2024

Requirements

What is the PR trying to do? Is this the right thing? Are there bugs in the requirements?
No

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: no loops
    • A way to remove items no loops
    • Can be upgraded to get unstuck no loops
    • Size can only controlled by admins no loops
  • Contracts with for loops must not allow end users to add unlimited items to a loop that is used by others or admins. no loops

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 no loops

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 it doesn't but it mimics what OETH is doing using mulTruncateon high resolution credits and high resolution credits per token. That should ensure the correct rounding of the values
  • 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. no new dependancies added
  • 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_no need we only deal with our own OETH - trusted contract_
    • 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. _ it doesn't_

Deploy

  • Deployer permissions are removed after deploy

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?
Nothing special needs to happen on deploy

Internal State

  • What can be always said about relationships between stored state
  • What must hold true about state before a function can run correctly (preconditions)
  • What must hold true about the return or any changes to state after a function has run.

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?
Yes

Attack

What could the impacts of code failure in this code be.
Incorrect OETH balance tracking within WOETH contract. Which would result in incorrect pricing of WOETH.

What conditions could cause this code to fail if they were not true.
Math tracking credits within WOETH differentiating from the one in OETH.

Does this code successfully block all attacks.
yes

Flavor

Could this code be simpler?
No
Could this code be less vulnerable to other code behaving weirdly?
No

@sparrowDom sparrowDom requested a review from DanielVF as a code owner June 26, 2024 15:45
using StableMath for uint256;
// doesn't need to be public, but convenient to be able to confirm the state on the mainnet
uint256 public oethCreditsHighres;
bool _oethCreditsInitialized;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Missing an explicit visibility.

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 nice catch, corrected

@DanielVF
Copy link
Collaborator

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?

@DanielVF
Copy link
Collaborator

It also feels really scary that were are minting and burning using old ratios. That doesn't cause rektness?

@DanielVF
Copy link
Collaborator

I misread the code and got the credits per token and the credits switched. Nevermind everything I said - I'll have to stare at it more!

function totalAssets() public view virtual override returns (uint256) {
(, uint256 creditsPerTokenHighres, ) = OETH(asset())
.creditsBalanceOfHighres(address(this));

Copy link
Collaborator

@DanielVF DanielVF Jun 27, 2024

Choose a reason for hiding this comment

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

If we used in credits here as well from the call from creditsBalanceOfHighres, we would have ever piece of data already loaded inside this function to check that our actual assets was equal or greater than our expected assets.

May or may not be such a great idea though to revert in a view function.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, that would be a good check that would detect possible mathematical irregularities (perhaps due to rounding). Though as you point out it would be weird to revert in a view function.

Monitoring sounds like a good place for it though. Have Grafana regularly query both credits values and check that the ones in OETH.sol are greater or equal to the ones in WOETH.sol.

Copy link
Member Author

Choose a reason for hiding this comment

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

@DanielVF DanielVF changed the title WOETH lending markets adjustements WOETH: Ignore donations Jun 28, 2024
@DanielVF
Copy link
Collaborator

Invariants I can think of:

  • Assuming credits creditsPerTokenHighres in OETH only moves down, then woeth.convertToAssets() should only ever move up
  • We should always be able to withdraw all WOETH to OETH. (This is harder to test, so another way of phrasing this is that we should always have enough OETH on hand for everyone to withdraw)
  • OETH Donations to wOETH should not affect the convertToAssets() result
  • WETH donations to oeth can affect the convertToAssets() result upwards after a rebase, and that's fine.

Copy link

WOETH: Ignore donations

Generated at commit: 56d0b7d9eefc22b48877b8b980db284e6e41313e

🚨 Report Summary

Severity Level Results
Contracts Critical
High
Medium
Low
Note
Total
3
3
0
18
43
67
Dependencies Critical
High
Medium
Low
Note
Total
0
0
0
0
0
0

For more details view the full report in OpenZeppelin Code Inspector

@pandadefi
Copy link

A donation to increase the OETH price remains possible via the OETH contract but will require significant funds to increase the entire OETH vault value.

@sparrowDom
Copy link
Member Author

@pandadefi yes the donation attack on OETH with donating WETH to the Vault and calling rebase is ok. This is the way OETH increases its value and I can't think of a way how it could be exploited

@sparrowDom sparrowDom changed the title WOETH: Ignore donations WOETH: Donation attack prevention Dec 17, 2024
shahthepro and others added 2 commits December 17, 2024 16:49
* 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]>
Comment on lines +29 to +31
using StableMath for uint256;
uint256 public oethCreditsHighres;
bool private _oethCreditsInitialized;
Copy link
Collaborator

Choose a reason for hiding this comment

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

WOETHBase inherits WOETH, so might as well add some gaps here now

Copy link
Member Author

Choose a reason for hiding this comment

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

good point, done

Comment on lines +81 to +82
//@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.
Copy link
Collaborator

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"?

* is internally being done in OETH:
*/
// solhint-disable-next-line max-line-length
/** https://github.com/OriginProtocol/origin-dollar/blob/2314cccf2933f5c1f76a6549c1f5c9cc935b6f05/contracts/contracts/token/OUSD.sol#L242-L249
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Points to older code (pre-yield-delegation). Would be nice to update it to point to latest commit hash on master

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants