From 8310aa568db08f96f199305db9257125bf95697c Mon Sep 17 00:00:00 2001 From: Erwan Beauvois Date: Mon, 28 Mar 2022 16:49:46 +0200 Subject: [PATCH 01/10] Add npm scripts for unit & integration tests --- package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 5f2c210..fca50a6 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,14 @@ "solhint": "solhint --config ./.solhint.json 'src/**/*.sol' --fix", "solhint:check": "solhint --config ./.solhint.json 'src/**/*.sol'", "lint": "npm run prettier && npm run solhint", - "lint:check": "npm run prettier:check && npm run solhint:check" - + "lint:check": "npm run prettier:check && npm run solhint:check", + "test": "forge test --no-match-contract Integration -vvv", + "test:integration": "FORK_BLOCK=14473700; forge test -vvv --fork-url https://eth-mainnet.alchemyapi.io/v2/$MAINNET_ALCHEMY_API_KEY --fork-block-number $FORK_BLOCK --match-contract Integration", + "test:integration:latest": "forge test -vvv --fork-url https://eth-mainnet.alchemyapi.io/v2/$MAINNET_ALCHEMY_API_KEY --match-contract Integration" }, "devDependencies": { "prettier": "^2.5.1", "prettier-plugin-solidity": "^1.0.0-beta.19", "solhint": "^3.3.6" } -} +} \ No newline at end of file From a2bc6ff90543455a78521ae0838c4ba56160996e Mon Sep 17 00:00:00 2001 From: Erwan Beauvois Date: Mon, 28 Mar 2022 16:49:59 +0200 Subject: [PATCH 02/10] Update CToken & Unitroller external interfaces --- src/external/CToken.sol | 11 +++++++++++ src/external/Unitroller.sol | 38 +++++++++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/external/CToken.sol b/src/external/CToken.sol index dd7e0c6..9807961 100644 --- a/src/external/CToken.sol +++ b/src/external/CToken.sol @@ -7,4 +7,15 @@ abstract contract CToken is CERC20 { function comptroller() external view virtual returns (address); function getCash() external view virtual returns (uint256); + + function getAccountSnapshot(address) + external + view + virtual + returns ( + uint256, + uint256, + uint256, + uint256 + ); } diff --git a/src/external/Unitroller.sol b/src/external/Unitroller.sol index beaa531..4d4ce7b 100644 --- a/src/external/Unitroller.sol +++ b/src/external/Unitroller.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.4; -import {CERC20} from "libcompound/interfaces/CERC20.sol"; - abstract contract Unitroller { struct Market { bool isListed; @@ -22,6 +20,27 @@ abstract contract Unitroller { mapping(address => address) public cTokensByUnderlying; mapping(address => uint256) public supplyCaps; + function getAccountLiquidity(address account) + public + view + virtual + returns ( + uint256 err, + uint256 liquidity, + uint256 shortfall + ); + + function getAssetsIn(address account) + external + view + virtual + returns (address[] memory); + + function enterMarkets(address[] memory cTokens) + public + virtual + returns (uint256[] memory); + function _setPendingAdmin(address newPendingAdmin) public virtual @@ -30,12 +49,12 @@ abstract contract Unitroller { function _setBorrowCapGuardian(address newBorrowCapGuardian) public virtual; function _setMarketSupplyCaps( - CERC20[] calldata cTokens, + address[] calldata cTokens, uint256[] calldata newSupplyCaps ) external virtual; function _setMarketBorrowCaps( - CERC20[] calldata cTokens, + address[] calldata cTokens, uint256[] calldata newBorrowCaps ) external virtual; @@ -44,12 +63,12 @@ abstract contract Unitroller { virtual returns (uint256); - function _setMintPaused(CERC20 cToken, bool state) + function _setMintPaused(address cToken, bool state) public virtual returns (bool); - function _setBorrowPaused(CERC20 cToken, bool borrowPaused) + function _setBorrowPaused(address cToken, bool borrowPaused) public virtual returns (bool); @@ -74,7 +93,7 @@ abstract contract Unitroller { returns (uint256); function _setCollateralFactor( - CERC20 cToken, + address cToken, uint256 newCollateralFactorMantissa ) public virtual returns (uint256); @@ -125,7 +144,10 @@ abstract contract Unitroller { bool[] calldata statuses ) external virtual returns (uint256); - function _unsupportMarket(CERC20 cToken) external virtual returns (uint256); + function _unsupportMarket(address cToken) + external + virtual + returns (uint256); function _toggleAutoImplementations(bool enabled) public From 0b10552dbd0f1d634c46660d176f702e2a058571 Mon Sep 17 00:00:00 2001 From: Erwan Beauvois Date: Mon, 28 Mar 2022 16:50:12 +0200 Subject: [PATCH 03/10] Add FuseERC4626 unit tests --- src/test/mocks/MockCToken.sol | 30 ++- src/test/mocks/MockERC20.sol | 22 ++ src/test/mocks/MockFusePriceOracle.sol | 12 + src/test/unit/FuseERC4626.t.sol | 319 +++++++++++++++++++++++++ 4 files changed, 378 insertions(+), 5 deletions(-) create mode 100644 src/test/mocks/MockERC20.sol create mode 100644 src/test/mocks/MockFusePriceOracle.sol create mode 100644 src/test/unit/FuseERC4626.t.sol diff --git a/src/test/mocks/MockCToken.sol b/src/test/mocks/MockCToken.sol index d5f52f5..b50a761 100644 --- a/src/test/mocks/MockCToken.sol +++ b/src/test/mocks/MockCToken.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.10; -import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {MockERC20} from "./MockERC20.sol"; import {CToken} from "../../external/CToken.sol"; import {InterestRateModel} from "libcompound/interfaces/InterestRateModel.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; @@ -26,15 +26,21 @@ contract MockInterestRateModel is InterestRateModel { } contract MockUnitroller { - function supplyCaps(address cToken) external view returns (uint256) { + function supplyCaps( + address /* cToken*/ + ) external view returns (uint256) { return 100e18; } - function mintGuardianPaused(address cToken) external view returns (bool) { + function mintGuardianPaused( + address /* cToken*/ + ) external view returns (bool) { return false; } - function borrowGuardianPaused(address cToken) external view returns (bool) { + function borrowGuardianPaused( + address /* cToken*/ + ) external view returns (bool) { return false; } } @@ -49,7 +55,7 @@ contract MockCToken is MockERC20, CToken { uint256 private constant EXCHANGE_RATE_SCALE = 1e18; uint256 public effectiveExchangeRate = 2e18; - constructor(address _token, bool _isCEther) MockERC20("token", "TKN", 18) { + constructor(address _token, bool _isCEther) { token = MockERC20(_token); isCEther = _isCEther; irm = new MockInterestRateModel(); @@ -130,6 +136,20 @@ contract MockCToken is MockERC20, CToken { return error ? 1 : 0; } + function getAccountSnapshot(address) + external + view + override + returns ( + uint256, + uint256, + uint256, + uint256 + ) + { + return (0, 0, 0, 0); + } + function exchangeRateStored() external view override returns (uint256) { return (EXCHANGE_RATE_SCALE * effectiveExchangeRate) / EXCHANGE_RATE_SCALE; // 2:1 diff --git a/src/test/mocks/MockERC20.sol b/src/test/mocks/MockERC20.sol new file mode 100644 index 0000000..26f49e4 --- /dev/null +++ b/src/test/mocks/MockERC20.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.10; + +import {ERC20} from "solmate/tokens/ERC20.sol"; + +contract MockERC20 is ERC20 { + constructor() ERC20("MockToken", "MCT", 18) {} + + function mint(address account, uint256 amount) public returns (bool) { + _mint(account, amount); + return true; + } + + function mockBurn(address account, uint256 amount) public returns (bool) { + _burn(account, amount); + return true; + } + + function approveOverride(address owner, address spender, uint256 amount) public { + allowance[owner][spender] = amount; + } +} diff --git a/src/test/mocks/MockFusePriceOracle.sol b/src/test/mocks/MockFusePriceOracle.sol new file mode 100644 index 0000000..d48182e --- /dev/null +++ b/src/test/mocks/MockFusePriceOracle.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.10; + +contract MockFusePriceOracle { + + bool public constant isPriceOracle = true; + mapping(address=>uint256) public getUnderlyingPrice; + + function mockSetPrice(address cToken, uint256 value) external { + getUnderlyingPrice[cToken] = value; + } +} diff --git a/src/test/unit/FuseERC4626.t.sol b/src/test/unit/FuseERC4626.t.sol new file mode 100644 index 0000000..54d58f5 --- /dev/null +++ b/src/test/unit/FuseERC4626.t.sol @@ -0,0 +1,319 @@ +pragma solidity ^0.8.10; + +import {MockERC20} from "../mocks/MockERC20.sol"; +import {MockCToken} from "../mocks/MockCToken.sol"; +import {FuseERC4626} from "../../vaults/fuse/FuseERC4626.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; + +contract TestFuseERC4626 is DSTestPlus { + using FixedPointMathLib for uint256; + + MockERC20 private token; + MockCToken private cToken; + FuseERC4626 private vault; + + function setUp() public { + token = new MockERC20(); + cToken = new MockCToken(address(token), false); + vault = new FuseERC4626( + address(cToken), + "fTRIBE-8 ERC4626 wrapper", + "4626-fTRIBE-8" + ); + } + + /*/////////////////////////////////////////////////////////////// + init + //////////////////////////////////////////////////////////////*/ + + function testInit() public view { + // wrapper metadata + require(address(vault.cToken()) == address(cToken)); + require(address(vault.cTokenUnderlying()) == address(token)); + + // vault metadata + require(address(vault.asset()) == address(token)); + require(vault.totalAssets() == 0); + + // balance checks + require(token.balanceOf(address(this)) == 0); + require(token.balanceOf(address(cToken)) == 0); + require(vault.balanceOf(address(this)) == 0); + } + + /*/////////////////////////////////////////////////////////////// + deposit() + //////////////////////////////////////////////////////////////*/ + + function testDeposit1(uint128 _assets) public { + uint256 assets = uint256(_assets) + 1; // don't fuzz with 0 + address receiver = address(0x42); + + token.mint(address(this), assets); + token.approve(address(vault), assets); + uint256 expectedShares = vault.previewDeposit(assets); + uint256 shares = vault.deposit(assets, receiver); + require(shares == expectedShares); + + require( + vault.totalAssets() == assets || vault.totalAssets() == assets - 1 + ); + require(token.balanceOf(address(this)) == 0); + require(token.balanceOf(address(cToken)) == assets); + require(vault.balanceOf(receiver) == expectedShares); + } + + function testDeposit2() public { + cToken.setError(true); + token.mint(address(this), 1e18); + token.approve(address(vault), 1e18); + + hevm.expectRevert(bytes("MINT_FAILED")); + vault.deposit(1e18, address(this)); + } + + /*/////////////////////////////////////////////////////////////// + mint() + //////////////////////////////////////////////////////////////*/ + + function testMint1(uint128 _shares) public { + uint256 shares = uint256(_shares) + 1; // don't fuzz with 0 + address receiver = address(0x42); + + uint256 expectedAssets = vault.previewMint(shares); + token.mint(address(this), expectedAssets); + token.approve(address(vault), expectedAssets); + uint256 assets = vault.mint(shares, receiver); + require(assets == expectedAssets); + + require( + vault.totalAssets() == expectedAssets || + vault.totalAssets() == expectedAssets - 1 + ); + require(token.balanceOf(address(this)) == 0); + require(token.balanceOf(address(cToken)) == expectedAssets); + require(vault.balanceOf(receiver) == shares); + } + + function testMint2() public { + cToken.setError(true); + token.mint(address(this), 1e18); + token.approve(address(vault), 1e18); + + hevm.expectRevert(bytes("MINT_FAILED")); + vault.mint(5e17, address(this)); + } + + /*/////////////////////////////////////////////////////////////// + withdraw() + //////////////////////////////////////////////////////////////*/ + + function testWithdraw() public { + uint256 assets = 1e18; + address receiver = address(0x42); + address owner = address(this); + + token.mint(owner, assets); + token.approve(address(vault), assets); + uint256 depositShares = vault.deposit(assets, owner); + uint256 withdrawShares = vault.withdraw(assets, receiver, owner); + require(withdrawShares == depositShares); + + require(vault.totalAssets() == 0); + require(token.balanceOf(receiver) == assets); + require(token.balanceOf(address(cToken)) == 0); + require(vault.balanceOf(owner) == 0); + } + + function testWithdraw2() public { + address receiver = address(0x42); + address owner = address(this); + + token.mint(owner, 1e18); + token.approve(address(vault), 1e18); + vault.deposit(1e18, owner); + cToken.setError(true); + + hevm.expectRevert(bytes("REDEEM_FAILED")); + vault.withdraw(1e18, receiver, owner); + } + + function testWithdraw3() public { + address receiver = address(0x42); + address owner = address(this); + + token.mint(owner, 1e18); + token.approve(address(vault), 1e18); + vault.deposit(1e18, owner); + + // panic code 11 Arithmetic over/underflow + hevm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11)); + hevm.prank(receiver); + vault.withdraw(1e18, receiver, owner); + } + + /*/////////////////////////////////////////////////////////////// + redeem() + //////////////////////////////////////////////////////////////*/ + + function testRedeem1() public { + address receiver = address(0x42); + address owner = address(this); + + uint256 shares = 1e18; + + token.mint(owner, 1e18); + token.approve(address(vault), 1e18); + uint256 depositAssets = vault.mint(shares, owner); + uint256 redeemAssets = vault.redeem(shares, receiver, owner); + require(redeemAssets == depositAssets); + + require(vault.totalSupply() == 0); + require(token.balanceOf(receiver) == 1e18); + require(vault.balanceOf(owner) == 0); + } + + function testRedeem2() public { + address receiver = address(0x42); + address owner = address(this); + + token.mint(owner, 1e18); + token.approve(address(vault), 1e18); + vault.mint(5e17, owner); + cToken.setError(true); + + hevm.expectRevert(bytes("REDEEM_FAILED")); + vault.redeem(5e17, receiver, owner); + } + + function testRedeem3() public { + address receiver = address(0x42); + address owner = address(this); + + token.mint(owner, 1e18); + token.approve(address(vault), 1e18); + vault.mint(5e17, owner); + + // panic code 11 Arithmetic over/underflow + hevm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11)); + hevm.prank(receiver); + vault.redeem(5e17, receiver, owner); + } + + /*/////////////////////////////////////////////////////////////// + vault accounting viewers + //////////////////////////////////////////////////////////////*/ + + function testConvertToShares() public { + uint256 assets = 1e18; + uint256 expectedShares = assets; // 1:1 initially + uint256 actual = vault.convertToShares(assets); + require(actual == expectedShares); + + // first user enter the vault, ratio is 1:1 + token.mint(address(this), assets); + token.approve(address(vault), assets); + vault.deposit(assets, address(this)); + expectedShares = assets; + actual = vault.convertToShares(assets); + require(actual == expectedShares); + + // donate some cTokens to the vault + token.mint(address(this), assets); + token.approve(address(cToken), assets); + cToken.mint(assets); // get some cTokens on the test contract + cToken.transfer(address(vault), cToken.balanceOf(address(this))); // send cTokens to the vault + + // vault shares should now be worth 2 assets + expectedShares = assets / 2; // 1:2 + actual = vault.convertToShares(assets); + require(actual == expectedShares); + } + + function testConvertToAssets() public { + uint256 shares = 1e18; + uint256 expectedAssets = shares; // 1:1 initially + uint256 actual = vault.convertToAssets(shares); + require(actual == expectedAssets); + + // first user enter the vault, ratio is 1:1 + token.mint(address(this), shares); + token.approve(address(vault), shares); + vault.deposit(shares, address(this)); + expectedAssets = shares; + actual = vault.convertToAssets(shares); + require(actual == expectedAssets); + + // donate some cTokens to the vault + token.mint(address(this), shares); + token.approve(address(cToken), shares); + cToken.mint(shares); // get some cTokens on the test contract + cToken.transfer(address(vault), cToken.balanceOf(address(this))); // send cTokens to the vault + + // vault shares should now be worth 2 assets + expectedAssets = shares * 2; // 1:2 + actual = vault.convertToAssets(shares); + require(actual == expectedAssets); + } + + function testMaxDeposit() public view { + address owner = address(0x42); + uint256 expected = 100e18; + uint256 actual = vault.maxDeposit(owner); + require(actual == expected); + } + + function testPreviewDeposit(uint128 assets) public view { + uint256 expected = uint256(assets); // 1:1 initially + uint256 actual = vault.previewDeposit(assets); + require(actual == expected); + } + + function testMaxMint() public view { + address owner = address(0x42); + uint256 expected = 100e18; + uint256 actual = vault.maxMint(owner); + require(actual == expected); + } + + function testPreviewMint(uint128 shares) public view { + uint256 expected = uint256(shares); // 1:1 initially + uint256 actual = vault.previewMint(shares); + require(actual == expected); + } + + function testMaxWithdraw() public { + address owner = address(0x42); + require(vault.maxWithdraw(owner) == 0); + token.mint(owner, 1e18); + hevm.prank(owner); + token.approve(address(vault), 1e18); + hevm.prank(owner); + vault.deposit(1e18, owner); + require(vault.maxWithdraw(owner) == 1e18); + } + + function testPreviewWithdraw(uint128 assets) public view { + uint256 expected = assets; // 1:1 initially + uint256 actual = vault.previewWithdraw(assets); + require(actual == expected); + } + + function testMaxRedeem() public { + address owner = address(0x42); + require(vault.maxRedeem(owner) == 0); + token.mint(owner, 1e18); + hevm.prank(owner); + token.approve(address(vault), 1e18); + hevm.prank(owner); + vault.mint(1e17, owner); + require(vault.maxRedeem(owner) == 1e17); + } + + function testPreviewRedeem(uint128 shares) public view { + uint256 expected = uint256(shares); // 1:1 initially + uint256 actual = vault.previewRedeem(shares); + require(actual == expected); + } +} From a6df915cbdbdf7cbf44e814306f8e4c60d176bcf Mon Sep 17 00:00:00 2001 From: Erwan Beauvois Date: Mon, 28 Mar 2022 16:50:40 +0200 Subject: [PATCH 04/10] Add FuseERC4526 integration tests --- src/test/integration/FuseERC4626.t.sol | 524 +++++++++++++++++++++++++ 1 file changed, 524 insertions(+) create mode 100644 src/test/integration/FuseERC4626.t.sol diff --git a/src/test/integration/FuseERC4626.t.sol b/src/test/integration/FuseERC4626.t.sol new file mode 100644 index 0000000..d0d5234 --- /dev/null +++ b/src/test/integration/FuseERC4626.t.sol @@ -0,0 +1,524 @@ +pragma solidity ^0.8.10; + +import {MockERC20} from "../mocks/MockERC20.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {MockFusePriceOracle} from "../mocks/MockFusePriceOracle.sol"; +import {CToken} from "../../external/CToken.sol"; +import {Unitroller} from "../../external/Unitroller.sol"; +import {FuseERC4626} from "../../vaults/fuse/FuseERC4626.sol"; +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; + +contract IntegrationTestFuseERC4626 is DSTestPlus { + MockERC20 private token; + MockERC20 private token2; + CToken private cToken; + CToken private cToken2; + MockFusePriceOracle oracle; + address private masterOracle; + Unitroller private troller; + FuseERC4626 private vault; + + function setUp() public { + hevm.label(address(this), "IntegrationTestFuseERC4626"); + + token = new MockERC20(); + token2 = new MockERC20(); + oracle = new MockFusePriceOracle(); + hevm.label(address(token2), "token2"); + hevm.label(address(token), "token"); + hevm.label(address(oracle), "oracle"); + + // Rari addresses + hevm.label( + address(0x91cE5566DC3170898C5aeE4ae4dD314654B47415), + "MasterPriceOracle InitializableClones" + ); + hevm.label( + address(0xb3c8eE7309BE658c186F986388c2377da436D8fb), + "MasterPriceOracle Implementation" + ); + hevm.label( + address(0x1887118E49e0F4A78Bd71B792a49dE03504A764D), + "MasterPriceOracle Rari (default)" + ); + hevm.label( + address(0x835482FE0532f169024d5E9410199369aAD5C77E), + "Rari Capital: Fuse Pool Directory" + ); + hevm.label( + address(0xE16DB319d9dA7Ce40b666DD2E365a4b8B3C18217), + "Rari Capital: Comptroller implementation" + ); + hevm.label( + address(0xbAB47e4B692195BF064923178A90Ef999A15f819), + "JumpRateModel" + ); + hevm.label( + address(0x67Db14E73C2Dce786B5bbBfa4D010dEab4BBFCF9), + "CErc20Delegate" + ); + + // create a new master price oracle + (bool success, bytes memory data) = address( + 0x91cE5566DC3170898C5aeE4ae4dD314654B47415 + ).call( + abi.encodeWithSignature( + "clone(address,bytes)", + address(0xb3c8eE7309BE658c186F986388c2377da436D8fb), // MasterPriceOracle implementation + abi.encodeWithSignature( + "initialize(address[],address[],address,address,bool)", + new address[](0), // underlyings + new address[](0), // oracles + address(0x1887118E49e0F4A78Bd71B792a49dE03504A764D), // default oracle = Rari master price oracle + address(this), // pool admin + true // canAdminOwerwrite + ) + ) + ); + require(success, "Error creating master price oracle"); + assembly { + sstore(masterOracle.slot, mload(add(data, 32))) + } + hevm.label(address(masterOracle), "Test MasterPriceOracle"); + + // create a new Rari pool (call Rari Capital: Fuse Pool Directory) + (success, data) = address(0x835482FE0532f169024d5E9410199369aAD5C77E) + .call( + abi.encodeWithSignature( + "deployPool(string,address,bool,uint256,uint256,address)", + "Test Pool", // name + address(0xE16DB319d9dA7Ce40b666DD2E365a4b8B3C18217), // implementation = Rari Capital: Comptroller Implementation + false, // enforceWhitelist + 0.5e18, // closeFactor + 1.08e18, // liquidationIncentive + masterOracle // priceOracle + ) + ); + require(success, "Error creating pool"); + assembly { + sstore(troller.slot, mload(add(data, 64))) + } + hevm.label(address(troller), "Test Troller"); + + // accept admin of the comptroller + troller._acceptAdmin(); + assertEq(troller.admin(), address(this)); + + // add token price to master oracle + address[] memory underlyings = new address[](2); + address[] memory oracles = new address[](2); + underlyings[0] = address(token); + oracles[0] = address(oracle); + underlyings[1] = address(token2); + oracles[1] = address(oracle); + (success, data) = masterOracle.call( + abi.encodeWithSignature( + "add(address[],address[])", + underlyings, + oracles + ) + ); + require(success, "Error setting new token price feed in master oracle"); + + // add mock token in the fuse pool + troller._deployMarket( + false, // isCEther + abi.encode( // CErc20Delegator constructor data + address(token), // underlying + address(troller), // comptroller + address(0xbAB47e4B692195BF064923178A90Ef999A15f819), // interestRateModel: JumpRateModel + "fToken-x", // name + "Fuse Token for MockToken", // symbol + address(0x67Db14E73C2Dce786B5bbBfa4D010dEab4BBFCF9), // implementation: CErc20Delegate + bytes(""), // becomeImplementationData + uint256(0), // reserveFactorMantissa + uint256(0) // adminFeeMantissa + ), + 0.7e18 // collateralFactorMantissa + ); + cToken = CToken(troller.cTokensByUnderlying(address(token))); + require( + address(cToken) != address(0), + "Error adding mock token to Fuse pool" + ); + hevm.label(address(cToken), "fToken-x"); + + // add mock token2 in the fuse pool + troller._deployMarket( + false, // isCEther + abi.encode( // CErc20Delegator constructor data + address(token2), // underlying + address(troller), // comptroller + address(0xbAB47e4B692195BF064923178A90Ef999A15f819), // interestRateModel: JumpRateModel + "fToken2-x", // name + "Fuse Token for MockToken2", // symbol + address(0x67Db14E73C2Dce786B5bbBfa4D010dEab4BBFCF9), // implementation: CErc20Delegate + bytes(""), // becomeImplementationData + uint256(0), // reserveFactorMantissa + uint256(0) // adminFeeMantissa + ), + 0.7e18 // collateralFactorMantissa + ); + cToken2 = CToken(troller.cTokensByUnderlying(address(token2))); + require( + address(cToken2) != address(0), + "Error adding mock token2 to Fuse pool" + ); + hevm.label(address(cToken2), "fToken2-x"); + + // set oracle price values (static = 1 ETH) + oracle.mockSetPrice(address(cToken), 1e18); + oracle.mockSetPrice(address(cToken2), 1e18); + + // create vault + vault = new FuseERC4626( + address(cToken), + "fToken-x ERC4626 wrapper", + "4626-fToken-x" + ); + hevm.label(address(vault), "vault"); + + // allow this contract to use both tokens as collateral + // and borrow both tokens + address[] memory cTokens = new address[](2); + cTokens[0] = address(cToken); + cTokens[1] = address(cToken2); + uint256[] memory results = troller.enterMarkets(cTokens); + require(results[0] == 0, "Failed to enter cToken market"); + require(results[1] == 0, "Failed to enter cToken2 market"); + } + + function testFeiRari() public { + hevm.label(0x956F47F50A910163D8BF957Cf5846D573E7f87CA, "fei"); + hevm.label(0xd8553552f8868C1Ef160eEdf031cF0BCf9686945, "fFei8"); + hevm.label(0xd51dbA7a94e1adEa403553A8235C302cEbF41a3c, "timelock"); + hevm.label(0x8d5ED43dCa8C2F7dFB20CF7b53CC7E593635d7b9, "core"); + + ERC20 fei = ERC20(0x956F47F50A910163D8BF957Cf5846D573E7f87CA); // FEI + CToken fFei8 = CToken(0xd8553552f8868C1Ef160eEdf031cF0BCf9686945); // fFEI-8 + + // call core.grantMinter(TestContract) as FEI DAO Timelock + hevm.prank(0xd51dbA7a94e1adEa403553A8235C302cEbF41a3c); // DAO timelock + (bool success, ) = address(0x8d5ED43dCa8C2F7dFB20CF7b53CC7E593635d7b9) // core + .call( + abi.encodeWithSignature("grantMinter(address)", address(this)) + ); + require(success, "failed to grant minter"); + FuseERC4626 fFei8Vault = new FuseERC4626( + address(fFei8), + "fFEI-8 ERC4626 wrapper", + "wfFEI-8" + ); + + // mint FEI to self + (success, ) = address(0x956F47F50A910163D8BF957Cf5846D573E7f87CA).call( // fei + abi.encodeWithSignature( + "mint(address,uint256)", + address(this), + 100000 ether + ) + ); + require(success, "failed to mint fei"); + + // allow this contract to use feirari + address[] memory cTokens = new address[](1); + cTokens[0] = address(fFei8); + uint256[] memory results = Unitroller(fFei8.comptroller()).enterMarkets( + cTokens + ); + require(results[0] == 0, "Failed to enter fFEI-8 market"); + + // deposit in the vault and in the cToken directly + fei.approve(address(fFei8Vault), 50000 ether); + fei.approve(address(fFei8), 50000 ether); + require(fFei8.mint(50000 ether) == 0, "mint failed"); + fFei8Vault.deposit(50000 ether, address(this)); + + // borrow + require(fFei8.borrow(25000 ether) == 0, "borrow failed"); + + // check balances + assertEq(fei.balanceOf(address(this)), 25000 ether); + assertApproxEq(fFei8Vault.totalAssets(), 50000 ether, 2); + assertApproxEq( + fFei8.balanceOf(address(fFei8Vault)), + fFei8.balanceOf(address(this)), + 1 + ); + } + + function testInit() public { + // wrapper metadata + assertEq(address(vault.cToken()), address(cToken)); + assertEq(address(vault.cTokenUnderlying()), address(token)); + + // vault metadata + assertEq(address(vault.asset()), address(token)); + assertEq(vault.totalAssets(), 0); + assertEq(vault.totalSupply(), 0); + + // balance checks + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(address(cToken)), 0); + assertEq(vault.balanceOf(address(this)), 0); + } + + function testMaxDepositPaused() public { + // there should be no maximum to deposit initially + assertEq(vault.maxDeposit(address(this)), type(uint256).max); + + // pause cToken.mint + troller._setMintPaused(address(cToken), true); + + // the max deposit should be updated + assertEq(vault.maxDeposit(address(this)), 0); + } + + function testMaxDepositSupplyCap(uint256 supplyCap) public { + hevm.assume(supplyCap > 0); // 0 = no supply cap + + // there should be no maximum to deposit initially + assertEq(vault.maxDeposit(address(this)), type(uint256).max); + + // set supply cap in Fuse market to supplyCap + address[] memory cTokens = new address[](1); + cTokens[0] = address(cToken); + uint256[] memory newSupplyCaps = new uint256[](1); + newSupplyCaps[0] = supplyCap; + troller._setMarketSupplyCaps(cTokens, newSupplyCaps); + + // the max deposit should be updated + assertEq(vault.maxDeposit(address(this)), supplyCap); + } + + function testMaxMintPaused() public { + // there should be no maximum to mint initially + assertEq(vault.maxMint(address(this)), type(uint256).max); + + // pause cToken.mint + troller._setMintPaused(address(cToken), true); + + // the max mint should be updated + assertEq(vault.maxMint(address(this)), 0); + } + + function testMaxMintSupplyCap(uint256 supplyCap) public { + hevm.assume(supplyCap > 0); // 0 = no supply cap + + // there should be no maximum to mint initially + assertEq(vault.maxMint(address(this)), type(uint256).max); + + // set supply cap in Fuse market to supplyCap + address[] memory cTokens = new address[](1); + cTokens[0] = address(cToken); + uint256[] memory newSupplyCaps = new uint256[](1); + newSupplyCaps[0] = supplyCap; + troller._setMarketSupplyCaps(cTokens, newSupplyCaps); + + // the max mint should be updated + assertEq(vault.maxMint(address(this)), supplyCap); + } + + function testMaxWithdrawNoCash() public { + // deposit + token.mint(address(this), 1e18); + token.approve(address(vault), 1e18); + vault.deposit(1e18, address(this)); + + // check the withdrawable amounts + assertEq(vault.maxWithdraw(address(this)), 1e18); + + // simulate a user borrowing half of the tokens using another token as collateral + uint256 cash = 0.5e18; + hevm.mockCall( + address(cToken), + abi.encodeWithSignature("getCash()"), + abi.encodePacked(cash) + ); + + // check updated withdrawable amounts + assertEq(cToken.getCash(), 0.5e18); + assertEq(vault.maxWithdraw(address(this)), 0.5e18); + } + + function testMaxRedeemNoCash() public { + // deposit + token.mint(address(this), 1e18); + token.approve(address(vault), 1e18); + vault.deposit(1e18, address(this)); + + // check the redeemable amounts + assertEq(vault.maxRedeem(address(this)), 1e18); + + // simulate a user borrowing half of the tokens using another token as collateral + uint256 cash = 0.5e18; + hevm.mockCall( + address(cToken), + abi.encodeWithSignature("getCash()"), + abi.encodePacked(cash) + ); + + // check updated redeemable amounts + assertEq(cToken.getCash(), 0.5e18); + assertEq(vault.maxRedeem(address(this)), 0.5e18); + } + + function testTotalAssets(uint256 deposit1) public { + // don't fuzz dust + hevm.assume(deposit1 > 1000); + // on high values, cToken reverts with MINT_EXCHANGE_CALCULATION_FAILED + // there are also rounding errors in solmate's wadMul functions above + // type(uint128).max + hevm.assume(deposit1 <= type(uint128).max); + + uint256 deposit2 = deposit1 / 2; + uint256 donation = deposit1; + + // the cToken.exchangeRate() can be off by 1 wei, and this exchange + // rate is used to convert between the number of cTokens held to the + // number of underlying tokens, so if we have 1000 tokens, we can + // have up to 5000 wei of error (5 cToken = 1 token). + uint256 tolerance = 5 * ((deposit1 + deposit2 + donation + 1) / 1e18); + // under this amount of tokens, the rounding errors become more + // significant (rounded down divisions etc), so we have to tolerate + // at least 8 wei of error : + // - the cToken rounds down + // - libfuse rounds 6 times (totalBorrows, interestAccumulated, totalReserves, totalAdminFees, totalFuseFees, final mulwadDown) + // - the vault rounds down + if (tolerance < 8) tolerance = 8; + + // Example of what happens if we tolerate a fixed error of 1000: + // 399 986201907602584529 DEPOSIT 1 + // 199 993100953801292264 DEPOSIT 2 + // 599 979302861403876793 DEPOSIT 1 + DEPOSIT 2 + // 999 965504769006461322 DEPOSIT 1 + DEPOSIT 2 + DONATION(=DEPOSIT 1) + // 999 965504769006460321 AFTER DONATION vault.totalAssets() + // => 1001 error + // 999 965504769006461322 TOKEN BALANCE ON CTOKEN + // 2999 013786576756947450 cToken.totalSupply() + // 200000000000000000 cToken.echangeRateStored() initial + // 333333333333333333 cToken.echangeRateStored() after donation + // ~3000 cTokens * 0.20 exchangeRate = 600 tokens => ok before donation + // ~3000 cTokens * 0.33 exchangeRate = 1000 tokens => ok after donation + + // there should be no assets initially + assertEq(vault.totalAssets(), 0); + + // first user deposit + token.mint(address(this), deposit1); + token.approve(address(vault), deposit1); + uint256 shares1 = vault.deposit(deposit1, address(this)); + + // after deposit1 checks + assertApproxEq(vault.totalAssets(), deposit1, tolerance); + assertEq(shares1, deposit1); // initial deposit is 1:1 + assertEq(vault.totalSupply(), deposit1); + + // second user deposit + token.mint(address(this), deposit2); + token.approve(address(vault), deposit2); + uint256 shares2 = vault.deposit(deposit2, address(this)); + + // after deposit2 checks + assertApproxEq(vault.totalAssets(), deposit1 + deposit2, tolerance); + assertEq(vault.totalSupply(), shares1 + shares2); + + // donation to cToken (increasing share price) + token.mint(address(cToken), donation); + assertEq( + token.balanceOf(address(cToken)), + deposit1 + deposit2 + donation + ); + + // after donation checks + assertApproxEq( + vault.totalAssets(), + deposit1 + deposit2 + donation, + tolerance + ); + assertEq(vault.totalSupply(), shares1 + shares2); // should not move + } + + function testMint(uint256 shares) public { + // Solmate standard implementation don't allow 0 deposit/mints + hevm.assume(shares > 0); + // on high values, cToken reverts with MINT_EXCHANGE_CALCULATION_FAILED + hevm.assume(shares <= type(uint128).max); + + uint256 previewAssets = vault.previewMint(shares); + token.mint(address(this), previewAssets); + token.approve(address(vault), previewAssets); + uint256 balanceBefore = token.balanceOf(address(this)); + uint256 returnedAssets = vault.mint(shares, address(this)); + uint256 spentAssets = balanceBefore - token.balanceOf(address(this)); + + assertEq(previewAssets, spentAssets); + assertEq(returnedAssets, spentAssets); + } + + function testDeposit(uint256 assets) public { + // Solmate standard implementation don't allow 0 deposit/mints + hevm.assume(assets > 0); + // on high values, cToken reverts with MINT_EXCHANGE_CALCULATION_FAILED + hevm.assume(assets <= type(uint128).max); + + uint256 previewShares = vault.previewDeposit(assets); + token.mint(address(this), assets); + token.approve(address(vault), assets); + uint256 balanceBefore = vault.balanceOf(address(this)); + uint256 returnedShares = vault.deposit(assets, address(this)); + uint256 receivedShares = vault.balanceOf(address(this)) - balanceBefore; + + assertEq(previewShares, receivedShares); + assertEq(returnedShares, receivedShares); + } + + function testRedeem(uint256 shares) public { + // Solmate standard implementation don't allow 0 deposit/mints + hevm.assume(shares > 0); + // on high values, cToken reverts with MINT_EXCHANGE_CALCULATION_FAILED + hevm.assume(shares <= type(uint128).max); + + token.mint(address(this), type(uint256).max); + token.approve(address(vault), type(uint256).max); + vault.mint(shares, address(this)); + assertEq(vault.balanceOf(address(this)), shares); + + uint256 previewAssets = vault.previewRedeem(shares); + uint256 balanceBefore = token.balanceOf(address(this)); + uint256 returnedAssets = vault.redeem( + shares, + address(this), + address(this) + ); + uint256 receivedAssets = token.balanceOf(address(this)) - balanceBefore; + + assertEq(previewAssets, receivedAssets); + assertEq(returnedAssets, receivedAssets); + } + + function testWithdraw(uint256 assets) public { + // Solmate standard implementation don't allow 0 deposit/mints + hevm.assume(assets > 0); + // on high values, cToken reverts with MINT_EXCHANGE_CALCULATION_FAILED + hevm.assume(assets <= type(uint128).max - 2); + + // +2 because cToken rounds down & vault rounds down + // so that's the worst rounding error we can get + token.mint(address(this), assets + 2); + token.approve(address(vault), assets + 2); + vault.deposit(assets + 2, address(this)); + + uint256 previewShares = vault.previewWithdraw(assets); + uint256 balanceBefore = vault.balanceOf(address(this)); + uint256 returnedShares = vault.withdraw( + assets, + address(this), + address(this) + ); + uint256 spentShares = balanceBefore - vault.balanceOf(address(this)); + + assertEq(spentShares, previewShares); + assertEq(returnedShares, spentShares); + } +} From e9a4b8224fa772e70e9cbde0cf911ec12bf8250e Mon Sep 17 00:00:00 2001 From: Erwan Beauvois Date: Tue, 29 Mar 2022 14:21:33 +0200 Subject: [PATCH 05/10] Use hevm.assume in FuseERC4626 unit tests --- src/test/unit/FuseERC4626.t.sol | 38 +++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/test/unit/FuseERC4626.t.sol b/src/test/unit/FuseERC4626.t.sol index 54d58f5..5b3e1e5 100644 --- a/src/test/unit/FuseERC4626.t.sol +++ b/src/test/unit/FuseERC4626.t.sol @@ -46,8 +46,10 @@ contract TestFuseERC4626 is DSTestPlus { deposit() //////////////////////////////////////////////////////////////*/ - function testDeposit1(uint128 _assets) public { - uint256 assets = uint256(_assets) + 1; // don't fuzz with 0 + function testDeposit1(uint256 assets) public { + hevm.assume(assets > 0); + hevm.assume(assets <= type(uint128).max); + address receiver = address(0x42); token.mint(address(this), assets); @@ -77,8 +79,10 @@ contract TestFuseERC4626 is DSTestPlus { mint() //////////////////////////////////////////////////////////////*/ - function testMint1(uint128 _shares) public { - uint256 shares = uint256(_shares) + 1; // don't fuzz with 0 + function testMint1(uint256 shares) public { + hevm.assume(shares > 0); + hevm.assume(shares <= type(uint128).max); + address receiver = address(0x42); uint256 expectedAssets = vault.previewMint(shares); @@ -264,8 +268,11 @@ contract TestFuseERC4626 is DSTestPlus { require(actual == expected); } - function testPreviewDeposit(uint128 assets) public view { - uint256 expected = uint256(assets); // 1:1 initially + function testPreviewDeposit(uint256 assets) public { + hevm.assume(assets > 0); + hevm.assume(assets <= type(uint128).max); + + uint256 expected = assets; // 1:1 initially uint256 actual = vault.previewDeposit(assets); require(actual == expected); } @@ -277,8 +284,11 @@ contract TestFuseERC4626 is DSTestPlus { require(actual == expected); } - function testPreviewMint(uint128 shares) public view { - uint256 expected = uint256(shares); // 1:1 initially + function testPreviewMint(uint256 shares) public { + hevm.assume(shares > 0); + hevm.assume(shares <= type(uint128).max); + + uint256 expected = shares; // 1:1 initially uint256 actual = vault.previewMint(shares); require(actual == expected); } @@ -294,7 +304,10 @@ contract TestFuseERC4626 is DSTestPlus { require(vault.maxWithdraw(owner) == 1e18); } - function testPreviewWithdraw(uint128 assets) public view { + function testPreviewWithdraw(uint256 assets) public { + hevm.assume(assets > 0); + hevm.assume(assets <= type(uint128).max); + uint256 expected = assets; // 1:1 initially uint256 actual = vault.previewWithdraw(assets); require(actual == expected); @@ -311,8 +324,11 @@ contract TestFuseERC4626 is DSTestPlus { require(vault.maxRedeem(owner) == 1e17); } - function testPreviewRedeem(uint128 shares) public view { - uint256 expected = uint256(shares); // 1:1 initially + function testPreviewRedeem(uint256 shares) public { + hevm.assume(shares > 0); + hevm.assume(shares <= type(uint128).max); + + uint256 expected = shares; // 1:1 initially uint256 actual = vault.previewRedeem(shares); require(actual == expected); } From f11e83ffd043351840ffc638b2626489b471d2f2 Mon Sep 17 00:00:00 2001 From: Erwan Beauvois Date: Tue, 29 Mar 2022 14:22:40 +0200 Subject: [PATCH 06/10] Split Fuse pool creation in a separate util library --- src/test/integration/FuseERC4626.t.sol | 163 ++++++------------------- src/test/utils/FusePoolUtils.sol | 106 ++++++++++++++++ 2 files changed, 140 insertions(+), 129 deletions(-) create mode 100644 src/test/utils/FusePoolUtils.sol diff --git a/src/test/integration/FuseERC4626.t.sol b/src/test/integration/FuseERC4626.t.sol index d0d5234..956a6ce 100644 --- a/src/test/integration/FuseERC4626.t.sol +++ b/src/test/integration/FuseERC4626.t.sol @@ -7,6 +7,7 @@ import {CToken} from "../../external/CToken.sol"; import {Unitroller} from "../../external/Unitroller.sol"; import {FuseERC4626} from "../../vaults/fuse/FuseERC4626.sol"; import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; +import {FusePoolUtils} from "../utils/FusePoolUtils.sol"; contract IntegrationTestFuseERC4626 is DSTestPlus { MockERC20 private token; @@ -24,151 +25,58 @@ contract IntegrationTestFuseERC4626 is DSTestPlus { token = new MockERC20(); token2 = new MockERC20(); oracle = new MockFusePriceOracle(); - hevm.label(address(token2), "token2"); - hevm.label(address(token), "token"); + + address[] memory tokens = new address[](2); + tokens[0] = address(token); + tokens[1] = address(token2); + hevm.label(tokens[0], "tokens[0]"); + hevm.label(tokens[1], "tokens[1]"); hevm.label(address(oracle), "oracle"); // Rari addresses hevm.label( - address(0x91cE5566DC3170898C5aeE4ae4dD314654B47415), + FusePoolUtils.MASTER_PRICE_ORACLE_INITIALIZABLECLONES, "MasterPriceOracle InitializableClones" ); hevm.label( - address(0xb3c8eE7309BE658c186F986388c2377da436D8fb), + FusePoolUtils.MASTER_PRICE_ORACLE_IMPLEMENTATION, "MasterPriceOracle Implementation" ); hevm.label( - address(0x1887118E49e0F4A78Bd71B792a49dE03504A764D), + FusePoolUtils.MASTER_PRICE_ORACLE_RARI_DEFAULT, "MasterPriceOracle Rari (default)" ); hevm.label( - address(0x835482FE0532f169024d5E9410199369aAD5C77E), + FusePoolUtils.FUSE_POOL_DIRECTORY, "Rari Capital: Fuse Pool Directory" ); hevm.label( - address(0xE16DB319d9dA7Ce40b666DD2E365a4b8B3C18217), + FusePoolUtils.FUSE_COMPTROLLER_IMPLEMENTATION, "Rari Capital: Comptroller implementation" ); - hevm.label( - address(0xbAB47e4B692195BF064923178A90Ef999A15f819), - "JumpRateModel" - ); - hevm.label( - address(0x67Db14E73C2Dce786B5bbBfa4D010dEab4BBFCF9), - "CErc20Delegate" - ); - - // create a new master price oracle - (bool success, bytes memory data) = address( - 0x91cE5566DC3170898C5aeE4ae4dD314654B47415 - ).call( - abi.encodeWithSignature( - "clone(address,bytes)", - address(0xb3c8eE7309BE658c186F986388c2377da436D8fb), // MasterPriceOracle implementation - abi.encodeWithSignature( - "initialize(address[],address[],address,address,bool)", - new address[](0), // underlyings - new address[](0), // oracles - address(0x1887118E49e0F4A78Bd71B792a49dE03504A764D), // default oracle = Rari master price oracle - address(this), // pool admin - true // canAdminOwerwrite - ) - ) - ); - require(success, "Error creating master price oracle"); - assembly { - sstore(masterOracle.slot, mload(add(data, 32))) - } - hevm.label(address(masterOracle), "Test MasterPriceOracle"); - - // create a new Rari pool (call Rari Capital: Fuse Pool Directory) - (success, data) = address(0x835482FE0532f169024d5E9410199369aAD5C77E) - .call( - abi.encodeWithSignature( - "deployPool(string,address,bool,uint256,uint256,address)", - "Test Pool", // name - address(0xE16DB319d9dA7Ce40b666DD2E365a4b8B3C18217), // implementation = Rari Capital: Comptroller Implementation - false, // enforceWhitelist - 0.5e18, // closeFactor - 1.08e18, // liquidationIncentive - masterOracle // priceOracle - ) - ); - require(success, "Error creating pool"); - assembly { - sstore(troller.slot, mload(add(data, 64))) - } - hevm.label(address(troller), "Test Troller"); - - // accept admin of the comptroller - troller._acceptAdmin(); - assertEq(troller.admin(), address(this)); - - // add token price to master oracle - address[] memory underlyings = new address[](2); - address[] memory oracles = new address[](2); - underlyings[0] = address(token); - oracles[0] = address(oracle); - underlyings[1] = address(token2); - oracles[1] = address(oracle); - (success, data) = masterOracle.call( - abi.encodeWithSignature( - "add(address[],address[])", - underlyings, - oracles - ) - ); - require(success, "Error setting new token price feed in master oracle"); - - // add mock token in the fuse pool - troller._deployMarket( - false, // isCEther - abi.encode( // CErc20Delegator constructor data - address(token), // underlying - address(troller), // comptroller - address(0xbAB47e4B692195BF064923178A90Ef999A15f819), // interestRateModel: JumpRateModel - "fToken-x", // name - "Fuse Token for MockToken", // symbol - address(0x67Db14E73C2Dce786B5bbBfa4D010dEab4BBFCF9), // implementation: CErc20Delegate - bytes(""), // becomeImplementationData - uint256(0), // reserveFactorMantissa - uint256(0) // adminFeeMantissa - ), - 0.7e18 // collateralFactorMantissa - ); - cToken = CToken(troller.cTokensByUnderlying(address(token))); - require( - address(cToken) != address(0), - "Error adding mock token to Fuse pool" - ); - hevm.label(address(cToken), "fToken-x"); - - // add mock token2 in the fuse pool - troller._deployMarket( - false, // isCEther - abi.encode( // CErc20Delegator constructor data - address(token2), // underlying - address(troller), // comptroller - address(0xbAB47e4B692195BF064923178A90Ef999A15f819), // interestRateModel: JumpRateModel - "fToken2-x", // name - "Fuse Token for MockToken2", // symbol - address(0x67Db14E73C2Dce786B5bbBfa4D010dEab4BBFCF9), // implementation: CErc20Delegate - bytes(""), // becomeImplementationData - uint256(0), // reserveFactorMantissa - uint256(0) // adminFeeMantissa - ), - 0.7e18 // collateralFactorMantissa - ); - cToken2 = CToken(troller.cTokensByUnderlying(address(token2))); - require( - address(cToken2) != address(0), - "Error adding mock token2 to Fuse pool" - ); - hevm.label(address(cToken2), "fToken2-x"); + hevm.label(FusePoolUtils.FUSE_JUMP_RATE_MODEL, "JumpRateModel"); + hevm.label(FusePoolUtils.FUSE_CERC20_DELEGATE, "CErc20Delegate"); + + // create the Fuse pool + ( + address _masterOracle, + address _troller, + address[] memory _cTokens + ) = FusePoolUtils.createPool(tokens, address(oracle)); + + // set state variables & hevm labels + masterOracle = _masterOracle; + troller = Unitroller(_troller); + cToken = CToken(_cTokens[0]); + cToken2 = CToken(_cTokens[1]); + hevm.label(_masterOracle, "masterOracle"); + hevm.label(_troller, "troller"); + hevm.label(_cTokens[0], "cToken"); + hevm.label(_cTokens[1], "cToken2"); // set oracle price values (static = 1 ETH) - oracle.mockSetPrice(address(cToken), 1e18); - oracle.mockSetPrice(address(cToken2), 1e18); + oracle.mockSetPrice(_cTokens[0], 1e18); + oracle.mockSetPrice(_cTokens[1], 1e18); // create vault vault = new FuseERC4626( @@ -180,10 +88,7 @@ contract IntegrationTestFuseERC4626 is DSTestPlus { // allow this contract to use both tokens as collateral // and borrow both tokens - address[] memory cTokens = new address[](2); - cTokens[0] = address(cToken); - cTokens[1] = address(cToken2); - uint256[] memory results = troller.enterMarkets(cTokens); + uint256[] memory results = troller.enterMarkets(_cTokens); require(results[0] == 0, "Failed to enter cToken market"); require(results[1] == 0, "Failed to enter cToken2 market"); } diff --git a/src/test/utils/FusePoolUtils.sol b/src/test/utils/FusePoolUtils.sol new file mode 100644 index 0000000..27fce19 --- /dev/null +++ b/src/test/utils/FusePoolUtils.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {Unitroller} from "../../external/Unitroller.sol"; + +library FusePoolUtils { + address public constant MASTER_PRICE_ORACLE_INITIALIZABLECLONES = + address(0x91cE5566DC3170898C5aeE4ae4dD314654B47415); + address public constant MASTER_PRICE_ORACLE_IMPLEMENTATION = + address(0xb3c8eE7309BE658c186F986388c2377da436D8fb); + address public constant MASTER_PRICE_ORACLE_RARI_DEFAULT = + address(0x1887118E49e0F4A78Bd71B792a49dE03504A764D); + address public constant FUSE_POOL_DIRECTORY = + address(0x835482FE0532f169024d5E9410199369aAD5C77E); + address public constant FUSE_COMPTROLLER_IMPLEMENTATION = + address(0xE16DB319d9dA7Ce40b666DD2E365a4b8B3C18217); + address public constant FUSE_JUMP_RATE_MODEL = + address(0xbAB47e4B692195BF064923178A90Ef999A15f819); + address public constant FUSE_CERC20_DELEGATE = + address(0x67Db14E73C2Dce786B5bbBfa4D010dEab4BBFCF9); + + function createPool(address[] memory tokens, address oracle) + external + returns ( + address masterOracle, + address troller, + address[] memory cTokens + ) + { + // create a new master price oracle + (bool success, bytes memory data) = MASTER_PRICE_ORACLE_INITIALIZABLECLONES + .call( + abi.encodeWithSignature( + "clone(address,bytes)", + MASTER_PRICE_ORACLE_IMPLEMENTATION, + abi.encodeWithSignature( + "initialize(address[],address[],address,address,bool)", + new address[](0), // underlyings + new address[](0), // oracles + MASTER_PRICE_ORACLE_RARI_DEFAULT, // default oracle + address(this), // pool admin + true // canAdminOwerwrite + ) + ) + ); + require(success, "Error creating master price oracle"); + assembly { + masterOracle := mload(add(data, 32)) + } + + // create a new Rari pool (call Rari Capital: Fuse Pool Directory) + (success, data) = FUSE_POOL_DIRECTORY.call( + abi.encodeWithSignature( + "deployPool(string,address,bool,uint256,uint256,address)", + "Test Pool", // name + FUSE_COMPTROLLER_IMPLEMENTATION, // implementation + false, // enforceWhitelist + 0.5e18, // closeFactor + 1.08e18, // liquidationIncentive + masterOracle // priceOracle + ) + ); + require(success, "Error creating pool"); + assembly { + troller := mload(add(data, 64)) + } + + // accept admin of the comptroller + Unitroller(troller)._acceptAdmin(); + + // add token price to master oracle + address[] memory oracles = new address[](tokens.length); + for (uint256 i = 0; i < oracles.length; i++) { + oracles[i] = oracle; + } + (success, data) = masterOracle.call( + abi.encodeWithSignature("add(address[],address[])", tokens, oracles) + ); + require(success, "Error setting new token price feed in master oracle"); + + // add tokens in the fuse pool + cTokens = new address[](tokens.length); + for (uint256 i = 0; i < tokens.length; i++) { + Unitroller(troller)._deployMarket( + false, // isCEther + abi.encode( // CErc20Delegator constructor data + tokens[i], // underlying + troller, // comptroller + FUSE_JUMP_RATE_MODEL, // interestRateModel + "fToken-x", // name + "Fuse pool x Token", // symbol + FUSE_CERC20_DELEGATE, // implementation + bytes(""), // becomeImplementationData + uint256(0), // reserveFactorMantissa + uint256(0) // adminFeeMantissa + ), + 0.7e18 // collateralFactorMantissa + ); + cTokens[i] = Unitroller(troller).cTokensByUnderlying(tokens[i]); + require( + cTokens[i] != address(0), + "Error adding token to Fuse pool" + ); + } + } +} From 2044875f616c1efab4e76b42e8e31cce9ebabbd5 Mon Sep 17 00:00:00 2001 From: Erwan Beauvois Date: Tue, 29 Mar 2022 16:46:49 +0200 Subject: [PATCH 07/10] Add reverts on fuzz edges & add revert test cases --- src/test/integration/FuseERC4626.t.sol | 164 ++++++++++++++----------- src/vaults/fuse/FuseERC4626.sol | 40 ++++++ 2 files changed, 134 insertions(+), 70 deletions(-) diff --git a/src/test/integration/FuseERC4626.t.sol b/src/test/integration/FuseERC4626.t.sol index 956a6ce..e5c105a 100644 --- a/src/test/integration/FuseERC4626.t.sol +++ b/src/test/integration/FuseERC4626.t.sol @@ -180,8 +180,6 @@ contract IntegrationTestFuseERC4626 is DSTestPlus { } function testMaxDepositSupplyCap(uint256 supplyCap) public { - hevm.assume(supplyCap > 0); // 0 = no supply cap - // there should be no maximum to deposit initially assertEq(vault.maxDeposit(address(this)), type(uint256).max); @@ -193,7 +191,13 @@ contract IntegrationTestFuseERC4626 is DSTestPlus { troller._setMarketSupplyCaps(cTokens, newSupplyCaps); // the max deposit should be updated - assertEq(vault.maxDeposit(address(this)), supplyCap); + if (supplyCap != 0) { + // if non-zero, should be updated to the value + assertEq(vault.maxDeposit(address(this)), supplyCap); + } else { + // if zero, should not have set a maximum + assertEq(vault.maxDeposit(address(this)), type(uint256).max); + } } function testMaxMintPaused() public { @@ -208,8 +212,6 @@ contract IntegrationTestFuseERC4626 is DSTestPlus { } function testMaxMintSupplyCap(uint256 supplyCap) public { - hevm.assume(supplyCap > 0); // 0 = no supply cap - // there should be no maximum to mint initially assertEq(vault.maxMint(address(this)), type(uint256).max); @@ -221,7 +223,13 @@ contract IntegrationTestFuseERC4626 is DSTestPlus { troller._setMarketSupplyCaps(cTokens, newSupplyCaps); // the max mint should be updated - assertEq(vault.maxMint(address(this)), supplyCap); + if (supplyCap != 0) { + // if non-zero, should be updated to the value + assertEq(vault.maxMint(address(this)), supplyCap); + } else { + // if zero, should not have set a maximum + assertEq(vault.maxMint(address(this)), type(uint256).max); + } } function testMaxWithdrawNoCash() public { @@ -269,11 +277,8 @@ contract IntegrationTestFuseERC4626 is DSTestPlus { } function testTotalAssets(uint256 deposit1) public { - // don't fuzz dust - hevm.assume(deposit1 > 1000); - // on high values, cToken reverts with MINT_EXCHANGE_CALCULATION_FAILED - // there are also rounding errors in solmate's wadMul functions above - // type(uint128).max + // deposit() reverts for 0 & overly large values + hevm.assume(deposit1 > 1); hevm.assume(deposit1 <= type(uint128).max); uint256 deposit2 = deposit1 / 2; @@ -345,85 +350,104 @@ contract IntegrationTestFuseERC4626 is DSTestPlus { } function testMint(uint256 shares) public { - // Solmate standard implementation don't allow 0 deposit/mints - hevm.assume(shares > 0); - // on high values, cToken reverts with MINT_EXCHANGE_CALCULATION_FAILED - hevm.assume(shares <= type(uint128).max); - uint256 previewAssets = vault.previewMint(shares); token.mint(address(this), previewAssets); token.approve(address(vault), previewAssets); - uint256 balanceBefore = token.balanceOf(address(this)); - uint256 returnedAssets = vault.mint(shares, address(this)); - uint256 spentAssets = balanceBefore - token.balanceOf(address(this)); - assertEq(previewAssets, spentAssets); - assertEq(returnedAssets, spentAssets); + if (shares == 0) { + hevm.expectRevert(bytes("ZERO_SHARES")); + vault.mint(shares, address(this)); + } else if (shares > type(uint128).max) { + hevm.expectRevert(bytes("MANY_SHARES")); + vault.mint(shares, address(this)); + } else { + uint256 balanceBefore = token.balanceOf(address(this)); + uint256 returnedAssets = vault.mint(shares, address(this)); + uint256 spentAssets = balanceBefore - + token.balanceOf(address(this)); + + assertEq(previewAssets, spentAssets); + assertEq(returnedAssets, spentAssets); + } } function testDeposit(uint256 assets) public { - // Solmate standard implementation don't allow 0 deposit/mints - hevm.assume(assets > 0); - // on high values, cToken reverts with MINT_EXCHANGE_CALCULATION_FAILED - hevm.assume(assets <= type(uint128).max); - uint256 previewShares = vault.previewDeposit(assets); token.mint(address(this), assets); token.approve(address(vault), assets); - uint256 balanceBefore = vault.balanceOf(address(this)); - uint256 returnedShares = vault.deposit(assets, address(this)); - uint256 receivedShares = vault.balanceOf(address(this)) - balanceBefore; - assertEq(previewShares, receivedShares); - assertEq(returnedShares, receivedShares); + if (assets == 0) { + hevm.expectRevert(bytes("ZERO_ASSETS")); + vault.deposit(assets, address(this)); + } else if (assets > type(uint128).max) { + hevm.expectRevert(bytes("MANY_ASSETS")); + vault.deposit(assets, address(this)); + } else { + uint256 balanceBefore = vault.balanceOf(address(this)); + uint256 returnedShares = vault.deposit(assets, address(this)); + uint256 receivedShares = vault.balanceOf(address(this)) - + balanceBefore; + + assertEq(previewShares, receivedShares); + assertEq(returnedShares, receivedShares); + } } function testRedeem(uint256 shares) public { - // Solmate standard implementation don't allow 0 deposit/mints - hevm.assume(shares > 0); - // on high values, cToken reverts with MINT_EXCHANGE_CALCULATION_FAILED - hevm.assume(shares <= type(uint128).max); - + // seed the current contract with vault shares token.mint(address(this), type(uint256).max); token.approve(address(vault), type(uint256).max); - vault.mint(shares, address(this)); - assertEq(vault.balanceOf(address(this)), shares); - - uint256 previewAssets = vault.previewRedeem(shares); - uint256 balanceBefore = token.balanceOf(address(this)); - uint256 returnedAssets = vault.redeem( - shares, - address(this), - address(this) - ); - uint256 receivedAssets = token.balanceOf(address(this)) - balanceBefore; + vault.mint(type(uint128).max, address(this)); + vault.mint(1e18, address(this)); + + if (shares == 0) { + hevm.expectRevert(bytes("ZERO_SHARES")); + vault.redeem(shares, address(this), address(this)); + } else if (shares > type(uint128).max) { + hevm.expectRevert(bytes("MANY_SHARES")); + vault.redeem(shares, address(this), address(this)); + } else { + uint256 previewAssets = vault.previewRedeem(shares); + uint256 balanceBefore = token.balanceOf(address(this)); + uint256 returnedAssets = vault.redeem( + shares, + address(this), + address(this) + ); + uint256 receivedAssets = token.balanceOf(address(this)) - + balanceBefore; - assertEq(previewAssets, receivedAssets); - assertEq(returnedAssets, receivedAssets); + assertEq(previewAssets, receivedAssets); + assertEq(returnedAssets, receivedAssets); + } } function testWithdraw(uint256 assets) public { - // Solmate standard implementation don't allow 0 deposit/mints - hevm.assume(assets > 0); - // on high values, cToken reverts with MINT_EXCHANGE_CALCULATION_FAILED - hevm.assume(assets <= type(uint128).max - 2); - - // +2 because cToken rounds down & vault rounds down - // so that's the worst rounding error we can get - token.mint(address(this), assets + 2); - token.approve(address(vault), assets + 2); - vault.deposit(assets + 2, address(this)); - - uint256 previewShares = vault.previewWithdraw(assets); - uint256 balanceBefore = vault.balanceOf(address(this)); - uint256 returnedShares = vault.withdraw( - assets, - address(this), - address(this) - ); - uint256 spentShares = balanceBefore - vault.balanceOf(address(this)); + // seed the current contract with vault shares + token.mint(address(this), type(uint256).max); + token.approve(address(vault), type(uint256).max); + vault.mint(type(uint128).max, address(this)); + vault.mint(1e18, address(this)); + + if (assets == 0) { + hevm.expectRevert(bytes("ZERO_ASSETS")); + vault.withdraw(assets, address(this), address(this)); + } else if (assets > type(uint128).max) { + hevm.expectRevert(bytes("MANY_ASSETS")); + vault.withdraw(assets, address(this), address(this)); + } else { + uint256 previewShares = vault.previewWithdraw(assets); + uint256 balanceBefore = vault.balanceOf(address(this)); + uint256 returnedShares = vault.withdraw( + assets, + address(this), + address(this) + ); + uint256 spentShares = balanceBefore - + vault.balanceOf(address(this)); - assertEq(spentShares, previewShares); - assertEq(returnedShares, spentShares); + assertEq(spentShares, previewShares); + assertEq(returnedShares, spentShares); + } } } diff --git a/src/vaults/fuse/FuseERC4626.sol b/src/vaults/fuse/FuseERC4626.sol index 8eec555..4d807c6 100644 --- a/src/vaults/fuse/FuseERC4626.sol +++ b/src/vaults/fuse/FuseERC4626.sol @@ -56,6 +56,46 @@ contract FuseERC4626 is ERC4626 { require(cToken.mint(underlyingAmount) == 0, "MINT_FAILED"); } + function deposit(uint256 amount, address receiver) + public + override + returns (uint256) + { + require(amount != 0, "ZERO_ASSETS"); // Deposit 0 makes no sense + require(amount <= type(uint128).max, "MANY_ASSETS"); // Check for overly large values + return super.deposit(amount, receiver); + } + + function mint(uint256 shares, address receiver) + public + override + returns (uint256) + { + require(shares != 0, "ZERO_SHARES"); // Minting 0 makes no sense + require(shares <= type(uint128).max, "MANY_SHARES"); // Check for overly large values + return super.mint(shares, receiver); + } + + function withdraw( + uint256 assets, + address receiver, + address owner + ) public override returns (uint256) { + require(assets != 0, "ZERO_ASSETS"); // Withdraw 0 makes no sense + require(assets <= type(uint128).max, "MANY_ASSETS"); // Check for overly large values + return super.withdraw(assets, receiver, owner); + } + + function redeem( + uint256 shares, + address receiver, + address owner + ) public override returns (uint256) { + require(shares != 0, "ZERO_SHARES"); // Redeem 0 makes no sense + require(shares <= type(uint128).max, "MANY_SHARES"); // Check for overly large values + return super.redeem(shares, receiver, owner); + } + /// @notice Total amount of the underlying asset that /// is "managed" by Vault. function totalAssets() public view override returns (uint256) { From b140e7f3c383042bd9c994be81a5d93c89651fe2 Mon Sep 17 00:00:00 2001 From: Erwan Beauvois Date: Tue, 29 Mar 2022 18:16:41 +0200 Subject: [PATCH 08/10] Update testTotalAssets fuzz boundaries --- src/test/integration/FuseERC4626.t.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/integration/FuseERC4626.t.sol b/src/test/integration/FuseERC4626.t.sol index e5c105a..4b7dfb0 100644 --- a/src/test/integration/FuseERC4626.t.sol +++ b/src/test/integration/FuseERC4626.t.sol @@ -276,10 +276,10 @@ contract IntegrationTestFuseERC4626 is DSTestPlus { assertEq(vault.maxRedeem(address(this)), 0.5e18); } - function testTotalAssets(uint256 deposit1) public { + function testTotalAssets(uint128 _deposit1) public { // deposit() reverts for 0 & overly large values - hevm.assume(deposit1 > 1); - hevm.assume(deposit1 <= type(uint128).max); + hevm.assume(_deposit1 > 1); + uint256 deposit1 = uint256(_deposit1); uint256 deposit2 = deposit1 / 2; uint256 donation = deposit1; From 53549b60838ebe67a00ac5658b53b2208bd0756c Mon Sep 17 00:00:00 2001 From: Erwan Beauvois Date: Mon, 4 Apr 2022 19:12:42 +0200 Subject: [PATCH 09/10] Fuzz with uint112 & remove revert cases on 0 & >uint128.max --- src/test/integration/FuseERC4626.t.sol | 155 ++++---- src/test/mocks/MockCToken.sol | 470 ++++++++++++------------- src/test/mocks/MockERC20.sol | 44 +-- src/test/mocks/MockFusePriceOracle.sol | 24 +- src/test/utils/FusePoolUtils.sol | 212 +++++------ src/vaults/fuse/FuseERC4626.sol | 40 --- 6 files changed, 449 insertions(+), 496 deletions(-) diff --git a/src/test/integration/FuseERC4626.t.sol b/src/test/integration/FuseERC4626.t.sol index 4b7dfb0..decaa6f 100644 --- a/src/test/integration/FuseERC4626.t.sol +++ b/src/test/integration/FuseERC4626.t.sol @@ -349,105 +349,98 @@ contract IntegrationTestFuseERC4626 is DSTestPlus { assertEq(vault.totalSupply(), shares1 + shares2); // should not move } - function testMint(uint256 shares) public { + function testMint(uint128 _shares) public { + uint256 shares = uint256(_shares); + uint256 previewAssets = vault.previewMint(shares); token.mint(address(this), previewAssets); token.approve(address(vault), previewAssets); - if (shares == 0) { - hevm.expectRevert(bytes("ZERO_SHARES")); - vault.mint(shares, address(this)); - } else if (shares > type(uint128).max) { - hevm.expectRevert(bytes("MANY_SHARES")); - vault.mint(shares, address(this)); - } else { - uint256 balanceBefore = token.balanceOf(address(this)); - uint256 returnedAssets = vault.mint(shares, address(this)); - uint256 spentAssets = balanceBefore - - token.balanceOf(address(this)); + uint256 balanceBefore = token.balanceOf(address(this)); + uint256 returnedAssets = vault.mint(shares, address(this)); + uint256 spentAssets = balanceBefore - token.balanceOf(address(this)); - assertEq(previewAssets, spentAssets); - assertEq(returnedAssets, spentAssets); - } + assertEq(previewAssets, spentAssets); + assertEq(returnedAssets, spentAssets); } - function testDeposit(uint256 assets) public { - uint256 previewShares = vault.previewDeposit(assets); + function testDeposit(uint128 _assets) public { + // depositing 0 or 1 wei gives 0 cToken shares + // and the solmate impl reverts with ZERO_SHARES + hevm.assume(_assets > 0); + + uint256 assets = uint256(_assets); + token.mint(address(this), assets); token.approve(address(vault), assets); - if (assets == 0) { - hevm.expectRevert(bytes("ZERO_ASSETS")); - vault.deposit(assets, address(this)); - } else if (assets > type(uint128).max) { - hevm.expectRevert(bytes("MANY_ASSETS")); - vault.deposit(assets, address(this)); - } else { - uint256 balanceBefore = vault.balanceOf(address(this)); - uint256 returnedShares = vault.deposit(assets, address(this)); - uint256 receivedShares = vault.balanceOf(address(this)) - - balanceBefore; + uint256 previewShares = vault.previewDeposit(assets); + uint256 balanceBefore = vault.balanceOf(address(this)); + uint256 returnedShares = vault.deposit(assets, address(this)); + uint256 receivedShares = vault.balanceOf(address(this)) - balanceBefore; - assertEq(previewShares, receivedShares); - assertEq(returnedShares, receivedShares); - } + assertEq(previewShares, receivedShares); + assertEq(returnedShares, receivedShares); } - function testRedeem(uint256 shares) public { + function testRedeem(uint112 _shares) public { + // redeeming 0 shares fails with ZERO_ASSETS (solmate impl) + hevm.assume(_shares > 0); + + uint256 shares = uint256(_shares); + // seed the current contract with vault shares - token.mint(address(this), type(uint256).max); - token.approve(address(vault), type(uint256).max); - vault.mint(type(uint128).max, address(this)); - vault.mint(1e18, address(this)); - - if (shares == 0) { - hevm.expectRevert(bytes("ZERO_SHARES")); - vault.redeem(shares, address(this), address(this)); - } else if (shares > type(uint128).max) { - hevm.expectRevert(bytes("MANY_SHARES")); - vault.redeem(shares, address(this), address(this)); - } else { - uint256 previewAssets = vault.previewRedeem(shares); - uint256 balanceBefore = token.balanceOf(address(this)); - uint256 returnedAssets = vault.redeem( - shares, - address(this), - address(this) - ); - uint256 receivedAssets = token.balanceOf(address(this)) - - balanceBefore; + uint256 seedAssets = uint256(type(uint112).max) + 1e18; + token.mint(address(this), seedAssets); + token.approve(address(vault), seedAssets); + vault.deposit(seedAssets, address(this)); + + uint256 previewAssets = vault.previewRedeem(shares); + uint256 balanceBefore = token.balanceOf(address(this)); + uint256 returnedAssets = vault.redeem( + shares, + address(this), + address(this) + ); + uint256 receivedAssets = token.balanceOf(address(this)) - balanceBefore; - assertEq(previewAssets, receivedAssets); - assertEq(returnedAssets, receivedAssets); - } + assertEq(previewAssets, receivedAssets); + assertEq(returnedAssets, receivedAssets); } - function testWithdraw(uint256 assets) public { + function testWithdraw(uint112 _assets) public { + uint256 assets = uint256(_assets); + // seed the current contract with vault shares - token.mint(address(this), type(uint256).max); - token.approve(address(vault), type(uint256).max); - vault.mint(type(uint128).max, address(this)); - vault.mint(1e18, address(this)); - - if (assets == 0) { - hevm.expectRevert(bytes("ZERO_ASSETS")); - vault.withdraw(assets, address(this), address(this)); - } else if (assets > type(uint128).max) { - hevm.expectRevert(bytes("MANY_ASSETS")); - vault.withdraw(assets, address(this), address(this)); - } else { - uint256 previewShares = vault.previewWithdraw(assets); - uint256 balanceBefore = vault.balanceOf(address(this)); - uint256 returnedShares = vault.withdraw( - assets, - address(this), - address(this) - ); - uint256 spentShares = balanceBefore - - vault.balanceOf(address(this)); + uint256 seedAssets = uint256(type(uint112).max) + 1e18; + token.mint(address(this), seedAssets); + token.approve(address(vault), seedAssets); + vault.deposit(seedAssets, address(this)); + + uint256 previewShares = vault.previewWithdraw(assets); + uint256 balanceBefore = vault.balanceOf(address(this)); + uint256 returnedShares = vault.withdraw( + assets, + address(this), + address(this) + ); + uint256 spentShares = balanceBefore - vault.balanceOf(address(this)); - assertEq(spentShares, previewShares); - assertEq(returnedShares, spentShares); - } + assertEq(spentShares, previewShares); + assertEq(returnedShares, spentShares); + } + + function testZeroRevert() public { + // seed the current contract with vault shares + uint256 seedAssets = uint256(type(uint128).max) + 1e18; + token.mint(address(this), seedAssets); + token.approve(address(vault), seedAssets); + vault.mint(seedAssets, address(this)); + + // test revert cases + hevm.expectRevert(bytes("ZERO_ASSETS")); + vault.redeem(0, address(this), address(this)); + hevm.expectRevert(bytes("ZERO_SHARES")); + vault.deposit(0, address(this)); } } diff --git a/src/test/mocks/MockCToken.sol b/src/test/mocks/MockCToken.sol index b50a761..1d29559 100644 --- a/src/test/mocks/MockCToken.sol +++ b/src/test/mocks/MockCToken.sol @@ -1,235 +1,235 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.10; - -import {MockERC20} from "./MockERC20.sol"; -import {CToken} from "../../external/CToken.sol"; -import {InterestRateModel} from "libcompound/interfaces/InterestRateModel.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; - -contract MockInterestRateModel is InterestRateModel { - function getBorrowRate( - uint256, - uint256, - uint256 - ) external view override returns (uint256) { - return 0; - } - - function getSupplyRate( - uint256, - uint256, - uint256, - uint256 - ) external view override returns (uint256) { - return 0; - } -} - -contract MockUnitroller { - function supplyCaps( - address /* cToken*/ - ) external view returns (uint256) { - return 100e18; - } - - function mintGuardianPaused( - address /* cToken*/ - ) external view returns (bool) { - return false; - } - - function borrowGuardianPaused( - address /* cToken*/ - ) external view returns (bool) { - return false; - } -} - -contract MockCToken is MockERC20, CToken { - MockERC20 public token; - bool public error; - bool public isCEther; - InterestRateModel public irm; - address public override comptroller; - - uint256 private constant EXCHANGE_RATE_SCALE = 1e18; - uint256 public effectiveExchangeRate = 2e18; - - constructor(address _token, bool _isCEther) { - token = MockERC20(_token); - isCEther = _isCEther; - irm = new MockInterestRateModel(); - comptroller = address(new MockUnitroller()); - } - - function setError(bool _error) external { - error = _error; - } - - function setEffectiveExchangeRate(uint256 _effectiveExchangeRate) external { - effectiveExchangeRate = _effectiveExchangeRate; - } - - function isCToken() external pure returns (bool) { - return true; - } - - function underlying() external view override returns (ERC20) { - return ERC20(address(token)); - } - - function balanceOfUnderlying(address) - external - view - override - returns (uint256) - { - return 0; - } - - function mint() external payable { - _mint( - msg.sender, - (msg.value * EXCHANGE_RATE_SCALE) / effectiveExchangeRate - ); - } - - function mint(uint256 amount) external override returns (uint256) { - token.transferFrom(msg.sender, address(this), amount); - _mint( - msg.sender, - (amount * EXCHANGE_RATE_SCALE) / effectiveExchangeRate - ); - return error ? 1 : 0; - } - - function borrow(uint256) external override returns (uint256) { - return 0; - } - - function redeem(uint256 redeemTokens) external returns (uint256) { - _burn(msg.sender, redeemTokens); - uint256 redeemAmount = (redeemTokens * effectiveExchangeRate) / - EXCHANGE_RATE_SCALE; - if (address(this).balance >= redeemAmount) { - payable(msg.sender).transfer(redeemAmount); - } else { - token.transfer(msg.sender, redeemAmount); - } - return error ? 1 : 0; - } - - function redeemUnderlying(uint256 redeemAmount) - external - override - returns (uint256) - { - _burn( - msg.sender, - (redeemAmount * EXCHANGE_RATE_SCALE) / effectiveExchangeRate - ); - if (address(this).balance >= redeemAmount) { - payable(msg.sender).transfer(redeemAmount); - } else { - token.transfer(msg.sender, redeemAmount); - } - return error ? 1 : 0; - } - - function getAccountSnapshot(address) - external - view - override - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return (0, 0, 0, 0); - } - - function exchangeRateStored() external view override returns (uint256) { - return - (EXCHANGE_RATE_SCALE * effectiveExchangeRate) / EXCHANGE_RATE_SCALE; // 2:1 - } - - function exchangeRateCurrent() external override returns (uint256) { - // fake state operation to not allow "view" modifier - effectiveExchangeRate = effectiveExchangeRate; - - return - (EXCHANGE_RATE_SCALE * effectiveExchangeRate) / EXCHANGE_RATE_SCALE; // 2:1 - } - - function getCash() external view override returns (uint256) { - return token.balanceOf(address(this)); - } - - function totalBorrows() external pure override returns (uint256) { - return 0; - } - - function totalReserves() external pure override returns (uint256) { - return 0; - } - - function totalFuseFees() external view override returns (uint256) { - return 0; - } - - function totalAdminFees() external view override returns (uint256) { - return 0; - } - - function interestRateModel() - external - view - override - returns (InterestRateModel) - { - return irm; - } - - function reserveFactorMantissa() external view override returns (uint256) { - return 0; - } - - function fuseFeeMantissa() external view override returns (uint256) { - return 0; - } - - function adminFeeMantissa() external view override returns (uint256) { - return 0; - } - - function initialExchangeRateMantissa() - external - view - override - returns (uint256) - { - return 0; - } - - function repayBorrow(uint256) external override returns (uint256) { - return 0; - } - - function repayBorrowBehalf(address, uint256) - external - override - returns (uint256) - { - return 0; - } - - function borrowBalanceCurrent(address) external override returns (uint256) { - return 0; - } - - function accrualBlockNumber() external view override returns (uint256) { - return block.number; - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.10; + +import {MockERC20} from "./MockERC20.sol"; +import {CToken} from "../../external/CToken.sol"; +import {InterestRateModel} from "libcompound/interfaces/InterestRateModel.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; + +contract MockInterestRateModel is InterestRateModel { + function getBorrowRate( + uint256, + uint256, + uint256 + ) external view override returns (uint256) { + return 0; + } + + function getSupplyRate( + uint256, + uint256, + uint256, + uint256 + ) external view override returns (uint256) { + return 0; + } +} + +contract MockUnitroller { + function supplyCaps( + address /* cToken*/ + ) external view returns (uint256) { + return 100e18; + } + + function mintGuardianPaused( + address /* cToken*/ + ) external view returns (bool) { + return false; + } + + function borrowGuardianPaused( + address /* cToken*/ + ) external view returns (bool) { + return false; + } +} + +contract MockCToken is MockERC20, CToken { + MockERC20 public token; + bool public error; + bool public isCEther; + InterestRateModel public irm; + address public override comptroller; + + uint256 private constant EXCHANGE_RATE_SCALE = 1e18; + uint256 public effectiveExchangeRate = 2e18; + + constructor(address _token, bool _isCEther) { + token = MockERC20(_token); + isCEther = _isCEther; + irm = new MockInterestRateModel(); + comptroller = address(new MockUnitroller()); + } + + function setError(bool _error) external { + error = _error; + } + + function setEffectiveExchangeRate(uint256 _effectiveExchangeRate) external { + effectiveExchangeRate = _effectiveExchangeRate; + } + + function isCToken() external pure returns (bool) { + return true; + } + + function underlying() external view override returns (ERC20) { + return ERC20(address(token)); + } + + function balanceOfUnderlying(address) + external + view + override + returns (uint256) + { + return 0; + } + + function mint() external payable { + _mint( + msg.sender, + (msg.value * EXCHANGE_RATE_SCALE) / effectiveExchangeRate + ); + } + + function mint(uint256 amount) external override returns (uint256) { + token.transferFrom(msg.sender, address(this), amount); + _mint( + msg.sender, + (amount * EXCHANGE_RATE_SCALE) / effectiveExchangeRate + ); + return error ? 1 : 0; + } + + function borrow(uint256) external override returns (uint256) { + return 0; + } + + function redeem(uint256 redeemTokens) external returns (uint256) { + _burn(msg.sender, redeemTokens); + uint256 redeemAmount = (redeemTokens * effectiveExchangeRate) / + EXCHANGE_RATE_SCALE; + if (address(this).balance >= redeemAmount) { + payable(msg.sender).transfer(redeemAmount); + } else { + token.transfer(msg.sender, redeemAmount); + } + return error ? 1 : 0; + } + + function redeemUnderlying(uint256 redeemAmount) + external + override + returns (uint256) + { + _burn( + msg.sender, + (redeemAmount * EXCHANGE_RATE_SCALE) / effectiveExchangeRate + ); + if (address(this).balance >= redeemAmount) { + payable(msg.sender).transfer(redeemAmount); + } else { + token.transfer(msg.sender, redeemAmount); + } + return error ? 1 : 0; + } + + function getAccountSnapshot(address) + external + view + override + returns ( + uint256, + uint256, + uint256, + uint256 + ) + { + return (0, 0, 0, 0); + } + + function exchangeRateStored() external view override returns (uint256) { + return + (EXCHANGE_RATE_SCALE * effectiveExchangeRate) / EXCHANGE_RATE_SCALE; // 2:1 + } + + function exchangeRateCurrent() external override returns (uint256) { + // fake state operation to not allow "view" modifier + effectiveExchangeRate = effectiveExchangeRate; + + return + (EXCHANGE_RATE_SCALE * effectiveExchangeRate) / EXCHANGE_RATE_SCALE; // 2:1 + } + + function getCash() external view override returns (uint256) { + return token.balanceOf(address(this)); + } + + function totalBorrows() external pure override returns (uint256) { + return 0; + } + + function totalReserves() external pure override returns (uint256) { + return 0; + } + + function totalFuseFees() external view override returns (uint256) { + return 0; + } + + function totalAdminFees() external view override returns (uint256) { + return 0; + } + + function interestRateModel() + external + view + override + returns (InterestRateModel) + { + return irm; + } + + function reserveFactorMantissa() external view override returns (uint256) { + return 0; + } + + function fuseFeeMantissa() external view override returns (uint256) { + return 0; + } + + function adminFeeMantissa() external view override returns (uint256) { + return 0; + } + + function initialExchangeRateMantissa() + external + view + override + returns (uint256) + { + return 0; + } + + function repayBorrow(uint256) external override returns (uint256) { + return 0; + } + + function repayBorrowBehalf(address, uint256) + external + override + returns (uint256) + { + return 0; + } + + function borrowBalanceCurrent(address) external override returns (uint256) { + return 0; + } + + function accrualBlockNumber() external view override returns (uint256) { + return block.number; + } +} diff --git a/src/test/mocks/MockERC20.sol b/src/test/mocks/MockERC20.sol index 26f49e4..6818498 100644 --- a/src/test/mocks/MockERC20.sol +++ b/src/test/mocks/MockERC20.sol @@ -1,22 +1,22 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.10; - -import {ERC20} from "solmate/tokens/ERC20.sol"; - -contract MockERC20 is ERC20 { - constructor() ERC20("MockToken", "MCT", 18) {} - - function mint(address account, uint256 amount) public returns (bool) { - _mint(account, amount); - return true; - } - - function mockBurn(address account, uint256 amount) public returns (bool) { - _burn(account, amount); - return true; - } - - function approveOverride(address owner, address spender, uint256 amount) public { - allowance[owner][spender] = amount; - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.10; + +import {ERC20} from "solmate/tokens/ERC20.sol"; + +contract MockERC20 is ERC20 { + constructor() ERC20("MockToken", "MCT", 18) {} + + function mint(address account, uint256 amount) public returns (bool) { + _mint(account, amount); + return true; + } + + function mockBurn(address account, uint256 amount) public returns (bool) { + _burn(account, amount); + return true; + } + + function approveOverride(address owner, address spender, uint256 amount) public { + allowance[owner][spender] = amount; + } +} diff --git a/src/test/mocks/MockFusePriceOracle.sol b/src/test/mocks/MockFusePriceOracle.sol index d48182e..b032808 100644 --- a/src/test/mocks/MockFusePriceOracle.sol +++ b/src/test/mocks/MockFusePriceOracle.sol @@ -1,12 +1,12 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.10; - -contract MockFusePriceOracle { - - bool public constant isPriceOracle = true; - mapping(address=>uint256) public getUnderlyingPrice; - - function mockSetPrice(address cToken, uint256 value) external { - getUnderlyingPrice[cToken] = value; - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.10; + +contract MockFusePriceOracle { + + bool public constant isPriceOracle = true; + mapping(address=>uint256) public getUnderlyingPrice; + + function mockSetPrice(address cToken, uint256 value) external { + getUnderlyingPrice[cToken] = value; + } +} diff --git a/src/test/utils/FusePoolUtils.sol b/src/test/utils/FusePoolUtils.sol index 27fce19..09314d7 100644 --- a/src/test/utils/FusePoolUtils.sol +++ b/src/test/utils/FusePoolUtils.sol @@ -1,106 +1,106 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import {Unitroller} from "../../external/Unitroller.sol"; - -library FusePoolUtils { - address public constant MASTER_PRICE_ORACLE_INITIALIZABLECLONES = - address(0x91cE5566DC3170898C5aeE4ae4dD314654B47415); - address public constant MASTER_PRICE_ORACLE_IMPLEMENTATION = - address(0xb3c8eE7309BE658c186F986388c2377da436D8fb); - address public constant MASTER_PRICE_ORACLE_RARI_DEFAULT = - address(0x1887118E49e0F4A78Bd71B792a49dE03504A764D); - address public constant FUSE_POOL_DIRECTORY = - address(0x835482FE0532f169024d5E9410199369aAD5C77E); - address public constant FUSE_COMPTROLLER_IMPLEMENTATION = - address(0xE16DB319d9dA7Ce40b666DD2E365a4b8B3C18217); - address public constant FUSE_JUMP_RATE_MODEL = - address(0xbAB47e4B692195BF064923178A90Ef999A15f819); - address public constant FUSE_CERC20_DELEGATE = - address(0x67Db14E73C2Dce786B5bbBfa4D010dEab4BBFCF9); - - function createPool(address[] memory tokens, address oracle) - external - returns ( - address masterOracle, - address troller, - address[] memory cTokens - ) - { - // create a new master price oracle - (bool success, bytes memory data) = MASTER_PRICE_ORACLE_INITIALIZABLECLONES - .call( - abi.encodeWithSignature( - "clone(address,bytes)", - MASTER_PRICE_ORACLE_IMPLEMENTATION, - abi.encodeWithSignature( - "initialize(address[],address[],address,address,bool)", - new address[](0), // underlyings - new address[](0), // oracles - MASTER_PRICE_ORACLE_RARI_DEFAULT, // default oracle - address(this), // pool admin - true // canAdminOwerwrite - ) - ) - ); - require(success, "Error creating master price oracle"); - assembly { - masterOracle := mload(add(data, 32)) - } - - // create a new Rari pool (call Rari Capital: Fuse Pool Directory) - (success, data) = FUSE_POOL_DIRECTORY.call( - abi.encodeWithSignature( - "deployPool(string,address,bool,uint256,uint256,address)", - "Test Pool", // name - FUSE_COMPTROLLER_IMPLEMENTATION, // implementation - false, // enforceWhitelist - 0.5e18, // closeFactor - 1.08e18, // liquidationIncentive - masterOracle // priceOracle - ) - ); - require(success, "Error creating pool"); - assembly { - troller := mload(add(data, 64)) - } - - // accept admin of the comptroller - Unitroller(troller)._acceptAdmin(); - - // add token price to master oracle - address[] memory oracles = new address[](tokens.length); - for (uint256 i = 0; i < oracles.length; i++) { - oracles[i] = oracle; - } - (success, data) = masterOracle.call( - abi.encodeWithSignature("add(address[],address[])", tokens, oracles) - ); - require(success, "Error setting new token price feed in master oracle"); - - // add tokens in the fuse pool - cTokens = new address[](tokens.length); - for (uint256 i = 0; i < tokens.length; i++) { - Unitroller(troller)._deployMarket( - false, // isCEther - abi.encode( // CErc20Delegator constructor data - tokens[i], // underlying - troller, // comptroller - FUSE_JUMP_RATE_MODEL, // interestRateModel - "fToken-x", // name - "Fuse pool x Token", // symbol - FUSE_CERC20_DELEGATE, // implementation - bytes(""), // becomeImplementationData - uint256(0), // reserveFactorMantissa - uint256(0) // adminFeeMantissa - ), - 0.7e18 // collateralFactorMantissa - ); - cTokens[i] = Unitroller(troller).cTokensByUnderlying(tokens[i]); - require( - cTokens[i] != address(0), - "Error adding token to Fuse pool" - ); - } - } -} +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {Unitroller} from "../../external/Unitroller.sol"; + +library FusePoolUtils { + address public constant MASTER_PRICE_ORACLE_INITIALIZABLECLONES = + address(0x91cE5566DC3170898C5aeE4ae4dD314654B47415); + address public constant MASTER_PRICE_ORACLE_IMPLEMENTATION = + address(0xb3c8eE7309BE658c186F986388c2377da436D8fb); + address public constant MASTER_PRICE_ORACLE_RARI_DEFAULT = + address(0x1887118E49e0F4A78Bd71B792a49dE03504A764D); + address public constant FUSE_POOL_DIRECTORY = + address(0x835482FE0532f169024d5E9410199369aAD5C77E); + address public constant FUSE_COMPTROLLER_IMPLEMENTATION = + address(0xE16DB319d9dA7Ce40b666DD2E365a4b8B3C18217); + address public constant FUSE_JUMP_RATE_MODEL = + address(0xbAB47e4B692195BF064923178A90Ef999A15f819); + address public constant FUSE_CERC20_DELEGATE = + address(0x67Db14E73C2Dce786B5bbBfa4D010dEab4BBFCF9); + + function createPool(address[] memory tokens, address oracle) + external + returns ( + address masterOracle, + address troller, + address[] memory cTokens + ) + { + // create a new master price oracle + (bool success, bytes memory data) = MASTER_PRICE_ORACLE_INITIALIZABLECLONES + .call( + abi.encodeWithSignature( + "clone(address,bytes)", + MASTER_PRICE_ORACLE_IMPLEMENTATION, + abi.encodeWithSignature( + "initialize(address[],address[],address,address,bool)", + new address[](0), // underlyings + new address[](0), // oracles + MASTER_PRICE_ORACLE_RARI_DEFAULT, // default oracle + address(this), // pool admin + true // canAdminOwerwrite + ) + ) + ); + require(success, "Error creating master price oracle"); + assembly { + masterOracle := mload(add(data, 32)) + } + + // create a new Rari pool (call Rari Capital: Fuse Pool Directory) + (success, data) = FUSE_POOL_DIRECTORY.call( + abi.encodeWithSignature( + "deployPool(string,address,bool,uint256,uint256,address)", + "Test Pool", // name + FUSE_COMPTROLLER_IMPLEMENTATION, // implementation + false, // enforceWhitelist + 0.5e18, // closeFactor + 1.08e18, // liquidationIncentive + masterOracle // priceOracle + ) + ); + require(success, "Error creating pool"); + assembly { + troller := mload(add(data, 64)) + } + + // accept admin of the comptroller + Unitroller(troller)._acceptAdmin(); + + // add token price to master oracle + address[] memory oracles = new address[](tokens.length); + for (uint256 i = 0; i < oracles.length; i++) { + oracles[i] = oracle; + } + (success, data) = masterOracle.call( + abi.encodeWithSignature("add(address[],address[])", tokens, oracles) + ); + require(success, "Error setting new token price feed in master oracle"); + + // add tokens in the fuse pool + cTokens = new address[](tokens.length); + for (uint256 i = 0; i < tokens.length; i++) { + Unitroller(troller)._deployMarket( + false, // isCEther + abi.encode( // CErc20Delegator constructor data + tokens[i], // underlying + troller, // comptroller + FUSE_JUMP_RATE_MODEL, // interestRateModel + "fToken-x", // name + "Fuse pool x Token", // symbol + FUSE_CERC20_DELEGATE, // implementation + bytes(""), // becomeImplementationData + uint256(0), // reserveFactorMantissa + uint256(0) // adminFeeMantissa + ), + 0.7e18 // collateralFactorMantissa + ); + cTokens[i] = Unitroller(troller).cTokensByUnderlying(tokens[i]); + require( + cTokens[i] != address(0), + "Error adding token to Fuse pool" + ); + } + } +} diff --git a/src/vaults/fuse/FuseERC4626.sol b/src/vaults/fuse/FuseERC4626.sol index 4d807c6..8eec555 100644 --- a/src/vaults/fuse/FuseERC4626.sol +++ b/src/vaults/fuse/FuseERC4626.sol @@ -56,46 +56,6 @@ contract FuseERC4626 is ERC4626 { require(cToken.mint(underlyingAmount) == 0, "MINT_FAILED"); } - function deposit(uint256 amount, address receiver) - public - override - returns (uint256) - { - require(amount != 0, "ZERO_ASSETS"); // Deposit 0 makes no sense - require(amount <= type(uint128).max, "MANY_ASSETS"); // Check for overly large values - return super.deposit(amount, receiver); - } - - function mint(uint256 shares, address receiver) - public - override - returns (uint256) - { - require(shares != 0, "ZERO_SHARES"); // Minting 0 makes no sense - require(shares <= type(uint128).max, "MANY_SHARES"); // Check for overly large values - return super.mint(shares, receiver); - } - - function withdraw( - uint256 assets, - address receiver, - address owner - ) public override returns (uint256) { - require(assets != 0, "ZERO_ASSETS"); // Withdraw 0 makes no sense - require(assets <= type(uint128).max, "MANY_ASSETS"); // Check for overly large values - return super.withdraw(assets, receiver, owner); - } - - function redeem( - uint256 shares, - address receiver, - address owner - ) public override returns (uint256) { - require(shares != 0, "ZERO_SHARES"); // Redeem 0 makes no sense - require(shares <= type(uint128).max, "MANY_SHARES"); // Check for overly large values - return super.redeem(shares, receiver, owner); - } - /// @notice Total amount of the underlying asset that /// is "managed" by Vault. function totalAssets() public view override returns (uint256) { From a76831e56ffeab352d0d118dbafa8988955c30ef Mon Sep 17 00:00:00 2001 From: Erwan Beauvois Date: Tue, 5 Apr 2022 17:20:56 +0200 Subject: [PATCH 10/10] lint & prettier --- src/test/mocks/MockCToken.sol | 470 ++++++++++++------------- src/test/mocks/MockERC20.sol | 48 +-- src/test/mocks/MockFusePriceOracle.sol | 23 +- src/test/utils/Console.sol | 8 +- src/test/utils/FusePoolUtils.sol | 212 +++++------ 5 files changed, 382 insertions(+), 379 deletions(-) diff --git a/src/test/mocks/MockCToken.sol b/src/test/mocks/MockCToken.sol index 1d29559..b50a761 100644 --- a/src/test/mocks/MockCToken.sol +++ b/src/test/mocks/MockCToken.sol @@ -1,235 +1,235 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.10; - -import {MockERC20} from "./MockERC20.sol"; -import {CToken} from "../../external/CToken.sol"; -import {InterestRateModel} from "libcompound/interfaces/InterestRateModel.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; - -contract MockInterestRateModel is InterestRateModel { - function getBorrowRate( - uint256, - uint256, - uint256 - ) external view override returns (uint256) { - return 0; - } - - function getSupplyRate( - uint256, - uint256, - uint256, - uint256 - ) external view override returns (uint256) { - return 0; - } -} - -contract MockUnitroller { - function supplyCaps( - address /* cToken*/ - ) external view returns (uint256) { - return 100e18; - } - - function mintGuardianPaused( - address /* cToken*/ - ) external view returns (bool) { - return false; - } - - function borrowGuardianPaused( - address /* cToken*/ - ) external view returns (bool) { - return false; - } -} - -contract MockCToken is MockERC20, CToken { - MockERC20 public token; - bool public error; - bool public isCEther; - InterestRateModel public irm; - address public override comptroller; - - uint256 private constant EXCHANGE_RATE_SCALE = 1e18; - uint256 public effectiveExchangeRate = 2e18; - - constructor(address _token, bool _isCEther) { - token = MockERC20(_token); - isCEther = _isCEther; - irm = new MockInterestRateModel(); - comptroller = address(new MockUnitroller()); - } - - function setError(bool _error) external { - error = _error; - } - - function setEffectiveExchangeRate(uint256 _effectiveExchangeRate) external { - effectiveExchangeRate = _effectiveExchangeRate; - } - - function isCToken() external pure returns (bool) { - return true; - } - - function underlying() external view override returns (ERC20) { - return ERC20(address(token)); - } - - function balanceOfUnderlying(address) - external - view - override - returns (uint256) - { - return 0; - } - - function mint() external payable { - _mint( - msg.sender, - (msg.value * EXCHANGE_RATE_SCALE) / effectiveExchangeRate - ); - } - - function mint(uint256 amount) external override returns (uint256) { - token.transferFrom(msg.sender, address(this), amount); - _mint( - msg.sender, - (amount * EXCHANGE_RATE_SCALE) / effectiveExchangeRate - ); - return error ? 1 : 0; - } - - function borrow(uint256) external override returns (uint256) { - return 0; - } - - function redeem(uint256 redeemTokens) external returns (uint256) { - _burn(msg.sender, redeemTokens); - uint256 redeemAmount = (redeemTokens * effectiveExchangeRate) / - EXCHANGE_RATE_SCALE; - if (address(this).balance >= redeemAmount) { - payable(msg.sender).transfer(redeemAmount); - } else { - token.transfer(msg.sender, redeemAmount); - } - return error ? 1 : 0; - } - - function redeemUnderlying(uint256 redeemAmount) - external - override - returns (uint256) - { - _burn( - msg.sender, - (redeemAmount * EXCHANGE_RATE_SCALE) / effectiveExchangeRate - ); - if (address(this).balance >= redeemAmount) { - payable(msg.sender).transfer(redeemAmount); - } else { - token.transfer(msg.sender, redeemAmount); - } - return error ? 1 : 0; - } - - function getAccountSnapshot(address) - external - view - override - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return (0, 0, 0, 0); - } - - function exchangeRateStored() external view override returns (uint256) { - return - (EXCHANGE_RATE_SCALE * effectiveExchangeRate) / EXCHANGE_RATE_SCALE; // 2:1 - } - - function exchangeRateCurrent() external override returns (uint256) { - // fake state operation to not allow "view" modifier - effectiveExchangeRate = effectiveExchangeRate; - - return - (EXCHANGE_RATE_SCALE * effectiveExchangeRate) / EXCHANGE_RATE_SCALE; // 2:1 - } - - function getCash() external view override returns (uint256) { - return token.balanceOf(address(this)); - } - - function totalBorrows() external pure override returns (uint256) { - return 0; - } - - function totalReserves() external pure override returns (uint256) { - return 0; - } - - function totalFuseFees() external view override returns (uint256) { - return 0; - } - - function totalAdminFees() external view override returns (uint256) { - return 0; - } - - function interestRateModel() - external - view - override - returns (InterestRateModel) - { - return irm; - } - - function reserveFactorMantissa() external view override returns (uint256) { - return 0; - } - - function fuseFeeMantissa() external view override returns (uint256) { - return 0; - } - - function adminFeeMantissa() external view override returns (uint256) { - return 0; - } - - function initialExchangeRateMantissa() - external - view - override - returns (uint256) - { - return 0; - } - - function repayBorrow(uint256) external override returns (uint256) { - return 0; - } - - function repayBorrowBehalf(address, uint256) - external - override - returns (uint256) - { - return 0; - } - - function borrowBalanceCurrent(address) external override returns (uint256) { - return 0; - } - - function accrualBlockNumber() external view override returns (uint256) { - return block.number; - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.10; + +import {MockERC20} from "./MockERC20.sol"; +import {CToken} from "../../external/CToken.sol"; +import {InterestRateModel} from "libcompound/interfaces/InterestRateModel.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; + +contract MockInterestRateModel is InterestRateModel { + function getBorrowRate( + uint256, + uint256, + uint256 + ) external view override returns (uint256) { + return 0; + } + + function getSupplyRate( + uint256, + uint256, + uint256, + uint256 + ) external view override returns (uint256) { + return 0; + } +} + +contract MockUnitroller { + function supplyCaps( + address /* cToken*/ + ) external view returns (uint256) { + return 100e18; + } + + function mintGuardianPaused( + address /* cToken*/ + ) external view returns (bool) { + return false; + } + + function borrowGuardianPaused( + address /* cToken*/ + ) external view returns (bool) { + return false; + } +} + +contract MockCToken is MockERC20, CToken { + MockERC20 public token; + bool public error; + bool public isCEther; + InterestRateModel public irm; + address public override comptroller; + + uint256 private constant EXCHANGE_RATE_SCALE = 1e18; + uint256 public effectiveExchangeRate = 2e18; + + constructor(address _token, bool _isCEther) { + token = MockERC20(_token); + isCEther = _isCEther; + irm = new MockInterestRateModel(); + comptroller = address(new MockUnitroller()); + } + + function setError(bool _error) external { + error = _error; + } + + function setEffectiveExchangeRate(uint256 _effectiveExchangeRate) external { + effectiveExchangeRate = _effectiveExchangeRate; + } + + function isCToken() external pure returns (bool) { + return true; + } + + function underlying() external view override returns (ERC20) { + return ERC20(address(token)); + } + + function balanceOfUnderlying(address) + external + view + override + returns (uint256) + { + return 0; + } + + function mint() external payable { + _mint( + msg.sender, + (msg.value * EXCHANGE_RATE_SCALE) / effectiveExchangeRate + ); + } + + function mint(uint256 amount) external override returns (uint256) { + token.transferFrom(msg.sender, address(this), amount); + _mint( + msg.sender, + (amount * EXCHANGE_RATE_SCALE) / effectiveExchangeRate + ); + return error ? 1 : 0; + } + + function borrow(uint256) external override returns (uint256) { + return 0; + } + + function redeem(uint256 redeemTokens) external returns (uint256) { + _burn(msg.sender, redeemTokens); + uint256 redeemAmount = (redeemTokens * effectiveExchangeRate) / + EXCHANGE_RATE_SCALE; + if (address(this).balance >= redeemAmount) { + payable(msg.sender).transfer(redeemAmount); + } else { + token.transfer(msg.sender, redeemAmount); + } + return error ? 1 : 0; + } + + function redeemUnderlying(uint256 redeemAmount) + external + override + returns (uint256) + { + _burn( + msg.sender, + (redeemAmount * EXCHANGE_RATE_SCALE) / effectiveExchangeRate + ); + if (address(this).balance >= redeemAmount) { + payable(msg.sender).transfer(redeemAmount); + } else { + token.transfer(msg.sender, redeemAmount); + } + return error ? 1 : 0; + } + + function getAccountSnapshot(address) + external + view + override + returns ( + uint256, + uint256, + uint256, + uint256 + ) + { + return (0, 0, 0, 0); + } + + function exchangeRateStored() external view override returns (uint256) { + return + (EXCHANGE_RATE_SCALE * effectiveExchangeRate) / EXCHANGE_RATE_SCALE; // 2:1 + } + + function exchangeRateCurrent() external override returns (uint256) { + // fake state operation to not allow "view" modifier + effectiveExchangeRate = effectiveExchangeRate; + + return + (EXCHANGE_RATE_SCALE * effectiveExchangeRate) / EXCHANGE_RATE_SCALE; // 2:1 + } + + function getCash() external view override returns (uint256) { + return token.balanceOf(address(this)); + } + + function totalBorrows() external pure override returns (uint256) { + return 0; + } + + function totalReserves() external pure override returns (uint256) { + return 0; + } + + function totalFuseFees() external view override returns (uint256) { + return 0; + } + + function totalAdminFees() external view override returns (uint256) { + return 0; + } + + function interestRateModel() + external + view + override + returns (InterestRateModel) + { + return irm; + } + + function reserveFactorMantissa() external view override returns (uint256) { + return 0; + } + + function fuseFeeMantissa() external view override returns (uint256) { + return 0; + } + + function adminFeeMantissa() external view override returns (uint256) { + return 0; + } + + function initialExchangeRateMantissa() + external + view + override + returns (uint256) + { + return 0; + } + + function repayBorrow(uint256) external override returns (uint256) { + return 0; + } + + function repayBorrowBehalf(address, uint256) + external + override + returns (uint256) + { + return 0; + } + + function borrowBalanceCurrent(address) external override returns (uint256) { + return 0; + } + + function accrualBlockNumber() external view override returns (uint256) { + return block.number; + } +} diff --git a/src/test/mocks/MockERC20.sol b/src/test/mocks/MockERC20.sol index 6818498..bf552cc 100644 --- a/src/test/mocks/MockERC20.sol +++ b/src/test/mocks/MockERC20.sol @@ -1,22 +1,26 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.10; - -import {ERC20} from "solmate/tokens/ERC20.sol"; - -contract MockERC20 is ERC20 { - constructor() ERC20("MockToken", "MCT", 18) {} - - function mint(address account, uint256 amount) public returns (bool) { - _mint(account, amount); - return true; - } - - function mockBurn(address account, uint256 amount) public returns (bool) { - _burn(account, amount); - return true; - } - - function approveOverride(address owner, address spender, uint256 amount) public { - allowance[owner][spender] = amount; - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.10; + +import {ERC20} from "solmate/tokens/ERC20.sol"; + +contract MockERC20 is ERC20 { + constructor() ERC20("MockToken", "MCT", 18) {} + + function mint(address account, uint256 amount) public returns (bool) { + _mint(account, amount); + return true; + } + + function mockBurn(address account, uint256 amount) public returns (bool) { + _burn(account, amount); + return true; + } + + function approveOverride( + address owner, + address spender, + uint256 amount + ) public { + allowance[owner][spender] = amount; + } +} diff --git a/src/test/mocks/MockFusePriceOracle.sol b/src/test/mocks/MockFusePriceOracle.sol index b032808..ced08f5 100644 --- a/src/test/mocks/MockFusePriceOracle.sol +++ b/src/test/mocks/MockFusePriceOracle.sol @@ -1,12 +1,11 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.10; - -contract MockFusePriceOracle { - - bool public constant isPriceOracle = true; - mapping(address=>uint256) public getUnderlyingPrice; - - function mockSetPrice(address cToken, uint256 value) external { - getUnderlyingPrice[cToken] = value; - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.10; + +contract MockFusePriceOracle { + bool public constant isPriceOracle = true; + mapping(address => uint256) public getUnderlyingPrice; + + function mockSetPrice(address cToken, uint256 value) external { + getUnderlyingPrice[cToken] = value; + } +} diff --git a/src/test/utils/Console.sol b/src/test/utils/Console.sol index 019619f..ca03394 100644 --- a/src/test/utils/Console.sol +++ b/src/test/utils/Console.sol @@ -179,7 +179,7 @@ library console { } function log(bytes32[] memory p0) internal view { - for (uint256 i; i < p0.length;) { + for (uint256 i; i < p0.length; ) { _sendLogPayload(abi.encodeWithSignature("log(bytes32)", p0[i])); unchecked { ++i; @@ -192,7 +192,7 @@ library console { } function log(uint256[] memory p0) internal view { - for (uint256 i; i < p0.length;) { + for (uint256 i; i < p0.length; ) { _sendLogPayload(abi.encodeWithSignature("log(uint)", p0[i])); unchecked { ++i; @@ -205,7 +205,7 @@ library console { } function log(string[] memory p0) internal view { - for (uint256 i; i < p0.length;) { + for (uint256 i; i < p0.length; ) { _sendLogPayload(abi.encodeWithSignature("log(string)", p0[i])); unchecked { ++i; @@ -218,7 +218,7 @@ library console { } function log(bool[] memory p0) internal view { - for (uint256 i; i < p0.length;) { + for (uint256 i; i < p0.length; ) { _sendLogPayload(abi.encodeWithSignature("log(bool)", p0[i])); unchecked { ++i; diff --git a/src/test/utils/FusePoolUtils.sol b/src/test/utils/FusePoolUtils.sol index 09314d7..27fce19 100644 --- a/src/test/utils/FusePoolUtils.sol +++ b/src/test/utils/FusePoolUtils.sol @@ -1,106 +1,106 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import {Unitroller} from "../../external/Unitroller.sol"; - -library FusePoolUtils { - address public constant MASTER_PRICE_ORACLE_INITIALIZABLECLONES = - address(0x91cE5566DC3170898C5aeE4ae4dD314654B47415); - address public constant MASTER_PRICE_ORACLE_IMPLEMENTATION = - address(0xb3c8eE7309BE658c186F986388c2377da436D8fb); - address public constant MASTER_PRICE_ORACLE_RARI_DEFAULT = - address(0x1887118E49e0F4A78Bd71B792a49dE03504A764D); - address public constant FUSE_POOL_DIRECTORY = - address(0x835482FE0532f169024d5E9410199369aAD5C77E); - address public constant FUSE_COMPTROLLER_IMPLEMENTATION = - address(0xE16DB319d9dA7Ce40b666DD2E365a4b8B3C18217); - address public constant FUSE_JUMP_RATE_MODEL = - address(0xbAB47e4B692195BF064923178A90Ef999A15f819); - address public constant FUSE_CERC20_DELEGATE = - address(0x67Db14E73C2Dce786B5bbBfa4D010dEab4BBFCF9); - - function createPool(address[] memory tokens, address oracle) - external - returns ( - address masterOracle, - address troller, - address[] memory cTokens - ) - { - // create a new master price oracle - (bool success, bytes memory data) = MASTER_PRICE_ORACLE_INITIALIZABLECLONES - .call( - abi.encodeWithSignature( - "clone(address,bytes)", - MASTER_PRICE_ORACLE_IMPLEMENTATION, - abi.encodeWithSignature( - "initialize(address[],address[],address,address,bool)", - new address[](0), // underlyings - new address[](0), // oracles - MASTER_PRICE_ORACLE_RARI_DEFAULT, // default oracle - address(this), // pool admin - true // canAdminOwerwrite - ) - ) - ); - require(success, "Error creating master price oracle"); - assembly { - masterOracle := mload(add(data, 32)) - } - - // create a new Rari pool (call Rari Capital: Fuse Pool Directory) - (success, data) = FUSE_POOL_DIRECTORY.call( - abi.encodeWithSignature( - "deployPool(string,address,bool,uint256,uint256,address)", - "Test Pool", // name - FUSE_COMPTROLLER_IMPLEMENTATION, // implementation - false, // enforceWhitelist - 0.5e18, // closeFactor - 1.08e18, // liquidationIncentive - masterOracle // priceOracle - ) - ); - require(success, "Error creating pool"); - assembly { - troller := mload(add(data, 64)) - } - - // accept admin of the comptroller - Unitroller(troller)._acceptAdmin(); - - // add token price to master oracle - address[] memory oracles = new address[](tokens.length); - for (uint256 i = 0; i < oracles.length; i++) { - oracles[i] = oracle; - } - (success, data) = masterOracle.call( - abi.encodeWithSignature("add(address[],address[])", tokens, oracles) - ); - require(success, "Error setting new token price feed in master oracle"); - - // add tokens in the fuse pool - cTokens = new address[](tokens.length); - for (uint256 i = 0; i < tokens.length; i++) { - Unitroller(troller)._deployMarket( - false, // isCEther - abi.encode( // CErc20Delegator constructor data - tokens[i], // underlying - troller, // comptroller - FUSE_JUMP_RATE_MODEL, // interestRateModel - "fToken-x", // name - "Fuse pool x Token", // symbol - FUSE_CERC20_DELEGATE, // implementation - bytes(""), // becomeImplementationData - uint256(0), // reserveFactorMantissa - uint256(0) // adminFeeMantissa - ), - 0.7e18 // collateralFactorMantissa - ); - cTokens[i] = Unitroller(troller).cTokensByUnderlying(tokens[i]); - require( - cTokens[i] != address(0), - "Error adding token to Fuse pool" - ); - } - } -} +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {Unitroller} from "../../external/Unitroller.sol"; + +library FusePoolUtils { + address public constant MASTER_PRICE_ORACLE_INITIALIZABLECLONES = + address(0x91cE5566DC3170898C5aeE4ae4dD314654B47415); + address public constant MASTER_PRICE_ORACLE_IMPLEMENTATION = + address(0xb3c8eE7309BE658c186F986388c2377da436D8fb); + address public constant MASTER_PRICE_ORACLE_RARI_DEFAULT = + address(0x1887118E49e0F4A78Bd71B792a49dE03504A764D); + address public constant FUSE_POOL_DIRECTORY = + address(0x835482FE0532f169024d5E9410199369aAD5C77E); + address public constant FUSE_COMPTROLLER_IMPLEMENTATION = + address(0xE16DB319d9dA7Ce40b666DD2E365a4b8B3C18217); + address public constant FUSE_JUMP_RATE_MODEL = + address(0xbAB47e4B692195BF064923178A90Ef999A15f819); + address public constant FUSE_CERC20_DELEGATE = + address(0x67Db14E73C2Dce786B5bbBfa4D010dEab4BBFCF9); + + function createPool(address[] memory tokens, address oracle) + external + returns ( + address masterOracle, + address troller, + address[] memory cTokens + ) + { + // create a new master price oracle + (bool success, bytes memory data) = MASTER_PRICE_ORACLE_INITIALIZABLECLONES + .call( + abi.encodeWithSignature( + "clone(address,bytes)", + MASTER_PRICE_ORACLE_IMPLEMENTATION, + abi.encodeWithSignature( + "initialize(address[],address[],address,address,bool)", + new address[](0), // underlyings + new address[](0), // oracles + MASTER_PRICE_ORACLE_RARI_DEFAULT, // default oracle + address(this), // pool admin + true // canAdminOwerwrite + ) + ) + ); + require(success, "Error creating master price oracle"); + assembly { + masterOracle := mload(add(data, 32)) + } + + // create a new Rari pool (call Rari Capital: Fuse Pool Directory) + (success, data) = FUSE_POOL_DIRECTORY.call( + abi.encodeWithSignature( + "deployPool(string,address,bool,uint256,uint256,address)", + "Test Pool", // name + FUSE_COMPTROLLER_IMPLEMENTATION, // implementation + false, // enforceWhitelist + 0.5e18, // closeFactor + 1.08e18, // liquidationIncentive + masterOracle // priceOracle + ) + ); + require(success, "Error creating pool"); + assembly { + troller := mload(add(data, 64)) + } + + // accept admin of the comptroller + Unitroller(troller)._acceptAdmin(); + + // add token price to master oracle + address[] memory oracles = new address[](tokens.length); + for (uint256 i = 0; i < oracles.length; i++) { + oracles[i] = oracle; + } + (success, data) = masterOracle.call( + abi.encodeWithSignature("add(address[],address[])", tokens, oracles) + ); + require(success, "Error setting new token price feed in master oracle"); + + // add tokens in the fuse pool + cTokens = new address[](tokens.length); + for (uint256 i = 0; i < tokens.length; i++) { + Unitroller(troller)._deployMarket( + false, // isCEther + abi.encode( // CErc20Delegator constructor data + tokens[i], // underlying + troller, // comptroller + FUSE_JUMP_RATE_MODEL, // interestRateModel + "fToken-x", // name + "Fuse pool x Token", // symbol + FUSE_CERC20_DELEGATE, // implementation + bytes(""), // becomeImplementationData + uint256(0), // reserveFactorMantissa + uint256(0) // adminFeeMantissa + ), + 0.7e18 // collateralFactorMantissa + ); + cTokens[i] = Unitroller(troller).cTokensByUnderlying(tokens[i]); + require( + cTokens[i] != address(0), + "Error adding token to Fuse pool" + ); + } + } +}