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/scripts/woeth_manipulation.py b/brownie/scripts/woeth_manipulation.py new file mode 100644 index 0000000000..58b16ffdcb --- /dev/null +++ b/brownie/scripts/woeth_manipulation.py @@ -0,0 +1,27 @@ +from world import * + +def expect_approximate(woeth_holder, expected_balance): + balance = woeth.balanceOf(woeth_holder) + diff = abs(expected_balance - balance) + if (diff != 0): + raise Exception("Unexpected balance for account: %s".format(woeth_holder)) + +def confirm_balances_after_upgrade(): + expect_approximate("0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb", 1013453939109688661944) + expect_approximate("0xC460B0b6c9b578A4Cb93F99A691e16dB96Ee5833", 575896531839923556165) + expect_approximate("0xdca0a2341ed5438e06b9982243808a76b9add6d0", 319671606657733042618) + expect_approximate("0x8a9d46d28003673cd4fe7a56ecfcfa2be6372e64", 182355401624955452064) + expect_approximate("0xf65ecb5610000100befba41b9f9cf5ca32838078", 97352556026536192865) + expect_approximate("0x0a26e7ab5c554232314a8d459eff0ede72333f08", 91628532171545105831) + + +def manipulate_price(): + OETH_WHALE="0xa4C637e0F704745D182e4D38cAb7E7485321d059" + whl = {'from': OETH_WHALE } + + woeth.convertToAssets(1e18) / 1e18 + oeth.transfer(woeth.address, 10_000 * 1e18, whl) + woeth.convertToAssets(1e18) / 1e18 + + oeth.approve(woeth.address, 1e50, whl) + woeth.deposit(5_000 * 1e18, OETH_WHALE, whl) \ No newline at end of file diff --git a/brownie/world.py b/brownie/world.py index b1dfce55be..9342dbbf05 100644 --- a/brownie/world.py +++ b/brownie/world.py @@ -19,6 +19,7 @@ weth = load_contract('ERC20', WETH) ousd = load_contract('ousd', OUSD) oeth = load_contract('ousd', OETH) +woeth = load_contract('erc4626', WOETH) usdt = load_contract('usdt', USDT) usdc = load_contract('usdc', USDC) dai = load_contract('dai', DAI) diff --git a/contracts/contracts/mocks/MockLimitedWrappedOusd.sol b/contracts/contracts/mocks/MockLimitedWrappedOusd.sol index 71ba1b55de..96e8c878d6 100644 --- a/contracts/contracts/mocks/MockLimitedWrappedOusd.sol +++ b/contracts/contracts/mocks/MockLimitedWrappedOusd.sol @@ -5,11 +5,7 @@ import { WrappedOusd } from "../token/WrappedOusd.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MockLimitedWrappedOusd is WrappedOusd { - constructor( - ERC20 underlying_, - string memory name_, - string memory symbol_ - ) WrappedOusd(underlying_, name_, symbol_) {} + constructor(ERC20 underlying_) WrappedOusd(underlying_) {} function maxDeposit(address) public diff --git a/contracts/contracts/token/WOETH.sol b/contracts/contracts/token/WOETH.sol index 7cd68103ab..b707bed000 100644 --- a/contracts/contracts/token/WOETH.sol +++ b/contracts/contracts/token/WOETH.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; import { ERC4626 } from "../../lib/openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; @@ -12,24 +12,63 @@ import { Initializable } from "../utils/Initializable.sol"; import { OETH } from "./OETH.sol"; /** - * @title OETH Token Contract + * @title Wrapped 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 the contract logic checks the actual underlying OETH token balance only once + * (either on a fresh contract creation or upgrade) and considering the WOETH supply and + * rebasingCreditsPerToken calculates the _adjuster. Once the adjuster is calculated any donations + * to the contract are ignored. The totalSupply (instead of querying OETH balance) works off of + * adjuster the current WOETH supply and rebasingCreditsPerToken. This makes WOETH value accrual + * completely follow OETH's value accrual. + * WOETH is safe to use in lending markets as the VualtCore's _rebase contains safeguards preventing + * any sudden large rebases. */ contract WOETH is ERC4626, Governable, Initializable { using SafeERC20 for IERC20; + /* This is a 1e27 adjustment constant that expresses the difference in exchange rate between + * OETH's rebase since inception (expressed with rebasingCreditsPerToken) and WOETH to OETH + * conversion. + * + * If WOETH and OETH are deployed at the same time, the value of adjuster is a neutral 1e27 + */ + uint256 public adjuster; + uint256[49] private __gap; - constructor( - ERC20 underlying_, - string memory name_, - string memory symbol_ - ) ERC20(name_, symbol_) ERC4626(underlying_) Governable() {} + // no need to set ERC20 name and symbol since they are overridden in WOETH & WOETHBase + constructor(ERC20 underlying_) ERC20("", "") ERC4626(underlying_) {} /** * @notice Enable OETH rebasing for this contract */ function initialize() external onlyGovernor initializer { OETH(address(asset())).rebaseOptIn(); + + initialize2(); + } + + /** + * @notice secondary initializer that newly deployed contracts will execute as part + * of primary initialize function and the existing contracts will have it called + * as a governance operation. + */ + function initialize2() public onlyGovernor { + require(adjuster == 0, "Initialize2 already called"); + + if (totalSupply() == 0) { + adjuster = 1e27; + } else { + adjuster = + (rebasingCreditsPerTokenHighres() * + ERC20(asset()).balanceOf(address(this))) / + totalSupply(); + } } function name() @@ -62,7 +101,38 @@ contract WOETH is ERC4626, Governable, Initializable { external onlyGovernor { - require(asset_ != address(asset()), "Cannot collect OETH"); + require(asset_ != address(asset()), "Cannot collect core asset"); IERC20(asset_).safeTransfer(governor(), amount_); } + + /// @inheritdoc ERC4626 + function convertToShares(uint256 assets) + public + view + virtual + override + returns (uint256 shares) + { + return (assets * rebasingCreditsPerTokenHighres()) / adjuster; + } + + /// @inheritdoc ERC4626 + function convertToAssets(uint256 shares) + public + view + virtual + override + returns (uint256 assets) + { + return (shares * adjuster) / rebasingCreditsPerTokenHighres(); + } + + /// @inheritdoc ERC4626 + function totalAssets() public view override returns (uint256) { + return (totalSupply() * adjuster) / rebasingCreditsPerTokenHighres(); + } + + function rebasingCreditsPerTokenHighres() internal view returns (uint256) { + return OETH(asset()).rebasingCreditsPerTokenHighres(); + } } diff --git a/contracts/contracts/token/WOETHBase.sol b/contracts/contracts/token/WOETHBase.sol index b640bcddac..2db348f1ec 100644 --- a/contracts/contracts/token/WOETHBase.sol +++ b/contracts/contracts/token/WOETHBase.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; import { WOETH } from "./WOETH.sol"; @@ -10,9 +10,7 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; */ contract WOETHBase is WOETH { - constructor(ERC20 underlying_) - WOETH(underlying_, "Wrapped Super OETH", "wsuperOETHb") - {} + constructor(ERC20 underlying_) WOETH(underlying_) {} function name() public view virtual override returns (string memory) { return "Wrapped Super OETH"; diff --git a/contracts/contracts/token/WOSonic.sol b/contracts/contracts/token/WOSonic.sol index cc97f2dce6..dc9c58de0f 100644 --- a/contracts/contracts/token/WOSonic.sol +++ b/contracts/contracts/token/WOSonic.sol @@ -1,41 +1,23 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; -import { ERC4626 } from "../../lib/openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { Governable } from "../governance/Governable.sol"; -import { Initializable } from "../utils/Initializable.sol"; -import { OSonic } from "./OSonic.sol"; +import { WOETH } from "./WOETH.sol"; /** * @title Wrapped Origin Sonic (wOS) token on Sonic * @author Origin Protocol Inc */ -contract WOSonic is ERC4626, Governable, Initializable { - using SafeERC20 for IERC20; - - constructor( - ERC20 underlying_, - string memory name_, - string memory symbol_ - ) ERC20(name_, symbol_) ERC4626(underlying_) Governable() {} - - /** - * @notice Enable Origin Sonic rebasing for this contract - */ - function initialize() external onlyGovernor initializer { - OSonic(address(asset())).rebaseOptIn(); - } +contract WOSonic is WOETH { + constructor(ERC20 underlying_) WOETH(underlying_) {} function name() public view virtual - override(ERC20, IERC20Metadata) + override(WOETH) returns (string memory) { return "Wrapped OS"; @@ -45,23 +27,9 @@ contract WOSonic is ERC4626, Governable, Initializable { public view virtual - override(ERC20, IERC20Metadata) + override(WOETH) returns (string memory) { return "wOS"; } - - /** - * @notice Transfer token to governor. Intended for recovering tokens stuck in - * contract, i.e. mistaken sends. Cannot transfer Origin S - * @param asset_ Address for the asset - * @param amount_ Amount of the asset to transfer - */ - function transferToken(address asset_, uint256 amount_) - external - onlyGovernor - { - require(asset_ != address(asset()), "Cannot collect OS"); - IERC20(asset_).safeTransfer(governor(), amount_); - } } diff --git a/contracts/contracts/token/WrappedOusd.sol b/contracts/contracts/token/WrappedOusd.sol index 7fe5d2b90a..67a349d7ba 100644 --- a/contracts/contracts/token/WrappedOusd.sol +++ b/contracts/contracts/token/WrappedOusd.sol @@ -1,36 +1,23 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; -import { ERC4626 } from "../../lib/openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { Governable } from "../governance/Governable.sol"; -import { Initializable } from "../utils/Initializable.sol"; -import { OUSD } from "./OUSD.sol"; +import { WOETH } from "./WOETH.sol"; -contract WrappedOusd is ERC4626, Governable, Initializable { - using SafeERC20 for IERC20; - - constructor( - ERC20 underlying_, - string memory name_, - string memory symbol_ - ) ERC20(name_, symbol_) ERC4626(underlying_) Governable() {} - - /** - * @notice Enable OUSD rebasing for this contract - */ - function initialize() external onlyGovernor initializer { - OUSD(address(asset())).rebaseOptIn(); - } +/** + * @title Wrapped OUSD Token Contract + * @author Origin Protocol Inc + */ +contract WrappedOusd is WOETH { + constructor(ERC20 underlying_) WOETH(underlying_) {} function name() public view - override(ERC20, IERC20Metadata) + virtual + override(WOETH) returns (string memory) { return "Wrapped OUSD"; @@ -39,23 +26,10 @@ contract WrappedOusd is ERC4626, Governable, Initializable { function symbol() public view - override(ERC20, IERC20Metadata) + virtual + override(WOETH) returns (string memory) { return "WOUSD"; } - - /** - * @notice Transfer token to governor. Intended for recovering tokens stuck in - * contract, i.e. mistaken sends. Cannot transfer OUSD - * @param asset_ Address for the asset - * @param amount_ Amount of the asset to transfer - */ - function transferToken(address asset_, uint256 amount_) - external - onlyGovernor - { - require(asset_ != address(asset()), "Cannot collect OUSD"); - IERC20(asset_).safeTransfer(governor(), amount_); - } } diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 892f05e810..0ab8c628e9 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1222,8 +1222,6 @@ const deployWOusd = async () => { const ousd = await ethers.getContract("OUSDProxy"); const dWrappedOusdImpl = await deployWithConfirmation("WrappedOusd", [ ousd.address, - "Wrapped OUSD IMPL", - "WOUSD IMPL", ]); await deployWithConfirmation("WrappedOUSDProxy"); const wousdProxy = await ethers.getContract("WrappedOUSDProxy"); @@ -1237,6 +1235,26 @@ const deployWOusd = async () => { ](dWrappedOusdImpl.address, governorAddr, initData); }; +const deployWOeth = async () => { + const { deployerAddr, governorAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + const oeth = await ethers.getContract("OETHProxy"); + const dWrappedOethImpl = await deployWithConfirmation("WOETH", [ + oeth.address, + ]); + 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); +}; + const deployOETHSwapper = async () => { const { deployerAddr, governorAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); @@ -1391,6 +1409,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 9d3193a229..b454ca6612 100644 --- a/contracts/deploy/mainnet/001_core.js +++ b/contracts/deploy/mainnet/001_core.js @@ -17,6 +17,7 @@ const { deployUniswapV3Pool, deployVaultValueChecker, deployWOusd, + deployWOeth, deployOETHSwapper, deployOUSDSwapper, } = require("../deployActions"); @@ -42,6 +43,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/deploy/mainnet/112_upgrade_woeth.js b/contracts/deploy/mainnet/112_upgrade_woeth.js new file mode 100644 index 0000000000..ec80567530 --- /dev/null +++ b/contracts/deploy/mainnet/112_upgrade_woeth.js @@ -0,0 +1,42 @@ +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "112_upgrade_woeth", + forceDeploy: false, + //forceSkip: true, + reduceQueueTime: true, + deployerIsProposer: false, + // proposalId: + }, + async ({ deployWithConfirmation, ethers }) => { + const cOETHProxy = await ethers.getContract("OETHProxy"); + const cWOETHProxy = await ethers.getContract("WOETHProxy"); + + const dWOETHImpl = await deployWithConfirmation("WOETH", [ + cOETHProxy.address, + ]); + + const cWOETH = await ethers.getContractAt("WOETH", cWOETHProxy.address); + + // Governance Actions + // ---------------- + return { + name: `Upgrade WOETH to a new implementation.`, + actions: [ + // 1. Upgrade WOETH + { + contract: cWOETHProxy, + signature: "upgradeTo(address)", + args: [dWOETHImpl.address], + }, + // 2. Run the second initializer + { + contract: cWOETH, + signature: "initialize2()", + args: [], + }, + ], + }; + } +); diff --git a/contracts/deploy/mainnet/132_upgrade_wousd.js b/contracts/deploy/mainnet/132_upgrade_wousd.js new file mode 100644 index 0000000000..ae11563857 --- /dev/null +++ b/contracts/deploy/mainnet/132_upgrade_wousd.js @@ -0,0 +1,45 @@ +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "132_upgrade_wousd", + forceDeploy: false, + //forceSkip: true, + reduceQueueTime: true, + deployerIsProposer: false, + // proposalId: + }, + async ({ deployWithConfirmation, ethers }) => { + const cOUSDProxy = await ethers.getContract("OUSDProxy"); + const cWOUSDProxy = await ethers.getContract("WrappedOUSDProxy"); + + const dWOUSDImpl = await deployWithConfirmation("WrappedOusd", [ + cOUSDProxy.address, + ]); + + const cWOUSD = await ethers.getContractAt( + "WrappedOusd", + cWOUSDProxy.address + ); + + // Governance Actions + // ---------------- + return { + name: `Upgrade WrappedOusd to a new implementation.`, + actions: [ + // 1. Upgrade WrappedOusd + { + contract: cWOUSDProxy, + signature: "upgradeTo(address)", + args: [dWOUSDImpl.address], + }, + // 2. Run the second initializer + { + contract: cWOUSD, + signature: "initialize2()", + args: [], + }, + ], + }; + } +); diff --git a/contracts/deploy/sonic/012_wrapped_sonic.js b/contracts/deploy/sonic/012_wrapped_sonic.js new file mode 100644 index 0000000000..4192227c20 --- /dev/null +++ b/contracts/deploy/sonic/012_wrapped_sonic.js @@ -0,0 +1,38 @@ +const { deployOnSonic } = require("../../utils/deploy-l2"); +const { deployWithConfirmation } = require("../../utils/deploy.js"); + +module.exports = deployOnSonic( + { + deployName: "012_wrapped_sonic", + }, + async ({ ethers }) => { + const cOSonicProxy = await ethers.getContract("OSonicProxy"); + const dWOSonicProxy = await ethers.getContract("WOSonicProxy"); + + const dWSonicImpl = await deployWithConfirmation("WOSonic", [ + cOSonicProxy.address, + ]); + + const cWOSonic = await ethers.getContractAt( + "WOSonic", + dWOSonicProxy.address + ); + + return { + actions: [ + // 1. Upgrade WOSonic + { + contract: dWOSonicProxy, + signature: "upgradeTo(address)", + args: [dWSonicImpl.address], + }, + // 2. Run the second initializer + { + contract: cWOSonic, + signature: "initialize2()", + args: [], + }, + ], + }; + } +); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index d9305082c2..56d5644031 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -80,6 +80,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", @@ -201,6 +204,7 @@ const simpleOETHFixture = deployments.createFixture(async () => { // OETH oethVault, oeth, + woeth, nativeStakingSSVStrategy, oethDripper, oethFixedRateDripper, @@ -230,12 +234,12 @@ const getVaultAndTokenConracts = async () => { ); const oeth = await ethers.getContractAt("OETH", oethProxy.address); - let woeth, woethProxy, mockNonRebasing, mockNonRebasingTwo; + let mockNonRebasing, mockNonRebasingTwo; - if (isFork) { - woethProxy = await ethers.getContract("WOETHProxy"); - woeth = await ethers.getContractAt("WOETH", woethProxy.address); - } else { + const woethProxy = await ethers.getContract("WOETHProxy"); + const woeth = await ethers.getContractAt("WOETH", woethProxy.address); + + if (!isFork) { // Mock contracts for testing rebase opt out mockNonRebasing = await ethers.getContract("MockNonRebasing"); await mockNonRebasing.setOUSD(ousd.address); @@ -991,7 +995,7 @@ 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 usds .connect(user) @@ -999,6 +1003,13 @@ const defaultFixture = deployments.createFixture(async () => { await vaultAndTokenConracts.vault .connect(user) .mint(usds.address, usdsUnits("100"), 0); + + // Fund WETH contract + await hardhatSetBalance(user.address, "500"); + await weth.connect(user).deposit({ value: oethUnits("100") }); + await weth + .connect(user) + .approve(vaultAndTokenConracts.oethVault.address, oethUnits("100")); } } return { diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index a7072204b6..4b614f3ca7 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -130,6 +130,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..3c0106c7f4 --- /dev/null +++ b/contracts/test/token/woeth.js @@ -0,0 +1,207 @@ +const { expect } = require("chai"); + +const { loadDefaultFixture } = require("../_fixture"); +const { oethUnits, usdsUnits, isFork } = require("../helpers"); +const { hardhatSetBalance } = require("../_fund"); + +describe("WOETH", function () { + if (isFork) { + this.timeout(0); + } + + let oeth, weth, woeth, oethVault, usds, matt, josh, governor; + + beforeEach(async () => { + const fixture = await loadDefaultFixture(); + oeth = fixture.oeth; + woeth = fixture.woeth; + oethVault = fixture.oethVault; + usds = fixture.usds; + matt = fixture.matt; + josh = fixture.josh; + 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); + + // 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.rebase(); + }; + + 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 () => { + 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"); + }); + + 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")); + }); + + it("should be allowed to deposit 0", async () => { + await woeth.connect(josh).deposit(oethUnits("0"), josh.address); + }); + + it("should be allowed to mint 0", async () => { + await woeth.connect(josh).mint(oethUnits("0"), josh.address); + }); + + it("should be allowed to redeem 0", async () => { + await woeth + .connect(josh) + .redeem(oethUnits("0"), josh.address, josh.address); + }); + + it("should be allowed to withdraw 0", async () => { + await woeth + .connect(josh) + .withdraw(oethUnits("0"), josh.address, josh.address); + }); + }); + + 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 increaseOETHSupplyAndRebase(oethUnits("200")); + + 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(await woeth.totalAssets()).to.equal("0"); + 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 usds.connect(matt).transfer(woeth.address, usdsUnits("2")); + await expect(woeth).to.have.a.balanceOf("2", usds); + await expect(governor).to.have.a.balanceOf("1000", usds); + await woeth.connect(governor).transferToken(usds.address, usdsUnits("2")); + await expect(woeth).to.have.a.balanceOf("0", usds); + await expect(governor).to.have.a.balanceOf("1002", usds); + }); + 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 core asset"); + }); + 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"); + }); + }); +}); 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..cf070e77e1 --- /dev/null +++ b/contracts/test/token/woeth.mainnet.fork-test.js @@ -0,0 +1,338 @@ +const { expect } = require("chai"); +const { BigNumber } = require("ethers"); + +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 have correct name and symbol and adjuster", async () => { + const { woeth } = fixture; + + expect(await woeth.name()).to.equal("Wrapped OETH"); + expect(await woeth.symbol()).to.equal("wOETH"); + expect(await woeth.adjuster()).to.be.gt(0); + }); + + 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, oeth, oethVault, woeth, domen, josh } = fixture; + + // Mint some WOETH + const initialDeposit = oethUnits("50"); + await woeth.connect(domen).deposit(initialDeposit, domen.address); + const initialOethBalance = await oeth.balanceOf(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(); + const oethBalanceAfter = await oeth.balanceOf(domen.address); + 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 + + // 1e18 denominated + const oethRateIncrease = oethBalanceAfter + .sub(initialOethBalance) + .mul(BigNumber.from("1000000000000000000")) + .div(initialOethBalance); + const woethRateIncrease = assetTransfered + .sub(initialDeposit) + .mul(BigNumber.from("1000000000000000000")) + .div(initialDeposit); + + // 1-2 wei rounding error might be needed in the future here + await expect(oethRateIncrease).to.equal(woethRateIncrease); + + await expect(assetTransfered > initialDeposit); + await expect(burnedShares).to.be.approxEqual( + await woeth.convertToShares(assetTransfered) + ); + await expect(domen).to.have.a.balanceOf("0", woeth); + }); + }); +}); + +// we mustn't use the fixture above tests use +describe("ForkTest: wOETH redeem balances", function () { + // manually test these values locally with the fork set to 22116006 + it.skip("upgrade of WOETH shouldn't change WOETH balances, or redemption amounts", async () => { + const { woeth } = await simpleOETHFixture(); + + // the values pulled from the mainnet at block number 22116006 before 112_upgrade_woeth + // written down is the account's WOETH balance, and how much OETH one would receive + // when redeeming the whole balance amount. It is important that the upgrade doesn't + // affect both of the numbers. + const accountBalances = { + "0xdCa0A2341ed5438E06B9982243808A76B9ADD6d0": { + woethBalance: BigNumber.from("16231824385055731314284"), + oethRedeemAmount: BigNumber.from("18229274520877989755302"), + }, + "0xC460B0b6c9b578A4Cb93F99A691e16dB96Ee5833": { + woethBalance: BigNumber.from("575896531839923556165"), + oethRedeemAmount: BigNumber.from("646765004690227456060"), + }, + "0x8a9D46d28003673Cd4FE7a56EcFCFA2BE6372e64": { + woethBalance: BigNumber.from("182355401624955452064"), + oethRedeemAmount: BigNumber.from("204795628496744585951"), + }, + "0x66ceac5EE8F093059C4BC9628C06e63076505B15": { + woethBalance: BigNumber.from("934212489717768182"), + oethRedeemAmount: BigNumber.from("1049174481679166505"), + }, + "0xc407d71801610E5023f2caC3F691FAa09959E5e9": { + woethBalance: BigNumber.from("1824215988713080"), + oethRedeemAmount: BigNumber.from("2048699718205546"), + }, + }; + + for (const account of Object.keys(accountBalances)) { + expect(accountBalances[account].woethBalance).to.equal( + await woeth.balanceOf(account) + ); + const previewRedeemAmount = await woeth.previewRedeem( + accountBalances[account].woethBalance + ); + + // allow 1 WEI difference for rounding errors + expect(accountBalances[account].oethRedeemAmount) + .to.gte(previewRedeemAmount.sub(1)) + .to.lte(previewRedeemAmount.add(1)); + } + }); +}); diff --git a/contracts/test/token/wos.sonic.fork-test.js b/contracts/test/token/wos.sonic.fork-test.js new file mode 100644 index 0000000000..bd690676d0 --- /dev/null +++ b/contracts/test/token/wos.sonic.fork-test.js @@ -0,0 +1,21 @@ +const { createFixtureLoader } = require("../_fixture"); +const { defaultSonicFixture } = require("../_fixture-sonic"); +const { expect } = require("chai"); + +const sonicFixture = createFixtureLoader(defaultSonicFixture); + +describe("ForkTest: Wrapped Origin Sonic Token", function () { + let fixture; + beforeEach(async () => { + fixture = await sonicFixture(); + }); + + it("Should have right config", async () => { + const { wOSonic } = fixture; + + expect(await wOSonic.decimals()).to.equal(18); + expect(await wOSonic.symbol()).to.equal("wOS"); + expect(await wOSonic.name()).to.equal("Wrapped OS"); + expect(await wOSonic.adjuster()).to.be.gt(0); + }); +}); diff --git a/contracts/test/token/wousd.js b/contracts/test/token/wousd.js index 805d50c4e4..6f20b8fb91 100644 --- a/contracts/test/token/wousd.js +++ b/contracts/test/token/wousd.js @@ -7,7 +7,6 @@ describe("WOUSD", function () { if (isFork) { this.timeout(0); } - let ousd, wousd, vault, usds, matt, josh, governor; beforeEach(async () => { @@ -20,31 +19,39 @@ describe("WOUSD", function () { josh = fixture.josh; governor = fixture.governor; - // Josh wraps 50 + /* Matt and Josh start out with 100 OUSD + * Josh wraps 50 OUSD to WOETH + */ await ousd.connect(josh).approve(wousd.address, ousdUnits("1000")); await wousd.connect(josh).deposit(ousdUnits("50"), josh.address); // Matt gives money to wOUSD, which counts as yield and changes the effective price of WOUSD + // 1 WOUSD will be worth 2 OUSD - await ousd.connect(matt).transfer(wousd.address, ousdUnits("50")); + await increaseOUSDSupplyAndRebase(await ousd.totalSupply()); }); + const increaseOUSDSupplyAndRebase = async (usdsAmount) => { + await usds.connect(matt).transfer(vault.address, usdsAmount); + await vault.rebase(); + }; + describe("Funds in, Funds out", async () => { it("should deposit at the correct ratio", async () => { await wousd.connect(josh).deposit(ousdUnits("50"), josh.address); await expect(josh).to.have.a.balanceOf("75", wousd); - await expect(josh).to.have.a.balanceOf("0", ousd); + await expect(josh).to.have.a.balanceOf("50", ousd); }); it("should withdraw at the correct ratio", async () => { await wousd .connect(josh) .withdraw(ousdUnits("50"), josh.address, josh.address); await expect(josh).to.have.a.balanceOf("25", wousd); - await expect(josh).to.have.a.balanceOf("100", ousd); + await expect(josh).to.have.a.balanceOf("150", ousd); }); it("should mint at the correct ratio", async () => { await wousd.connect(josh).mint(ousdUnits("25"), josh.address); await expect(josh).to.have.a.balanceOf("75", wousd); - await expect(josh).to.have.a.balanceOf("0", ousd); + await expect(josh).to.have.a.balanceOf("50", ousd); }); it("should redeem at the correct ratio", async () => { await expect(josh).to.have.a.balanceOf("50", wousd); @@ -52,14 +59,14 @@ describe("WOUSD", function () { .connect(josh) .redeem(ousdUnits("50"), josh.address, josh.address); await expect(josh).to.have.a.balanceOf("0", wousd); - await expect(josh).to.have.a.balanceOf("150", ousd); + await expect(josh).to.have.a.balanceOf("200", ousd); }); }); describe("Collects Rebase", async () => { it("should increase with an OUSD rebase", async () => { await expect(wousd).to.have.approxBalanceOf("100", ousd); - await usds.connect(josh).transfer(vault.address, usdsUnits("100")); + await usds.connect(josh).transfer(vault.address, usdsUnits("200")); await vault.rebase(); await expect(wousd).to.have.approxBalanceOf("150", ousd); }); @@ -85,7 +92,7 @@ describe("WOUSD", function () { it("should not allow a governor to collect OUSD", async () => { await expect( wousd.connect(governor).transferToken(ousd.address, ousdUnits("2")) - ).to.be.revertedWith("Cannot collect OUSD"); + ).to.be.revertedWith("Cannot collect core asset"); }); it("should not allow a non governor to recover tokens ", async () => { await expect( @@ -99,11 +106,7 @@ describe("WOUSD", function () { // Do upgrade const cWrappedOUSDProxy = await ethers.getContract("WrappedOUSDProxy"); const factory = await ethers.getContractFactory("MockLimitedWrappedOusd"); - const dNewImpl = await factory.deploy( - ousd.address, - "WOUSD", - "Wrapped OUSD" - ); + const dNewImpl = await factory.deploy(ousd.address); await cWrappedOUSDProxy.connect(governor).upgradeTo(dNewImpl.address); // Test basics diff --git a/contracts/test/token/wousd.mainnet.fork-test.js b/contracts/test/token/wousd.mainnet.fork-test.js new file mode 100644 index 0000000000..f9daff98f7 --- /dev/null +++ b/contracts/test/token/wousd.mainnet.fork-test.js @@ -0,0 +1,24 @@ +const { expect } = require("chai"); + +const { loadDefaultFixture } = require("./../_fixture"); +const { isCI } = require("./../helpers"); + +describe("ForkTest: wOUSD", function () { + this.timeout(0); + + // Retry up to 3 times on CI + this.retries(isCI ? 3 : 0); + + let fixture; + beforeEach(async () => { + fixture = await loadDefaultFixture(); + }); + + it("Should have correct name, symbol and adjuster", async () => { + const { wousd } = fixture; + + expect(await wousd.name()).to.equal("Wrapped OUSD"); + expect(await wousd.symbol()).to.equal("WOUSD"); + expect(await wousd.adjuster()).to.be.gt(0); + }); +});