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..791b5e63de --- /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: The sum of WETH held by all actors never exceeds the sum of their WETH starting balances" + ); + } + } + + /** + * @notice Test total OETH vs starting balance + */ + function totalOethVsStartingBalance() internal { + uint256 totalStarting = getTotalWethStartingBalance(); + uint256 totalOeth = getTotalOethBalance(); + + lte( + totalOeth, + totalStarting, + "GLOBAL-02: The sum of OETH held by all actors never exceeds the sum of their WETH starting balances" + ); + } + + /** + * @notice Test total yield vs donated + */ + function totalYieldVsDonated() internal { + uint256 diff = diff(totalYield, totalDonated); + + lte( + diff, + DONATE_VS_YIELD_TOLERANCE, + "GLOBAL-03: The total amount of generated yield equals the total amount of WETH donated to the Vault" + ); + } + + /** + * @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: 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" + ); + } + + /** + * @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: The total supply of OETH never exceeds the total supply of WETH" + ); + } + + /** + * @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: The Vault WETH balance never exceeds the total amount of OETH held by all actors and outsiders" + ); + } +} 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..0c82c203d8 --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/FuzzOETH.sol @@ -0,0 +1,70 @@ +// 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: Transfering OETH does not unexpectedly revert"); + } + } + + /** + * @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: Opting in to rebase does not unexpectedly revert" + ); + } + } + + /** + * @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: Opting out of rebase does not unexpectedly revert" + ); + } + } +} 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..9d7a91d6c3 --- /dev/null +++ b/contracts/contracts/fuzz/oethvault/FuzzVault.sol @@ -0,0 +1,233 @@ +// 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, + "VMINT-01: Actor WETH balance decreases by amount minted after successful mint" + ); + lte( + oethBalDiff, + MINT_TOLERANCE, + "VMINT-02: Actor OETH balance increases by amount minted after successful mint" + ); + lte( + vaultBalDiff, + MINT_TOLERANCE, + "VMINT-03: Vault WETH balance increases by amount minted after successful mint" + ); + } catch { + t(false, "VMINT-04: Minting OETH does not unexpectedly revert"); + } + } + + /** + * @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, + "VREDEEM-01: Actor WETH balance increases by amount redeemed after successful redeem" + ); + lte( + oethBalDiff, + REDEEM_TOLERANCE, + "VREDEEM-02: Actor OETH balance decreases by amount redeemed after successful redeem" + ); + lte( + vaultBalDiff, + REDEEM_TOLERANCE, + "VREDEEM-03: Vault WETH balance decreases by amount redeemed after successful redeem" + ); + } catch { + t(false, "VREDEEM-04: Redeeming OETH does not unexpectedly revert"); + } + } + + /** + * @notice Redeem all OETH + */ + function redeemAll() public setCurrentActor { + vm.prank(currentActor); + vault.redeemAll(0); + + uint256 balanceAfter = oeth.balanceOf(currentActor); + lte( + balanceAfter, + REDEEM_TOLERANCE, + "VREDEEM-05: Actor OETH balance is zero after successfully redeeming all" + ); + } + + /** + * @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, + "VREBASE-01: Donating WETH to the Vault does not unexpectedly 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, + "VREBASE-02: Rebasing never decreases OETH balance for any actor" + ); + } + } + + if (totalOethAfter > totalOethBefore) { + totalYield += totalOethAfter - totalOethBefore; + } + } catch { + t(false, "VREBASE-03: Rebasing vault does not unexpectedly revert"); + } + } + + /** + * @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 can always redeem all OETH" + ); + } + + vm.prank(ADDRESS_OUTSIDER_REBASING); + try vault.redeemAll(0) {} catch { + t( + false, + "GLOBAL-07: Any actor can always 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 can always redeem all OETH" + ); + } + } + + vm.selectFork(0); + } +} 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) {} +} 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-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/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 5f728f7549..7e390de988 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -24,7 +24,8 @@ "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", + "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", @@ -46,6 +47,7 @@ "@openzeppelin/contracts": "4.4.2", "@openzeppelin/defender-sdk": "^1.3.0", "@openzeppelin/hardhat-upgrades": "^1.10.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 8aa18ddbbc..c50ea95412 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.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" resolved "https://registry.yarnpkg.com/@resolver-engine/core/-/core-0.3.3.tgz#590f77d85d45bc7ecc4e06c654f41345db6ca967"