diff --git a/test/blue_tests.tree b/test/blue_tests.tree index b3ab93e27..7bbd154ab 100644 --- a/test/blue_tests.tree +++ b/test/blue_tests.tree @@ -1,6 +1,182 @@ +. +└── transferOwnership(address newOwner) external onlyOwner/ + ├── when msg.sender not owner/ + │ └── revert with NOT_OWNER + └── when msg.sender is owner/ + └── should set owner to newOwner +. +└── enableIrm(IIrm irm) external onlyOwner/ + ├── when msg.sender not owner/ + │ └── revert with NOT_OWNER + └── when msg.sender is owner/ + └── should set isIrmEnabled[irm] to true +. +└── enableLltv(uint256 lltv) external onlyOwner/ + ├── when msg.sender not owner/ + │ └── revert with NOT_OWNER + └── when msg.sender is owner/ + ├── when lltv >= WAD/ + │ └── revert with LLTV_TOO_HIGH + └── when lltv < WAD/ + └── should set isLltvEnabled[lltv] to true +. +└── setFee(Market memory market, uint256 newFee) external onlyOwner/ + ├── when msg.sender not owner/ + │ └── revert with NOT_OWNER + └── when msg.sender is owner/ + ├── when market is not created/ + │ └── revert with MARKET_NOT_CREATED + └── when market is created/ + ├── when newFee > MAX_FEE/ + │ └── revert with MAX_FEE_EXCEEDED + └── when newFee <= MAX_FEE/ + └── should set fee[id] to newFee +. +└── setFeeRecipient(address recipient) external onlyOwner/ + ├── when msg.sender not owner/ + │ └── revert with NOT_OWNER + └── when msg.sender is owner/ + └── should set feeRecipient to recipient +. +└── createMarket(Market memory market) external/ + ├── when irm is not enabled/ + │ └── revert with IRM_NOT_ENABLED + └── when irm is enabled/ + ├── when lltv is not enabled/ + │ └── revert with LLTV_NOT_ENABLED + └── when lltv is enabled/ + ├── when market is already created/ + │ └── revert with MARKET_CREATED + └── when market is not already created/ + └── it should create market + . └── supply(Market calldata market, uint256 amount, address onBehalf) external/ ├── when market is not created/ │ └── revert with MARKET_NOT_CREATED - └── when the amount to supply is zero/ - └── revert with ZERO_AMOUNT \ No newline at end of file + └── when market is created/ + ├── when the amount to supply is zero/ + │ └── revert with ZERO_AMOUNT + └── when the amount to supply is not zero/ + ├── it should add amount.toSharesDown(totalSupply[id], totalSupplyShares[id]) to supplyShare[id][onBehalf] + ├── it should add amount.toSharesDown(totalSupply[id], totalSupplyShares[id]) to totalSupplyShares[id] + ├── it should add amount to totalSupply[id] + └── it should make the ERC-20 transfer (checks on blue and supplier balances) +. +└── withdraw(Market memory market, uint256 amount, address onBehalf) external/ + ├── when market is not created/ + │ └── revert with MARKET_NOT_CREATED + └── when market is created/ + ├── when the amount to withdraw is zero/ + │ └── revert with ZERO_AMOUNT + └── when the amount to withdraw is not zero/ + ├── when not sender and not approved/ + │ └── revert with MANAGER_NOT_APPROVED + └── when sender or approved/ + ├── when totalBorrow > totalSupply/ + │ └── revert with INSUFFICIENT_LIQUIDITY + └── when totalBorrow <= totalSupply/ + ├── it should remove amount.toSharesUp(totalSupply[id], totalSupplyShares[id]) to supplyShare[id][onBehalf] + ├── it should remove amount.toSharesUp(totalSupply[id], totalSupplyShares[id]) to totalSupplyShares[id] + ├── it should remouve amount from totalSupply[id] + └── it should make the ERC-20 transfer (checks on blue and withdrawer balances) +. +└── borrow(Market memory market, uint256 amount, address onBehalf) external/ + ├── when market is not created/ + │ └── revert with MARKET_NOT_CREATED + └── when market is created/ + ├── when the amount to borrow is zero/ + │ └── revert with ZERO_AMOUNT + └── when the amount to borrow is not zero/ + ├── when not sender and not approved/ + │ └── revert with MANAGER_NOT_APPROVED + └── when sender or approved/ + ├── when position not healthy/ + │ └── revert with INSUFFICIENT_COLLATERAL + └── when position healthy/ + ├── when totalBorrow > totalSupply/ + │ └── revert with INSUFFICIENT_LIQUIDITY + └── when totalBorrow <= totalSupply/ + ├── it should add amount.toSharesUp(totalBorrow[id], totalBorrowShares[id]) to borrowShare[id][onBehalf] + ├── it should add amount.toSharesUp(totalBorrow[id], totalBorrowShares[id]) to totalBorrowShares[id] + ├── it should add amount to totalBorrow[id] + └── it should make the ERC-20 transfer (checks on blue and borrower balances) +. +└── repay(Market memory market, uint256 amount, address onBehalf) external/ + ├── when market is not created/ + │ └── revert with MARKET_NOT_CREATED + └── when market is created/ + ├── when the amount to repay is zero/ + │ └── revert with ZERO_AMOUNT + └── when the amount to repay is not zero/ + ├── it should remove amount.toSharesDown(totalBorrow[id], totalBorrowShares[id]) from borrowShare[id][onBehalf] + ├── it should remove amount.toSharesDown(totalBorrow[id], totalBorrowShares[id]) from totalBorrowShares[id], + ├── it should remove amount from totalBorrow[id] + └── it should make the ERC-20 transfer (checks on blue and repayer balances) +. +└── supplyCollateral(Market memory market, uint256 amount, address onBehalf) external/ + ├── when market is not created/ + │ └── revert with MARKET_NOT_CREATED + └── when market is created/ + ├── when the amount to supply is zero/ + │ └── revert with ZERO_AMOUNT + └── when the amount to supply is not zero/ + ├── it should add amount to collateral[id][onBehalf] + └── it should make the ERC-20 transfer (checks on blue and supplier balances) +. +└── withdrawCollateral(Market memory market, uint256 amount, address onBehalf) external/ + ├── when market is not created/ + │ └── revert with MARKET_NOT_CREATED + └── when market is created/ + ├── when the amount to withdraw is zero/ + │ └── revert with ZERO_AMOUNT + └── when the amount to withdraw is not zero/ + ├── when not sender and not approved/ + │ └── revert with MANAGER_NOT_APPROVED + └── when sender or approved/ + ├── when position not healthy/ + │ └── revert with INSUFFICIENT_COLLATERAL + └── when position healthy/ + ├── it should remove amount from collateral[id][onBehalf] + └── it should make the ERC-20 transfer (checks on blue and withdrawer balances) +. +└── liquidate(Market memory market, address borrower, address onBehalf) external/ + ├── when market is not created/ + │ └── revert with MARKET_NOT_CREATED + └── when market is created/ + ├── when the amount to seized is zero/ + │ └── revert with ZERO_AMOUNT + └── when the amount to seized is not zero/ + ├── when position is healthy/ + │ └── revert with HEALTHY_POSITION + └── when the position not healthy/ + ├── it should compute repaid = seized.mulWadUp(collateralPrice).divWadUp(incentive).divWadUp(borrowablePrice); + ├── it should remove repaid.toSharesDown(totalBorrow[id], totalBorrowShares[id]) from borrowShare[id][borrower] + ├── it should remove repaid.toSharesDown(totalBorrow[id], totalBorrowShares[id]) from totalBorrowShares[id] + ├── it should remove repaid from totalBorrow[id] + ├── it should remove seized from collateral[id][borrower] + ├── it should make the ERC-20 transfers (checks on blue and liquidator balances) + └── if after the liquidation the borrower's collateral is 0/ + └── it should realise bad debt/ + ├── it should compute badDebt = borrowShare[id][borrower].toAssetsUp(totalBorrow[id], totalBorrowShares[id]) + ├── it should remove bad debt from totalSupply[id] + ├── it should remove bad debt from totalBorrow[id] + ├── it should remove borrowShare[id][borrower] from totalBorrowShares[id] + └── it should set borrowShare[id][borrower] to 0 +. +└── setApproval(address manager, bool isAllowed) external/ + └── should set isApproved[msg.sender][manager] to isAllowed +. +└── _accrueInterests(Market memory market, Id id) internal/ + ├── when marketTotalBorrow is 0/ + │ └── it should just set lastUpdate to block.timestamp + └── when marketTotalBorrow is not 0/ + ├── when lastUpdate = block.timestamp/ + │ └── it should just set lastUpdate to block.timestamp + └── when lastUpdate doesn't equal block.timestamp/ + ├── it should add accruedInterests to totalBorrow + ├── it should add accruedInterests to totalSupply + └── when fee[id] != 0/ + ├── it should add accruedInterests.mulWadDown(fee[id]) to feeAmount + ├── it should add feeAmount.mulDivDown(totalSupplyShares[id], totalSupply[id] - feeAmount) to supplyShare[id][feeRecipient] + └── it should add feeAmount.mulDivDown(totalSupplyShares[id], totalSupply[id] - feeAmount) to totalSupplyShares[id] \ No newline at end of file diff --git a/test/forge/BlueBase.t.sol b/test/forge/BlueBase.t.sol new file mode 100644 index 000000000..cec424d30 --- /dev/null +++ b/test/forge/BlueBase.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "src/Blue.sol"; +import {ERC20Mock as ERC20} from "src/mocks/ERC20Mock.sol"; +import {OracleMock as Oracle} from "src/mocks/OracleMock.sol"; +import {IrmMock as Irm} from "src/mocks/IrmMock.sol"; + +contract BlueBaseTest is Test { + using FixedPointMathLib for uint256; + + address private constant BORROWER = address(1234); + address private constant LIQUIDATOR = address(5678); + uint256 private constant LLTV = 0.8 ether; + address private constant OWNER = address(0xdead); + + Blue private blue; + ERC20 private borrowableAsset; + ERC20 private collateralAsset; + Oracle private borrowableOracle; + Oracle private collateralOracle; + Irm private irm; + Market public market; + Id public id; + + function setUp() public { + // Create Blue. + blue = new Blue(OWNER); + + // List a market. + borrowableAsset = new ERC20("borrowable", "B", 18); + collateralAsset = new ERC20("collateral", "C", 18); + borrowableOracle = new Oracle(); + collateralOracle = new Oracle(); + + irm = new Irm(blue); + market = Market( + IERC20(address(borrowableAsset)), + IERC20(address(collateralAsset)), + borrowableOracle, + collateralOracle, + irm, + LLTV + ); + id = Id.wrap(keccak256(abi.encode(market))); + + vm.startPrank(OWNER); + blue.enableIrm(irm); + blue.enableLltv(LLTV); + blue.createMarket(market); + vm.stopPrank(); + + // We set the price of the borrowable asset to zero so that borrowers + // don't need to deposit any collateral. + borrowableOracle.setPrice(0); + collateralOracle.setPrice(1e18); + + borrowableAsset.approve(address(blue), type(uint256).max); + collateralAsset.approve(address(blue), type(uint256).max); + vm.startPrank(BORROWER); + borrowableAsset.approve(address(blue), type(uint256).max); + collateralAsset.approve(address(blue), type(uint256).max); + vm.stopPrank(); + vm.startPrank(LIQUIDATOR); + borrowableAsset.approve(address(blue), type(uint256).max); + collateralAsset.approve(address(blue), type(uint256).max); + vm.stopPrank(); + } + + function netWorth(address user) internal view returns (uint256) { + uint256 collateralAssetValue = collateralAsset.balanceOf(user).mulWadDown(collateralOracle.price()); + uint256 borrowableAssetValue = borrowableAsset.balanceOf(user).mulWadDown(borrowableOracle.price()); + return collateralAssetValue + borrowableAssetValue; + } + + function supplyBalance(address user) internal view returns (uint256) { + uint256 supplyShares = blue.supplyShare(id, user); + if (supplyShares == 0) return 0; + + uint256 totalShares = blue.totalSupplyShares(id); + uint256 totalSupply = blue.totalSupply(id); + return supplyShares.divWadDown(totalShares).mulWadDown(totalSupply); + } + + function borrowBalance(address user) internal view returns (uint256) { + uint256 borrowerShares = blue.borrowShare(id, user); + if (borrowerShares == 0) return 0; + + uint256 totalShares = blue.totalBorrowShares(id); + uint256 totalBorrow = blue.totalBorrow(id); + return borrowerShares.divWadUp(totalShares).mulWadUp(totalBorrow); + } +} diff --git a/test/forge/integration/TestIntegrationSupply.sol b/test/forge/integration/TestIntegrationSupply.sol new file mode 100644 index 000000000..6f91f3274 --- /dev/null +++ b/test/forge/integration/TestIntegrationSupply.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +import "test/forge/BlueBase.t.sol"; + +contract BlueBaseTest is BlueBaseTest { + function testSupplyUnknownMarket(Market memory marketFuzz) public { + vm.assume(neq(marketFuzz, market)); + + vm.expectRevert("unknown market"); + blue.supply(marketFuzz, 1, address(this)); + } + + function testSupplyUnknownMarket() public { + vm.expectRevert("zero amount"); + blue.supply(market, 0, address(this)); + } + + function testSupply(uint256 amount) {} + + function testSupplyOnBehalf(uint256 amount, address onBehalf) {} +}