From a21653b0cd34e86cbe2e2c62bf53563b7d17a074 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:44:56 +0530 Subject: [PATCH 01/13] Simplify Mint & Redeem --- contracts/contracts/interfaces/IVault.sol | 8 ++ contracts/contracts/vault/OETHVaultCore.sol | 115 +++++++++++++++++- contracts/contracts/vault/VaultCore.sol | 14 +-- contracts/deploy/001_core.js | 24 ++-- contracts/deploy/086_simplified_oeth_vault.js | 52 ++++++++ contracts/test/vault/oeth-vault.fork-test.js | 29 +++++ 6 files changed, 216 insertions(+), 26 deletions(-) create mode 100644 contracts/deploy/086_simplified_oeth_vault.js diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index c27d85e414..1650382ff7 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -200,4 +200,12 @@ interface IVault { function netOusdMintedForStrategy() external view returns (int256); function weth() external view returns (address); + + function cacheWETHAssetIndex() external; + + function wethAssetIndex() external view returns (uint256); + + function initialize(address, address) external; + + function setAdminImpl(address) external; } diff --git a/contracts/contracts/vault/OETHVaultCore.sol b/contracts/contracts/vault/OETHVaultCore.sol index 7ce7d90d9d..b98d796142 100644 --- a/contracts/contracts/vault/OETHVaultCore.sol +++ b/contracts/contracts/vault/OETHVaultCore.sol @@ -1,26 +1,137 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import { StableMath } from "../utils/StableMath.sol"; import { VaultCore } from "./VaultCore.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IStrategy } from "../interfaces/IStrategy.sol"; + /** * @title OETH VaultCore Contract * @author Origin Protocol Inc */ contract OETHVaultCore is VaultCore { + using SafeERC20 for IERC20; + using StableMath for uint256; + address public immutable weth; + uint256 public wethAssetIndex; constructor(address _weth) { weth = _weth; } + /** + * @dev Caches WETH's index in `allAssets` variable. + * Reduces gas usage by redeem by caching that. + */ + function cacheWETHAssetIndex() external onlyGovernor { + uint256 assetCount = allAssets.length; + for (uint256 i = 0; i < assetCount; ++i) { + if (allAssets[i] == weth) { + wethAssetIndex = i; + break; + } + } + + require(allAssets[wethAssetIndex] == weth, "Invalid WETH Asset Index"); + } + // @inheritdoc VaultCore function mint( address _asset, uint256 _amount, - uint256 _minimumOusdAmount + uint256 ) external virtual override whenNotCapitalPaused nonReentrant { require(_asset == weth, "Unsupported asset for minting"); - _mint(_asset, _amount, _minimumOusdAmount); + require(_amount > 0, "Amount must be greater than 0"); + + emit Mint(msg.sender, _amount); + + // Rebase must happen before any transfers occur. + if (!rebasePaused && _amount >= rebaseThreshold) { + _rebase(); + } + + // Mint oTokens + oUSD.mint(msg.sender, _amount); + + // Transfer the deposited coins to the vault + IERC20(_asset).safeTransferFrom(msg.sender, address(this), _amount); + + // Auto-allocate if necessary + if (_amount >= autoAllocateThreshold) { + _allocate(); + } + } + + // @inheritdoc VaultCore + function _calculateRedeemOutputs(uint256 _amount) + internal + view + virtual + override + returns (uint256[] memory outputs) + { + // Overrides `VaultCore._calculateRedeemOutputs` to redeem with only + // WETH instead of LST-mix and gets rid of fee. Doesn't change the + // function signature for backward compatibility + + // Ensure that the WETH index is cached + uint256 wethIndex = wethAssetIndex; + require(allAssets[wethIndex] == weth, "WETH Asset index not cached"); + + outputs = new uint256[](allAssets.length); + outputs[wethIndex] = _amount; + } + + // @inheritdoc VaultCore + function _redeem(uint256 _amount, uint256) internal virtual override { + // Override `VaultCore._redeem` to simplify it. Gets rid of oracle + // usage and looping through all assets for LST-mix redeem. Instead + // does a simple WETH-only redeem with zero fee. + emit Redeem(msg.sender, _amount); + + if (IERC20(weth).balanceOf(address(this)) >= _amount) { + // Use Vault funds first if sufficient + IERC20(weth).safeTransfer(msg.sender, _amount); + } else { + address strategyAddr = assetDefaultStrategies[weth]; + if (strategyAddr != address(0)) { + // Nothing in Vault, but something in Strategy, send from there + IStrategy strategy = IStrategy(strategyAddr); + strategy.withdraw(msg.sender, weth, _amount); + } else { + // Cant find funds anywhere + revert("Liquidity error"); + } + } + + // Burn OETH from user + oUSD.burn(msg.sender, _amount); + + // Until we can prove that we won't affect the prices of our assets + // by withdrawing them, this should be here. + // It's possible that a strategy was off on its asset total, perhaps + // a reward token sold for more or for less than anticipated. + uint256 totalUnits = 0; + if (_amount >= rebaseThreshold && !rebasePaused) { + totalUnits = _rebase(); + } else { + totalUnits = _totalValue(); + } + + // Check that the OTokens are backed by enough assets + if (maxSupplyDiff > 0) { + // Allow a max difference of maxSupplyDiff% between + // backing assets value and OETH total supply + uint256 diff = oUSD.totalSupply().divPrecisely(totalUnits); + require( + (diff > 1e18 ? diff - 1e18 : 1e18 - diff) <= maxSupplyDiff, + "Backing supply liquidity error" + ); + } } } diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index b019a747d5..ab47c03177 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -63,14 +63,6 @@ contract VaultCore is VaultInitializer { uint256 _amount, uint256 _minimumOusdAmount ) external virtual whenNotCapitalPaused nonReentrant { - _mint(_asset, _amount, _minimumOusdAmount); - } - - function _mint( - address _asset, - uint256 _amount, - uint256 _minimumOusdAmount - ) internal { require(assets[_asset].isSupported, "Asset is not supported"); require(_amount > 0, "Amount must be greater than 0"); @@ -158,7 +150,10 @@ contract VaultCore is VaultInitializer { * @param _amount Amount of OTokens to burn * @param _minimumUnitAmount Minimum stablecoin units to receive in return */ - function _redeem(uint256 _amount, uint256 _minimumUnitAmount) internal { + function _redeem(uint256 _amount, uint256 _minimumUnitAmount) + internal + virtual + { // Calculate redemption outputs uint256[] memory outputs = _calculateRedeemOutputs(_amount); @@ -503,6 +498,7 @@ contract VaultCore is VaultInitializer { function _calculateRedeemOutputs(uint256 _amount) internal view + virtual returns (uint256[] memory outputs) { // We always give out coins in proportion to how many we have, diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index 2533038be8..758476ad90 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -418,12 +418,6 @@ const configureVault = async () => { // Signers const sGovernor = await ethers.provider.getSigner(governorAddr); - await ethers.getContractAt( - "VaultInitializer", - ( - await ethers.getContract("VaultProxy") - ).address - ); const cVault = await ethers.getContractAt( "VaultAdmin", ( @@ -461,14 +455,8 @@ const configureOETHVault = async () => { // Signers const sGovernor = await ethers.provider.getSigner(governorAddr); - await ethers.getContractAt( - "VaultInitializer", - ( - await ethers.getContract("OETHVaultProxy") - ).address - ); const cVault = await ethers.getContractAt( - "VaultAdmin", + "IVault", ( await ethers.getContract("OETHVaultProxy") ).address @@ -487,6 +475,12 @@ const configureOETHVault = async () => { await withConfirmation( cVault.connect(sGovernor).setStrategistAddr(strategistAddr) ); + + // Cache WETH asset address + await withConfirmation(cVault.connect(sGovernor).cacheWETHAssetIndex()); + + // Redeem fee to 0 + await withConfirmation(cVault.connect(sGovernor).setRedeemFeeBps(0)); }; /** @@ -804,7 +798,7 @@ const deployCore = async () => { const cVaultProxy = await ethers.getContract("VaultProxy"); const cOUSD = await ethers.getContractAt("OUSD", cOUSDProxy.address); const cOracleRouter = await ethers.getContract("OracleRouter"); - const cVault = await ethers.getContractAt("Vault", cVaultProxy.address); + const cVault = await ethers.getContractAt("IVault", cVaultProxy.address); const cOETHProxy = await ethers.getContract("OETHProxy"); const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); @@ -813,7 +807,7 @@ const deployCore = async () => { ? await ethers.getContract("OETHOracleRouter") : cOracleRouter; const cOETHVault = await ethers.getContractAt( - "Vault", + "IVault", cOETHVaultProxy.address ); diff --git a/contracts/deploy/086_simplified_oeth_vault.js b/contracts/deploy/086_simplified_oeth_vault.js new file mode 100644 index 0000000000..fe56905975 --- /dev/null +++ b/contracts/deploy/086_simplified_oeth_vault.js @@ -0,0 +1,52 @@ +const addresses = require("../utils/addresses"); +const { + deploymentWithGovernanceProposal, + deployWithConfirmation, +} = require("../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "086_simplified_oeth_vault", + forceDeploy: false, + // forceSkip: true, + // onlyOnFork: true, // this is only executed in forked environment + // reduceQueueTime: true, // just to solve the issue of later active proposals failing + proposalId: "", + }, + async ({ ethers }) => { + const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); + + // Deploy VaultCore implementation + await deployWithConfirmation("OETHVaultCore", [addresses.mainnet.WETH]); + const dVaultImpl = await ethers.getContract("OETHVaultCore"); + + const cOETHVault = await ethers.getContractAt( + "IVault", + addresses.mainnet.OETHVaultProxy + ); + + return { + name: "Simplified OETH mint and redeem\n\ + \n\ + Part of simplified OETH proposal. Trims down mint and redeem complexity on OETH Vault. Set redeem fees to zero. \ + ", + actions: [ + { + contract: cOETHVaultProxy, + signature: "upgradeTo(address)", + args: [dVaultImpl.address], + }, + { + contract: cOETHVault, + signature: "setRedeemFeeBps(uint256)", + args: [0], + }, + { + contract: cOETHVault, + signature: "cacheWETHAssetIndex()", + args: [], + }, + ], + }; + } +); diff --git a/contracts/test/vault/oeth-vault.fork-test.js b/contracts/test/vault/oeth-vault.fork-test.js index 75f1036c36..a1e99f86e2 100644 --- a/contracts/test/vault/oeth-vault.fork-test.js +++ b/contracts/test/vault/oeth-vault.fork-test.js @@ -53,6 +53,14 @@ describe("ForkTest: OETH Vault", function () { ); } }); + + it("Should have correct WETH asset index cached", async () => { + const { oethVault, weth } = fixture; + const index = await oethVault.wethAssetIndex(); + const assets = await oethVault.getAllAssets(); + + expect(assets[index]).to.equal(weth.address); + }); }); describe("user operations", () => { @@ -92,6 +100,27 @@ describe("ForkTest: OETH Vault", function () { } }); + it("should have no redeem fee", async () => { + const { oethVault } = fixture; + + expect(await oethVault.redeemFeeBps()).to.equal(0); + }); + + it("should return only WETH in redeem calculations", async () => { + const { oethVault } = fixture; + + const output = await oethVault.calculateRedeemOutputs(oethUnits("123")); + const index = await oethVault.wethAssetIndex(); + + expect(output[index]).to.equal(oethUnits("123")); + + output.map((x, i) => { + if (i !== index.toNumber()) { + expect(x).to.equal("0"); + } + }); + }); + it("should partially redeem", async () => { const { oeth, oethVault } = fixture; From 0948528aa6d0b1a0b5f724f67cfc1a384bbd8fa5 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:55:40 +0530 Subject: [PATCH 02/13] Slither --- contracts/contracts/vault/VaultStorage.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index 066754e38f..d9beb4fe7a 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -72,6 +72,7 @@ contract VaultStorage is Initializable, Governable { // slither-disable-next-line uninitialized-state mapping(address => Asset) internal assets; /// @dev list of all assets supported by the vault. + // slither-disable-next-line uninitialized-state address[] internal allAssets; // Strategies approved for use by the Vault @@ -121,9 +122,11 @@ contract VaultStorage is Initializable, Governable { /// @notice Mapping of asset address to the Strategy that they should automatically // be allocated to + // slither-disable-next-line uninitialized-state mapping(address => address) public assetDefaultStrategies; /// @notice Max difference between total supply and total value of assets. 18 decimals. + // slither-disable-next-line uninitialized-state uint256 public maxSupplyDiff; /// @notice Trustee contract that can collect a percentage of yield From f9081173bfee618a25e086cb60b70d6c72448fcb Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:28:42 +0530 Subject: [PATCH 03/13] Add unit tests for OETH Vault --- contracts/contracts/mocks/MockOETHVault.sol | 23 +++ contracts/contracts/mocks/MockStrategy.sol | 48 +++++ contracts/contracts/vault/VaultStorage.sol | 3 + contracts/deploy/001_core.js | 14 ++ contracts/test/vault/oeth-vault.js | 217 ++++++++++++++++++++ 5 files changed, 305 insertions(+) create mode 100644 contracts/contracts/mocks/MockOETHVault.sol create mode 100644 contracts/contracts/mocks/MockStrategy.sol create mode 100644 contracts/test/vault/oeth-vault.js diff --git a/contracts/contracts/mocks/MockOETHVault.sol b/contracts/contracts/mocks/MockOETHVault.sol new file mode 100644 index 0000000000..78cece7110 --- /dev/null +++ b/contracts/contracts/mocks/MockOETHVault.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { OETHVaultCore } from "../vault/OETHVaultCore.sol"; +import { StableMath } from "../utils/StableMath.sol"; +import "../utils/Helpers.sol"; + +contract MockOETHVault is OETHVaultCore { + using StableMath for uint256; + + constructor(address _weth) OETHVaultCore(_weth) {} + + function supportAsset(address asset) external { + assets[asset] = Asset({ + isSupported: true, + unitConversion: UnitConversion(0), + decimals: 18, + allowedOracleSlippageBps: 0 + }); + + allAssets.push(asset); + } +} diff --git a/contracts/contracts/mocks/MockStrategy.sol b/contracts/contracts/mocks/MockStrategy.sol new file mode 100644 index 0000000000..6e068944d0 --- /dev/null +++ b/contracts/contracts/mocks/MockStrategy.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract MockStrategy { + address[] public assets; + + constructor() {} + + function deposit(address asset, uint256 amount) external {} + + function depositAll() external {} + + function withdraw( + address recipient, + address asset, + uint256 amount + ) external { + IERC20(asset).transfer(recipient, amount); + } + + function withdrawAll() external { + require(false, "Not implemented"); + } + + function checkBalance(address asset) + external + view + returns (uint256 balance) + { + balance = IERC20(asset).balanceOf(address(this)); + } + + function supportsAsset(address) external view returns (bool) { + return true; + } + + function collectRewardTokens() external {} + + function getRewardTokenAddresses() + external + view + returns (address[] memory) + { + return new address[](0); + } +} diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index d9beb4fe7a..196e537796 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -163,6 +163,9 @@ contract VaultStorage is Initializable, Governable { } SwapConfig internal swapConfig = SwapConfig(address(0), 0); + // For future use + uint256[50] private __gap; + /** * @notice set the implementation for the admin, this needs to be in a base class else we cannot set it * @param newImpl address of the implementation diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index 758476ad90..f8b99f101e 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -481,6 +481,20 @@ const configureOETHVault = async () => { // Redeem fee to 0 await withConfirmation(cVault.connect(sGovernor).setRedeemFeeBps(0)); + + // Allocate threshold + await withConfirmation( + cVault + .connect(sGovernor) + .setAutoAllocateThreshold(ethers.utils.parseUnits("25", 18)) + ); + + // Rebase threshold + await withConfirmation( + cVault + .connect(sGovernor) + .setAutoAllocateThreshold(ethers.utils.parseUnits("5", 18)) + ); }; /** diff --git a/contracts/test/vault/oeth-vault.js b/contracts/test/vault/oeth-vault.js new file mode 100644 index 0000000000..51bdc1e214 --- /dev/null +++ b/contracts/test/vault/oeth-vault.js @@ -0,0 +1,217 @@ +const { expect } = require("chai"); +const hre = require("hardhat"); + +const { createFixtureLoader, oethDefaultFixture } = require("../_fixture"); +const { parseUnits } = require("ethers/lib/utils"); +const { deployWithConfirmation } = require("../../utils/deploy"); +const { oethUnits } = require("../helpers"); + +const oethFixture = createFixtureLoader(oethDefaultFixture); + +describe("OETH Vault", function () { + let fixture; + beforeEach(async () => { + fixture = await oethFixture(); + }); + + describe("Mint", () => { + it("should mint with WETH", async () => { + const { oethVault, weth, josh } = fixture; + + const amount = parseUnits("1", 18); + const minOeth = parseUnits("0.8", 18); + + await weth.connect(josh).approve(oethVault.address, amount); + + const tx = await oethVault + .connect(josh) + .mint(weth.address, amount, minOeth); + + await expect(tx) + .to.emit(oethVault, "Mint") + .withArgs(josh.address, amount); + }); + + it("should not mint with any other asset", async () => { + const { oethVault, frxETH, stETH, reth, josh } = fixture; + + const amount = parseUnits("1", 18); + const minOeth = parseUnits("0.8", 18); + + for (const asset of [frxETH, stETH, reth]) { + await asset.connect(josh).approve(oethVault.address, amount); + const tx = oethVault.connect(josh).mint(asset.address, amount, minOeth); + + await expect(tx).to.be.revertedWith("Unsupported asset for minting"); + } + }); + + it("should revert if mint amount is zero", async () => { + const { oethVault, weth, josh } = fixture; + + const tx = oethVault.connect(josh).mint(weth.address, "0", "0"); + await expect(tx).to.be.revertedWith("Amount must be greater than 0"); + }); + + it("should revert if capital is paused", async () => { + const { oethVault, weth, governor } = fixture; + + await oethVault.connect(governor).pauseCapital(); + expect(await oethVault.capitalPaused()).to.equal(true); + + const tx = oethVault + .connect(governor) + .mint(weth.address, oethUnits("10"), "0"); + await expect(tx).to.be.revertedWith("Capital paused"); + }); + + it("Should allocate if beyond allocate threshold", async () => { + const { oethVault, weth, domen, governor } = fixture; + + const mockStrategy = await deployWithConfirmation("MockStrategy"); + await oethVault.connect(governor).approveStrategy(mockStrategy.address); + await oethVault + .connect(governor) + .setAssetDefaultStrategy(weth.address, mockStrategy.address); + + // Mint some WETH + await weth.connect(domen).approve(oethVault.address, oethUnits("10000")); + await oethVault.connect(domen).mint(weth.address, oethUnits("100"), "0"); + + expect(await weth.balanceOf(mockStrategy.address)).to.eq( + oethUnits("100") + ); + }); + }); + + describe("Redeem", () => { + it("should return only WETH in redeem calculations", async () => { + const { oethVault, weth } = fixture; + + const outputs = await oethVault.calculateRedeemOutputs( + oethUnits("1234.43") + ); + + const assets = await oethVault.getAllAssets(); + + expect(assets.length).to.equal(outputs.length); + + for (let i = 0; i < assets.length; i++) { + expect(outputs[i]).to.equal( + assets[i] == weth.address ? oethUnits("1234.43") : "0" + ); + } + }); + + it("should revert if WETH index isn't cached", async () => { + const { frxETH, weth } = fixture; + + await deployWithConfirmation("MockOETHVault", [weth.address]); + const mockVault = await hre.ethers.getContract("MockOETHVault"); + + await mockVault.supportAsset(frxETH.address); + + const tx = mockVault.calculateRedeemOutputs(oethUnits("12343")); + await expect(tx).to.be.revertedWith("WETH Asset index not cached"); + }); + + it("should update total supply correctly", async () => { + const { oethVault, oeth, weth, daniel } = fixture; + await oethVault.connect(daniel).mint(weth.address, oethUnits("10"), "0"); + + const userBalanceBefore = await weth.balanceOf(daniel.address); + const vaultBalanceBefore = await weth.balanceOf(oethVault.address); + const supplyBefore = await oeth.totalSupply(); + + await oethVault.connect(daniel).redeem(oethUnits("10"), "0"); + + const userBalanceAfter = await weth.balanceOf(daniel.address); + const vaultBalanceAfter = await weth.balanceOf(oethVault.address); + const supplyAfter = await oeth.totalSupply(); + + // Make sure the total supply went down + expect(userBalanceAfter.sub(userBalanceBefore)).to.eq(oethUnits("10")); + expect(vaultBalanceBefore.sub(vaultBalanceAfter)).to.eq(oethUnits("10")); + expect(supplyBefore.sub(supplyAfter)).to.eq(oethUnits("10")); + }); + + it("Should withdraw from strategy if necessary", async () => { + const { oethVault, weth, domen, governor } = fixture; + + const mockStrategy = await deployWithConfirmation("MockStrategy"); + await oethVault.connect(governor).approveStrategy(mockStrategy.address); + await oethVault + .connect(governor) + .setAssetDefaultStrategy(weth.address, mockStrategy.address); + + // Mint some WETH + await weth.connect(domen).approve(oethVault.address, oethUnits("10000")); + await oethVault.connect(domen).mint(weth.address, oethUnits("100"), "0"); + + // Mint a small amount that won't get allocated to the strategy + await oethVault.connect(domen).mint(weth.address, oethUnits("1.23"), "0"); + + const vaultBalanceBefore = await weth.balanceOf(oethVault.address); + const stratBalanceBefore = await weth.balanceOf(mockStrategy.address); + const userBalanceBefore = await weth.balanceOf(domen.address); + + // Withdraw something more than what the Vault holds + await oethVault.connect(domen).redeem(oethUnits("12.55"), "0"); + + const vaultBalanceAfter = await weth.balanceOf(oethVault.address); + const stratBalanceAfter = await weth.balanceOf(mockStrategy.address); + const userBalanceAfter = await weth.balanceOf(domen.address); + + expect(userBalanceAfter.sub(userBalanceBefore)).to.eq(oethUnits("12.55")); + + expect(stratBalanceBefore.sub(stratBalanceAfter)).to.eq( + oethUnits("12.55") + ); + + expect(vaultBalanceBefore).to.eq(vaultBalanceAfter); + }); + + it("should revert on liquidity error", async () => { + const { oethVault, weth, daniel } = fixture; + const tx = oethVault + .connect(daniel) + .redeem(weth.address, oethUnits("1023232323232"), "0"); + await expect(tx).to.be.revertedWith("Liquidity error"); + }); + }); + + describe("Config", () => { + it("should allow caching WETH index", async () => { + const { oethVault, weth, governor } = fixture; + + await oethVault.connect(governor).cacheWETHAssetIndex(); + + const index = (await oethVault.wethAssetIndex()).toNumber(); + + const assets = await oethVault.getAllAssets(); + + expect(assets[index]).to.equal(weth.address); + }); + + it("should not allow anyone other than Governor to change cached index", async () => { + const { oethVault, strategist } = fixture; + + const tx = oethVault.connect(strategist).cacheWETHAssetIndex(); + await expect(tx).to.be.revertedWith("Caller is not the Governor"); + }); + + it("should revert if WETH is not an supported asset", async () => { + const { frxETH, weth } = fixture; + const { deployerAddr } = await hre.getNamedAccounts(); + const sDeployer = hre.ethers.provider.getSigner(deployerAddr); + + await deployWithConfirmation("MockOETHVault", [weth.address]); + const mockVault = await hre.ethers.getContract("MockOETHVault"); + + await mockVault.supportAsset(frxETH.address); + + const tx = mockVault.connect(sDeployer).cacheWETHAssetIndex(); + await expect(tx).to.be.revertedWith("Invalid WETH Asset Index"); + }); + }); +}); From 45c5a2b70ccf83866a00c959cabad9402ea64f64 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:55:29 +0530 Subject: [PATCH 04/13] Fix failing test --- contracts/test/vault/oeth-vault.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/test/vault/oeth-vault.js b/contracts/test/vault/oeth-vault.js index 51bdc1e214..04f2121c9c 100644 --- a/contracts/test/vault/oeth-vault.js +++ b/contracts/test/vault/oeth-vault.js @@ -172,10 +172,10 @@ describe("OETH Vault", function () { }); it("should revert on liquidity error", async () => { - const { oethVault, weth, daniel } = fixture; + const { oethVault, daniel } = fixture; const tx = oethVault .connect(daniel) - .redeem(weth.address, oethUnits("1023232323232"), "0"); + .redeem(oethUnits("1023232323232"), "0"); await expect(tx).to.be.revertedWith("Liquidity error"); }); }); From fe0665ddbf25e9fea2e67b675e0eea94d0178c73 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 21 Mar 2024 23:29:34 +0530 Subject: [PATCH 05/13] 0.1% redeem fee and code tweaks --- contracts/contracts/vault/OETHVaultCore.sol | 72 ++++++++++--------- contracts/contracts/vault/VaultCore.sol | 14 +++- ..._vault.js => 089_simplified_oeth_vault.js} | 7 +- contracts/test/vault/oeth-vault.fork-test.js | 16 +---- 4 files changed, 57 insertions(+), 52 deletions(-) rename contracts/deploy/{086_simplified_oeth_vault.js => 089_simplified_oeth_vault.js} (88%) diff --git a/contracts/contracts/vault/OETHVaultCore.sol b/contracts/contracts/vault/OETHVaultCore.sol index b98d796142..3d2d0971f2 100644 --- a/contracts/contracts/vault/OETHVaultCore.sol +++ b/contracts/contracts/vault/OETHVaultCore.sol @@ -40,13 +40,17 @@ contract OETHVaultCore is VaultCore { } // @inheritdoc VaultCore - function mint( + function _mint( address _asset, uint256 _amount, - uint256 - ) external virtual override whenNotCapitalPaused nonReentrant { + uint256 _minimumOusdAmount + ) internal virtual override { require(_asset == weth, "Unsupported asset for minting"); require(_amount > 0, "Amount must be greater than 0"); + require( + _amount >= _minimumOusdAmount, + "Mint amount lower than minimum" + ); emit Mint(msg.sender, _amount); @@ -76,8 +80,14 @@ contract OETHVaultCore is VaultCore { returns (uint256[] memory outputs) { // Overrides `VaultCore._calculateRedeemOutputs` to redeem with only - // WETH instead of LST-mix and gets rid of fee. Doesn't change the - // function signature for backward compatibility + // WETH instead of LST-mix. Doesn't change the function signature + // for backward compatibility + + // Calculate redeem fee + if (redeemFeeBps > 0) { + uint256 redeemFee = _amount.mulTruncateScale(redeemFeeBps, 1e4); + _amount = _amount - redeemFee; + } // Ensure that the WETH index is cached uint256 wethIndex = wethAssetIndex; @@ -88,50 +98,48 @@ contract OETHVaultCore is VaultCore { } // @inheritdoc VaultCore - function _redeem(uint256 _amount, uint256) internal virtual override { + function _redeem(uint256 _amount, uint256 _minimumUnitAmount) + internal + virtual + override + { // Override `VaultCore._redeem` to simplify it. Gets rid of oracle // usage and looping through all assets for LST-mix redeem. Instead - // does a simple WETH-only redeem with zero fee. + // does a simple WETH-only redeem. emit Redeem(msg.sender, _amount); - if (IERC20(weth).balanceOf(address(this)) >= _amount) { + require(_amount > 0, "Amount must be greater than 0"); + + // Amount excluding fees + uint256 amountMinusFee = _calculateRedeemOutputs(_amount)[ + wethAssetIndex + ]; + + if (_minimumUnitAmount > 0) { + require( + amountMinusFee >= _minimumUnitAmount, + "Redeem amount lower than minimum" + ); + } + + if (IERC20(weth).balanceOf(address(this)) >= amountMinusFee) { // Use Vault funds first if sufficient - IERC20(weth).safeTransfer(msg.sender, _amount); + IERC20(weth).safeTransfer(msg.sender, amountMinusFee); } else { address strategyAddr = assetDefaultStrategies[weth]; if (strategyAddr != address(0)) { // Nothing in Vault, but something in Strategy, send from there IStrategy strategy = IStrategy(strategyAddr); - strategy.withdraw(msg.sender, weth, _amount); + strategy.withdraw(msg.sender, weth, amountMinusFee); } else { // Cant find funds anywhere revert("Liquidity error"); } } - // Burn OETH from user + // Burn OETH from user (including fees) oUSD.burn(msg.sender, _amount); - // Until we can prove that we won't affect the prices of our assets - // by withdrawing them, this should be here. - // It's possible that a strategy was off on its asset total, perhaps - // a reward token sold for more or for less than anticipated. - uint256 totalUnits = 0; - if (_amount >= rebaseThreshold && !rebasePaused) { - totalUnits = _rebase(); - } else { - totalUnits = _totalValue(); - } - - // Check that the OTokens are backed by enough assets - if (maxSupplyDiff > 0) { - // Allow a max difference of maxSupplyDiff% between - // backing assets value and OETH total supply - uint256 diff = oUSD.totalSupply().divPrecisely(totalUnits); - require( - (diff > 1e18 ? diff - 1e18 : 1e18 - diff) <= maxSupplyDiff, - "Backing supply liquidity error" - ); - } + _postRedeem(_amount); } } diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index ab47c03177..814dcda9e5 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -62,7 +62,15 @@ contract VaultCore is VaultInitializer { address _asset, uint256 _amount, uint256 _minimumOusdAmount - ) external virtual whenNotCapitalPaused nonReentrant { + ) external whenNotCapitalPaused nonReentrant { + _mint(_asset, _amount, _minimumOusdAmount); + } + + function _mint( + address _asset, + uint256 _amount, + uint256 _minimumOusdAmount + ) internal virtual { require(assets[_asset].isSupported, "Asset is not supported"); require(_amount > 0, "Amount must be greater than 0"); @@ -195,6 +203,10 @@ contract VaultCore is VaultInitializer { oUSD.burn(msg.sender, _amount); + _postRedeem(_amount); + } + + function _postRedeem(uint256 _amount) internal { // Until we can prove that we won't affect the prices of our assets // by withdrawing them, this should be here. // It's possible that a strategy was off on its asset total, perhaps diff --git a/contracts/deploy/086_simplified_oeth_vault.js b/contracts/deploy/089_simplified_oeth_vault.js similarity index 88% rename from contracts/deploy/086_simplified_oeth_vault.js rename to contracts/deploy/089_simplified_oeth_vault.js index fe56905975..59a28ba3bf 100644 --- a/contracts/deploy/086_simplified_oeth_vault.js +++ b/contracts/deploy/089_simplified_oeth_vault.js @@ -6,7 +6,7 @@ const { module.exports = deploymentWithGovernanceProposal( { - deployName: "086_simplified_oeth_vault", + deployName: "089_simplified_oeth_vault", forceDeploy: false, // forceSkip: true, // onlyOnFork: true, // this is only executed in forked environment @@ -36,11 +36,6 @@ module.exports = deploymentWithGovernanceProposal( signature: "upgradeTo(address)", args: [dVaultImpl.address], }, - { - contract: cOETHVault, - signature: "setRedeemFeeBps(uint256)", - args: [0], - }, { contract: cOETHVault, signature: "cacheWETHAssetIndex()", diff --git a/contracts/test/vault/oeth-vault.fork-test.js b/contracts/test/vault/oeth-vault.fork-test.js index a1e99f86e2..8b5c0c3fec 100644 --- a/contracts/test/vault/oeth-vault.fork-test.js +++ b/contracts/test/vault/oeth-vault.fork-test.js @@ -100,10 +100,10 @@ describe("ForkTest: OETH Vault", function () { } }); - it("should have no redeem fee", async () => { + it("should have 0.1% redeem fee", async () => { const { oethVault } = fixture; - expect(await oethVault.redeemFeeBps()).to.equal(0); + expect(await oethVault.redeemFeeBps()).to.equal(10); }); it("should return only WETH in redeem calculations", async () => { @@ -112,7 +112,7 @@ describe("ForkTest: OETH Vault", function () { const output = await oethVault.calculateRedeemOutputs(oethUnits("123")); const index = await oethVault.wethAssetIndex(); - expect(output[index]).to.equal(oethUnits("123")); + expect(output[index]).to.equal(oethUnits("123").mul("9990").div("10000")); output.map((x, i) => { if (i !== index.toNumber()) { @@ -184,16 +184,6 @@ describe("ForkTest: OETH Vault", function () { expect((await oethVault.weth()).toLowerCase()).to.equal( addresses.mainnet.WETH.toLowerCase() ); - - const amount = parseUnits("100", 18); - const minEth = parseUnits("99.4", 18); - - const tx = await oethVault - .connect(oethWhaleSigner) - .redeem(amount, minEth); - await expect(tx) - .to.emit(oethVault, "Redeem") - .withNamedArgs({ _addr: oethWhaleAddress }); }); }); From 58c56ac3770b2803c704309b28383ccb746461e8 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 22 Mar 2024 19:48:13 +0530 Subject: [PATCH 06/13] Remove revert on zero amount --- contracts/contracts/vault/OETHVaultCore.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/vault/OETHVaultCore.sol b/contracts/contracts/vault/OETHVaultCore.sol index 3d2d0971f2..65c76ceaec 100644 --- a/contracts/contracts/vault/OETHVaultCore.sol +++ b/contracts/contracts/vault/OETHVaultCore.sol @@ -108,7 +108,9 @@ contract OETHVaultCore is VaultCore { // does a simple WETH-only redeem. emit Redeem(msg.sender, _amount); - require(_amount > 0, "Amount must be greater than 0"); + if (_amount == 0) { + return; + } // Amount excluding fees uint256 amountMinusFee = _calculateRedeemOutputs(_amount)[ From 3fbd025e1b4bd5c1bd674f8da80b3f02e4c4c644 Mon Sep 17 00:00:00 2001 From: Rappie Date: Fri, 22 Mar 2024 21:21:41 +0100 Subject: [PATCH 07/13] Add fuzzlib --- contracts/package.json | 1 + contracts/yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/contracts/package.json b/contracts/package.json index 5f728f7549..c3cfea8f6b 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -46,6 +46,7 @@ "@openzeppelin/contracts": "4.4.2", "@openzeppelin/defender-sdk": "^1.3.0", "@openzeppelin/hardhat-upgrades": "^1.10.0", + "@perimetersec/fuzzlib": "^0.2.0", "@uniswap/v3-core": "^1.0.0", "@uniswap/v3-periphery": "^1.1.1", "axios": "^1.4.0", diff --git a/contracts/yarn.lock b/contracts/yarn.lock index 8aa18ddbbc..f7bfe2ae3c 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -1147,6 +1147,11 @@ proper-lockfile "^4.1.1" solidity-ast "^0.4.15" +"@perimetersec/fuzzlib@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@perimetersec/fuzzlib/-/fuzzlib-0.2.0.tgz#bbf56975aa64430dd219c1322f70c4f2ab4456ae" + integrity sha512-XVXUO37r72oIMx+Nhg1xtRbr82oT8IAmwjLZDXeXTg99tbFFsCxgq4m0nEIv4mUet1ZZw4uQcC4e+xmI0bvGTw== + "@resolver-engine/core@^0.3.3": version "0.3.3" resolved "https://registry.yarnpkg.com/@resolver-engine/core/-/core-0.3.3.tgz#590f77d85d45bc7ecc4e06c654f41345db6ca967" From eea0a2085aa24401eb6ed5c448764a89a8394260 Mon Sep 17 00:00:00 2001 From: Rappie Date: Mon, 25 Mar 2024 13:06:59 +0100 Subject: [PATCH 08/13] Relocate OUSD fuzzing campaign --- contracts/contracts/{echidna => fuzz/ousd}/Debugger.sol | 0 contracts/contracts/{echidna => fuzz/ousd}/Echidna.sol | 0 contracts/contracts/{echidna => fuzz/ousd}/EchidnaConfig.sol | 0 contracts/contracts/{echidna => fuzz/ousd}/EchidnaDebug.sol | 2 +- contracts/contracts/{echidna => fuzz/ousd}/EchidnaHelper.sol | 0 contracts/contracts/{echidna => fuzz/ousd}/EchidnaSetup.sol | 0 .../contracts/{echidna => fuzz/ousd}/EchidnaTestAccounting.sol | 0 .../contracts/{echidna => fuzz/ousd}/EchidnaTestApproval.sol | 0 .../contracts/{echidna => fuzz/ousd}/EchidnaTestMintBurn.sol | 0 .../contracts/{echidna => fuzz/ousd}/EchidnaTestSupply.sol | 2 +- .../contracts/{echidna => fuzz/ousd}/EchidnaTestTransfer.sol | 0 contracts/contracts/{echidna => fuzz/ousd}/IHevm.sol | 0 contracts/contracts/{echidna => fuzz/ousd}/OUSDEchidna.sol | 2 +- contracts/{echidna-config.yaml => echidna-config-ousd.yaml} | 0 contracts/package.json | 2 +- 15 files changed, 4 insertions(+), 4 deletions(-) rename contracts/contracts/{echidna => fuzz/ousd}/Debugger.sol (100%) rename contracts/contracts/{echidna => fuzz/ousd}/Echidna.sol (100%) rename contracts/contracts/{echidna => fuzz/ousd}/EchidnaConfig.sol (100%) rename contracts/contracts/{echidna => fuzz/ousd}/EchidnaDebug.sol (95%) rename contracts/contracts/{echidna => fuzz/ousd}/EchidnaHelper.sol (100%) rename contracts/contracts/{echidna => fuzz/ousd}/EchidnaSetup.sol (100%) rename contracts/contracts/{echidna => fuzz/ousd}/EchidnaTestAccounting.sol (100%) rename contracts/contracts/{echidna => fuzz/ousd}/EchidnaTestApproval.sol (100%) rename contracts/contracts/{echidna => fuzz/ousd}/EchidnaTestMintBurn.sol (100%) rename contracts/contracts/{echidna => fuzz/ousd}/EchidnaTestSupply.sol (98%) rename contracts/contracts/{echidna => fuzz/ousd}/EchidnaTestTransfer.sol (100%) rename contracts/contracts/{echidna => fuzz/ousd}/IHevm.sol (100%) rename contracts/contracts/{echidna => fuzz/ousd}/OUSDEchidna.sol (89%) rename contracts/{echidna-config.yaml => echidna-config-ousd.yaml} (100%) diff --git a/contracts/contracts/echidna/Debugger.sol b/contracts/contracts/fuzz/ousd/Debugger.sol similarity index 100% rename from contracts/contracts/echidna/Debugger.sol rename to contracts/contracts/fuzz/ousd/Debugger.sol diff --git a/contracts/contracts/echidna/Echidna.sol b/contracts/contracts/fuzz/ousd/Echidna.sol similarity index 100% rename from contracts/contracts/echidna/Echidna.sol rename to contracts/contracts/fuzz/ousd/Echidna.sol diff --git a/contracts/contracts/echidna/EchidnaConfig.sol b/contracts/contracts/fuzz/ousd/EchidnaConfig.sol similarity index 100% rename from contracts/contracts/echidna/EchidnaConfig.sol rename to contracts/contracts/fuzz/ousd/EchidnaConfig.sol diff --git a/contracts/contracts/echidna/EchidnaDebug.sol b/contracts/contracts/fuzz/ousd/EchidnaDebug.sol similarity index 95% rename from contracts/contracts/echidna/EchidnaDebug.sol rename to contracts/contracts/fuzz/ousd/EchidnaDebug.sol index 9851498a0e..57a94d42dd 100644 --- a/contracts/contracts/echidna/EchidnaDebug.sol +++ b/contracts/contracts/fuzz/ousd/EchidnaDebug.sol @@ -6,7 +6,7 @@ import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import "./EchidnaHelper.sol"; import "./Debugger.sol"; -import "../token/OUSD.sol"; +import "../../token/OUSD.sol"; /** * @title Room for random debugging functions diff --git a/contracts/contracts/echidna/EchidnaHelper.sol b/contracts/contracts/fuzz/ousd/EchidnaHelper.sol similarity index 100% rename from contracts/contracts/echidna/EchidnaHelper.sol rename to contracts/contracts/fuzz/ousd/EchidnaHelper.sol diff --git a/contracts/contracts/echidna/EchidnaSetup.sol b/contracts/contracts/fuzz/ousd/EchidnaSetup.sol similarity index 100% rename from contracts/contracts/echidna/EchidnaSetup.sol rename to contracts/contracts/fuzz/ousd/EchidnaSetup.sol diff --git a/contracts/contracts/echidna/EchidnaTestAccounting.sol b/contracts/contracts/fuzz/ousd/EchidnaTestAccounting.sol similarity index 100% rename from contracts/contracts/echidna/EchidnaTestAccounting.sol rename to contracts/contracts/fuzz/ousd/EchidnaTestAccounting.sol diff --git a/contracts/contracts/echidna/EchidnaTestApproval.sol b/contracts/contracts/fuzz/ousd/EchidnaTestApproval.sol similarity index 100% rename from contracts/contracts/echidna/EchidnaTestApproval.sol rename to contracts/contracts/fuzz/ousd/EchidnaTestApproval.sol diff --git a/contracts/contracts/echidna/EchidnaTestMintBurn.sol b/contracts/contracts/fuzz/ousd/EchidnaTestMintBurn.sol similarity index 100% rename from contracts/contracts/echidna/EchidnaTestMintBurn.sol rename to contracts/contracts/fuzz/ousd/EchidnaTestMintBurn.sol diff --git a/contracts/contracts/echidna/EchidnaTestSupply.sol b/contracts/contracts/fuzz/ousd/EchidnaTestSupply.sol similarity index 98% rename from contracts/contracts/echidna/EchidnaTestSupply.sol rename to contracts/contracts/fuzz/ousd/EchidnaTestSupply.sol index 6ff8bc6243..dafa7f116c 100644 --- a/contracts/contracts/echidna/EchidnaTestSupply.sol +++ b/contracts/contracts/fuzz/ousd/EchidnaTestSupply.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "./EchidnaDebug.sol"; import "./EchidnaTestTransfer.sol"; -import { StableMath } from "../utils/StableMath.sol"; +import { StableMath } from "../../utils/StableMath.sol"; /** * @title Mixin for testing supply related functions diff --git a/contracts/contracts/echidna/EchidnaTestTransfer.sol b/contracts/contracts/fuzz/ousd/EchidnaTestTransfer.sol similarity index 100% rename from contracts/contracts/echidna/EchidnaTestTransfer.sol rename to contracts/contracts/fuzz/ousd/EchidnaTestTransfer.sol diff --git a/contracts/contracts/echidna/IHevm.sol b/contracts/contracts/fuzz/ousd/IHevm.sol similarity index 100% rename from contracts/contracts/echidna/IHevm.sol rename to contracts/contracts/fuzz/ousd/IHevm.sol diff --git a/contracts/contracts/echidna/OUSDEchidna.sol b/contracts/contracts/fuzz/ousd/OUSDEchidna.sol similarity index 89% rename from contracts/contracts/echidna/OUSDEchidna.sol rename to contracts/contracts/fuzz/ousd/OUSDEchidna.sol index cca5a6a6f5..aecccaea06 100644 --- a/contracts/contracts/echidna/OUSDEchidna.sol +++ b/contracts/contracts/fuzz/ousd/OUSDEchidna.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "../token/OUSD.sol"; +import "../../token/OUSD.sol"; contract OUSDEchidna is OUSD { constructor() OUSD() {} diff --git a/contracts/echidna-config.yaml b/contracts/echidna-config-ousd.yaml similarity index 100% rename from contracts/echidna-config.yaml rename to contracts/echidna-config-ousd.yaml diff --git a/contracts/package.json b/contracts/package.json index c3cfea8f6b..471e4ddabe 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -24,7 +24,7 @@ "test:arb-fork": "FORK_NETWORK_NAME=arbitrumOne ./fork-test.sh", "test:fork:w_trace": "TRACE=true ./fork-test.sh", "fund": "FORK=true npx hardhat fund --network localhost", - "echidna": "yarn run clean && rm -rf echidna-corpus && echidna . --contract Echidna --config echidna-config.yaml", + "fuzz-ousd": "yarn run clean && rm -rf echidna-corpus && echidna . --contract Echidna --config echidna-config-ousd.yaml", "compute-merkle-proofs-local": "HARDHAT_NETWORK=localhost node scripts/staking/airDrop.js reimbursements.csv scripts/staking/merkleProofedAccountsToBeCompensated.json && cp scripts/staking/merkleProofedAccountsToBeCompensated.json ../dapp/src/constants/merkleProofedAccountsToBeCompensated.json", "compute-merkle-proofs-mainnet": "HARDHAT_NETWORK=mainnet node scripts/staking/airDrop.js reimbursements.csv scripts/staking/merkleProofedAccountsToBeCompensated.json && cp scripts/staking/merkleProofedAccountsToBeCompensated.json ../dapp/src/constants/merkleProofedAccountsToBeCompensated.json", "slither": "yarn run clean && slither . --config-file slither.config.json", From 9fddeea4ca0e9f5edb81543d814cd7448d7d4091 Mon Sep 17 00:00:00 2001 From: Rappie Date: Mon, 25 Mar 2024 13:23:27 +0100 Subject: [PATCH 09/13] Add OETHVault fuzzing campaign --- contracts/contracts/fuzz/oethvault/Dummy.sol | 9 + contracts/contracts/fuzz/oethvault/Fuzz.sol | 19 ++ .../contracts/fuzz/oethvault/FuzzActor.sol | 46 ++++ .../contracts/fuzz/oethvault/FuzzConfig.sol | 64 +++++ .../contracts/fuzz/oethvault/FuzzGlobal.sol | 121 ++++++++++ .../contracts/fuzz/oethvault/FuzzHelper.sol | 75 ++++++ .../contracts/fuzz/oethvault/FuzzOETH.sol | 64 +++++ .../contracts/fuzz/oethvault/FuzzSelfTest.sol | 112 +++++++++ .../contracts/fuzz/oethvault/FuzzSetup.sol | 88 +++++++ .../contracts/fuzz/oethvault/FuzzVault.sol | 221 ++++++++++++++++++ .../contracts/fuzz/oethvault/MockOracle.sol | 13 ++ .../fuzz/oethvault/OETHVaultFuzzWrapper.sol | 15 ++ 12 files changed, 847 insertions(+) create mode 100644 contracts/contracts/fuzz/oethvault/Dummy.sol create mode 100644 contracts/contracts/fuzz/oethvault/Fuzz.sol create mode 100644 contracts/contracts/fuzz/oethvault/FuzzActor.sol create mode 100644 contracts/contracts/fuzz/oethvault/FuzzConfig.sol create mode 100644 contracts/contracts/fuzz/oethvault/FuzzGlobal.sol create mode 100644 contracts/contracts/fuzz/oethvault/FuzzHelper.sol create mode 100644 contracts/contracts/fuzz/oethvault/FuzzOETH.sol create mode 100644 contracts/contracts/fuzz/oethvault/FuzzSelfTest.sol create mode 100644 contracts/contracts/fuzz/oethvault/FuzzSetup.sol create mode 100644 contracts/contracts/fuzz/oethvault/FuzzVault.sol create mode 100644 contracts/contracts/fuzz/oethvault/MockOracle.sol create mode 100644 contracts/contracts/fuzz/oethvault/OETHVaultFuzzWrapper.sol diff --git a/contracts/contracts/fuzz/oethvault/Dummy.sol b/contracts/contracts/fuzz/oethvault/Dummy.sol new file mode 100644 index 0000000000..e9157b5fd8 --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/Dummy.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +/** + * @title Dummy contract to simulate smart contract actors. + * @author Rappie + * @dev This contract gets deployed by Echidna. See `echidna-config.yaml` + * for more details. + */ +contract Dummy {} diff --git a/contracts/contracts/fuzz/oethvault/Fuzz.sol b/contracts/contracts/fuzz/oethvault/Fuzz.sol new file mode 100644 index 0000000000..ff04630b6e --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/Fuzz.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +import {FuzzSetup} from "./FuzzSetup.sol"; +import {FuzzOETH} from "./FuzzOETH.sol"; +import {FuzzVault} from "./FuzzVault.sol"; +import {FuzzGlobal} from "./FuzzGlobal.sol"; +import {FuzzSelfTest} from "./FuzzSelfTest.sol"; + +/** + * @title Top-level Fuzz contract to be deployed by Echidna. + * @author Rappie + */ +contract Fuzz is + FuzzOETH, // Fuzz tests for OETH + FuzzVault, // Fuzz tests for Vault + FuzzGlobal, // Global invariants + FuzzSelfTest // Self-tests (for debugging) +{ + constructor() payable FuzzSetup() {} +} diff --git a/contracts/contracts/fuzz/oethvault/FuzzActor.sol b/contracts/contracts/fuzz/oethvault/FuzzActor.sol new file mode 100644 index 0000000000..c0e1377970 --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/FuzzActor.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +import {FuzzConfig} from "./FuzzConfig.sol"; + +/** + * @title Contract containing the actor setup. + * @author Rappie + */ +contract FuzzActor is FuzzConfig { + // Actors are the addresses to be used as senders. + address internal constant ADDRESS_ACTOR1 = address(0x10000); + address internal constant ADDRESS_ACTOR2 = address(0x20000); + address internal constant ADDRESS_ACTOR3 = address(0x30000); + address internal constant ADDRESS_ACTOR4 = address(0x40000); + + // Outsiders are addresses meant to contain funds but not take actions. + address internal constant ADDRESS_OUTSIDER_REBASING = address(0x50000); + address internal constant ADDRESS_OUTSIDER_NONREBASING = address(0x60000); + + // List of all actors + address[] internal ACTORS = [ + ADDRESS_ACTOR1, + ADDRESS_ACTOR2, + ADDRESS_ACTOR3, + ADDRESS_ACTOR4 + ]; + + // Variable containing current actor. + address internal currentActor; + + // Debug toggle to disable setting the current actor. + bool internal constant DEBUG_TOGGLE_SET_ACTOR = true; + + /// @notice Modifier storing `msg.sender` for the duration of the function call. + modifier setCurrentActor() { + address previousActor = currentActor; + if (DEBUG_TOGGLE_SET_ACTOR) { + currentActor = msg.sender; + } + + _; + + if (DEBUG_TOGGLE_SET_ACTOR) { + currentActor = previousActor; + } + } +} diff --git a/contracts/contracts/fuzz/oethvault/FuzzConfig.sol b/contracts/contracts/fuzz/oethvault/FuzzConfig.sol new file mode 100644 index 0000000000..e19ce4968a --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/FuzzConfig.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +/** + * @title Contract containing configuration variables for the fuzzing suite. + * @author Rappie + */ +contract FuzzConfig { + // Starting balance for actors that will interact with the system. + uint256 internal constant STARTING_BALANCE = 1_000_000_000_000e18; + + // Starting balance for outsides that will not interact with the system. + // + // We need these to have initial balances to prevent problems caused by + // rounding errors. + // We want this amount to be considerably lower than the starting balance + // of the actors, to be able to reach lower Credits Per Token (CPT) values. + // + uint256 internal constant STARTING_BALANCE_OUTSIDER = 1_000_000_000e18; + + // Tolerance for rounding errors when mining or redeeming OETH. + uint256 internal constant MINT_TOLERANCE = 1; + uint256 internal constant REDEEM_TOLERANCE = 1; + + // Tolerance for rounding errors in balance changes after rebasing. + uint256 internal constant BALANCE_AFTER_REBASE_TOLERANCE = 1; + + // Tolerance for rounding errors in amount of yield generated by donating + // and rebasing. + uint256 internal constant YIELD_TOLERANCE = 10_000; + + // Tolerance for the amount of WETH that should be available in the vault + // as a buffer for all actors (including outsiders) to be able to redeem + // all their OETH. + uint256 internal constant REDEEM_ALL_TOLERANCE = 1 ether / 100; + + // Tolerance for the difference between the total generated yield and the + // total donated amount. + // + // Max difference found with quick optimization: 11_359_396 + // + uint256 internal constant DONATE_VS_YIELD_TOLERANCE = 2e7; + + // Tolerance for the difference between the vault balance and the total + // OETH in the system. + // + // This is strongly related to the donate vs yield tolerance, so it makes + // sense to have the same value. + // + // Max difference found with quick optimization: 11_923_059 + // + uint256 internal constant VAULT_BALANCE_VS_TOTAL_OETH_TOLERANCE = 2e7; + + // Tolerance used to the major "accounting" global invariant. + // + // See `globalAccounting` for more info. + // + uint256 internal constant ACCOUNTING_TOLERANCE = 10; + + // Total amount of WETH donated to the vault. + uint256 totalDonated; + + // Total amount of OETH yield generated from donations to the vault. + uint256 totalYield; +} diff --git a/contracts/contracts/fuzz/oethvault/FuzzGlobal.sol b/contracts/contracts/fuzz/oethvault/FuzzGlobal.sol new file mode 100644 index 0000000000..45dbe59f4a --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/FuzzGlobal.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +import {FuzzHelper} from "./FuzzHelper.sol"; + +/** + * @title Contract containing fuzz tests for global invariants + * @author Rappie + */ +contract FuzzGlobal is FuzzHelper { + /** + * @notice Run all global invariants fuzz tests + * @dev We use one single function to run all global invariants fuzz tests. + * This is done to minize the search space for the fuzzer + */ + function globalInvariants() public { + totalWethVsStartingBalance(); + totalOethVsStartingBalance(); + totalYieldVsDonated(); + globalAccounting(); + globalOethVsWethTotalSupply(); + globalVaultBalanceVsOethTotalBalance(); + } + + /** + * @notice Test total WETH vs starting balance + */ + function totalWethVsStartingBalance() internal { + uint256 totalStarting = getTotalWethStartingBalance(); + uint256 totalWeth = getTotalWethBalance(); + + if (totalWeth > totalStarting) { + uint diff = diff(totalStarting, totalWeth); + + lte( + diff, + YIELD_TOLERANCE, + "GLOBAL-01: total WETH should never exceed total WETH starting balance" + ); + } + } + + /** + * @notice Test total OETH vs starting balance + */ + function totalOethVsStartingBalance() internal { + uint256 totalStarting = getTotalWethStartingBalance(); + uint256 totalOeth = getTotalOethBalance(); + + lte( + totalOeth, + totalStarting, + "GLOBAL-02: total OETH should never exceed total WETH starting balance" + ); + } + + /** + * @notice Test total yield vs donated + */ + function totalYieldVsDonated() internal { + uint256 diff = diff(totalYield, totalDonated); + + lte( + diff, + DONATE_VS_YIELD_TOLERANCE, + "GLOBAL-03: total yield should be equal to total donated" + ); + } + + /** + * @notice Test global accounting + */ + function globalAccounting() internal { + uint256 totalStarting = getTotalWethStartingBalanceInclOutsiders(); + uint256 totalWeth = getTotalWethBalanceInclOutsiders(); + uint256 totalOeth = getTotalOethBalanceInclOutsiders(); + + // Invariant: + // totalStarting - totalDonated = totalWeth + totalOeth - totalYield + int256 left = int256(totalStarting) - int256(totalDonated); + int256 right = int256(totalWeth) + + int256(totalOeth) - + int256(totalYield); + + uint256 diff = diff(left, right); + + lte( + diff, + ACCOUNTING_TOLERANCE, + "GLOBAL-04: totalStarting - totalDonated = totalWeth + totalOeth - totalYield" + ); + } + + /** + * @notice Test OETH total supply vs WETH total supply + */ + function globalOethVsWethTotalSupply() internal { + uint256 wethTotalSupply = weth.totalSupply(); + uint256 oethTotalSupply = oeth.totalSupply(); + + lte( + oethTotalSupply, + wethTotalSupply, + "GLOBAL-05: OETH total supply should never exceed WETH total supply" + ); + } + + /** + * @notice Test vault balance vs total OETH balance + */ + function globalVaultBalanceVsOethTotalBalance() internal { + uint256 vaultBalance = weth.balanceOf(address(vault)); + uint256 oethTotalBalance = getTotalOethBalanceInclOutsiders(); + + uint256 diff = diff(vaultBalance, oethTotalBalance); + + lte( + diff, + VAULT_BALANCE_VS_TOTAL_OETH_TOLERANCE, + "GLOBAL-06: vault balance should never exceed total OETH balance" + ); + } +} diff --git a/contracts/contracts/fuzz/oethvault/FuzzHelper.sol b/contracts/contracts/fuzz/oethvault/FuzzHelper.sol new file mode 100644 index 0000000000..7704c4e770 --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/FuzzHelper.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +import {FuzzSetup} from "./FuzzSetup.sol"; + +/** + * @title Contract containing internal helper functions. + * @author Rappie + */ +contract FuzzHelper is FuzzSetup { + /** + * @notice Get the total starting balance of OETH for all actors + * @return total Total starting balance of OETH + */ + function getTotalWethStartingBalance() internal returns (uint256 total) { + total += STARTING_BALANCE * ACTORS.length; + } + + /** + * @notice Get the total starting balance of OETH for all actors including outsiders + * @return total Total starting balance of OETH including outsiders + */ + function getTotalWethStartingBalanceInclOutsiders() + internal + returns (uint256 total) + { + total += getTotalWethStartingBalance(); + total += STARTING_BALANCE_OUTSIDER; // rebasing outsider + total += STARTING_BALANCE_OUTSIDER; // non-rebasing outsider + } + + /** + * @notice Get the total OETH balance of all actors + * @return total Total OETH balance of all actors + */ + function getTotalOethBalance() internal returns (uint256 total) { + for (uint256 i = 0; i < ACTORS.length; i++) { + total += oeth.balanceOf(ACTORS[i]); + } + } + + /** + * @notice Get the total OETH balance of all actors including outsiders + * @return total Total OETH balance of all actors including outsiders + */ + function getTotalOethBalanceInclOutsiders() + internal + returns (uint256 total) + { + total += getTotalOethBalance(); + total += oeth.balanceOf(ADDRESS_OUTSIDER_NONREBASING); + total += oeth.balanceOf(ADDRESS_OUTSIDER_REBASING); + } + + /** + * @notice Get the total WETH balance of all actors + * @return total Total WETH balance of all actors + */ + function getTotalWethBalance() internal returns (uint256 total) { + for (uint256 i = 0; i < ACTORS.length; i++) { + total += weth.balanceOf(ACTORS[i]); + } + } + + /** + * @notice Get the total WETH balance of all actors including outsiders + * @return total Total WETH balance of all actors including outsiders + */ + function getTotalWethBalanceInclOutsiders() + internal + returns (uint256 total) + { + total += getTotalWethBalance(); + total += weth.balanceOf(ADDRESS_OUTSIDER_NONREBASING); + total += weth.balanceOf(ADDRESS_OUTSIDER_REBASING); + } +} diff --git a/contracts/contracts/fuzz/oethvault/FuzzOETH.sol b/contracts/contracts/fuzz/oethvault/FuzzOETH.sol new file mode 100644 index 0000000000..e1ed58f422 --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/FuzzOETH.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +import {FuzzSetup} from "./FuzzSetup.sol"; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {OUSD} from "../../token/OUSD.sol"; + +/** + * @title Contract containing fuzz tests for OETH + * @author Rappie + */ +contract FuzzOETH is FuzzSetup { + /** + * @notice Transfer OETH to another actor + * @param toActorIndex Index of the actor to transfer to + * @param amount Amount of OETH to transfer + */ + function transfer( + uint8 toActorIndex, + uint256 amount + ) public setCurrentActor { + address to = ACTORS[clampBetween(toActorIndex, 0, ACTORS.length - 1)]; + amount = clampBetween(amount, 0, oeth.balanceOf(currentActor)); + + vm.prank(currentActor); + try oeth.transfer(to, amount) {} catch { + t(false, "OETH-01: No unwanted reverts in transfer"); + } + } + + /** + * @notice Opt in to rebase + */ + function optIn() public setCurrentActor { + if (oeth.rebaseState(currentActor) == OUSD.RebaseOptions.OptIn) + revert FuzzRequireError(); + if ( + !Address.isContract(currentActor) && + oeth.rebaseState(currentActor) == OUSD.RebaseOptions.NotSet + ) revert FuzzRequireError(); + + vm.prank(currentActor); + try oeth.rebaseOptIn() {} catch { + t(false, "OETH-02: No unwanted reverts in optIn"); + } + } + + /** + * @notice Opt out of rebase + */ + function optOut() public setCurrentActor { + if (oeth.rebaseState(currentActor) == OUSD.RebaseOptions.OptOut) + revert FuzzRequireError(); + if ( + Address.isContract(currentActor) && + oeth.rebaseState(currentActor) == OUSD.RebaseOptions.NotSet + ) revert FuzzRequireError(); + + vm.prank(currentActor); + try oeth.rebaseOptOut() {} catch { + t(false, "OETH-03: No unwanted reverts in optOut"); + } + } +} diff --git a/contracts/contracts/fuzz/oethvault/FuzzSelfTest.sol b/contracts/contracts/fuzz/oethvault/FuzzSelfTest.sol new file mode 100644 index 0000000000..4ab11fc38f --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/FuzzSelfTest.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +import {FuzzOETH} from "./FuzzOETH.sol"; +import {FuzzVault} from "./FuzzVault.sol"; +import {FuzzGlobal} from "./FuzzGlobal.sol"; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title Contract containing self tests for the Fuzzing campaign + * @author Rappie + * @dev This contract is used to test the Fuzzing campaign itself. It is + * used to test all fuzz tests for unwanted reverts. It is for debugging + * only and can be disabled in production. + */ +contract FuzzSelfTest is FuzzVault { + function selfTestRedeemClamped(uint256 amount) public { + bytes memory callData = abi.encodeWithSelector( + FuzzVault.redeemClamped.selector, + amount + ); + _testSelf(callData, "SELF-07: Redeem failed"); + } + + function selfTestMintClamped(uint256 amount) public { + bytes memory callData = abi.encodeWithSelector( + FuzzVault.mintClamped.selector, + amount + ); + _testSelf(callData, "SELF-08: Mint failed"); + } + + function selfTestTransfer(uint8 toActorIndex, uint256 amount) public { + bytes memory callData = abi.encodeWithSelector( + FuzzOETH.transfer.selector, + toActorIndex, + amount + ); + _testSelf(callData, "SELF-09: Transfer failed"); + } + + function selfTestOptIn() public { + bytes memory callData = abi.encodeWithSelector(FuzzOETH.optIn.selector); + _testSelf(callData, "SELF-10: OptIn failed"); + } + + function selfTestOptOut() public { + bytes memory callData = abi.encodeWithSelector( + FuzzOETH.optOut.selector + ); + _testSelf(callData, "SELF-11: OptOut failed"); + } + + function selfTestRedeemAll() public { + bytes memory callData = abi.encodeWithSelector( + FuzzVault.redeemAll.selector + ); + _testSelf(callData, "SELF-12: RedeemAll failed"); + } + + function selfTestDonateAndRebase(uint256 amount) public { + bytes memory callData = abi.encodeWithSelector( + FuzzVault.donateAndRebase.selector, + amount + ); + _testSelf(callData, "SELF-13: DonateAndRebase failed"); + } + + function selfTestGlobalInvariants() public { + bytes memory callData = abi.encodeWithSelector( + FuzzGlobal.globalInvariants.selector + ); + _testSelf(callData, "SELF-14: GlobalInvariants failed"); + } + + function _testSelf(bytes memory callData, string memory message) internal { + (bool success, bytes memory returnData) = address(this).delegatecall( + callData + ); + + bytes4 errorSelector = bytes4(returnData); + if (!(errorSelector == FuzzRequireError.selector)) { + t(success, message); + } + } + + function selfTestActorUserVsContract() public { + t( + !Address.isContract(ADDRESS_OUTSIDER_NONREBASING), + "SELF-01: Deployer should not be a contract" + ); + t( + !Address.isContract(ADDRESS_OUTSIDER_REBASING), + "SELF-02: Deployer should not be a contract" + ); + t( + !Address.isContract(ADDRESS_ACTOR1), + "SELF-03: Actor 1 should not be a contract" + ); + t( + !Address.isContract(ADDRESS_ACTOR2), + "SELF-04: Actor 2 should not be a contract" + ); + t( + Address.isContract(ADDRESS_ACTOR3), + "SELF-05: Actor 3 should be a contract" + ); + t( + Address.isContract(ADDRESS_ACTOR4), + "SELF-06: Actor 4 should be a contract" + ); + } +} diff --git a/contracts/contracts/fuzz/oethvault/FuzzSetup.sol b/contracts/contracts/fuzz/oethvault/FuzzSetup.sol new file mode 100644 index 0000000000..a2d160f066 --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/FuzzSetup.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +import {FuzzBase} from "@perimetersec/fuzzlib/src/FuzzBase.sol"; + +import {FuzzActor} from "./FuzzActor.sol"; + +import {MockWETH} from "../../mocks/MockWETH.sol"; +import {MockOracle} from "./MockOracle.sol"; +import {OUSD} from "../../token/OUSD.sol"; +import {OETHVaultFuzzWrapper} from "./OETHVaultFuzzWrapper.sol"; + +/** + * @title Contract containing the setup for the fuzzing suite + * @author Rappie + */ +contract FuzzSetup is FuzzActor, FuzzBase { + /// @notice Error to be thrown instead of `require` statements. + error FuzzRequireError(); + + MockWETH weth; + MockOracle oracle; + OUSD oeth; + OETHVaultFuzzWrapper vault; + + constructor() FuzzBase() { + // Deploy contracts + weth = new MockWETH(); + oracle = new MockOracle(); + oeth = new OUSD(); + vault = new OETHVaultFuzzWrapper(address(weth)); + + // Initialize contracts + oeth.initialize( + "TOETH", + "OETH Test Token", + address(vault), + 1e27 - 1 // utils.parseUnits("1", 27).sub(BigNumber.from(1)) + ); + vault.initialize(address(this), address(oeth)); + + // Vault setup, based on hardhat-deploy scripts + vault.setAutoAllocateThreshold(10e18); + vault.setRebaseThreshold(1e18); + vault.setMaxSupplyDiff(3e16); + vault.setStrategistAddr(address(this)); + vault.setTrusteeAddress(address(0)); // this disables yield fees + vault.setTrusteeFeeBps(2000); + vault.unpauseCapital(); + + // Use zero redeem fee + vault.setRedeemFeeBps(0); + + // Add weth as supported asset + vault.setPriceProvider(address(oracle)); // actual price is ignored + vault.supportAsset(address(weth), 0); // UnitConversion.DECIMALS + + // Outsider opts out of rebasing + vm.prank(ADDRESS_OUTSIDER_NONREBASING); + oeth.rebaseOptOut(); + + // Set up outsiders + setupActor(ADDRESS_OUTSIDER_NONREBASING, STARTING_BALANCE_OUTSIDER); + setupActor(ADDRESS_OUTSIDER_REBASING, STARTING_BALANCE_OUTSIDER); + + // Mint OEHT to outsiders + vm.prank(ADDRESS_OUTSIDER_NONREBASING); + vault.mint(address(weth), STARTING_BALANCE_OUTSIDER, 0); + vm.prank(ADDRESS_OUTSIDER_REBASING); + vault.mint(address(weth), STARTING_BALANCE_OUTSIDER, 0); + + // Set up actors + for (uint256 i = 0; i < ACTORS.length; i++) { + setupActor(ACTORS[i], STARTING_BALANCE); + } + } + + /** + * @notice Set up an actor with an initial balance of WETH + * @param actor Address of the actor + * @param amount Amount of WETH to set up + */ + function setupActor(address actor, uint amount) internal { + weth.mint(amount); + weth.transfer(actor, amount); + + vm.prank(actor); + weth.approve(address(vault), type(uint256).max); + } +} diff --git a/contracts/contracts/fuzz/oethvault/FuzzVault.sol b/contracts/contracts/fuzz/oethvault/FuzzVault.sol new file mode 100644 index 0000000000..076a2ca089 --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/FuzzVault.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: MIT +import {FuzzHelper} from "./FuzzHelper.sol"; + +/** + * @title Contract containing fuzz tests for Vault + * @author Rappie + */ +contract FuzzVault is FuzzHelper { + /** + * @notice Mint OETH without clamping + * @param amount Amount of OETH to mint + */ + function mint(uint256 amount) public setCurrentActor { + vm.prank(currentActor); + vault.mint(address(weth), amount, 0); + } + + /** + * @notice Mint OETH with clamping + * @param amount Amount of OETH to mint + */ + function mintClamped(uint256 amount) public setCurrentActor { + if (weth.balanceOf(currentActor) == 0) revert FuzzRequireError(); + amount = clampBetween(amount, 1, weth.balanceOf(currentActor)); + + uint256 wethBalBefore = weth.balanceOf(currentActor); + uint256 oethBalBefore = oeth.balanceOf(currentActor); + uint256 vaultBalBefore = weth.balanceOf(address(vault)); + + vm.prank(currentActor); + try vault.mint(address(weth), amount, 0) { + uint256 wethBalAfter = weth.balanceOf(currentActor); + uint256 oethBalAfter = oeth.balanceOf(currentActor); + uint256 vaultBalAfter = weth.balanceOf(address(vault)); + + uint256 wethBalDiff = diff(wethBalBefore - amount, wethBalAfter); + uint256 oethBalDiff = diff(oethBalBefore + amount, oethBalAfter); + uint256 vaultBalDiff = diff(vaultBalBefore + amount, vaultBalAfter); + + lte( + wethBalDiff, + MINT_TOLERANCE, + "VAULT-01: User WETH balance should decrease by mint amount" + ); + lte( + oethBalDiff, + MINT_TOLERANCE, + "VAULT-02: User OETH balance should increase by mint amount" + ); + lte( + vaultBalDiff, + MINT_TOLERANCE, + "VAULT-03: Vault WETH balance should increase by mint amount" + ); + } catch { + t(false, "VAULT-04: No unwanted reverts in mint"); + } + } + + /** + * @notice Redeem OETH without clamping + * @param amount Amount of OETH to redeem + */ + function redeem(uint256 amount) public setCurrentActor { + vm.prank(currentActor); + vault.redeem(amount, 0); + } + + /** + * @notice Redeem OETH with clamping + * @param amount Amount of OETH to redeem + */ + function redeemClamped(uint256 amount) public setCurrentActor { + if (oeth.balanceOf(currentActor) == 0) revert FuzzRequireError(); + amount = clampBetween(amount, 1, oeth.balanceOf(currentActor)); + + uint256 wethBalBefore = weth.balanceOf(currentActor); + uint256 oethBalBefore = oeth.balanceOf(currentActor); + uint256 vaultBalBefore = weth.balanceOf(address(vault)); + + vm.prank(currentActor); + try vault.redeem(amount, 0) { + uint256 wethBalAfter = weth.balanceOf(currentActor); + uint256 oethBalAfter = oeth.balanceOf(currentActor); + uint256 vaultBalAfter = weth.balanceOf(address(vault)); + + uint256 wethBalDiff = diff(wethBalBefore + amount, wethBalAfter); + uint256 oethBalDiff = diff(oethBalBefore - amount, oethBalAfter); + uint256 vaultBalDiff = diff(vaultBalBefore - amount, vaultBalAfter); + + lte( + wethBalDiff, + REDEEM_TOLERANCE, + "VAULT-05: User WETH balance should increase by redeem amount" + ); + lte( + oethBalDiff, + REDEEM_TOLERANCE, + "VAULT-06: User OETH balance should decrease by redeem amount" + ); + lte( + vaultBalDiff, + REDEEM_TOLERANCE, + "VAULT-07: Vault WETH balance should decrease by redeem amount" + ); + } catch { + t(false, "VAULT-08: No unwanted reverts in redeem"); + } + } + + /** + * @notice Redeem all OETH + */ + function redeemAll() public setCurrentActor { + vm.prank(currentActor); + vault.redeemAll(0); + + uint256 balanceAfter = oeth.balanceOf(currentActor); + lte( + balanceAfter, + REDEEM_TOLERANCE, + "VAULT-09: User OETH balance should be 0 after redeemAll" + ); + } + + /** + * @notice All users holding OETH should be able to redeem their holdings + * @dev This test does not change state + */ + function redeemAllShouldNotRevert() public { + uint256 forkId = vm.createFork(""); + vm.selectFork(forkId); + + // To prevent rounding issues we use extra outsiders to mint a small + // amount of OETH to the vault. + uint256 buffer = REDEEM_ALL_TOLERANCE / 2; + address outsider = address(0xDEADBEEF); + weth.mint(buffer); + weth.transfer(outsider, buffer); + vm.prank(outsider); + weth.approve(address(vault), type(uint256).max); + vm.prank(outsider); + vault.mint(address(weth), buffer, 0); + address outsider2 = address(0xDEADBEEF2); + vm.prank(outsider2); + oeth.rebaseOptOut(); + weth.mint(buffer); + weth.transfer(outsider2, buffer); + vm.prank(outsider2); + weth.approve(address(vault), type(uint256).max); + vm.prank(outsider2); + vault.mint(address(weth), buffer, 0); + + vm.prank(ADDRESS_OUTSIDER_NONREBASING); + try vault.redeemAll(0) {} catch { + t(false, "GLOBAL: redeemAll should never revert"); + } + + vm.prank(ADDRESS_OUTSIDER_REBASING); + try vault.redeemAll(0) {} catch { + t(false, "GLOBAL: redeemAll should never revert"); + } + + for (uint i = 0; i < ACTORS.length; i++) { + vm.prank(ACTORS[i]); + try vault.redeemAll(0) {} catch { + t(false, "GLOBAL: redeemAll should never revert"); + } + } + + vm.selectFork(0); + } + + /** + * @notice Donate WETH to the vault and rebase + * @param amount Amount of WETH to donate + * @dev This simulated yield generated from strategies + */ + function donateAndRebase(uint256 amount) public setCurrentActor { + if (weth.balanceOf(currentActor) == 0) revert FuzzRequireError(); + amount = clampBetween(amount, 1, weth.balanceOf(currentActor)); + vm.prank(currentActor); + + try weth.transfer(address(vault), amount) { + totalDonated += amount; + } catch { + t(false, "VAULT-10: Donating WETH to Vault should never revert"); + } + + uint totalOethBefore = getTotalOethBalanceInclOutsiders(); + + uint256[] memory balancesBefore = new uint256[](ACTORS.length); + for (uint256 i = 0; i < ACTORS.length; i++) { + balancesBefore[i] = oeth.balanceOf(ACTORS[i]); + } + + try vault.rebase() { + uint totalOethAfter = getTotalOethBalanceInclOutsiders(); + + for (uint256 i = 0; i < ACTORS.length; i++) { + uint256 balanceAfter = oeth.balanceOf(ACTORS[i]); + + if (balanceAfter < balancesBefore[i]) { + uint256 diff = diff(balanceAfter, balancesBefore[i]); + + lte( + diff, + BALANCE_AFTER_REBASE_TOLERANCE, + "VAULT-11: Rebase should never decrease OETH balance for users" + ); + } + } + + if (totalOethAfter > totalOethBefore) { + totalYield += totalOethAfter - totalOethBefore; + } + } catch { + t(false, "VAULT-12: Rebase should never revert"); + } + } +} diff --git a/contracts/contracts/fuzz/oethvault/MockOracle.sol b/contracts/contracts/fuzz/oethvault/MockOracle.sol new file mode 100644 index 0000000000..2ef16fef9e --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/MockOracle.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +/** + * @title Mock Oracle + * @author Rappie + */ +contract MockOracle { + mapping(address => uint256) public price; + + function setPrice(address asset, uint256 price_) external { + price[asset] = price_; + } +} diff --git a/contracts/contracts/fuzz/oethvault/OETHVaultFuzzWrapper.sol b/contracts/contracts/fuzz/oethvault/OETHVaultFuzzWrapper.sol new file mode 100644 index 0000000000..06eb6bc6b8 --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/OETHVaultFuzzWrapper.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {OETHVault} from "../../vault/OETHVault.sol"; +import {OETHVaultCore} from "../../vault/OETHVaultCore.sol"; + +/** + * @title OETH Vault Fuzz Wrapper Contract + * @author Rappie + * @dev This contract is used to simplify deployment of the Vault and + * prevent the use of proxies. + */ +contract OETHVaultFuzzWrapper is OETHVault, OETHVaultCore { + constructor(address _weth) OETHVaultCore(_weth) {} +} From a9fc457c4398ca660d2feeed9c9ecd4a4e0f3896 Mon Sep 17 00:00:00 2001 From: Rappie Date: Mon, 25 Mar 2024 13:27:18 +0100 Subject: [PATCH 10/13] Add `fuzz-oethvault` script command to `package.json` --- contracts/echidna-config-oethvault.yaml | 30 +++++++++++++++++++++++++ contracts/package.json | 1 + 2 files changed, 31 insertions(+) create mode 100644 contracts/echidna-config-oethvault.yaml diff --git a/contracts/echidna-config-oethvault.yaml b/contracts/echidna-config-oethvault.yaml new file mode 100644 index 0000000000..edfc43a3b1 --- /dev/null +++ b/contracts/echidna-config-oethvault.yaml @@ -0,0 +1,30 @@ +# multi-abi: true + +workers: 1 +# workers: 2 +symExec: true + +testMode: assertion +# testMode: optimization + +prefix: echidna_ +corpusDir: echidna-corpus + +testLimit: 100000000000 +# testLimit: 10000 # 10K +# testLimit: 10000000 # 10M +# testLimit: 100000000 # 100M + +# shrinkLimit: 100000000000 +# shrinkLimit: 100000 # 100K + +# seqLen: 30 +# seqLen: 250 + +balanceContract: 0xffffffffffffffffffffffffffffffffffffffffffffffff + +codeSize: 0x8000 + +deployer: "0xfffff" +sender: ["0x10000", "0x20000", "0x30000", "0x40000"] +deployContracts: [["0x30000", "Dummy"],["0x40000", "Dummy"]] diff --git a/contracts/package.json b/contracts/package.json index 471e4ddabe..15ae89e103 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -25,6 +25,7 @@ "test:fork:w_trace": "TRACE=true ./fork-test.sh", "fund": "FORK=true npx hardhat fund --network localhost", "fuzz-ousd": "yarn run clean && rm -rf echidna-corpus && echidna . --contract Echidna --config echidna-config-ousd.yaml", + "fuzz-oethvault": "yarn run clean && rm -rf echidna-corpus && echidna . --contract Fuzz --config echidna-config-oethvault.yaml", "compute-merkle-proofs-local": "HARDHAT_NETWORK=localhost node scripts/staking/airDrop.js reimbursements.csv scripts/staking/merkleProofedAccountsToBeCompensated.json && cp scripts/staking/merkleProofedAccountsToBeCompensated.json ../dapp/src/constants/merkleProofedAccountsToBeCompensated.json", "compute-merkle-proofs-mainnet": "HARDHAT_NETWORK=mainnet node scripts/staking/airDrop.js reimbursements.csv scripts/staking/merkleProofedAccountsToBeCompensated.json && cp scripts/staking/merkleProofedAccountsToBeCompensated.json ../dapp/src/constants/merkleProofedAccountsToBeCompensated.json", "slither": "yarn run clean && slither . --config-file slither.config.json", From 76a3f7d93f42cf4cca408d9fac1b8d47bcc7b132 Mon Sep 17 00:00:00 2001 From: Rappie Date: Mon, 25 Mar 2024 15:07:03 +0100 Subject: [PATCH 11/13] Update fuzzlib to 0.2.1 to support Solidity 0.8.7 --- contracts/package.json | 2 +- contracts/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/package.json b/contracts/package.json index 15ae89e103..7e390de988 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -47,7 +47,7 @@ "@openzeppelin/contracts": "4.4.2", "@openzeppelin/defender-sdk": "^1.3.0", "@openzeppelin/hardhat-upgrades": "^1.10.0", - "@perimetersec/fuzzlib": "^0.2.0", + "@perimetersec/fuzzlib": "0.2.1", "@uniswap/v3-core": "^1.0.0", "@uniswap/v3-periphery": "^1.1.1", "axios": "^1.4.0", diff --git a/contracts/yarn.lock b/contracts/yarn.lock index f7bfe2ae3c..c50ea95412 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -1147,10 +1147,10 @@ proper-lockfile "^4.1.1" solidity-ast "^0.4.15" -"@perimetersec/fuzzlib@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@perimetersec/fuzzlib/-/fuzzlib-0.2.0.tgz#bbf56975aa64430dd219c1322f70c4f2ab4456ae" - integrity sha512-XVXUO37r72oIMx+Nhg1xtRbr82oT8IAmwjLZDXeXTg99tbFFsCxgq4m0nEIv4mUet1ZZw4uQcC4e+xmI0bvGTw== +"@perimetersec/fuzzlib@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@perimetersec/fuzzlib/-/fuzzlib-0.2.1.tgz#21fbcf5f813ee58c8a345f58d60194a2652ecd2e" + integrity sha512-WuRMQFMHxqpT+fr2xUNcm+6qmSN9RXc1kaatFPjxW2TvIdD57akM2mai+ZOWrPgF++saMhO6N7Sv82gVBsnDJw== "@resolver-engine/core@^0.3.3": version "0.3.3" From a2a8a60d97887fb6f3cfab88d27b6318d03e7225 Mon Sep 17 00:00:00 2001 From: Rappie Date: Wed, 27 Mar 2024 17:36:23 +0100 Subject: [PATCH 12/13] Improve invariant descriptions. Reorder code. --- .../contracts/fuzz/oethvault/FuzzGlobal.sol | 12 +- .../contracts/fuzz/oethvault/FuzzOETH.sol | 6 +- .../contracts/fuzz/oethvault/FuzzVault.sol | 132 ++++++++++-------- 3 files changed, 81 insertions(+), 69 deletions(-) diff --git a/contracts/contracts/fuzz/oethvault/FuzzGlobal.sol b/contracts/contracts/fuzz/oethvault/FuzzGlobal.sol index 45dbe59f4a..4bfe6d84a5 100644 --- a/contracts/contracts/fuzz/oethvault/FuzzGlobal.sol +++ b/contracts/contracts/fuzz/oethvault/FuzzGlobal.sol @@ -33,7 +33,7 @@ contract FuzzGlobal is FuzzHelper { lte( diff, YIELD_TOLERANCE, - "GLOBAL-01: total WETH should never exceed total WETH starting balance" + "GLOBAL-01: The sum of WETH held by all actors should never exceed the sum of their WETH starting balances" ); } } @@ -48,7 +48,7 @@ contract FuzzGlobal is FuzzHelper { lte( totalOeth, totalStarting, - "GLOBAL-02: total OETH should never exceed total WETH starting balance" + "GLOBAL-02: The sum of OETH held by all actors should never exceed the sum of their WETH starting balances" ); } @@ -61,7 +61,7 @@ contract FuzzGlobal is FuzzHelper { lte( diff, DONATE_VS_YIELD_TOLERANCE, - "GLOBAL-03: total yield should be equal to total donated" + "GLOBAL-03: The total amount of generated yield should equal the total amount of WETH donated to the Vault" ); } @@ -85,7 +85,7 @@ contract FuzzGlobal is FuzzHelper { lte( diff, ACCOUNTING_TOLERANCE, - "GLOBAL-04: totalStarting - totalDonated = totalWeth + totalOeth - totalYield" + "GLOBAL-04: The sum of all starting balances minus the total amount of WETH donated should equal the sum of all WETH and OETH balances minus the total amount of yield generated" ); } @@ -99,7 +99,7 @@ contract FuzzGlobal is FuzzHelper { lte( oethTotalSupply, wethTotalSupply, - "GLOBAL-05: OETH total supply should never exceed WETH total supply" + "GLOBAL-05: The total supply of OETH should never exceed the total supply of WETH" ); } @@ -115,7 +115,7 @@ contract FuzzGlobal is FuzzHelper { lte( diff, VAULT_BALANCE_VS_TOTAL_OETH_TOLERANCE, - "GLOBAL-06: vault balance should never exceed total OETH balance" + "GLOBAL-06: The Vault WETH balance should never exceed total the amount of OETH held by all actors and outsiders" ); } } diff --git a/contracts/contracts/fuzz/oethvault/FuzzOETH.sol b/contracts/contracts/fuzz/oethvault/FuzzOETH.sol index e1ed58f422..5e7c098fee 100644 --- a/contracts/contracts/fuzz/oethvault/FuzzOETH.sol +++ b/contracts/contracts/fuzz/oethvault/FuzzOETH.sol @@ -24,7 +24,7 @@ contract FuzzOETH is FuzzSetup { vm.prank(currentActor); try oeth.transfer(to, amount) {} catch { - t(false, "OETH-01: No unwanted reverts in transfer"); + t(false, "OETH-01: No unwanted reverts when transfering OETH"); } } @@ -41,7 +41,7 @@ contract FuzzOETH is FuzzSetup { vm.prank(currentActor); try oeth.rebaseOptIn() {} catch { - t(false, "OETH-02: No unwanted reverts in optIn"); + t(false, "OETH-02: No unwanted reverts when opting in to rebase"); } } @@ -58,7 +58,7 @@ contract FuzzOETH is FuzzSetup { vm.prank(currentActor); try oeth.rebaseOptOut() {} catch { - t(false, "OETH-03: No unwanted reverts in optOut"); + t(false, "OETH-03: No unwanted reverts when opting out of rebase"); } } } diff --git a/contracts/contracts/fuzz/oethvault/FuzzVault.sol b/contracts/contracts/fuzz/oethvault/FuzzVault.sol index 076a2ca089..06d5460bc3 100644 --- a/contracts/contracts/fuzz/oethvault/FuzzVault.sol +++ b/contracts/contracts/fuzz/oethvault/FuzzVault.sol @@ -40,20 +40,20 @@ contract FuzzVault is FuzzHelper { lte( wethBalDiff, MINT_TOLERANCE, - "VAULT-01: User WETH balance should decrease by mint amount" + "VMINT-01: Actor WETH balance should decrease by amount minted" ); lte( oethBalDiff, MINT_TOLERANCE, - "VAULT-02: User OETH balance should increase by mint amount" + "VMINT-02: Actor OETH balance should increase by amount minted" ); lte( vaultBalDiff, MINT_TOLERANCE, - "VAULT-03: Vault WETH balance should increase by mint amount" + "VMINT-03: Vault WETH balance should increase by amount minted" ); } catch { - t(false, "VAULT-04: No unwanted reverts in mint"); + t(false, "VMINT-04: No unwanted reverts when minting OETH"); } } @@ -91,20 +91,20 @@ contract FuzzVault is FuzzHelper { lte( wethBalDiff, REDEEM_TOLERANCE, - "VAULT-05: User WETH balance should increase by redeem amount" + "VREDEEM-01: Actor WETH balance should increase by amount redeemed" ); lte( oethBalDiff, REDEEM_TOLERANCE, - "VAULT-06: User OETH balance should decrease by redeem amount" + "VREDEEM-02: Actor OETH balance should decrease by amount redeemed" ); lte( vaultBalDiff, REDEEM_TOLERANCE, - "VAULT-07: Vault WETH balance should decrease by redeem amount" + "VREDEEM-03: Vault WETH balance should decrease by amount redeemed" ); } catch { - t(false, "VAULT-08: No unwanted reverts in redeem"); + t(false, "VREDEEM-04: No unwanted reverts when redeeming OETH"); } } @@ -119,58 +119,10 @@ contract FuzzVault is FuzzHelper { lte( balanceAfter, REDEEM_TOLERANCE, - "VAULT-09: User OETH balance should be 0 after redeemAll" + "VREDEEM-05: After redeeming all, actor OETH balance should be zero" ); } - /** - * @notice All users holding OETH should be able to redeem their holdings - * @dev This test does not change state - */ - function redeemAllShouldNotRevert() public { - uint256 forkId = vm.createFork(""); - vm.selectFork(forkId); - - // To prevent rounding issues we use extra outsiders to mint a small - // amount of OETH to the vault. - uint256 buffer = REDEEM_ALL_TOLERANCE / 2; - address outsider = address(0xDEADBEEF); - weth.mint(buffer); - weth.transfer(outsider, buffer); - vm.prank(outsider); - weth.approve(address(vault), type(uint256).max); - vm.prank(outsider); - vault.mint(address(weth), buffer, 0); - address outsider2 = address(0xDEADBEEF2); - vm.prank(outsider2); - oeth.rebaseOptOut(); - weth.mint(buffer); - weth.transfer(outsider2, buffer); - vm.prank(outsider2); - weth.approve(address(vault), type(uint256).max); - vm.prank(outsider2); - vault.mint(address(weth), buffer, 0); - - vm.prank(ADDRESS_OUTSIDER_NONREBASING); - try vault.redeemAll(0) {} catch { - t(false, "GLOBAL: redeemAll should never revert"); - } - - vm.prank(ADDRESS_OUTSIDER_REBASING); - try vault.redeemAll(0) {} catch { - t(false, "GLOBAL: redeemAll should never revert"); - } - - for (uint i = 0; i < ACTORS.length; i++) { - vm.prank(ACTORS[i]); - try vault.redeemAll(0) {} catch { - t(false, "GLOBAL: redeemAll should never revert"); - } - } - - vm.selectFork(0); - } - /** * @notice Donate WETH to the vault and rebase * @param amount Amount of WETH to donate @@ -184,7 +136,10 @@ contract FuzzVault is FuzzHelper { try weth.transfer(address(vault), amount) { totalDonated += amount; } catch { - t(false, "VAULT-10: Donating WETH to Vault should never revert"); + t( + false, + "VREBASE-01: No unwanted reverts when donating WETH to Vault" + ); } uint totalOethBefore = getTotalOethBalanceInclOutsiders(); @@ -206,7 +161,7 @@ contract FuzzVault is FuzzHelper { lte( diff, BALANCE_AFTER_REBASE_TOLERANCE, - "VAULT-11: Rebase should never decrease OETH balance for users" + "VREBASE-02: Rebasing should never decrease OETH balance for any actor" ); } } @@ -215,7 +170,64 @@ contract FuzzVault is FuzzHelper { totalYield += totalOethAfter - totalOethBefore; } } catch { - t(false, "VAULT-12: Rebase should never revert"); + t(false, "VREBASE-03: No unwanted reverts when rebasing Vault"); } } + + /** + * @notice All users holding OETH should be able to redeem their holdings + * @dev This test does not change state + */ + function redeemAllShouldNotRevert() public { + uint256 forkId = vm.createFork(""); + vm.selectFork(forkId); + + // To prevent rounding issues we use extra outsiders to mint a small + // amount of OETH to the vault. + uint256 buffer = REDEEM_ALL_TOLERANCE / 2; + address outsider = address(0xDEADBEEF); + weth.mint(buffer); + weth.transfer(outsider, buffer); + vm.prank(outsider); + weth.approve(address(vault), type(uint256).max); + vm.prank(outsider); + vault.mint(address(weth), buffer, 0); + address outsider2 = address(0xDEADBEEF2); + vm.prank(outsider2); + oeth.rebaseOptOut(); + weth.mint(buffer); + weth.transfer(outsider2, buffer); + vm.prank(outsider2); + weth.approve(address(vault), type(uint256).max); + vm.prank(outsider2); + vault.mint(address(weth), buffer, 0); + + vm.prank(ADDRESS_OUTSIDER_NONREBASING); + try vault.redeemAll(0) {} catch { + t( + false, + "GLOBAL-07: Any actor should always be enable to redeem all OETH" + ); + } + + vm.prank(ADDRESS_OUTSIDER_REBASING); + try vault.redeemAll(0) {} catch { + t( + false, + "GLOBAL-07: Any actor should always be enable to redeem all OETH" + ); + } + + for (uint i = 0; i < ACTORS.length; i++) { + vm.prank(ACTORS[i]); + try vault.redeemAll(0) {} catch { + t( + false, + "GLOBAL-07: Any actor should always be enable to redeem all OETH" + ); + } + } + + vm.selectFork(0); + } } From af9cddf4be798b36b343628dd0261a662f35490e Mon Sep 17 00:00:00 2001 From: Rappie Date: Wed, 10 Apr 2024 16:30:43 +0200 Subject: [PATCH 13/13] Improve invariant descriptions --- .../contracts/fuzz/oethvault/FuzzGlobal.sol | 12 ++++---- .../contracts/fuzz/oethvault/FuzzOETH.sol | 12 ++++++-- .../contracts/fuzz/oethvault/FuzzVault.sol | 30 +++++++++---------- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/contracts/contracts/fuzz/oethvault/FuzzGlobal.sol b/contracts/contracts/fuzz/oethvault/FuzzGlobal.sol index 4bfe6d84a5..791b5e63de 100644 --- a/contracts/contracts/fuzz/oethvault/FuzzGlobal.sol +++ b/contracts/contracts/fuzz/oethvault/FuzzGlobal.sol @@ -33,7 +33,7 @@ contract FuzzGlobal is FuzzHelper { lte( diff, YIELD_TOLERANCE, - "GLOBAL-01: The sum of WETH held by all actors should never exceed the sum of their WETH starting balances" + "GLOBAL-01: The sum of WETH held by all actors never exceeds the sum of their WETH starting balances" ); } } @@ -48,7 +48,7 @@ contract FuzzGlobal is FuzzHelper { lte( totalOeth, totalStarting, - "GLOBAL-02: The sum of OETH held by all actors should never exceed the sum of their WETH starting balances" + "GLOBAL-02: The sum of OETH held by all actors never exceeds the sum of their WETH starting balances" ); } @@ -61,7 +61,7 @@ contract FuzzGlobal is FuzzHelper { lte( diff, DONATE_VS_YIELD_TOLERANCE, - "GLOBAL-03: The total amount of generated yield should equal the total amount of WETH donated to the Vault" + "GLOBAL-03: The total amount of generated yield equals the total amount of WETH donated to the Vault" ); } @@ -85,7 +85,7 @@ contract FuzzGlobal is FuzzHelper { lte( diff, ACCOUNTING_TOLERANCE, - "GLOBAL-04: The sum of all starting balances minus the total amount of WETH donated should equal the sum of all WETH and OETH balances minus the total amount of yield generated" + "GLOBAL-04: The sum of all starting balances minus the total amount of WETH donated equals the sum of all WETH and OETH balances minus the total amount of yield generated" ); } @@ -99,7 +99,7 @@ contract FuzzGlobal is FuzzHelper { lte( oethTotalSupply, wethTotalSupply, - "GLOBAL-05: The total supply of OETH should never exceed the total supply of WETH" + "GLOBAL-05: The total supply of OETH never exceeds the total supply of WETH" ); } @@ -115,7 +115,7 @@ contract FuzzGlobal is FuzzHelper { lte( diff, VAULT_BALANCE_VS_TOTAL_OETH_TOLERANCE, - "GLOBAL-06: The Vault WETH balance should never exceed total the amount of OETH held by all actors and outsiders" + "GLOBAL-06: The Vault WETH balance never exceeds the total amount of OETH held by all actors and outsiders" ); } } diff --git a/contracts/contracts/fuzz/oethvault/FuzzOETH.sol b/contracts/contracts/fuzz/oethvault/FuzzOETH.sol index 5e7c098fee..0c82c203d8 100644 --- a/contracts/contracts/fuzz/oethvault/FuzzOETH.sol +++ b/contracts/contracts/fuzz/oethvault/FuzzOETH.sol @@ -24,7 +24,7 @@ contract FuzzOETH is FuzzSetup { vm.prank(currentActor); try oeth.transfer(to, amount) {} catch { - t(false, "OETH-01: No unwanted reverts when transfering OETH"); + t(false, "OETH-01: Transfering OETH does not unexpectedly revert"); } } @@ -41,7 +41,10 @@ contract FuzzOETH is FuzzSetup { vm.prank(currentActor); try oeth.rebaseOptIn() {} catch { - t(false, "OETH-02: No unwanted reverts when opting in to rebase"); + t( + false, + "OETH-02: Opting in to rebase does not unexpectedly revert" + ); } } @@ -58,7 +61,10 @@ contract FuzzOETH is FuzzSetup { vm.prank(currentActor); try oeth.rebaseOptOut() {} catch { - t(false, "OETH-03: No unwanted reverts when opting out of rebase"); + t( + false, + "OETH-03: Opting out of rebase does not unexpectedly revert" + ); } } } diff --git a/contracts/contracts/fuzz/oethvault/FuzzVault.sol b/contracts/contracts/fuzz/oethvault/FuzzVault.sol index 06d5460bc3..9d7a91d6c3 100644 --- a/contracts/contracts/fuzz/oethvault/FuzzVault.sol +++ b/contracts/contracts/fuzz/oethvault/FuzzVault.sol @@ -40,20 +40,20 @@ contract FuzzVault is FuzzHelper { lte( wethBalDiff, MINT_TOLERANCE, - "VMINT-01: Actor WETH balance should decrease by amount minted" + "VMINT-01: Actor WETH balance decreases by amount minted after successful mint" ); lte( oethBalDiff, MINT_TOLERANCE, - "VMINT-02: Actor OETH balance should increase by amount minted" + "VMINT-02: Actor OETH balance increases by amount minted after successful mint" ); lte( vaultBalDiff, MINT_TOLERANCE, - "VMINT-03: Vault WETH balance should increase by amount minted" + "VMINT-03: Vault WETH balance increases by amount minted after successful mint" ); } catch { - t(false, "VMINT-04: No unwanted reverts when minting OETH"); + t(false, "VMINT-04: Minting OETH does not unexpectedly revert"); } } @@ -91,20 +91,20 @@ contract FuzzVault is FuzzHelper { lte( wethBalDiff, REDEEM_TOLERANCE, - "VREDEEM-01: Actor WETH balance should increase by amount redeemed" + "VREDEEM-01: Actor WETH balance increases by amount redeemed after successful redeem" ); lte( oethBalDiff, REDEEM_TOLERANCE, - "VREDEEM-02: Actor OETH balance should decrease by amount redeemed" + "VREDEEM-02: Actor OETH balance decreases by amount redeemed after successful redeem" ); lte( vaultBalDiff, REDEEM_TOLERANCE, - "VREDEEM-03: Vault WETH balance should decrease by amount redeemed" + "VREDEEM-03: Vault WETH balance decreases by amount redeemed after successful redeem" ); } catch { - t(false, "VREDEEM-04: No unwanted reverts when redeeming OETH"); + t(false, "VREDEEM-04: Redeeming OETH does not unexpectedly revert"); } } @@ -119,7 +119,7 @@ contract FuzzVault is FuzzHelper { lte( balanceAfter, REDEEM_TOLERANCE, - "VREDEEM-05: After redeeming all, actor OETH balance should be zero" + "VREDEEM-05: Actor OETH balance is zero after successfully redeeming all" ); } @@ -138,7 +138,7 @@ contract FuzzVault is FuzzHelper { } catch { t( false, - "VREBASE-01: No unwanted reverts when donating WETH to Vault" + "VREBASE-01: Donating WETH to the Vault does not unexpectedly revert" ); } @@ -161,7 +161,7 @@ contract FuzzVault is FuzzHelper { lte( diff, BALANCE_AFTER_REBASE_TOLERANCE, - "VREBASE-02: Rebasing should never decrease OETH balance for any actor" + "VREBASE-02: Rebasing never decreases OETH balance for any actor" ); } } @@ -170,7 +170,7 @@ contract FuzzVault is FuzzHelper { totalYield += totalOethAfter - totalOethBefore; } } catch { - t(false, "VREBASE-03: No unwanted reverts when rebasing Vault"); + t(false, "VREBASE-03: Rebasing vault does not unexpectedly revert"); } } @@ -206,7 +206,7 @@ contract FuzzVault is FuzzHelper { try vault.redeemAll(0) {} catch { t( false, - "GLOBAL-07: Any actor should always be enable to redeem all OETH" + "GLOBAL-07: Any actor can always redeem all OETH" ); } @@ -214,7 +214,7 @@ contract FuzzVault is FuzzHelper { try vault.redeemAll(0) {} catch { t( false, - "GLOBAL-07: Any actor should always be enable to redeem all OETH" + "GLOBAL-07: Any actor can always redeem all OETH" ); } @@ -223,7 +223,7 @@ contract FuzzVault is FuzzHelper { try vault.redeemAll(0) {} catch { t( false, - "GLOBAL-07: Any actor should always be enable to redeem all OETH" + "GLOBAL-07: Any actor can always redeem all OETH" ); } }