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: Ignore donations #2106

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

Conversation

sparrowDom
Copy link
Member

@sparrowDom sparrowDom commented Jun 21, 2024

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 56d0b7d

Copy link

codecov bot commented Jun 21, 2024

Codecov Report

Attention: Patch coverage is 96.07843% with 2 lines in your changes missing coverage. Please review.

Project coverage is 62.76%. Comparing base (c9f069e) to head (56d0b7d).
Report is 5 commits behind head on master.

Files Patch % Lines
contracts/contracts/token/WOETH.sol 96.07% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2106      +/-   ##
==========================================
+ Coverage   61.86%   62.76%   +0.90%     
==========================================
  Files          66       66              
  Lines        3322     3373      +51     
  Branches      863      657     -206     
==========================================
+ Hits         2055     2117      +62     
+ Misses       1264     1253      -11     
  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

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?

@sparrowDom
Copy link
Member Author

sparrowDom commented Jun 27, 2024

@DanielVF not completely sure what you mean. So the logic is that:

  • when the contract is initialized (on proposal execution) the internal credits of OETH are read and stored in WOETH. From that point forward the WOETH doesn't ask for internal credits OETH contract is holding but rather keeps its own accounting
  • on each mint, deposit, withdraw, redeem the current credits per token are fetched from OETH (since we don't know if a rebase happened since the last time we queried it) and the accounting of credits in WOETH contracts are updated. Since only those 4 functions trigger the update of credits a normal transfer of OETH to WOETH contract has no effect. There is also a test for this where you can see that after OETH transfer the WOETH contract will still have a balance of 50 OETH and totalSupply & totalAssets of 0.

@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

Comment on lines +47 to +49
if (_oethCreditsInitialized) {
require(false, "Initialize2 already called");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not just require(!_oethCreditsInitialized, "Initialize2 already called");?

* @param oethAmount Amount of OETH to be converted to OETH credits
* @return amount of OETH credits the OETH amount corresponds to
*/
function _creditsPerAsset(uint256 oethAmount)
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: This function name confuses me a bit. When I see per asset, I assume it's gonna return credits for 1 OETH. But it takes in a oethAmount and multiplies the credits by it. So, it sounds more like creditsForAssetAmount than creditsPerAsset

Comment on lines +132 to +153
require(
assets <= maxDeposit(receiver),
"ERC4626: deposit more then max"
);

address caller = _msgSender();
uint256 shares = previewDeposit(assets);

// if _asset is ERC777, transferFrom can call reenter BEFORE the transfer happens through
// the tokensToSend hook, so we need to transfer before we mint to keep the invariants.
SafeERC20.safeTransferFrom(
IERC20(asset()),
caller,
address(this),
assets
);
_mint(receiver, shares);
oethCreditsHighres += _creditsPerAsset(assets);

emit Deposit(caller, receiver, assets, shares);

return shares;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could this be simplified into just (to avoid code-duplication):

shares = super.deposit(assets, receiver);
oethCreditsHighres += _creditsPerAsset(assets);

Copy link
Collaborator

Choose a reason for hiding this comment

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

Same with other overridden functions as well

Comment on lines +204 to +205
_burn(owner, shares);
oethCreditsHighres -= _creditsPerAsset(assets);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Haven't tested it yet but I wonder if we should compute the credits before burn? Let me see if it makes any difference or if it's breakable

Copy link
Collaborator

Choose a reason for hiding this comment

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

I tried running some tests. Change in order didn't affect the numbers at all

}

/** @dev See {IERC4262-mint} */
function mint(uint256 shares, address receiver)

Choose a reason for hiding this comment

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

You could save on some code complexity calling deposit from the mint function to prevent having most of the same code twice.

@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.

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