From 2839dc1a4b9a8e80c9bb5a34256fd5ff59c01db2 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 21 Jun 2024 13:43:17 +0200 Subject: [PATCH 01/32] intermittent work committed --- brownie/hardhat.config.js | 15 ++++++++ contracts/deploy/mainnet/099_upgrade_woeth.js | 37 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 brownie/hardhat.config.js create mode 100644 contracts/deploy/mainnet/099_upgrade_woeth.js diff --git a/brownie/hardhat.config.js b/brownie/hardhat.config.js new file mode 100644 index 0000000000..fd3c6f1024 --- /dev/null +++ b/brownie/hardhat.config.js @@ -0,0 +1,15 @@ + +// autogenerated by brownie +// do not modify the existing settings +module.exports = { + networks: { + hardhat: { + hardfork: "london", + // base fee of 0 allows use of 0 gas price when testing + initialBaseFeePerGas: 0, + // brownie expects calls and transactions to throw on revert + throwOnTransactionFailures: true, + throwOnCallFailures: true + } + } +} \ No newline at end of file diff --git a/contracts/deploy/mainnet/099_upgrade_woeth.js b/contracts/deploy/mainnet/099_upgrade_woeth.js new file mode 100644 index 0000000000..dc662d8b6f --- /dev/null +++ b/contracts/deploy/mainnet/099_upgrade_woeth.js @@ -0,0 +1,37 @@ +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "099_upgrade_woeth", + forceDeploy: false, + //forceSkip: true, + reduceQueueTime: true, + deployerIsProposer: false, + // proposalId: + }, + async ({ deployWithConfirmation, ethers, getTxOpts, withConfirmation }) => { + const cOETHProxy = await ethers.getContract("OETHProxy"); + const cWOETHProxy = await ethers.getContract("WOETHProxy"); + + const dWOETHImpl = await deployWithConfirmation("WOETH", [ + cOETHProxy.address, + "Wrapped OETH", + "WOETH", + ]); + + // Governance Actions + // ---------------- + return { + name: `Upgrade WOETH to a new implementation.`, + actions: [ + // 1. Upgrade WOETH + { + contract: cWOETHProxy, + signature: "upgradeTo(address)", + args: [dWOETHImpl.address], + }, + ], + }; + } +); From 2e6be6293772ca31697ac6f341dc650601723b7f Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 21 Jun 2024 22:42:35 +0200 Subject: [PATCH 02/32] add draft solution for preparing WOETH for the landing markets --- brownie/abi/ERC4626.json | 662 ++++++++++++++++++ brownie/world.py | 1 + contracts/contracts/token/WOETH.sol | 112 +++ contracts/deploy/mainnet/099_upgrade_woeth.js | 11 +- 4 files changed, 784 insertions(+), 2 deletions(-) create mode 100644 brownie/abi/ERC4626.json diff --git a/brownie/abi/ERC4626.json b/brownie/abi/ERC4626.json new file mode 100644 index 0000000000..b45ec3da73 --- /dev/null +++ b/brownie/abi/ERC4626.json @@ -0,0 +1,662 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "asset", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "convertToAssets", + "outputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "name": "convertToShares", + "outputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "deposit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "maxDeposit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "maxMint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "maxRedeem", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "maxWithdraw", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "name": "previewDeposit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "previewMint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "previewRedeem", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "name": "previewWithdraw", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "redeem", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalAssets", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "withdraw", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ] \ No newline at end of file diff --git a/brownie/world.py b/brownie/world.py index bab6f9867a..fb32084e5d 100644 --- a/brownie/world.py +++ b/brownie/world.py @@ -35,6 +35,7 @@ def load_contract(name, address): 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) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index a15aa6283e..fdab000908 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -6,6 +6,7 @@ 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"; @@ -17,6 +18,10 @@ import { OETH } from "./OETH.sol"; contract WOETH is ERC4626, Governable, Initializable { using SafeERC20 for IERC20; + using StableMath for uint256; + + uint256 oethCredits; + bool oethCreditsInitialized; constructor( ERC20 underlying_, @@ -31,6 +36,13 @@ contract WOETH is ERC4626, Governable, Initializable { OETH(address(asset())).rebaseOptIn(); } + function initialize2() external onlyGovernor { + if (!oethCreditsInitialized) { + oethCreditsInitialized = true; + (oethCredits, ) = OETH(asset()).creditsBalanceOf(address(this)); + } + } + function name() public view override returns (string memory) { return "Wrapped OETH"; } @@ -52,4 +64,104 @@ contract WOETH is ERC4626, Governable, Initializable { require(asset_ != address(asset()), "Cannot collect OETH"); IERC20(asset_).safeTransfer(governor(), amount_); } + + function _oethToOethCredits(uint256 oethAmount) internal returns(uint256){ + (,uint256 creditsPerToken) = OETH(asset()).creditsBalanceOf(address(this)); + return oethAmount.mulTruncate(creditsPerToken); + } + + /** @dev See {IERC4262-deposit} */ + function deposit(uint256 assets, address receiver) public virtual override returns (uint256) { + 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); + oethCredits += _oethToOethCredits(assets); + + emit Deposit(caller, receiver, assets, shares); + + return shares; + } + + /** @dev See {IERC4262-mint} */ + function mint(uint256 shares, address receiver) public virtual override returns (uint256) { + require(shares <= maxMint(receiver), "ERC4626: mint more then max"); + + address caller = _msgSender(); + uint256 assets = previewMint(shares); + + // 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); + oethCredits += _oethToOethCredits(assets); + + emit Deposit(caller, receiver, assets, shares); + + return assets; + } + + /** @dev See {IERC4262-withdraw} */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) public virtual override returns (uint256) { + require(assets <= maxWithdraw(owner), "ERC4626: withdraw more then max"); + + address caller = _msgSender(); + uint256 shares = previewWithdraw(assets); + + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + + // if _asset is ERC777, transfer can call reenter AFTER the transfer happens through + // the tokensReceived hook, so we need to transfer after we burn to keep the invariants. + _burn(owner, shares); + SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); + oethCredits -= _oethToOethCredits(assets); + + emit Withdraw(caller, receiver, owner, assets, shares); + + return shares; + } + + /** @dev See {IERC4262-redeem} */ + function redeem( + uint256 shares, + address receiver, + address owner + ) public virtual override returns (uint256) { + require(shares <= maxRedeem(owner), "ERC4626: redeem more then max"); + + address caller = _msgSender(); + uint256 assets = previewRedeem(shares); + + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + + // if _asset is ERC777, transfer can call reenter AFTER the transfer happens through + // the tokensReceived hook, so we need to transfer after we burn to keep the invariants. + _burn(owner, shares); + SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); + oethCredits -= _oethToOethCredits(assets); + + emit Withdraw(caller, receiver, owner, assets, shares); + + return assets; + } + + /** @dev See {IERC4262-totalAssets} */ + function totalAssets() public view virtual override returns (uint256) { + (,uint256 creditsPerToken) = OETH(asset()).creditsBalanceOf(address(this)); + + return oethCredits.divPrecisely(creditsPerToken); + } } diff --git a/contracts/deploy/mainnet/099_upgrade_woeth.js b/contracts/deploy/mainnet/099_upgrade_woeth.js index dc662d8b6f..ce85526441 100644 --- a/contracts/deploy/mainnet/099_upgrade_woeth.js +++ b/contracts/deploy/mainnet/099_upgrade_woeth.js @@ -1,5 +1,4 @@ const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); -const addresses = require("../../utils/addresses"); module.exports = deploymentWithGovernanceProposal( { @@ -10,7 +9,7 @@ module.exports = deploymentWithGovernanceProposal( deployerIsProposer: false, // proposalId: }, - async ({ deployWithConfirmation, ethers, getTxOpts, withConfirmation }) => { + async ({ deployWithConfirmation, ethers }) => { const cOETHProxy = await ethers.getContract("OETHProxy"); const cWOETHProxy = await ethers.getContract("WOETHProxy"); @@ -20,6 +19,8 @@ module.exports = deploymentWithGovernanceProposal( "WOETH", ]); + const cWOETH = await ethers.getContractAt("WOETH", cWOETHProxy.address); + // Governance Actions // ---------------- return { @@ -31,6 +32,12 @@ module.exports = deploymentWithGovernanceProposal( signature: "upgradeTo(address)", args: [dWOETHImpl.address], }, + // 2. Run the second initializer + { + contract: cWOETH, + signature: "initialize2()", + args: [], + }, ], }; } From 5495d4b20f76a52eb8ae20c3788cdcac666117bf Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 24 Jun 2024 21:11:34 +0200 Subject: [PATCH 03/32] add tests that confirm lending market protection works as exepcted --- brownie/scripts/woeth_manipulation.py | 11 +++ contracts/contracts/token/WOETH.sol | 70 ++++++++++--- contracts/deploy/deployActions.js | 29 ++++++ contracts/deploy/mainnet/001_core.js | 2 + contracts/test/_fixture.js | 16 +-- contracts/test/helpers.js | 9 ++ contracts/test/token/woeth.js | 137 ++++++++++++++++++++++++++ 7 files changed, 255 insertions(+), 19 deletions(-) create mode 100644 brownie/scripts/woeth_manipulation.py create mode 100644 contracts/test/token/woeth.js diff --git a/brownie/scripts/woeth_manipulation.py b/brownie/scripts/woeth_manipulation.py new file mode 100644 index 0000000000..189c0b44b1 --- /dev/null +++ b/brownie/scripts/woeth_manipulation.py @@ -0,0 +1,11 @@ +from world import * + +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) \ No newline at end of file diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index fdab000908..e584866017 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -19,7 +19,10 @@ import { OETH } from "./OETH.sol"; contract WOETH is ERC4626, Governable, Initializable { using SafeERC20 for IERC20; using StableMath for uint256; + uint256 private constant OETH_RESOLUTION_INCREASE = 1e9; + /* - Add extensive comments on how this works + */ uint256 oethCredits; bool oethCreditsInitialized; @@ -39,7 +42,9 @@ contract WOETH is ERC4626, Governable, Initializable { function initialize2() external onlyGovernor { if (!oethCreditsInitialized) { oethCreditsInitialized = true; - (oethCredits, ) = OETH(asset()).creditsBalanceOf(address(this)); + (uint256 oethCreditsHighres, , ) = OETH(asset()) + .creditsBalanceOfHighres(address(this)); + oethCredits = oethCreditsHighres / OETH_RESOLUTION_INCREASE; } } @@ -61,35 +66,66 @@ 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. require(asset_ != address(asset()), "Cannot collect OETH"); IERC20(asset_).safeTransfer(governor(), amount_); } - function _oethToOethCredits(uint256 oethAmount) internal returns(uint256){ - (,uint256 creditsPerToken) = OETH(asset()).creditsBalanceOf(address(this)); - return oethAmount.mulTruncate(creditsPerToken); + function _oethToOethCredits(uint256 oethAmount) internal returns (uint256) { + (, uint256 creditsPerToken, ) = OETH(asset()).creditsBalanceOfHighres( + address(this) + ); + return + oethAmount.mulTruncate(creditsPerToken / OETH_RESOLUTION_INCREASE); } /** @dev See {IERC4262-deposit} */ - function deposit(uint256 assets, address receiver) public virtual override returns (uint256) { - require(assets <= maxDeposit(receiver), "ERC4626: deposit more then max"); + function deposit(uint256 assets, address receiver) + public + virtual + override + returns (uint256) + { + 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); + SafeERC20.safeTransferFrom( + IERC20(asset()), + caller, + address(this), + assets + ); _mint(receiver, shares); oethCredits += _oethToOethCredits(assets); + uint256 credits1; + uint256 cpt1; + bool upgraded; + + (credits1, cpt1, upgraded) = OETH(asset()).creditsBalanceOfHighres( + address(this) + ); + emit Deposit(caller, receiver, assets, shares); return shares; } /** @dev See {IERC4262-mint} */ - function mint(uint256 shares, address receiver) public virtual override returns (uint256) { + function mint(uint256 shares, address receiver) + public + virtual + override + returns (uint256) + { require(shares <= maxMint(receiver), "ERC4626: mint more then max"); address caller = _msgSender(); @@ -97,7 +133,12 @@ contract WOETH is ERC4626, Governable, Initializable { // 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); + SafeERC20.safeTransferFrom( + IERC20(asset()), + caller, + address(this), + assets + ); _mint(receiver, shares); oethCredits += _oethToOethCredits(assets); @@ -112,7 +153,10 @@ contract WOETH is ERC4626, Governable, Initializable { address receiver, address owner ) public virtual override returns (uint256) { - require(assets <= maxWithdraw(owner), "ERC4626: withdraw more then max"); + require( + assets <= maxWithdraw(owner), + "ERC4626: withdraw more then max" + ); address caller = _msgSender(); uint256 shares = previewWithdraw(assets); @@ -158,9 +202,11 @@ contract WOETH is ERC4626, Governable, Initializable { return assets; } - /** @dev See {IERC4262-totalAssets} */ + /** @dev See {IERC4262-totalAssets} */ function totalAssets() public view virtual override returns (uint256) { - (,uint256 creditsPerToken) = OETH(asset()).creditsBalanceOf(address(this)); + (, uint256 creditsPerToken) = OETH(asset()).creditsBalanceOf( + address(this) + ); return oethCredits.divPrecisely(creditsPerToken); } diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 2892193eb5..cffcfbbe9f 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1471,6 +1471,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); @@ -1564,6 +1592,7 @@ module.exports = { deployUniswapV3Pool, deployVaultValueChecker, deployWOusd, + deployWOeth, deployOETHSwapper, deployOUSDSwapper, upgradeNativeStakingSSVStrategy, diff --git a/contracts/deploy/mainnet/001_core.js b/contracts/deploy/mainnet/001_core.js index 61b7451e4d..d7039eadb7 100644 --- a/contracts/deploy/mainnet/001_core.js +++ b/contracts/deploy/mainnet/001_core.js @@ -23,6 +23,7 @@ const { deployUniswapV3Pool, deployVaultValueChecker, deployWOusd, + deployWOeth, deployOETHSwapper, deployOUSDSwapper, } = require("../deployActions"); @@ -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."); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index bfb127f981..0447e814d8 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -226,12 +226,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( @@ -662,10 +658,16 @@ 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")); + await oethVault.connect(user).mint(weth.address, oethUnits("100"), 0); } } return { diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index bbd6e72a52..41348d0c95 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -118,6 +118,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) { diff --git a/contracts/test/token/woeth.js b/contracts/test/token/woeth.js new file mode 100644 index 0000000000..4b9cfb9642 --- /dev/null +++ b/contracts/test/token/woeth.js @@ -0,0 +1,137 @@ +const { expect } = require("chai"); + +const { loadDefaultFixture } = require("../_fixture"); +const { oethUnits, daiUnits, isFork } = require("../helpers"); +const { hardhatSetBalance } = require("../_fund"); + +describe("WOETH", function () { + if (isFork) { + this.timeout(0); + } + + let oeth, weth, woeth, oethVault, dai, matt, josh, governor; + + beforeEach(async () => { + const fixture = await loadDefaultFixture(); + oeth = fixture.oeth; + woeth = fixture.woeth; + oethVault = fixture.oethVault; + dai = fixture.dai; + matt = fixture.matt; + josh = fixture.josh; + weth = fixture.weth; + governor = fixture.governor; + + // Josh wraps 50 OETH to WOETH + await oeth.connect(josh).approve(woeth.address, oethUnits("1000")); + await woeth.connect(josh).deposit(oethUnits("50"), josh.address); + + // START: below steps raise the worth of 1 WOETH from 1 to 2 OETH units + const oethSupply = await oeth.totalSupply(); + await weth.connect(josh).deposit({ value: oethSupply }); + // send 50% of the WETH to inc + await weth.connect(josh).transfer(oethVault.address, oethSupply); + await oethVault.connect(josh).rebase(); + // END OF raising worth of WOETH + + // josh account starts each test with 100 OETH + }); + + describe("Funds in, Funds out", async () => { + it("should deposit at the correct ratio", async () => { + await expect(woeth).to.have.a.totalSupply("50"); + await woeth.connect(josh).deposit(oethUnits("50"), josh.address); + await expect(josh).to.have.a.balanceOf("75", woeth); + await expect(josh).to.have.a.balanceOf("50", oeth); + await expect(woeth).to.have.a.totalSupply("75"); + }); + + it("should withdraw at the correct ratio", async () => { + await expect(woeth).to.have.a.totalSupply("50"); + await woeth + .connect(josh) + .withdraw(oethUnits("50"), josh.address, josh.address); + await expect(josh).to.have.a.balanceOf("25", woeth); + await expect(josh).to.have.a.balanceOf("150", oeth); + await expect(woeth).to.have.a.totalSupply("25"); + }); + it("should mint at the correct ratio", async () => { + await expect(woeth).to.have.a.totalSupply("50"); + await woeth.connect(josh).mint(oethUnits("25"), josh.address); + await expect(josh).to.have.a.balanceOf("75", woeth); + await expect(josh).to.have.a.balanceOf("50", oeth); + await expect(woeth).to.have.a.totalSupply("75"); + }); + it("should redeem at the correct ratio", async () => { + await expect(woeth).to.have.a.totalSupply("50"); + await expect(josh).to.have.a.balanceOf("50", woeth); + await woeth + .connect(josh) + .redeem(oethUnits("50"), josh.address, josh.address); + await expect(josh).to.have.a.balanceOf("0", woeth); + await expect(josh).to.have.a.balanceOf("200", oeth); + await expect(woeth).to.have.a.totalSupply("0"); + }); + }); + + describe("Collects Rebase", async () => { + it("should increase with an OETH rebase", async () => { + await expect(woeth).to.have.a.totalSupply("50"); + await expect(woeth).to.have.approxBalanceOf("100", oeth); + await hardhatSetBalance(josh.address, "250"); + await weth.connect(josh).deposit({ value: oethUnits("200") }); + await weth.connect(josh).transfer(oethVault.address, oethUnits("200")); + await oethVault.rebase(); + await expect(woeth).to.have.approxBalanceOf("150", oeth); + await expect(woeth).to.have.a.totalSupply("50"); + }); + + it("should not increase exchange rate when OETH is transferred to the contract", async () => { + await expect(woeth).to.have.a.totalSupply("50"); + await expect(woeth).to.have.approxBalanceOf("100", oeth); + await expect(josh).to.have.a.balanceOf("50", woeth); + + // attempt to "attack" the contract to inflate the WOETH balance + await oeth.connect(josh).transfer(woeth.address, oethUnits("50")); + + // redeeming 50 WOETH should still yield 100 OETH and not let the transfer + // of OETH one line above affect it + await woeth + .connect(josh) + .redeem(oethUnits("50"), josh.address, josh.address); + + await expect(josh).to.have.a.balanceOf("0", woeth); + await expect(woeth).to.have.approxBalanceOf("50", oeth); + await expect(woeth).to.have.a.totalSupply("0"); + }); + }); + + describe("Check proxy", async () => { + it("should have correct ERC20 properties", async () => { + expect(await woeth.decimals()).to.eq(18); + expect(await woeth.name()).to.eq("Wrapped OETH"); + expect(await woeth.symbol()).to.eq("WOETH"); + }); + }); + + describe("Token recovery", async () => { + it("should allow a governor to recover tokens", async () => { + await dai.connect(matt).transfer(woeth.address, daiUnits("2")); + await expect(woeth).to.have.a.balanceOf("2", dai); + await expect(governor).to.have.a.balanceOf("1000", dai); + await woeth.connect(governor).transferToken(dai.address, daiUnits("2")); + await expect(woeth).to.have.a.balanceOf("0", dai); + await expect(governor).to.have.a.balanceOf("1002", dai); + }); + it("should not allow a governor to collect OETH", async () => { + await expect( + woeth.connect(governor).transferToken(oeth.address, oethUnits("2")) + ).to.be.revertedWith("Cannot collect OETH"); + }); + it("should not allow a non governor to recover tokens ", async () => { + await expect( + woeth.connect(josh).transferToken(oeth.address, oethUnits("2")) + ).to.be.revertedWith("Caller is not the Governor"); + }); + }); +}); From d0eb16f0b1faaa2dcbe21af8d36b8e3c316ae3b6 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 24 Jun 2024 21:42:26 +0200 Subject: [PATCH 04/32] add test to protect agains calling initialize twice --- contracts/contracts/token/WOETH.sol | 12 +++++++----- contracts/test/token/woeth.js | 8 ++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index e584866017..b747ac649d 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -40,12 +40,14 @@ contract WOETH is ERC4626, Governable, Initializable { } function initialize2() external onlyGovernor { - if (!oethCreditsInitialized) { - oethCreditsInitialized = true; - (uint256 oethCreditsHighres, , ) = OETH(asset()) - .creditsBalanceOfHighres(address(this)); - oethCredits = oethCreditsHighres / OETH_RESOLUTION_INCREASE; + if (oethCreditsInitialized) { + require(false, "Initialize2 already called"); } + + oethCreditsInitialized = true; + (uint256 oethCreditsHighres, , ) = OETH(asset()) + .creditsBalanceOfHighres(address(this)); + oethCredits = oethCreditsHighres / OETH_RESOLUTION_INCREASE; } function name() public view override returns (string memory) { diff --git a/contracts/test/token/woeth.js b/contracts/test/token/woeth.js index 4b9cfb9642..89dfe97dfd 100644 --- a/contracts/test/token/woeth.js +++ b/contracts/test/token/woeth.js @@ -37,6 +37,14 @@ describe("WOETH", function () { // josh account starts each test with 100 OETH }); + describe("General functionality", async () => { + it("Initialize2 should not be called twice", async () => { + await expect(woeth.connect(governor).initialize2()).to.be.revertedWith( + "Initialize2 already called" + ); + }); + }); + describe("Funds in, Funds out", async () => { it("should deposit at the correct ratio", async () => { await expect(woeth).to.have.a.totalSupply("50"); From fc5d87485948f980dc05257a099f9ee2631a30e7 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 10:43:01 +0200 Subject: [PATCH 05/32] add comments --- contracts/contracts/token/WOETH.sol | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index b747ac649d..2707fc8a7b 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -14,15 +14,21 @@ 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 private constant OETH_RESOLUTION_INCREASE = 1e9; - - /* - Add extensive comments on how this works - */ uint256 oethCredits; bool oethCreditsInitialized; @@ -45,6 +51,12 @@ contract WOETH is ERC4626, Governable, Initializable { } oethCreditsInitialized = true; + /* + * This contract is using creditsBalanceOfHighres rather than creditsBalanceOf since the + * latter will report the same values as creditsBalanceOfHighres if the account holding + * OETH is a new one. On mainnet this isn't a problem, but in unit test environment + * it is. + */ (uint256 oethCreditsHighres, , ) = OETH(asset()) .creditsBalanceOfHighres(address(this)); oethCredits = oethCreditsHighres / OETH_RESOLUTION_INCREASE; From cfec4e5eafe2b28a1d64404820d91773bb6dcba5 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 10:45:12 +0200 Subject: [PATCH 06/32] remove unneeded files --- brownie/hardhat.config.js | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 brownie/hardhat.config.js diff --git a/brownie/hardhat.config.js b/brownie/hardhat.config.js deleted file mode 100644 index fd3c6f1024..0000000000 --- a/brownie/hardhat.config.js +++ /dev/null @@ -1,15 +0,0 @@ - -// autogenerated by brownie -// do not modify the existing settings -module.exports = { - networks: { - hardhat: { - hardfork: "london", - // base fee of 0 allows use of 0 gas price when testing - initialBaseFeePerGas: 0, - // brownie expects calls and transactions to throw on revert - throwOnTransactionFailures: true, - throwOnCallFailures: true - } - } -} \ No newline at end of file From 8405bfc007e947619d2a9d8af88d3212e76d04b4 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 10:52:11 +0200 Subject: [PATCH 07/32] minor bug fix --- contracts/contracts/token/WOETH.sol | 31 ++++++++++------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index 2707fc8a7b..7372793dbe 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -22,7 +22,6 @@ import { OETH } from "./OETH.sol"; * 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 { @@ -87,11 +86,20 @@ contract WOETH is ERC4626, Governable, Initializable { } function _oethToOethCredits(uint256 oethAmount) internal returns (uint256) { - (, uint256 creditsPerToken, ) = OETH(asset()).creditsBalanceOfHighres( + (, uint256 creditsPerTokenHighres, ) = OETH(asset()).creditsBalanceOfHighres( address(this) ); return - oethAmount.mulTruncate(creditsPerToken / OETH_RESOLUTION_INCREASE); + oethAmount.mulTruncate(creditsPerTokenHighres / OETH_RESOLUTION_INCREASE); + } + + /** @dev See {IERC4262-totalAssets} */ + function totalAssets() public view virtual override returns (uint256) { + (, uint256 creditsPerTokenHighres) = OETH(asset()).creditsBalanceOfHighres( + address(this) + ); + + return oethCredits.divPrecisely(creditsPerTokenHighres / RESOLUTION_INCREASE); } /** @dev See {IERC4262-deposit} */ @@ -120,14 +128,6 @@ contract WOETH is ERC4626, Governable, Initializable { _mint(receiver, shares); oethCredits += _oethToOethCredits(assets); - uint256 credits1; - uint256 cpt1; - bool upgraded; - - (credits1, cpt1, upgraded) = OETH(asset()).creditsBalanceOfHighres( - address(this) - ); - emit Deposit(caller, receiver, assets, shares); return shares; @@ -215,13 +215,4 @@ contract WOETH is ERC4626, Governable, Initializable { return assets; } - - /** @dev See {IERC4262-totalAssets} */ - function totalAssets() public view virtual override returns (uint256) { - (, uint256 creditsPerToken) = OETH(asset()).creditsBalanceOf( - address(this) - ); - - return oethCredits.divPrecisely(creditsPerToken); - } } From e03223c5a5fc982cf03da2b387606369ae79d4c2 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 11:51:14 +0200 Subject: [PATCH 08/32] add fork script tests and fix bug --- contracts/contracts/token/WOETH.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index 7372793dbe..e2484d9346 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -95,11 +95,11 @@ contract WOETH is ERC4626, Governable, Initializable { /** @dev See {IERC4262-totalAssets} */ function totalAssets() public view virtual override returns (uint256) { - (, uint256 creditsPerTokenHighres) = OETH(asset()).creditsBalanceOfHighres( + (, uint256 creditsPerTokenHighres, ) = OETH(asset()).creditsBalanceOfHighres( address(this) ); - return oethCredits.divPrecisely(creditsPerTokenHighres / RESOLUTION_INCREASE); + return oethCredits.divPrecisely(creditsPerTokenHighres / OETH_RESOLUTION_INCREASE); } /** @dev See {IERC4262-deposit} */ From 319467fc2574a8d38ab893458c06ab84a0158e80 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 12:00:51 +0200 Subject: [PATCH 09/32] add comments --- contracts/contracts/token/WOETH.sol | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index e2484d9346..779a14a9fe 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -85,21 +85,32 @@ contract WOETH is ERC4626, Governable, Initializable { 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 _oethToOethCredits(uint256 oethAmount) internal returns (uint256) { - (, uint256 creditsPerTokenHighres, ) = OETH(asset()).creditsBalanceOfHighres( - address(this) - ); + (, uint256 creditsPerTokenHighres, ) = OETH(asset()) + .creditsBalanceOfHighres(address(this)); return - oethAmount.mulTruncate(creditsPerTokenHighres / OETH_RESOLUTION_INCREASE); + oethAmount.mulTruncate( + creditsPerTokenHighres / OETH_RESOLUTION_INCREASE + ); } /** @dev See {IERC4262-totalAssets} */ function totalAssets() public view virtual override returns (uint256) { - (, uint256 creditsPerTokenHighres, ) = OETH(asset()).creditsBalanceOfHighres( - address(this) - ); + (, uint256 creditsPerTokenHighres, ) = OETH(asset()) + .creditsBalanceOfHighres(address(this)); - return oethCredits.divPrecisely(creditsPerTokenHighres / OETH_RESOLUTION_INCREASE); + return + oethCredits.divPrecisely( + creditsPerTokenHighres / OETH_RESOLUTION_INCREASE + ); } /** @dev See {IERC4262-deposit} */ From 1142a743f5379c6766805c018b82345e6eca0c67 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 12:06:20 +0200 Subject: [PATCH 10/32] add another test --- brownie/scripts/woeth_manipulation.py | 31 +++++++++++++++++++++------ contracts/test/token/woeth.js | 7 ++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/brownie/scripts/woeth_manipulation.py b/brownie/scripts/woeth_manipulation.py index 189c0b44b1..4b70f7d9ab 100644 --- a/brownie/scripts/woeth_manipulation.py +++ b/brownie/scripts/woeth_manipulation.py @@ -1,11 +1,28 @@ from world import * -OETH_WHALE="0xa4C637e0F704745D182e4D38cAb7E7485321d059" -whl = {'from': OETH_WHALE } +def expect_approximate(woeth_holder, expected_balance): + balance = woeth.balanceOf(woeth_holder) + diff = abs(expected_balance - balance) + #if (diff/expected_balance > 0.00000001): + if (diff != 0): + raise Exception("Unexpected balance for account: %s".format(woeth_holder)) -woeth.convertToAssets(1e18) / 1e18 -oeth.transfer(woeth.address, 10_000 * 1e18, whl) -woeth.convertToAssets(1e18) / 1e18 +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) -oeth.approve(woeth.address, 1e50, whl) -woeth.deposit(5_000 * 1e18, OETH_WHALE, whl) \ No newline at end of file + +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) \ No newline at end of file diff --git a/contracts/test/token/woeth.js b/contracts/test/token/woeth.js index 89dfe97dfd..9211688ecd 100644 --- a/contracts/test/token/woeth.js +++ b/contracts/test/token/woeth.js @@ -39,10 +39,17 @@ describe("WOETH", function () { describe("General functionality", async () => { it("Initialize2 should not be called twice", async () => { + // this function is already called by the fixture await expect(woeth.connect(governor).initialize2()).to.be.revertedWith( "Initialize2 already called" ); }); + + it("Initialize2 should not be called by non governor", async () => { + await expect(woeth.connect(josh).initialize2()).to.be.revertedWith( + "Caller is not the Governor" + ); + }); }); describe("Funds in, Funds out", async () => { From f911ba6d6deb3ad0b783c4fad9c8fc2b67ec72b2 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 12:32:15 +0200 Subject: [PATCH 11/32] divide after multiply --- contracts/contracts/token/WOETH.sol | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index 779a14a9fe..3674ed78f3 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -97,9 +97,7 @@ contract WOETH is ERC4626, Governable, Initializable { (, uint256 creditsPerTokenHighres, ) = OETH(asset()) .creditsBalanceOfHighres(address(this)); return - oethAmount.mulTruncate( - creditsPerTokenHighres / OETH_RESOLUTION_INCREASE - ); + oethAmount.mulTruncate(creditsPerTokenHighres) / OETH_RESOLUTION_INCREASE; } /** @dev See {IERC4262-totalAssets} */ @@ -108,9 +106,7 @@ contract WOETH is ERC4626, Governable, Initializable { .creditsBalanceOfHighres(address(this)); return - oethCredits.divPrecisely( - creditsPerTokenHighres / OETH_RESOLUTION_INCREASE - ); + (oethCredits * OETH_RESOLUTION_INCREASE).divPrecisely(creditsPerTokenHighres); } /** @dev See {IERC4262-deposit} */ From 3dc85239e749b7f9e1d76e2b34495360cade0be6 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 16:43:36 +0200 Subject: [PATCH 12/32] change credits per token to highres --- brownie/scripts/woeth_manipulation.py | 1 - contracts/contracts/token/WOETH.sol | 34 ++++++++++++++++----------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/brownie/scripts/woeth_manipulation.py b/brownie/scripts/woeth_manipulation.py index 4b70f7d9ab..58b16ffdcb 100644 --- a/brownie/scripts/woeth_manipulation.py +++ b/brownie/scripts/woeth_manipulation.py @@ -3,7 +3,6 @@ def expect_approximate(woeth_holder, expected_balance): balance = woeth.balanceOf(woeth_holder) diff = abs(expected_balance - balance) - #if (diff/expected_balance > 0.00000001): if (diff != 0): raise Exception("Unexpected balance for account: %s".format(woeth_holder)) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index 3674ed78f3..bd2eabd63e 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -27,8 +27,7 @@ import { OETH } from "./OETH.sol"; contract WOETH is ERC4626, Governable, Initializable { using SafeERC20 for IERC20; using StableMath for uint256; - uint256 private constant OETH_RESOLUTION_INCREASE = 1e9; - uint256 oethCredits; + uint256 oethCreditsHighres; bool oethCreditsInitialized; constructor( @@ -56,9 +55,9 @@ contract WOETH is ERC4626, Governable, Initializable { * OETH is a new one. On mainnet this isn't a problem, but in unit test environment * it is. */ - (uint256 oethCreditsHighres, , ) = OETH(asset()) - .creditsBalanceOfHighres(address(this)); - oethCredits = oethCreditsHighres / OETH_RESOLUTION_INCREASE; + (oethCreditsHighres, , ) = OETH(asset()).creditsBalanceOfHighres( + address(this) + ); } function name() public view override returns (string memory) { @@ -93,11 +92,19 @@ contract WOETH is ERC4626, Governable, Initializable { * @param oethAmount Amount of OETH to be converted to OETH credits * @return amount of OETH credits the OETH amount corresponds to */ - function _oethToOethCredits(uint256 oethAmount) internal returns (uint256) { + function _oethToOethCreditsHighres(uint256 oethAmount) + internal + returns (uint256) + { (, uint256 creditsPerTokenHighres, ) = OETH(asset()) .creditsBalanceOfHighres(address(this)); - return - oethAmount.mulTruncate(creditsPerTokenHighres) / OETH_RESOLUTION_INCREASE; + + /** + * Multiplying OETH amount with the creditsPerToken, dividing by resolution + * + TODOOOOO + */ + return oethAmount.mulTruncate(creditsPerTokenHighres); } /** @dev See {IERC4262-totalAssets} */ @@ -105,8 +112,7 @@ contract WOETH is ERC4626, Governable, Initializable { (, uint256 creditsPerTokenHighres, ) = OETH(asset()) .creditsBalanceOfHighres(address(this)); - return - (oethCredits * OETH_RESOLUTION_INCREASE).divPrecisely(creditsPerTokenHighres); + return (oethCreditsHighres).divPrecisely(creditsPerTokenHighres); } /** @dev See {IERC4262-deposit} */ @@ -133,7 +139,7 @@ contract WOETH is ERC4626, Governable, Initializable { assets ); _mint(receiver, shares); - oethCredits += _oethToOethCredits(assets); + oethCreditsHighres += _oethToOethCreditsHighres(assets); emit Deposit(caller, receiver, assets, shares); @@ -161,7 +167,7 @@ contract WOETH is ERC4626, Governable, Initializable { assets ); _mint(receiver, shares); - oethCredits += _oethToOethCredits(assets); + oethCreditsHighres += _oethToOethCreditsHighres(assets); emit Deposit(caller, receiver, assets, shares); @@ -190,7 +196,7 @@ contract WOETH is ERC4626, Governable, Initializable { // the tokensReceived hook, so we need to transfer after we burn to keep the invariants. _burn(owner, shares); SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); - oethCredits -= _oethToOethCredits(assets); + oethCreditsHighres -= _oethToOethCreditsHighres(assets); emit Withdraw(caller, receiver, owner, assets, shares); @@ -216,7 +222,7 @@ contract WOETH is ERC4626, Governable, Initializable { // the tokensReceived hook, so we need to transfer after we burn to keep the invariants. _burn(owner, shares); SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); - oethCredits -= _oethToOethCredits(assets); + oethCreditsHighres -= _oethToOethCreditsHighres(assets); emit Withdraw(caller, receiver, owner, assets, shares); From bad282bd8eed621d02b3b4b2e37974bc24ac0a76 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 16:48:10 +0200 Subject: [PATCH 13/32] add comment --- contracts/contracts/token/WOETH.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index bd2eabd63e..f8f01af6bf 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -100,9 +100,12 @@ contract WOETH is ERC4626, Governable, Initializable { .creditsBalanceOfHighres(address(this)); /** - * Multiplying OETH amount with the creditsPerToken, dividing by resolution - * - TODOOOOO + * Multiplying OETH amount with the creditsPerTokenHighres is exactly the math that + * is internally being done in OETH: + * https://github.com/OriginProtocol/origin-dollar/blob/2314cccf2933f5c1f76a6549c1f5c9cc935b6f05/contracts/contracts/token/OUSD.sol#L242-L249 + * + * This should make sure that the rounding will always be correct / mimic the rounding + * of OETH. */ return oethAmount.mulTruncate(creditsPerTokenHighres); } From 07b51cf055ee18d38963f4c20359f09f633686a9 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 16:50:47 +0200 Subject: [PATCH 14/32] comments --- contracts/contracts/token/WOETH.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index f8f01af6bf..2800def77f 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -50,10 +50,12 @@ contract WOETH is ERC4626, Governable, Initializable { oethCreditsInitialized = true; /* - * This contract is using creditsBalanceOfHighres rather than creditsBalanceOf since the - * latter will report the same values as creditsBalanceOfHighres if the account holding - * OETH is a new one. On mainnet this isn't a problem, but in unit test environment - * it is. + * 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) From c0333fb73391bdb31c258f0dbb044d28162661ea Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 17:05:07 +0200 Subject: [PATCH 15/32] lint --- contracts/contracts/token/WOETH.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index 2800def77f..73393fa753 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -104,7 +104,9 @@ contract WOETH is ERC4626, Governable, Initializable { /** * Multiplying OETH amount with the creditsPerTokenHighres is exactly the math that * is internally being done in OETH: - * https://github.com/OriginProtocol/origin-dollar/blob/2314cccf2933f5c1f76a6549c1f5c9cc935b6f05/contracts/contracts/token/OUSD.sol#L242-L249 + */ + // solhint-disable-next-line max-line-length + /** https://github.com/OriginProtocol/origin-dollar/blob/2314cccf2933f5c1f76a6549c1f5c9cc935b6f05/contracts/contracts/token/OUSD.sol#L242-L249 * * This should make sure that the rounding will always be correct / mimic the rounding * of OETH. From 2c965c3a437e78822fed32551d08013d2b67b6c8 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 17:12:25 +0200 Subject: [PATCH 16/32] minor changes --- contracts/contracts/token/WOETH.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index 73393fa753..ea84045b5b 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -27,8 +27,9 @@ import { OETH } from "./OETH.sol"; contract WOETH is ERC4626, Governable, Initializable { using SafeERC20 for IERC20; using StableMath for uint256; - uint256 oethCreditsHighres; - bool oethCreditsInitialized; + // doesn't need to be public, but convenient to be able to confirm the state on the mainnet + uint256 public oethCreditsHighres; + bool _oethCreditsInitialized; constructor( ERC20 underlying_, @@ -44,11 +45,11 @@ contract WOETH is ERC4626, Governable, Initializable { } function initialize2() external onlyGovernor { - if (oethCreditsInitialized) { + if (_oethCreditsInitialized) { require(false, "Initialize2 already called"); } - oethCreditsInitialized = true; + _oethCreditsInitialized = true; /* * This contract is using creditsBalanceOfHighres rather than creditsBalanceOf since this * ensures better accuracy when rounding. Also creditsBalanceOf can be a little From 13fd4dae3796af9eab1bfb6674a171a3fc8adbc7 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 26 Jun 2024 18:52:49 +0200 Subject: [PATCH 17/32] correct fixture --- contracts/test/_fixture.js | 1 - contracts/test/token/woeth.js | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 0447e814d8..049e412797 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -667,7 +667,6 @@ const defaultFixture = deployments.createFixture(async () => { await hardhatSetBalance(user.address, "500"); await weth.connect(user).deposit({ value: oethUnits("100") }); await weth.connect(user).approve(oethVault.address, oethUnits("100")); - await oethVault.connect(user).mint(weth.address, oethUnits("100"), 0); } } return { diff --git a/contracts/test/token/woeth.js b/contracts/test/token/woeth.js index 9211688ecd..e1264faf79 100644 --- a/contracts/test/token/woeth.js +++ b/contracts/test/token/woeth.js @@ -22,6 +22,11 @@ describe("WOETH", function () { weth = fixture.weth; governor = fixture.governor; + // mint some OETH + for (const user of [matt, josh]) { + await oethVault.connect(user).mint(weth.address, oethUnits("100"), 0); + } + // Josh wraps 50 OETH to WOETH await oeth.connect(josh).approve(woeth.address, oethUnits("1000")); await woeth.connect(josh).deposit(oethUnits("50"), josh.address); From 7923ddb25021327cd44e60070b2c933303c6b9e2 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 27 Jun 2024 00:44:38 +0200 Subject: [PATCH 18/32] make code slighlty better regarding re-entry --- contracts/contracts/token/WOETH.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index ea84045b5b..a9a6358ca0 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -203,9 +203,10 @@ contract WOETH is ERC4626, Governable, Initializable { // if _asset is ERC777, transfer can call reenter AFTER the transfer happens through // the tokensReceived hook, so we need to transfer after we burn to keep the invariants. _burn(owner, shares); - SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); oethCreditsHighres -= _oethToOethCreditsHighres(assets); + SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); + emit Withdraw(caller, receiver, owner, assets, shares); return shares; @@ -229,9 +230,10 @@ contract WOETH is ERC4626, Governable, Initializable { // if _asset is ERC777, transfer can call reenter AFTER the transfer happens through // the tokensReceived hook, so we need to transfer after we burn to keep the invariants. _burn(owner, shares); - SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); oethCreditsHighres -= _oethToOethCreditsHighres(assets); + SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); + emit Withdraw(caller, receiver, owner, assets, shares); return assets; From c69583f54f237266f7989e485f7ed83136e81924 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 27 Jun 2024 00:45:06 +0200 Subject: [PATCH 19/32] prettier --- contracts/contracts/token/WOETH.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index a9a6358ca0..ed5da06aa7 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -54,7 +54,7 @@ contract WOETH is ERC4626, Governable, Initializable { * 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 + * 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. */ @@ -106,7 +106,7 @@ contract WOETH is ERC4626, Governable, Initializable { * Multiplying OETH amount with the creditsPerTokenHighres is exactly the math that * is internally being done in OETH: */ - // solhint-disable-next-line max-line-length + // solhint-disable-next-line max-line-length /** https://github.com/OriginProtocol/origin-dollar/blob/2314cccf2933f5c1f76a6549c1f5c9cc935b6f05/contracts/contracts/token/OUSD.sol#L242-L249 * * This should make sure that the rounding will always be correct / mimic the rounding @@ -206,7 +206,7 @@ contract WOETH is ERC4626, Governable, Initializable { oethCreditsHighres -= _oethToOethCreditsHighres(assets); SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); - + emit Withdraw(caller, receiver, owner, assets, shares); return shares; From 6822937053781ea4a3be7aab00ef15a7823095f7 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 27 Jun 2024 22:24:50 +0200 Subject: [PATCH 20/32] add explicit visibility --- contracts/contracts/token/WOETH.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index ed5da06aa7..e8d592e6b3 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -29,7 +29,7 @@ contract WOETH is ERC4626, Governable, Initializable { 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; + bool private _oethCreditsInitialized; constructor( ERC20 underlying_, From ac855d937cb153b7e3f36a4563329e09972c788f Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 27 Jun 2024 22:39:37 +0200 Subject: [PATCH 21/32] minor test enhancement --- contracts/test/token/woeth.js | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/test/token/woeth.js b/contracts/test/token/woeth.js index e1264faf79..bb8c7527ba 100644 --- a/contracts/test/token/woeth.js +++ b/contracts/test/token/woeth.js @@ -122,6 +122,7 @@ describe("WOETH", function () { await expect(josh).to.have.a.balanceOf("0", woeth); await expect(woeth).to.have.approxBalanceOf("50", oeth); + await expect(await woeth.totalAssets()).to.equal("0"); await expect(woeth).to.have.a.totalSupply("0"); }); }); From 1c9fc649ba33782f0019150d3f7f946732a317c5 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 1 Jul 2024 22:51:11 +0200 Subject: [PATCH 22/32] better function name --- contracts/contracts/token/WOETH.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index e8d592e6b3..fc5c43c899 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -95,7 +95,7 @@ contract WOETH is ERC4626, Governable, Initializable { * @param oethAmount Amount of OETH to be converted to OETH credits * @return amount of OETH credits the OETH amount corresponds to */ - function _oethToOethCreditsHighres(uint256 oethAmount) + function _creditsPerAsset(uint256 oethAmount) internal returns (uint256) { @@ -147,7 +147,7 @@ contract WOETH is ERC4626, Governable, Initializable { assets ); _mint(receiver, shares); - oethCreditsHighres += _oethToOethCreditsHighres(assets); + oethCreditsHighres += _creditsPerAsset(assets); emit Deposit(caller, receiver, assets, shares); @@ -175,7 +175,7 @@ contract WOETH is ERC4626, Governable, Initializable { assets ); _mint(receiver, shares); - oethCreditsHighres += _oethToOethCreditsHighres(assets); + oethCreditsHighres += _creditsPerAsset(assets); emit Deposit(caller, receiver, assets, shares); @@ -203,7 +203,7 @@ contract WOETH is ERC4626, Governable, Initializable { // if _asset is ERC777, transfer can call reenter AFTER the transfer happens through // the tokensReceived hook, so we need to transfer after we burn to keep the invariants. _burn(owner, shares); - oethCreditsHighres -= _oethToOethCreditsHighres(assets); + oethCreditsHighres -= _creditsPerAsset(assets); SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); @@ -230,7 +230,7 @@ contract WOETH is ERC4626, Governable, Initializable { // if _asset is ERC777, transfer can call reenter AFTER the transfer happens through // the tokensReceived hook, so we need to transfer after we burn to keep the invariants. _burn(owner, shares); - oethCreditsHighres -= _oethToOethCreditsHighres(assets); + oethCreditsHighres -= _creditsPerAsset(assets); SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); From 56d0b7d9eefc22b48877b8b980db284e6e41313e Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 1 Jul 2024 22:58:57 +0200 Subject: [PATCH 23/32] remove comment --- contracts/contracts/token/WOETH.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index fc5c43c899..402c734609 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -27,7 +27,6 @@ import { OETH } from "./OETH.sol"; contract WOETH is ERC4626, Governable, Initializable { using SafeERC20 for IERC20; 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 private _oethCreditsInitialized; From 057469c08067ff67e31b8e1f524b61e249787499 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Dec 2024 11:14:46 +0100 Subject: [PATCH 24/32] simplify --- contracts/contracts/token/WOETH.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index 402c734609..95cf85459c 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -44,9 +44,7 @@ contract WOETH is ERC4626, Governable, Initializable { } function initialize2() external onlyGovernor { - if (_oethCreditsInitialized) { - require(false, "Initialize2 already called"); - } + require(!_oethCreditsInitialized, "Initialize2 already called"); _oethCreditsInitialized = true; /* From 1a6a16fc4af1f115f5c84a331df821da4b551d47 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Dec 2024 11:35:26 +0100 Subject: [PATCH 25/32] simplify code --- contracts/contracts/token/WOETH.sol | 90 +++-------------------------- 1 file changed, 8 insertions(+), 82 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index 95cf85459c..5969432bdd 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -125,30 +125,10 @@ contract WOETH is ERC4626, Governable, Initializable { public virtual override - returns (uint256) + returns (uint256 shares) { - 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); + shares = super.deposit(assets, receiver); oethCreditsHighres += _creditsPerAsset(assets); - - emit Deposit(caller, receiver, assets, shares); - - return shares; } /** @dev See {IERC4262-mint} */ @@ -156,27 +136,10 @@ contract WOETH is ERC4626, Governable, Initializable { public virtual override - returns (uint256) + returns (uint256 assets) { - require(shares <= maxMint(receiver), "ERC4626: mint more then max"); - - address caller = _msgSender(); - uint256 assets = previewMint(shares); - - // 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); + assets = super.mint(shares, receiver); oethCreditsHighres += _creditsPerAsset(assets); - - emit Deposit(caller, receiver, assets, shares); - - return assets; } /** @dev See {IERC4262-withdraw} */ @@ -184,29 +147,9 @@ contract WOETH is ERC4626, Governable, Initializable { uint256 assets, address receiver, address owner - ) public virtual override returns (uint256) { - require( - assets <= maxWithdraw(owner), - "ERC4626: withdraw more then max" - ); - - address caller = _msgSender(); - uint256 shares = previewWithdraw(assets); - - if (caller != owner) { - _spendAllowance(owner, caller, shares); - } - - // if _asset is ERC777, transfer can call reenter AFTER the transfer happens through - // the tokensReceived hook, so we need to transfer after we burn to keep the invariants. - _burn(owner, shares); + ) public virtual override returns (uint256 shares) { + shares = super.withdraw(assets, receiver, owner); oethCreditsHighres -= _creditsPerAsset(assets); - - SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); - - emit Withdraw(caller, receiver, owner, assets, shares); - - return shares; } /** @dev See {IERC4262-redeem} */ @@ -214,25 +157,8 @@ contract WOETH is ERC4626, Governable, Initializable { uint256 shares, address receiver, address owner - ) public virtual override returns (uint256) { - require(shares <= maxRedeem(owner), "ERC4626: redeem more then max"); - - address caller = _msgSender(); - uint256 assets = previewRedeem(shares); - - if (caller != owner) { - _spendAllowance(owner, caller, shares); - } - - // if _asset is ERC777, transfer can call reenter AFTER the transfer happens through - // the tokensReceived hook, so we need to transfer after we burn to keep the invariants. - _burn(owner, shares); + ) public virtual override returns (uint256 assets) { + assets = super.redeem(shares, receiver, owner); oethCreditsHighres -= _creditsPerAsset(assets); - - SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); - - emit Withdraw(caller, receiver, owner, assets, shares); - - return assets; } } From 2223600a1d8b8bf23c940a7debe124ddb1e4f44d Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Dec 2024 12:09:58 +0100 Subject: [PATCH 26/32] simplify code --- contracts/contracts/token/WOETH.sol | 34 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index 5969432bdd..bef187eac5 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -92,7 +92,7 @@ contract WOETH is ERC4626, Governable, Initializable { * @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) + function _oethToCredits(uint256 oethAmount) internal returns (uint256) { @@ -121,44 +121,44 @@ contract WOETH is ERC4626, Governable, Initializable { } /** @dev See {IERC4262-deposit} */ - function deposit(uint256 assets, address receiver) + function deposit(uint256 oethAmount, address receiver) public virtual override - returns (uint256 shares) + returns (uint256 woethAmount) { - shares = super.deposit(assets, receiver); - oethCreditsHighres += _creditsPerAsset(assets); + woethAmount = super.deposit(oethAmount, receiver); + oethCreditsHighres += _oethToCredits(oethAmount); } /** @dev See {IERC4262-mint} */ - function mint(uint256 shares, address receiver) + function mint(uint256 woethAmount, address receiver) public virtual override - returns (uint256 assets) + returns (uint256 oethAmount) { - assets = super.mint(shares, receiver); - oethCreditsHighres += _creditsPerAsset(assets); + oethAmount = super.mint(woethAmount, receiver); + oethCreditsHighres += _oethToCredits(oethAmount); } /** @dev See {IERC4262-withdraw} */ function withdraw( - uint256 assets, + uint256 oethAmount, address receiver, address owner - ) public virtual override returns (uint256 shares) { - shares = super.withdraw(assets, receiver, owner); - oethCreditsHighres -= _creditsPerAsset(assets); + ) public virtual override returns (uint256 woethAmount) { + woethAmount = super.withdraw(oethAmount, receiver, owner); + oethCreditsHighres -= _oethToCredits(oethAmount); } /** @dev See {IERC4262-redeem} */ function redeem( - uint256 shares, + uint256 woethAmount, address receiver, address owner - ) public virtual override returns (uint256 assets) { - assets = super.redeem(shares, receiver, owner); - oethCreditsHighres -= _creditsPerAsset(assets); + ) public virtual override returns (uint256 oethAmount) { + oethAmount = super.redeem(woethAmount, receiver, owner); + oethCreditsHighres -= _oethToCredits(oethAmount); } } From 39e8d4b758b66b79c97623cb60f2634104c1e45a Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Dec 2024 13:22:33 +0100 Subject: [PATCH 27/32] refactor --- contracts/test/token/woeth.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/contracts/test/token/woeth.js b/contracts/test/token/woeth.js index bb8c7527ba..cab65a187f 100644 --- a/contracts/test/token/woeth.js +++ b/contracts/test/token/woeth.js @@ -31,17 +31,18 @@ describe("WOETH", function () { await oeth.connect(josh).approve(woeth.address, oethUnits("1000")); await woeth.connect(josh).deposit(oethUnits("50"), josh.address); - // START: below steps raise the worth of 1 WOETH from 1 to 2 OETH units - const oethSupply = await oeth.totalSupply(); - await weth.connect(josh).deposit({ value: oethSupply }); - // send 50% of the WETH to inc - await weth.connect(josh).transfer(oethVault.address, oethSupply); - await oethVault.connect(josh).rebase(); - // END OF raising worth of WOETH + // rebase OETH balances in wallets by 2x + await increaseOETHSupplyAndRebase(await oeth.totalSupply()); // josh account starts each test with 100 OETH }); + const increaseOETHSupplyAndRebase = async (wethAmount) => { + await weth.connect(josh).deposit({ value: wethAmount }); + await weth.connect(josh).transfer(oethVault.address, wethAmount); + await oethVault.connect(josh).rebase(); + }; + describe("General functionality", async () => { it("Initialize2 should not be called twice", async () => { // this function is already called by the fixture @@ -99,9 +100,9 @@ describe("WOETH", function () { await expect(woeth).to.have.a.totalSupply("50"); await expect(woeth).to.have.approxBalanceOf("100", oeth); await hardhatSetBalance(josh.address, "250"); - await weth.connect(josh).deposit({ value: oethUnits("200") }); - await weth.connect(josh).transfer(oethVault.address, oethUnits("200")); - await oethVault.rebase(); + + await increaseOETHSupplyAndRebase(oethUnits("200")); + await expect(woeth).to.have.approxBalanceOf("150", oeth); await expect(woeth).to.have.a.totalSupply("50"); }); @@ -131,7 +132,7 @@ describe("WOETH", function () { it("should have correct ERC20 properties", async () => { expect(await woeth.decimals()).to.eq(18); expect(await woeth.name()).to.eq("Wrapped OETH"); - expect(await woeth.symbol()).to.eq("WOETH"); + expect(await woeth.symbol()).to.eq("wOETH"); }); }); From 3615ef7541050d9ee6c39ec1d2e3b5cbc898a032 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Dec 2024 13:38:27 +0100 Subject: [PATCH 28/32] add redeem all test --- contracts/test/token/woeth.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/contracts/test/token/woeth.js b/contracts/test/token/woeth.js index cab65a187f..0247d0f9a5 100644 --- a/contracts/test/token/woeth.js +++ b/contracts/test/token/woeth.js @@ -83,6 +83,7 @@ describe("WOETH", function () { await expect(josh).to.have.a.balanceOf("50", oeth); await expect(woeth).to.have.a.totalSupply("75"); }); + it("should redeem at the correct ratio", async () => { await expect(woeth).to.have.a.totalSupply("50"); await expect(josh).to.have.a.balanceOf("50", woeth); @@ -93,6 +94,35 @@ describe("WOETH", function () { await expect(josh).to.have.a.balanceOf("200", oeth); await expect(woeth).to.have.a.totalSupply("0"); }); + + it("should be able to redeem all WOETH", async () => { + await expect(woeth).to.have.a.totalSupply("50"); + await expect(josh).to.have.a.balanceOf("50", woeth); + await expect(matt).to.have.a.balanceOf("0", woeth); + + await oeth.connect(matt).approve(woeth.address, oethUnits("100")); + await woeth + .connect(matt) + .mint(oethUnits("50"), matt.address); + + await expect(woeth).to.have.a.totalSupply("100"); + await expect(await woeth.totalAssets()).to.equal(oethUnits("200")); + + // redeem all WOETH held by Josh and Matt + await woeth + .connect(josh) + .redeem(oethUnits("50"), josh.address, josh.address); + await woeth + .connect(matt) + .redeem(oethUnits("50"), matt.address, matt.address); + + await expect(josh).to.have.a.balanceOf("0", woeth); + await expect(matt).to.have.a.balanceOf("0", woeth); + await expect(josh).to.have.a.balanceOf("200", oeth); + await expect(matt).to.have.a.balanceOf("200", oeth); + await expect(woeth).to.have.a.totalSupply("0"); + await expect(await woeth.totalAssets()).to.equal(oethUnits("0")); + }); }); describe("Collects Rebase", async () => { From 466f95757a8e9089caca913f3f99e1883a6fb4ea Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Dec 2024 13:39:10 +0100 Subject: [PATCH 29/32] prettier --- contracts/contracts/token/WOETH.sol | 5 +---- contracts/test/token/woeth.js | 6 ++---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index dd10d20735..185821fd74 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -92,10 +92,7 @@ contract WOETH is ERC4626, Governable, Initializable { * @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) - { + function _oethToCredits(uint256 oethAmount) internal returns (uint256) { (, uint256 creditsPerTokenHighres, ) = OETH(asset()) .creditsBalanceOfHighres(address(this)); diff --git a/contracts/test/token/woeth.js b/contracts/test/token/woeth.js index 0247d0f9a5..a703c27b68 100644 --- a/contracts/test/token/woeth.js +++ b/contracts/test/token/woeth.js @@ -101,13 +101,11 @@ describe("WOETH", function () { await expect(matt).to.have.a.balanceOf("0", woeth); await oeth.connect(matt).approve(woeth.address, oethUnits("100")); - await woeth - .connect(matt) - .mint(oethUnits("50"), matt.address); + await woeth.connect(matt).mint(oethUnits("50"), matt.address); await expect(woeth).to.have.a.totalSupply("100"); await expect(await woeth.totalAssets()).to.equal(oethUnits("200")); - + // redeem all WOETH held by Josh and Matt await woeth .connect(josh) From 52eb2445c18b985e21820dd6c025c57bc8d754fb Mon Sep 17 00:00:00 2001 From: Shah <10547529+shahthepro@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:49:57 +0400 Subject: [PATCH 30/32] 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 --- contracts/test/_fixture.js | 4 + .../test/token/woeth.mainnet.fork-test.js | 265 ++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 contracts/test/token/woeth.mainnet.fork-test.js diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 93632faa70..7f0bb7315c 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -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", @@ -183,6 +186,7 @@ const simpleOETHFixture = deployments.createFixture(async () => { // OETH oethVault, oeth, + woeth, nativeStakingSSVStrategy, oethDripper, oethHarvester, diff --git a/contracts/test/token/woeth.mainnet.fork-test.js b/contracts/test/token/woeth.mainnet.fork-test.js new file mode 100644 index 0000000000..92c4cbd4d7 --- /dev/null +++ b/contracts/test/token/woeth.mainnet.fork-test.js @@ -0,0 +1,265 @@ +const { expect } = require("chai"); + +const { simpleOETHFixture, createFixtureLoader } = require("./../_fixture"); +const { hardhatSetBalance } = require("../_fund"); +const { oethUnits } = require("../helpers"); + +const oethWhaleFixture = async () => { + const fixture = await simpleOETHFixture(); + + const { weth, oeth, oethVault, woeth, domen } = fixture; + + // Domen is a OETH whale + await oethVault + .connect(domen) + .mint(weth.address, oethUnits("20000"), oethUnits("19999")); + + await oeth.connect(domen).approve(woeth.address, oethUnits("20000")); + + return fixture; +}; + +const loadFixture = createFixtureLoader(oethWhaleFixture); + +describe("ForkTest: wOETH", function () { + this.timeout(0); + + let fixture; + beforeEach(async () => { + fixture = await loadFixture(); + }); + + it("Should prevent total asset manipulation by donations", async () => { + const { oeth, woeth, domen } = fixture; + const totalAssetsBefore = await woeth.totalAssets(); + await oeth.connect(domen).transfer(woeth.address, oethUnits("100")); + const totalAssetsAfter = await woeth.totalAssets(); + + expect(totalAssetsBefore).to.be.equal(totalAssetsAfter); + }); + + it("Deposit should not be manipulated by donations", async () => { + const { oeth, woeth, domen } = fixture; + + await expect(domen).to.have.approxBalanceOf("0", woeth); + + // Wrap some OETH + await woeth.connect(domen).deposit(oethUnits("1000"), domen.address); + + const sharePriceBeforeDonate = await woeth.convertToAssets( + oethUnits("1000") + ); + + // Donate some OETH + oeth.connect(domen).transfer(woeth.address, oethUnits("10000")); + + // Ensure no change in share price + const sharePriceAfterDonate = await woeth.convertToAssets( + oethUnits("1000") + ); + expect(sharePriceBeforeDonate).to.approxEqual( + sharePriceAfterDonate, + "Price manipulation" + ); + + // Wrap again + await woeth.connect(domen).deposit(oethUnits("1000"), domen.address); + + // Ensure the balance is right + await expect(domen).to.have.approxBalanceOf( + // 2000 * 1000 / sharePrice(1000 OETH) + oethUnits("2000").mul(oethUnits("1000")).div(sharePriceAfterDonate), + woeth + ); + }); + + it("Withdraw should not be manipulated by donations", async () => { + const { oeth, woeth, domen } = fixture; + + await expect(domen).to.have.approxBalanceOf("0", woeth); + await expect(domen).to.have.approxBalanceOf("20000", oeth); + + // Wrap some OETH + await woeth.connect(domen).deposit(oethUnits("3000"), domen.address); + + const sharePriceBeforeDonate = await woeth.convertToAssets( + oethUnits("1000") + ); + + // Donate some OETH + oeth.connect(domen).transfer(woeth.address, oethUnits("10000")); + + // Ensure no change in share price + const sharePriceAfterDonate = await woeth.convertToAssets( + oethUnits("1000") + ); + expect(sharePriceBeforeDonate).to.approxEqual( + sharePriceAfterDonate, + "Price manipulation" + ); + + // Withdraw + await woeth + .connect(domen) + .withdraw( + await woeth.maxWithdraw(domen.address), + domen.address, + domen.address + ); + + // Ensure balance is right + await expect(domen).to.have.approxBalanceOf("10000", oeth); + }); + + describe("Funds in, Funds out", async () => { + it("should deposit at the correct ratio", async () => { + const { oeth, woeth, domen } = fixture; + + const totalSupply = await woeth.totalSupply(); + const balanceBefore = await oeth.balanceOf(domen.address); + + // Wrap some OETH + const txResponse = await woeth + .connect(domen) + .deposit(oethUnits("50"), domen.address); + const txReceipt = await txResponse.wait(); + const mintedShares = txReceipt.events[2].args.shares; // 0. transfer oeth, 1. transfer woeth, 2. deposit + const assetTransfered = txReceipt.events[2].args.assets; // 0. transfer oeth, 1. transfer woeth, 2. mint + + await expect(assetTransfered).to.be.equal(oethUnits("50")); + await expect( + await woeth.convertToShares(assetTransfered) + ).to.be.approxEqual(mintedShares); + await expect(woeth).to.have.a.totalSupply(totalSupply.add(mintedShares)); + await expect(await woeth.balanceOf(domen.address)).to.be.equal( + mintedShares + ); + await expect(await oeth.balanceOf(domen.address)).to.be.equal( + balanceBefore.sub(assetTransfered) + ); + }); + it("should withdraw at the correct ratio", async () => { + const { oeth, woeth, domen } = fixture; + // First wrap some OETH + await woeth.connect(domen).deposit(oethUnits("50"), domen.address); + + const totalSupply = await woeth.totalSupply(); + const balanceBefore = await oeth.balanceOf(domen.address); + + // Then unwrap some WOETH + const txResponse = await woeth + .connect(domen) + .withdraw( + await woeth.maxWithdraw(domen.address), + domen.address, + domen.address + ); + const txReceipt = await txResponse.wait(); + const burnedShares = txReceipt.events[2].args.shares; // 0. transfer oeth, 1. transfer woeth, 2. withdraw + const assetTransfered = txReceipt.events[2].args.assets; // 0. transfer oeth, 1. transfer woeth, 2. mint + + await expect(assetTransfered).to.be.approxEqual(oethUnits("50")); + await expect( + await woeth.convertToShares(assetTransfered) + ).to.be.approxEqual(burnedShares); + await expect(woeth).to.have.a.totalSupply(totalSupply.sub(burnedShares)); + await expect(await woeth.balanceOf(domen.address)).to.be.equal(0); + await expect(await oeth.balanceOf(domen.address)).to.be.approxEqual( + balanceBefore.add(assetTransfered) + ); + }); + it("should mint at the correct ratio", async () => { + const { oeth, woeth, domen } = fixture; + + const totalSupply = await woeth.totalSupply(); + const balanceBefore = await oeth.balanceOf(domen.address); + + // Mint some WOETH + const txResponse = await woeth + .connect(domen) + .mint(oethUnits("25"), domen.address); + const txReceipt = await txResponse.wait(); + const mintedShares = txReceipt.events[2].args.shares; // 0. transfer oeth, 1. transfer woeth, 2. mint + const assetTransfered = txReceipt.events[2].args.assets; // 0. transfer oeth, 1. transfer woeth, 2. mint + + await expect(mintedShares).to.be.equal(oethUnits("25")); + await expect(await woeth.convertToAssets(mintedShares)).to.be.approxEqual( + assetTransfered + ); + await expect(woeth).to.have.a.totalSupply(totalSupply.add(mintedShares)); + await expect(await woeth.balanceOf(domen.address)).to.be.equal( + mintedShares + ); + await expect(await oeth.balanceOf(domen.address)).to.be.equal( + balanceBefore.sub(assetTransfered) + ); + }); + it("should redeem at the correct ratio", async () => { + const { oeth, woeth, domen } = fixture; + + // Mint some WOETH + await woeth.connect(domen).mint(oethUnits("25"), domen.address); + + const totalSupply = await woeth.totalSupply(); + const balanceBefore = await oeth.balanceOf(domen.address); + + // Redeem some WOETH + const txResponse = await woeth + .connect(domen) + .redeem( + await woeth.maxRedeem(domen.address), + domen.address, + domen.address + ); + const txReceipt = await txResponse.wait(); + const burnedShares = txReceipt.events[2].args.shares; // 0. transfer oeth, 1. transfer woeth, 2. redeem + const assetTransfered = txReceipt.events[2].args.assets; // 0. transfer oeth, 1. transfer woeth, 2. redeem + + await expect(burnedShares).to.be.equal(oethUnits("25")); + await expect(await woeth.convertToAssets(burnedShares)).to.be.approxEqual( + assetTransfered + ); + await expect(woeth).to.have.a.totalSupply(totalSupply.sub(burnedShares)); + await expect(await woeth.balanceOf(domen.address)).to.be.equal(0); + await expect(await oeth.balanceOf(domen.address)).to.be.approxEqual( + balanceBefore.add(assetTransfered) + ); + }); + it("should redeem at the correct ratio after rebase", async () => { + const { weth, oethVault, woeth, domen, josh } = fixture; + + // Mint some WOETH + const initialDeposit = oethUnits("50"); + await woeth.connect(domen).deposit(initialDeposit, domen.address); + + const totalAssetsBefore = await woeth.totalAssets(); + // Rebase + await hardhatSetBalance(josh.address, "250"); + await weth.connect(josh).deposit({ value: oethUnits("200") }); + await weth.connect(josh).transfer(oethVault.address, oethUnits("200")); + await oethVault.rebase(); + + const totalAssetsAfter = await woeth.totalAssets(); + expect(totalAssetsAfter > totalAssetsBefore).to.be.true; + + // Then unwrap some WOETH + const txResponse = await woeth + .connect(domen) + .redeem( + await woeth.maxRedeem(domen.address), + domen.address, + domen.address + ); + + const txReceipt = await txResponse.wait(); + const burnedShares = txReceipt.events[2].args.shares; // 0. transfer oeth, 1. transfer woeth, 2. redeem + const assetTransfered = txReceipt.events[2].args.assets; // 0. transfer oeth, 1. transfer woeth, 2. redeem + + await expect(assetTransfered > initialDeposit); + await expect(burnedShares).to.be.approxEqual( + await woeth.convertToShares(assetTransfered) + ); + await expect(domen).to.have.a.balanceOf("0", woeth); + }); + }); +}); From f0580bc919f5e52d97a10f22ba92094872ee3d31 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Dec 2024 13:50:22 +0100 Subject: [PATCH 31/32] rename deploy script --- .../mainnet/{099_upgrade_woeth.js => 112_upgrade_woeth.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename contracts/deploy/mainnet/{099_upgrade_woeth.js => 112_upgrade_woeth.js} (96%) diff --git a/contracts/deploy/mainnet/099_upgrade_woeth.js b/contracts/deploy/mainnet/112_upgrade_woeth.js similarity index 96% rename from contracts/deploy/mainnet/099_upgrade_woeth.js rename to contracts/deploy/mainnet/112_upgrade_woeth.js index ce85526441..6482a1a266 100644 --- a/contracts/deploy/mainnet/099_upgrade_woeth.js +++ b/contracts/deploy/mainnet/112_upgrade_woeth.js @@ -2,7 +2,7 @@ const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); module.exports = deploymentWithGovernanceProposal( { - deployName: "099_upgrade_woeth", + deployName: "112_upgrade_woeth", forceDeploy: false, //forceSkip: true, reduceQueueTime: true, From 0a988d5bf326cf32f98dc8ca4daa92da03c6d2ee Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Dec 2024 14:22:59 +0100 Subject: [PATCH 32/32] add another test and simplify constructor --- contracts/contracts/token/WOETH.sol | 7 +++---- contracts/contracts/token/WOETHBase.sol | 2 +- contracts/deploy/mainnet/112_upgrade_woeth.js | 4 +--- contracts/test/token/woeth.mainnet.fork-test.js | 7 +++++++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index 185821fd74..4228f3b16c 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -30,11 +30,10 @@ contract WOETH is ERC4626, Governable, Initializable { uint256 public oethCreditsHighres; bool private _oethCreditsInitialized; + // no need to set ERC20 name and symbol since they are overridden in WOETH & WOETHBase constructor( - ERC20 underlying_, - string memory name_, - string memory symbol_ - ) ERC20(name_, symbol_) ERC4626(underlying_) Governable() {} + ERC20 underlying_ + ) ERC20("", "") ERC4626(underlying_) Governable() {} /** * @notice Enable OETH rebasing for this contract diff --git a/contracts/contracts/token/WOETHBase.sol b/contracts/contracts/token/WOETHBase.sol index b640bcddac..f84e1c237c 100644 --- a/contracts/contracts/token/WOETHBase.sol +++ b/contracts/contracts/token/WOETHBase.sol @@ -11,7 +11,7 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract WOETHBase is WOETH { constructor(ERC20 underlying_) - WOETH(underlying_, "Wrapped Super OETH", "wsuperOETHb") + WOETH(underlying_) {} function name() public view virtual override returns (string memory) { diff --git a/contracts/deploy/mainnet/112_upgrade_woeth.js b/contracts/deploy/mainnet/112_upgrade_woeth.js index 6482a1a266..8b8acbbf53 100644 --- a/contracts/deploy/mainnet/112_upgrade_woeth.js +++ b/contracts/deploy/mainnet/112_upgrade_woeth.js @@ -14,9 +14,7 @@ module.exports = deploymentWithGovernanceProposal( const cWOETHProxy = await ethers.getContract("WOETHProxy"); const dWOETHImpl = await deployWithConfirmation("WOETH", [ - cOETHProxy.address, - "Wrapped OETH", - "WOETH", + cOETHProxy.address ]); const cWOETH = await ethers.getContractAt("WOETH", cWOETHProxy.address); diff --git a/contracts/test/token/woeth.mainnet.fork-test.js b/contracts/test/token/woeth.mainnet.fork-test.js index 92c4cbd4d7..b5c5f9d905 100644 --- a/contracts/test/token/woeth.mainnet.fork-test.js +++ b/contracts/test/token/woeth.mainnet.fork-test.js @@ -29,6 +29,13 @@ describe("ForkTest: wOETH", function () { fixture = await loadFixture(); }); + it("Should have correct name and symbol", async () => { + const { woeth } = fixture; + + expect(await woeth.name()).to.equal("Wrapped OETH"); + expect(await woeth.symbol()).to.equal("wOETH"); + }); + it("Should prevent total asset manipulation by donations", async () => { const { oeth, woeth, domen } = fixture; const totalAssetsBefore = await woeth.totalAssets();