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
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2839dc1
intermittent work committed
sparrowDom Jun 21, 2024
2e6be62
add draft solution for preparing WOETH for the landing markets
sparrowDom Jun 21, 2024
5495d4b
add tests that confirm lending market protection works as exepcted
sparrowDom Jun 24, 2024
d0eb16f
add test to protect agains calling initialize twice
sparrowDom Jun 24, 2024
fc5d874
add comments
sparrowDom Jun 26, 2024
cfec4e5
remove unneeded files
sparrowDom Jun 26, 2024
8405bfc
minor bug fix
sparrowDom Jun 26, 2024
e03223c
add fork script tests and fix bug
sparrowDom Jun 26, 2024
319467f
add comments
sparrowDom Jun 26, 2024
1142a74
add another test
sparrowDom Jun 26, 2024
f911ba6
divide after multiply
sparrowDom Jun 26, 2024
3dc8523
change credits per token to highres
sparrowDom Jun 26, 2024
bad282b
add comment
sparrowDom Jun 26, 2024
07b51cf
comments
sparrowDom Jun 26, 2024
c0333fb
lint
sparrowDom Jun 26, 2024
2c965c3
minor changes
sparrowDom Jun 26, 2024
df57ef7
Merge remote-tracking branch 'origin/master' into sparrowDom/woeth_ha…
sparrowDom Jun 26, 2024
13fd4da
correct fixture
sparrowDom Jun 26, 2024
7923ddb
make code slighlty better regarding re-entry
sparrowDom Jun 26, 2024
c69583f
prettier
sparrowDom Jun 26, 2024
6822937
add explicit visibility
sparrowDom Jun 27, 2024
ac855d9
minor test enhancement
sparrowDom Jun 27, 2024
1c9fc64
better function name
sparrowDom Jul 1, 2024
56d0b7d
remove comment
sparrowDom Jul 1, 2024
057469c
simplify
sparrowDom Dec 17, 2024
1a6a16f
simplify code
sparrowDom Dec 17, 2024
2223600
simplify code
sparrowDom Dec 17, 2024
b34d811
Merge remote-tracking branch 'origin/master' into sparrowDom/woeth_ha…
sparrowDom Dec 17, 2024
39e8d4b
refactor
sparrowDom Dec 17, 2024
3615ef7
add redeem all test
sparrowDom Dec 17, 2024
466f957
prettier
sparrowDom Dec 17, 2024
52eb244
Add wOETH donation fork tests (#2122)
shahthepro Dec 17, 2024
f0580bc
rename deploy script
sparrowDom Dec 17, 2024
0a988d5
add another test and simplify constructor
sparrowDom Dec 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
662 changes: 662 additions & 0 deletions brownie/abi/ERC4626.json

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions brownie/scripts/woeth_manipulation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from world import *

def expect_approximate(woeth_holder, expected_balance):
balance = woeth.balanceOf(woeth_holder)
diff = abs(expected_balance - balance)
if (diff != 0):
raise Exception("Unexpected balance for account: %s".format(woeth_holder))

def confirm_balances_after_upgrade():
expect_approximate("0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb", 1013453939109688661944)
expect_approximate("0xC460B0b6c9b578A4Cb93F99A691e16dB96Ee5833", 575896531839923556165)
expect_approximate("0xdca0a2341ed5438e06b9982243808a76b9add6d0", 319671606657733042618)
expect_approximate("0x8a9d46d28003673cd4fe7a56ecfcfa2be6372e64", 182355401624955452064)
expect_approximate("0xf65ecb5610000100befba41b9f9cf5ca32838078", 97352556026536192865)
expect_approximate("0x0a26e7ab5c554232314a8d459eff0ede72333f08", 91628532171545105831)


def manipulate_price():
OETH_WHALE="0xa4C637e0F704745D182e4D38cAb7E7485321d059"
whl = {'from': OETH_WHALE }

woeth.convertToAssets(1e18) / 1e18
oeth.transfer(woeth.address, 10_000 * 1e18, whl)
woeth.convertToAssets(1e18) / 1e18

oeth.approve(woeth.address, 1e50, whl)
woeth.deposit(5_000 * 1e18, OETH_WHALE, whl)
1 change: 1 addition & 0 deletions brownie/world.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
weth = load_contract('ERC20', WETH)
ousd = load_contract('ousd', OUSD)
oeth = load_contract('ousd', OETH)
woeth = load_contract('erc4626', WOETH)
usdt = load_contract('usdt', USDT)
usdc = load_contract('usdc', USDC)
dai = load_contract('dai', DAI)
Expand Down
106 changes: 106 additions & 0 deletions contracts/contracts/token/WOETH.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,29 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import { StableMath } from "../utils/StableMath.sol";
import { Governable } from "../governance/Governable.sol";
import { Initializable } from "../utils/Initializable.sol";
import { OETH } from "./OETH.sol";

/**
* @title OETH Token Contract
* @author Origin Protocol Inc
*
* @dev An important capability of this contract is that it isn't susceptible to changes of the
* exchange rate of WOETH/OETH if/when someone sends the underlying asset (OETH) to the contract.
* If OETH weren't rebasing this could be achieved by solely tracking the ERC20 transfers of the OETH
* token on mint, deposit, redeem, withdraw. The issue is that OETH is rebasing and OETH balances
* will change when the token rebases. For that reason we are tracking the WOETH contract credits and
* credits per token in those 4 actions. That way WOETH can keep an accurate track of the OETH balance
* ignoring any unexpected transfers of OETH to this contract.
*/

contract WOETH is ERC4626, Governable, Initializable {
using SafeERC20 for IERC20;
using StableMath for uint256;
uint256 public oethCreditsHighres;
bool private _oethCreditsInitialized;
Comment on lines +29 to +31
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


constructor(
ERC20 underlying_,
Expand All @@ -31,6 +43,23 @@ contract WOETH is ERC4626, Governable, Initializable {
OETH(address(asset())).rebaseOptIn();
}

function initialize2() external onlyGovernor {
require(!_oethCreditsInitialized, "Initialize2 already called");

_oethCreditsInitialized = true;
/*
* This contract is using creditsBalanceOfHighres rather than creditsBalanceOf since this
* ensures better accuracy when rounding. Also creditsBalanceOf can be a little
* finicky since it reports Highres version of credits and creditsPerToken
* when the account is a fresh one. That doesn't have an effect on mainnet since
* WOETH has already seen transactions. But it is rather annoying in unit test
* environment.
*/
(oethCreditsHighres, , ) = OETH(asset()).creditsBalanceOfHighres(
address(this)
);
}

function name() public view virtual override returns (string memory) {
return "Wrapped OETH";
}
Expand All @@ -49,7 +78,84 @@ contract WOETH is ERC4626, Governable, Initializable {
external
onlyGovernor
{
//@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.
Comment on lines +80 to +81
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"?

require(asset_ != address(asset()), "Cannot collect OETH");
IERC20(asset_).safeTransfer(governor(), amount_);
}

/**
* @dev This function converts requested OETH token amount to its underlying OETH
* credits value that is stored internally in OETH.sol and is required in order to
* be able to rebase.
*
* @param oethAmount Amount of OETH to be converted to OETH credits
* @return amount of OETH credits the OETH amount corresponds to
*/
function _oethToCredits(uint256 oethAmount) internal returns (uint256) {
(, uint256 creditsPerTokenHighres, ) = OETH(asset())
.creditsBalanceOfHighres(address(this));

/**
* Multiplying OETH amount with the creditsPerTokenHighres is exactly the math that
* 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

*
* This should make sure that the rounding will always be correct / mimic the rounding
* of OETH.
*/
return oethAmount.mulTruncate(creditsPerTokenHighres);
}

/** @dev See {IERC4262-totalAssets} */
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.

return (oethCreditsHighres).divPrecisely(creditsPerTokenHighres);
}

/** @dev See {IERC4262-deposit} */
function deposit(uint256 oethAmount, address receiver)
public
virtual
override
returns (uint256 woethAmount)
{
woethAmount = super.deposit(oethAmount, receiver);
oethCreditsHighres += _oethToCredits(oethAmount);
}

/** @dev See {IERC4262-mint} */
function mint(uint256 woethAmount, address receiver)
public
virtual
override
returns (uint256 oethAmount)
{
oethAmount = super.mint(woethAmount, receiver);
oethCreditsHighres += _oethToCredits(oethAmount);
}

/** @dev See {IERC4262-withdraw} */
function withdraw(
uint256 oethAmount,
address receiver,
address owner
) public virtual override returns (uint256 woethAmount) {
woethAmount = super.withdraw(oethAmount, receiver, owner);
oethCreditsHighres -= _oethToCredits(oethAmount);
}

/** @dev See {IERC4262-redeem} */
function redeem(
uint256 woethAmount,
address receiver,
address owner
) public virtual override returns (uint256 oethAmount) {
oethAmount = super.redeem(woethAmount, receiver, owner);
oethCreditsHighres -= _oethToCredits(oethAmount);
}
}
29 changes: 29 additions & 0 deletions contracts/deploy/deployActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1508,6 +1508,34 @@ const deployWOusd = async () => {
](dWrappedOusdImpl.address, governorAddr, initData);
};

const deployWOeth = async () => {
const { deployerAddr, governorAddr } = await getNamedAccounts();
const sDeployer = await ethers.provider.getSigner(deployerAddr);
const sGovernor = await ethers.provider.getSigner(governorAddr);

const oeth = await ethers.getContract("OETHProxy");
const dWrappedOethImpl = await deployWithConfirmation("WOETH", [
oeth.address,
"Wrapped OETH IMPL",
"WOETH IMPL",
]);
await deployWithConfirmation("WOETHProxy");
const woethProxy = await ethers.getContract("WOETHProxy");
const woeth = await ethers.getContractAt("WOETH", woethProxy.address);

const initData = woeth.interface.encodeFunctionData("initialize()", []);

await woethProxy.connect(sDeployer)[
// eslint-disable-next-line no-unexpected-multiline
"initialize(address,address,bytes)"
](dWrappedOethImpl.address, governorAddr, initData);

await woeth.connect(sGovernor)[
// eslint-disable-next-line no-unexpected-multiline
"initialize2()"
]();
};

const deployOETHSwapper = async () => {
const { deployerAddr, governorAddr } = await getNamedAccounts();
const sDeployer = await ethers.provider.getSigner(deployerAddr);
Expand Down Expand Up @@ -1624,6 +1652,7 @@ module.exports = {
deployUniswapV3Pool,
deployVaultValueChecker,
deployWOusd,
deployWOeth,
deployOETHSwapper,
deployOUSDSwapper,
upgradeNativeStakingSSVStrategy,
Expand Down
2 changes: 2 additions & 0 deletions contracts/deploy/mainnet/001_core.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const {
deployUniswapV3Pool,
deployVaultValueChecker,
deployWOusd,
deployWOeth,
deployOETHSwapper,
deployOUSDSwapper,
} = require("../deployActions");
Expand Down Expand Up @@ -54,6 +55,7 @@ const main = async () => {
await deployUniswapV3Pool();
await deployVaultValueChecker();
await deployWOusd();
await deployWOeth();
await deployOETHSwapper();
await deployOUSDSwapper();
console.log("001_core deploy done.");
Expand Down
44 changes: 44 additions & 0 deletions contracts/deploy/mainnet/112_upgrade_woeth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const { deploymentWithGovernanceProposal } = require("../../utils/deploy");

module.exports = deploymentWithGovernanceProposal(
{
deployName: "112_upgrade_woeth",
forceDeploy: false,
//forceSkip: true,
reduceQueueTime: true,
deployerIsProposer: false,
// proposalId:
},
async ({ deployWithConfirmation, ethers }) => {
const cOETHProxy = await ethers.getContract("OETHProxy");
const cWOETHProxy = await ethers.getContract("WOETHProxy");

const dWOETHImpl = await deployWithConfirmation("WOETH", [
cOETHProxy.address,
"Wrapped OETH",
"WOETH",
]);

const cWOETH = await ethers.getContractAt("WOETH", cWOETHProxy.address);

// Governance Actions
// ----------------
return {
name: `Upgrade WOETH to a new implementation.`,
actions: [
// 1. Upgrade WOETH
{
contract: cWOETHProxy,
signature: "upgradeTo(address)",
args: [dWOETHImpl.address],
},
// 2. Run the second initializer
{
contract: cWOETH,
signature: "initialize2()",
args: [],
},
],
};
}
);
19 changes: 12 additions & 7 deletions contracts/test/_fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ const simpleOETHFixture = deployments.createFixture(async () => {
);
const oeth = await ethers.getContractAt("OETH", oethProxy.address);

const cWOETHProxy = await ethers.getContract("WOETHProxy");
const woeth = await ethers.getContractAt("WOETH", cWOETHProxy.address);

const oethHarvesterProxy = await ethers.getContract("OETHHarvesterProxy");
const oethHarvester = await ethers.getContractAt(
"OETHHarvester",
Expand Down Expand Up @@ -183,6 +186,7 @@ const simpleOETHFixture = deployments.createFixture(async () => {
// OETH
oethVault,
oeth,
woeth,
nativeStakingSSVStrategy,
oethDripper,
oethHarvester,
Expand Down Expand Up @@ -227,12 +231,8 @@ const defaultFixture = deployments.createFixture(async () => {
);
const oeth = await ethers.getContractAt("OETH", oethProxy.address);

let woeth, woethProxy;

if (isFork) {
woethProxy = await ethers.getContract("WOETHProxy");
woeth = await ethers.getContractAt("WOETH", woethProxy.address);
}
const woethProxy = await ethers.getContract("WOETHProxy");
const woeth = await ethers.getContractAt("WOETH", woethProxy.address);

const harvesterProxy = await ethers.getContract("HarvesterProxy");
const harvester = await ethers.getContractAt(
Expand Down Expand Up @@ -676,10 +676,15 @@ const defaultFixture = deployments.createFixture(async () => {
if (!isFork) {
await fundAccounts();

// Matt and Josh each have $100 OUSD
// Matt and Josh each have $100 OUSD & 100 OETH
for (const user of [matt, josh]) {
await dai.connect(user).approve(vault.address, daiUnits("100"));
await vault.connect(user).mint(dai.address, daiUnits("100"), 0);

// Fund WETH contract
await hardhatSetBalance(user.address, "500");
await weth.connect(user).deposit({ value: oethUnits("100") });
await weth.connect(user).approve(oethVault.address, oethUnits("100"));
}
}
return {
Expand Down
9 changes: 9 additions & 0 deletions contracts/test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ chai.Assertion.addMethod(
}
);

chai.Assertion.addMethod("totalSupply", async function (expected, message) {
const contract = this._obj;
const actual = await contract.totalSupply();
if (!BigNumber.isBigNumber(expected)) {
expected = parseUnits(expected, await decimalsFor(contract));
}
chai.expect(actual).to.equal(expected, message);
});

chai.Assertion.addMethod(
"assetBalanceOf",
async function (expected, asset, message) {
Expand Down
Loading
Loading