Skip to content

Commit d05a5c0

Browse files
sparrowDomshahtheproclement-uxDanielVF
authored
Simplified donation attack prevention (#2453)
* intermittent work committed * add draft solution for preparing WOETH for the landing markets * add tests that confirm lending market protection works as exepcted * add test to protect agains calling initialize twice * add comments * remove unneeded files * minor bug fix * add fork script tests and fix bug * add comments * add another test * divide after multiply * change credits per token to highres * add comment * comments * lint * minor changes * correct fixture * make code slighlty better regarding re-entry * prettier * add explicit visibility * minor test enhancement * better function name * remove comment * simplify * simplify code * simplify code * refactor * add redeem all test * prettier * Add wOETH donation fork tests (#2122) * 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]> * rename deploy script * add another test and simplify constructor * fix: specify override for name and symbol. * fix: import IERC20Metadata. * add gap * update reference to code * fix compile errors * move initialize2() into primary initializer * remove virtual where not needed * pick a more appropriate method for fetching highres CPT * round in the favour of the protocol * Revert "round in the favour of the protocol" This reverts commit 637cd3c. * rounding error fixes * Option 1 for WOETH rounding error (#2419) * option 1 to combat the rounding error * naming * simplify * add comment * prettier * add require 0 checks * allow 0 value mints and redeems * to establish exchange rate for WOETH trust OETH completely * remove unneeded overrides * add comment and prettier * fix a denomination bug * add fork test for the expected rate increase * add comment * add tests for WOETH upgrade not changing the existing WOETH balances * denominate all rates to 1e27 for better precision * undo the change * add comment * further simplify the WOETH rate calculation code * add comments * prettier * update test to confirm that redemption amounts of WOETH before and after the upgrade are a perfect (1 wei difference to rounding) match * Fix bug with rounding direction on deposits * nit: unify comment * sligth code simplification * fix typo * remove unused imports * remove todo * switch to BUSL * improve comment * no need to call governable anymore * fix comment * make WOSonic extend WOETH. Add deploy file and basic fork test * use the more appropriate natspec compliant inheritdoc * more appropriate naming * add aditiona test * make WOUSD inherit from WOETH * add deploy and fork test file for WOUSD --------- Co-authored-by: Shah <[email protected]> Co-authored-by: clement-ux <[email protected]> Co-authored-by: Daniel Von Fange <[email protected]>
1 parent 7324869 commit d05a5c0

20 files changed

+1570
-115
lines changed

brownie/abi/ERC4626.json

Lines changed: 662 additions & 0 deletions
Large diffs are not rendered by default.

brownie/scripts/woeth_manipulation.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from world import *
2+
3+
def expect_approximate(woeth_holder, expected_balance):
4+
balance = woeth.balanceOf(woeth_holder)
5+
diff = abs(expected_balance - balance)
6+
if (diff != 0):
7+
raise Exception("Unexpected balance for account: %s".format(woeth_holder))
8+
9+
def confirm_balances_after_upgrade():
10+
expect_approximate("0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb", 1013453939109688661944)
11+
expect_approximate("0xC460B0b6c9b578A4Cb93F99A691e16dB96Ee5833", 575896531839923556165)
12+
expect_approximate("0xdca0a2341ed5438e06b9982243808a76b9add6d0", 319671606657733042618)
13+
expect_approximate("0x8a9d46d28003673cd4fe7a56ecfcfa2be6372e64", 182355401624955452064)
14+
expect_approximate("0xf65ecb5610000100befba41b9f9cf5ca32838078", 97352556026536192865)
15+
expect_approximate("0x0a26e7ab5c554232314a8d459eff0ede72333f08", 91628532171545105831)
16+
17+
18+
def manipulate_price():
19+
OETH_WHALE="0xa4C637e0F704745D182e4D38cAb7E7485321d059"
20+
whl = {'from': OETH_WHALE }
21+
22+
woeth.convertToAssets(1e18) / 1e18
23+
oeth.transfer(woeth.address, 10_000 * 1e18, whl)
24+
woeth.convertToAssets(1e18) / 1e18
25+
26+
oeth.approve(woeth.address, 1e50, whl)
27+
woeth.deposit(5_000 * 1e18, OETH_WHALE, whl)

brownie/world.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
weth = load_contract('ERC20', WETH)
2020
ousd = load_contract('ousd', OUSD)
2121
oeth = load_contract('ousd', OETH)
22+
woeth = load_contract('erc4626', WOETH)
2223
usdt = load_contract('usdt', USDT)
2324
usdc = load_contract('usdc', USDC)
2425
dai = load_contract('dai', DAI)

contracts/contracts/mocks/MockLimitedWrappedOusd.sol

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ import { WrappedOusd } from "../token/WrappedOusd.sol";
55
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
66

77
contract MockLimitedWrappedOusd is WrappedOusd {
8-
constructor(
9-
ERC20 underlying_,
10-
string memory name_,
11-
string memory symbol_
12-
) WrappedOusd(underlying_, name_, symbol_) {}
8+
constructor(ERC20 underlying_) WrappedOusd(underlying_) {}
139

1410
function maxDeposit(address)
1511
public

contracts/contracts/token/WOETH.sol

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-License-Identifier: MIT
1+
// SPDX-License-Identifier: BUSL-1.1
22
pragma solidity ^0.8.0;
33

44
import { ERC4626 } from "../../lib/openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
@@ -12,24 +12,63 @@ import { Initializable } from "../utils/Initializable.sol";
1212
import { OETH } from "./OETH.sol";
1313

1414
/**
15-
* @title OETH Token Contract
15+
* @title Wrapped OETH Token Contract
1616
* @author Origin Protocol Inc
17+
*
18+
* @dev An important capability of this contract is that it isn't susceptible to changes of the
19+
* exchange rate of WOETH/OETH if/when someone sends the underlying asset (OETH) to the contract.
20+
* If OETH weren't rebasing this could be achieved by solely tracking the ERC20 transfers of the OETH
21+
* token on mint, deposit, redeem, withdraw. The issue is that OETH is rebasing and OETH balances
22+
* will change when the token rebases.
23+
* For that reason the contract logic checks the actual underlying OETH token balance only once
24+
* (either on a fresh contract creation or upgrade) and considering the WOETH supply and
25+
* rebasingCreditsPerToken calculates the _adjuster. Once the adjuster is calculated any donations
26+
* to the contract are ignored. The totalSupply (instead of querying OETH balance) works off of
27+
* adjuster the current WOETH supply and rebasingCreditsPerToken. This makes WOETH value accrual
28+
* completely follow OETH's value accrual.
29+
* WOETH is safe to use in lending markets as the VualtCore's _rebase contains safeguards preventing
30+
* any sudden large rebases.
1731
*/
1832

1933
contract WOETH is ERC4626, Governable, Initializable {
2034
using SafeERC20 for IERC20;
35+
/* This is a 1e27 adjustment constant that expresses the difference in exchange rate between
36+
* OETH's rebase since inception (expressed with rebasingCreditsPerToken) and WOETH to OETH
37+
* conversion.
38+
*
39+
* If WOETH and OETH are deployed at the same time, the value of adjuster is a neutral 1e27
40+
*/
41+
uint256 public adjuster;
42+
uint256[49] private __gap;
2143

22-
constructor(
23-
ERC20 underlying_,
24-
string memory name_,
25-
string memory symbol_
26-
) ERC20(name_, symbol_) ERC4626(underlying_) Governable() {}
44+
// no need to set ERC20 name and symbol since they are overridden in WOETH & WOETHBase
45+
constructor(ERC20 underlying_) ERC20("", "") ERC4626(underlying_) {}
2746

2847
/**
2948
* @notice Enable OETH rebasing for this contract
3049
*/
3150
function initialize() external onlyGovernor initializer {
3251
OETH(address(asset())).rebaseOptIn();
52+
53+
initialize2();
54+
}
55+
56+
/**
57+
* @notice secondary initializer that newly deployed contracts will execute as part
58+
* of primary initialize function and the existing contracts will have it called
59+
* as a governance operation.
60+
*/
61+
function initialize2() public onlyGovernor {
62+
require(adjuster == 0, "Initialize2 already called");
63+
64+
if (totalSupply() == 0) {
65+
adjuster = 1e27;
66+
} else {
67+
adjuster =
68+
(rebasingCreditsPerTokenHighres() *
69+
ERC20(asset()).balanceOf(address(this))) /
70+
totalSupply();
71+
}
3372
}
3473

3574
function name()
@@ -62,7 +101,38 @@ contract WOETH is ERC4626, Governable, Initializable {
62101
external
63102
onlyGovernor
64103
{
65-
require(asset_ != address(asset()), "Cannot collect OETH");
104+
require(asset_ != address(asset()), "Cannot collect core asset");
66105
IERC20(asset_).safeTransfer(governor(), amount_);
67106
}
107+
108+
/// @inheritdoc ERC4626
109+
function convertToShares(uint256 assets)
110+
public
111+
view
112+
virtual
113+
override
114+
returns (uint256 shares)
115+
{
116+
return (assets * rebasingCreditsPerTokenHighres()) / adjuster;
117+
}
118+
119+
/// @inheritdoc ERC4626
120+
function convertToAssets(uint256 shares)
121+
public
122+
view
123+
virtual
124+
override
125+
returns (uint256 assets)
126+
{
127+
return (shares * adjuster) / rebasingCreditsPerTokenHighres();
128+
}
129+
130+
/// @inheritdoc ERC4626
131+
function totalAssets() public view override returns (uint256) {
132+
return (totalSupply() * adjuster) / rebasingCreditsPerTokenHighres();
133+
}
134+
135+
function rebasingCreditsPerTokenHighres() internal view returns (uint256) {
136+
return OETH(asset()).rebasingCreditsPerTokenHighres();
137+
}
68138
}

contracts/contracts/token/WOETHBase.sol

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-License-Identifier: MIT
1+
// SPDX-License-Identifier: BUSL-1.1
22
pragma solidity ^0.8.0;
33

44
import { WOETH } from "./WOETH.sol";
@@ -10,9 +10,7 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
1010
*/
1111

1212
contract WOETHBase is WOETH {
13-
constructor(ERC20 underlying_)
14-
WOETH(underlying_, "Wrapped Super OETH", "wsuperOETHb")
15-
{}
13+
constructor(ERC20 underlying_) WOETH(underlying_) {}
1614

1715
function name() public view virtual override returns (string memory) {
1816
return "Wrapped Super OETH";

contracts/contracts/token/WOSonic.sol

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,23 @@
1-
// SPDX-License-Identifier: MIT
1+
// SPDX-License-Identifier: BUSL-1.1
22
pragma solidity ^0.8.0;
33

4-
import { ERC4626 } from "../../lib/openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
54
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
65
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7-
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
8-
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
96

10-
import { Governable } from "../governance/Governable.sol";
11-
import { Initializable } from "../utils/Initializable.sol";
12-
import { OSonic } from "./OSonic.sol";
7+
import { WOETH } from "./WOETH.sol";
138

149
/**
1510
* @title Wrapped Origin Sonic (wOS) token on Sonic
1611
* @author Origin Protocol Inc
1712
*/
18-
contract WOSonic is ERC4626, Governable, Initializable {
19-
using SafeERC20 for IERC20;
20-
21-
constructor(
22-
ERC20 underlying_,
23-
string memory name_,
24-
string memory symbol_
25-
) ERC20(name_, symbol_) ERC4626(underlying_) Governable() {}
26-
27-
/**
28-
* @notice Enable Origin Sonic rebasing for this contract
29-
*/
30-
function initialize() external onlyGovernor initializer {
31-
OSonic(address(asset())).rebaseOptIn();
32-
}
13+
contract WOSonic is WOETH {
14+
constructor(ERC20 underlying_) WOETH(underlying_) {}
3315

3416
function name()
3517
public
3618
view
3719
virtual
38-
override(ERC20, IERC20Metadata)
20+
override(WOETH)
3921
returns (string memory)
4022
{
4123
return "Wrapped OS";
@@ -45,23 +27,9 @@ contract WOSonic is ERC4626, Governable, Initializable {
4527
public
4628
view
4729
virtual
48-
override(ERC20, IERC20Metadata)
30+
override(WOETH)
4931
returns (string memory)
5032
{
5133
return "wOS";
5234
}
53-
54-
/**
55-
* @notice Transfer token to governor. Intended for recovering tokens stuck in
56-
* contract, i.e. mistaken sends. Cannot transfer Origin S
57-
* @param asset_ Address for the asset
58-
* @param amount_ Amount of the asset to transfer
59-
*/
60-
function transferToken(address asset_, uint256 amount_)
61-
external
62-
onlyGovernor
63-
{
64-
require(asset_ != address(asset()), "Cannot collect OS");
65-
IERC20(asset_).safeTransfer(governor(), amount_);
66-
}
6735
}
Lines changed: 12 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,23 @@
1-
// SPDX-License-Identifier: MIT
1+
// SPDX-License-Identifier: BUSL-1.1
22
pragma solidity ^0.8.0;
33

4-
import { ERC4626 } from "../../lib/openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
54
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
65
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7-
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
8-
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
96

10-
import { Governable } from "../governance/Governable.sol";
11-
import { Initializable } from "../utils/Initializable.sol";
12-
import { OUSD } from "./OUSD.sol";
7+
import { WOETH } from "./WOETH.sol";
138

14-
contract WrappedOusd is ERC4626, Governable, Initializable {
15-
using SafeERC20 for IERC20;
16-
17-
constructor(
18-
ERC20 underlying_,
19-
string memory name_,
20-
string memory symbol_
21-
) ERC20(name_, symbol_) ERC4626(underlying_) Governable() {}
22-
23-
/**
24-
* @notice Enable OUSD rebasing for this contract
25-
*/
26-
function initialize() external onlyGovernor initializer {
27-
OUSD(address(asset())).rebaseOptIn();
28-
}
9+
/**
10+
* @title Wrapped OUSD Token Contract
11+
* @author Origin Protocol Inc
12+
*/
13+
contract WrappedOusd is WOETH {
14+
constructor(ERC20 underlying_) WOETH(underlying_) {}
2915

3016
function name()
3117
public
3218
view
33-
override(ERC20, IERC20Metadata)
19+
virtual
20+
override(WOETH)
3421
returns (string memory)
3522
{
3623
return "Wrapped OUSD";
@@ -39,23 +26,10 @@ contract WrappedOusd is ERC4626, Governable, Initializable {
3926
function symbol()
4027
public
4128
view
42-
override(ERC20, IERC20Metadata)
29+
virtual
30+
override(WOETH)
4331
returns (string memory)
4432
{
4533
return "WOUSD";
4634
}
47-
48-
/**
49-
* @notice Transfer token to governor. Intended for recovering tokens stuck in
50-
* contract, i.e. mistaken sends. Cannot transfer OUSD
51-
* @param asset_ Address for the asset
52-
* @param amount_ Amount of the asset to transfer
53-
*/
54-
function transferToken(address asset_, uint256 amount_)
55-
external
56-
onlyGovernor
57-
{
58-
require(asset_ != address(asset()), "Cannot collect OUSD");
59-
IERC20(asset_).safeTransfer(governor(), amount_);
60-
}
6135
}

contracts/deploy/deployActions.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,8 +1222,6 @@ const deployWOusd = async () => {
12221222
const ousd = await ethers.getContract("OUSDProxy");
12231223
const dWrappedOusdImpl = await deployWithConfirmation("WrappedOusd", [
12241224
ousd.address,
1225-
"Wrapped OUSD IMPL",
1226-
"WOUSD IMPL",
12271225
]);
12281226
await deployWithConfirmation("WrappedOUSDProxy");
12291227
const wousdProxy = await ethers.getContract("WrappedOUSDProxy");
@@ -1237,6 +1235,26 @@ const deployWOusd = async () => {
12371235
](dWrappedOusdImpl.address, governorAddr, initData);
12381236
};
12391237

1238+
const deployWOeth = async () => {
1239+
const { deployerAddr, governorAddr } = await getNamedAccounts();
1240+
const sDeployer = await ethers.provider.getSigner(deployerAddr);
1241+
1242+
const oeth = await ethers.getContract("OETHProxy");
1243+
const dWrappedOethImpl = await deployWithConfirmation("WOETH", [
1244+
oeth.address,
1245+
]);
1246+
await deployWithConfirmation("WOETHProxy");
1247+
const woethProxy = await ethers.getContract("WOETHProxy");
1248+
const woeth = await ethers.getContractAt("WOETH", woethProxy.address);
1249+
1250+
const initData = woeth.interface.encodeFunctionData("initialize()", []);
1251+
1252+
await woethProxy.connect(sDeployer)[
1253+
// eslint-disable-next-line no-unexpected-multiline
1254+
"initialize(address,address,bytes)"
1255+
](dWrappedOethImpl.address, governorAddr, initData);
1256+
};
1257+
12401258
const deployOETHSwapper = async () => {
12411259
const { deployerAddr, governorAddr } = await getNamedAccounts();
12421260
const sDeployer = await ethers.provider.getSigner(deployerAddr);
@@ -1391,6 +1409,7 @@ module.exports = {
13911409
deployUniswapV3Pool,
13921410
deployVaultValueChecker,
13931411
deployWOusd,
1412+
deployWOeth,
13941413
deployOETHSwapper,
13951414
deployOUSDSwapper,
13961415
upgradeNativeStakingSSVStrategy,

contracts/deploy/mainnet/001_core.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const {
1717
deployUniswapV3Pool,
1818
deployVaultValueChecker,
1919
deployWOusd,
20+
deployWOeth,
2021
deployOETHSwapper,
2122
deployOUSDSwapper,
2223
} = require("../deployActions");
@@ -42,6 +43,7 @@ const main = async () => {
4243
await deployUniswapV3Pool();
4344
await deployVaultValueChecker();
4445
await deployWOusd();
46+
await deployWOeth();
4547
await deployOETHSwapper();
4648
await deployOUSDSwapper();
4749
console.log("001_core deploy done.");

0 commit comments

Comments
 (0)