diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03f5d5b..30db157 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,24 +19,24 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 - name: Run tests - run: forge test --no-match-path "test/invariant/**/*.sol" + run: forge test --no-match-path "test/fork/**/*.sol" env: FOUNDRY_PROFILE: ci FORK_TESTS: false - test-invariant: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 + # test-fork: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 + # - name: Install Foundry + # uses: foundry-rs/foundry-toolchain@v1 - - name: Run tests - run: forge test --match-path "test/invariant/**/*.sol" - env: - FOUNDRY_PROFILE: ci - FORK_TESTS: false + # - name: Run tests + # run: forge test --match-path "test/fork/**/*.sol" + # env: + # FOUNDRY_PROFILE: ci + # FORK_TESTS: false lint: runs-on: ubuntu-latest @@ -102,35 +102,4 @@ jobs: uses: zgosalvez/github-actions-report-lcov@v2 with: coverage-files: ./lcov.info - minimum-coverage: 60 # Set coverage threshold. - - # slither-analyze: - # runs-on: "ubuntu-latest" - # permissions: - # actions: "read" - # contents: "read" - # security-events: "write" - # steps: - # - name: "Check out the repo" - # uses: "actions/checkout@v3" - # with: - # submodules: "recursive" - - # - name: "Run Slither analysis" - # uses: "crytic/slither-action@v0.3.0" - # id: "slither" - # with: - # fail-on: "none" - # sarif: "results.sarif" - # solc-version: "0.8.21" - # target: "src/" - - # - name: Upload SARIF file - # uses: github/codeql-action/upload-sarif@v2 - # with: - # sarif_file: ${{ steps.slither.outputs.sarif }} - - # - name: "Add Slither summary" - # run: | - # echo "## Slither result" >> $GITHUB_STEP_SUMMARY - # echo "✅ Uploaded to GitHub code scanning" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + minimum-coverage: 60 # Set coverage threshold. \ No newline at end of file diff --git a/src/Conduit.sol b/src/Conduit.sol index 4dbf319..ab4baf3 100644 --- a/src/Conduit.sol +++ b/src/Conduit.sol @@ -4,10 +4,10 @@ pragma solidity 0.8.21; interface ERC20Like { function transferFrom(address from, address to, uint256 amount) external returns (bool); function transfer(address to, uint256 amount) external returns (bool); + function approve(address spender, uint256 value) external returns (bool); function balanceOf(address user) external returns (uint256 amount); function mint(address user, uint256 amount) external; function burn(address user, uint256 amount) external; - function approve(address user, uint256 amount) external; } interface OutputConduitLike { @@ -37,7 +37,6 @@ interface PoolManagerLike { function transfer(address currency, bytes32 recipient, uint128 amount) external; } -// https://forum.makerdao.com/t/rwa015-project-andromeda-technical-assessment/20974#drawing-dai-swapping-for-stablecoin-and-investing-into-bonds-13 contract Conduit { address public immutable psm; ERC20Like public immutable dai; @@ -63,9 +62,9 @@ contract Conduit { address public withdrawal; // Centrifuge pool - ERC7540Like pool; - PoolManagerLike poolManager; - bytes32 claimRecipient; + ERC7540Like public pool; + PoolManagerLike public poolManager; + bytes32 public depositRecipient; mapping(address => uint256) public wards; mapping(address => uint256) public can; @@ -172,8 +171,8 @@ contract Conduit { } function file(bytes32 what, bytes32 data) external onlyOperator { - if (what == "claimRecipient") { - claimRecipient = data; + if (what == "depositRecipient") { + depositRecipient = data; } else { revert("AndromedaPaymentConduit/unrecognised-param"); } @@ -184,7 +183,7 @@ contract Conduit { /// -- Invest -- /// @notice Submit investment request for LTF tokens function requestDeposit() public onlyMate { - // Get USDC from outputConduit + // Get gem from outputConduit outputConduit.pick(address(this)); outputConduit.hook(psm); outputConduit.push(); @@ -192,8 +191,9 @@ contract Conduit { // Mint deposit tokens uint256 amount = gem.balanceOf(address(this)); depositAsset.mint(address(this), amount); - depositAsset.approve(address(pool), amount); + // Deposit in pool + depositAsset.approve(address(pool), amount); pool.requestDeposit(amount, address(this), address(this), ""); } @@ -210,7 +210,6 @@ contract Conduit { uint256 amount = depositAsset.balanceOf(address(this)); depositAsset.burn(address(this), amount); - claimDeposit(); gem.transferFrom(address(this), withdrawal, amount); } @@ -223,17 +222,19 @@ contract Conduit { /// @notice Lock deposit tokens in pool function depositIntoPool() public onlyMate { + require(depositRecipient != "", "AndromedaPaymentConduit/deposit-recipient-is-zero"); + uint256 amount = gem.balanceOf(address(this)); depositAsset.mint(address(this), amount); - poolManager.transfer(address(depositAsset), claimRecipient, _toUint128(amount)); + poolManager.transfer(address(depositAsset), depositRecipient, _toUint128(amount)); } /// @notice Claim and burn redeemed deposit tokens function claimRedeem() public onlyMate { - uint256 claimable = pool.maxRedeem(address(this)); - pool.redeem(claimable, address(this), address(this)); + uint256 claimableShares = pool.maxRedeem(address(this)); + uint256 redeemedAssets = pool.redeem(claimableShares, address(this), address(this)); - depositAsset.burn(address(this), claimable); + depositAsset.burn(address(this), redeemedAssets); } /// @notice Send gem as interest to jar @@ -260,7 +261,7 @@ contract Conduit { /// -- Helpers -- function _toUint128(uint256 _value) internal pure returns (uint128 value) { if (_value > type(uint128).max) { - revert("MathLib/uint128-overflow"); + revert("AndromedaPaymentConduit/uint128-overflow"); } else { value = uint128(_value); } diff --git a/test/Conduit.t.sol b/test/Conduit.t.sol index 68cebdf..91fe825 100644 --- a/test/Conduit.t.sol +++ b/test/Conduit.t.sol @@ -5,58 +5,110 @@ import "forge-std/Test.sol"; import {MockInputConduit} from "test/mocks/MockInputConduit.sol"; import {MockOutputConduit} from "test/mocks/MockOutputConduit.sol"; import {MockLiquidityPool} from "test/mocks/MockLiquidityPool.sol"; +import {MockBrokenLiquidityPool} from "test/mocks/MockBrokenLiquidityPool.sol"; +import {MockPoolManager} from "test/mocks/MockPoolManager.sol"; import {Conduit} from "src/Conduit.sol"; import {ERC20} from "src/token/ERC20.sol"; contract ConduitTest is Test { address psm = makeAddr("PSM"); + address urn = makeAddr("Urn"); + address jar = makeAddr("Jar"); ERC20 dai; ERC20 gem; - ERC20 depositToken; + ERC20 depositAsset; MockOutputConduit outputConduit; MockInputConduit urnConduit; MockInputConduit jarConduit; address operator; + address withdrawal = makeAddr("Withdrawal"); + + bytes32 depositRecipient; MockLiquidityPool pool; + MockPoolManager poolManager; - address mate; + address mate = makeAddr("Mate"); Conduit conduit; function setUp() public { dai = new ERC20("DAI", "DAI", 18); gem = new ERC20("USD Coin", "USDC", 6); - depositToken = new ERC20("Andromeda USDC Deposit", "andrUSDC", 6); + depositAsset = new ERC20("Andromeda USDC Deposit", "andrUSDC", 6); outputConduit = new MockOutputConduit(address(gem)); - urnConduit = new MockInputConduit(address(gem)); - jarConduit = new MockInputConduit(address(gem)); + urnConduit = new MockInputConduit(address(gem), address(urn)); + jarConduit = new MockInputConduit(address(gem), address(jar)); gem.rely(address(outputConduit)); - conduit = new Conduit(psm, address(dai), address(gem), address(depositToken), address(outputConduit), address(urnConduit), address(jarConduit)); - depositToken.rely(address(conduit)); + conduit = new Conduit(psm, address(dai), address(gem), address(depositAsset), address(outputConduit), address(urnConduit), address(jarConduit)); + depositAsset.rely(address(conduit)); conduit.hope(operator); conduit.mate(mate); pool = new MockLiquidityPool(); - // TODO: poolManager + poolManager = new MockPoolManager(); + depositRecipient = "DepositRecipient"; vm.prank(operator); - conduit.file("pool", address(pool), address(0)); + conduit.file("pool", address(pool), address(poolManager)); vm.label(address(dai), "DAI"); vm.label(address(gem), "Gem"); - vm.label(address(depositToken), "DepositToken"); + vm.label(address(depositAsset), "depositAsset"); vm.label(address(outputConduit), "OutputConduit"); vm.label(address(urnConduit), "UrnConduit"); vm.label(address(jarConduit), "JarConduit"); vm.label(address(conduit), "Conduit"); } + function testPermissions(address anotherWard) public { + vm.assume(anotherWard != address(this)); + + conduit.nope(operator); + assertEq(conduit.can(operator), 0); + conduit.hope(operator); + assertEq(conduit.can(operator), 1); + + conduit.hate(mate); + assertEq(conduit.may(mate), 0); + conduit.mate(mate); + assertEq(conduit.may(mate), 1); + + conduit.rely(anotherWard); + assertEq(conduit.wards(anotherWard), 1); + conduit.deny(anotherWard); + assertEq(conduit.wards(anotherWard), 0); + } + + function testFile(address anotherWithdrawal, address anotherPool, address anotherPoolManager, bytes32 anotherDepositRecipient) public { + vm.expectRevert(bytes("AndromedaPaymentConduit/unrecognised-param")); + conduit.file("withdrawal2", anotherWithdrawal); + + conduit.file("withdrawal", anotherWithdrawal); + assertEq(conduit.withdrawal(), anotherWithdrawal); + + vm.startPrank(operator); + + vm.expectRevert(bytes("AndromedaPaymentConduit/unrecognised-param")); + conduit.file("pool2", anotherPool, anotherPoolManager); + + conduit.file("pool", anotherPool, anotherPoolManager); + assertEq(address(conduit.pool()), anotherPool); + assertEq(address(conduit.poolManager()), anotherPoolManager); + + vm.expectRevert(bytes("AndromedaPaymentConduit/unrecognised-param")); + conduit.file("depositRecipient2", anotherDepositRecipient); + + conduit.file("depositRecipient", anotherDepositRecipient); + assertEq(conduit.depositRecipient(), anotherDepositRecipient); + + } + function testWardSetup(address notDeployer) public { vm.assume(address(this) != notDeployer); @@ -75,14 +127,15 @@ contract ConduitTest is Test { outputConduit.setPush(gemAmount); assertEq(gem.balanceOf(address(outputConduit)), gemAmount); assertEq(gem.balanceOf(address(conduit)), 0); - assertEq(depositToken.balanceOf(address(conduit)), 0); + assertEq(depositAsset.balanceOf(address(conduit)), 0); vm.startPrank(mate); conduit.requestDeposit(); assertEq(gem.balanceOf(address(outputConduit)), 0); assertEq(gem.balanceOf(address(conduit)), gemAmount); - assertEq(depositToken.balanceOf(address(conduit)), gemAmount); + assertEq(depositAsset.balanceOf(address(conduit)), gemAmount); + assertEq(depositAsset.allowance(address(conduit), address(pool)), gemAmount); assertEq(pool.values_uint256("requestDeposit_assets"), gemAmount); assertEq(pool.values_address("requestDeposit_receiver"), address(conduit)); @@ -102,4 +155,201 @@ contract ConduitTest is Test { assertEq(pool.values_uint256("deposit_assets"), gemAmount); assertEq(pool.values_address("deposit_receiver"), address(conduit)); } + + function testWithdrawFromPool(address notMate, uint256 gemAmount) public { + vm.assume(mate != notMate); + + outputConduit.setPush(gemAmount); + pool.setReturn("maxDeposit", gemAmount); + + vm.expectRevert(bytes("AndromedaPaymentConduit/not-mate")); + vm.prank(notMate); + conduit.withdrawFromPool(); + + vm.prank(mate); + vm.expectRevert(bytes("AndromedaPaymentConduit/withdrawal-is-zero")); + conduit.withdrawFromPool(); + + conduit.file("withdrawal", withdrawal); + vm.startPrank(mate); + conduit.requestDeposit(); + + assertEq(depositAsset.balanceOf(address(conduit)), gemAmount); + assertEq(depositAsset.totalSupply(), gemAmount); + assertEq(gem.balanceOf(address(conduit)), gemAmount); + assertEq(gem.balanceOf(address(withdrawal)), 0); + + conduit.withdrawFromPool(); + + assertEq(depositAsset.balanceOf(address(conduit)), 0); + assertEq(depositAsset.totalSupply(), 0); + assertEq(gem.balanceOf(address(conduit)), 0); + assertEq(gem.balanceOf(address(withdrawal)), gemAmount); + } + + function testRequestRedeem(address notMate, uint256 gemAmount) public { + vm.assume(mate != notMate); + + outputConduit.setPush(gemAmount); + pool.setReturn("maxDeposit", gemAmount); + conduit.file("withdrawal", makeAddr("Withdrawal")); + + vm.startPrank(mate); + conduit.requestDeposit(); + conduit.withdrawFromPool(); + vm.stopPrank(); + + // Assumes price = 1.0 + uint256 shareAmount = gemAmount; + + vm.expectRevert(bytes("AndromedaPaymentConduit/not-mate")); + vm.prank(notMate); + conduit.requestRedeem(shareAmount); + + vm.startPrank(mate); + conduit.requestRedeem(shareAmount); + assertEq(pool.values_uint256("requestRedeem_shares"), shareAmount); + assertEq(pool.values_address("requestRedeem_receiver"), address(conduit)); + assertEq(pool.values_address("requestRedeem_owner"), address(conduit)); + assertEq(pool.values_bytes("requestRedeem_data"), ""); + } + + function testDepositIntoPool(address notMate, uint256 gemAmount) public { + vm.assume(mate != notMate); + gemAmount = bound(gemAmount, 0, type(uint128).max); + + vm.expectRevert(bytes("AndromedaPaymentConduit/not-mate")); + vm.prank(notMate); + conduit.depositIntoPool(); + + vm.startPrank(mate); + vm.expectRevert(bytes("AndromedaPaymentConduit/deposit-recipient-is-zero")); + conduit.depositIntoPool(); + assertEq(gem.balanceOf(address(conduit)), 0); + assertEq(depositAsset.balanceOf(address(conduit)), 0); + vm.stopPrank(); + + vm.startPrank(operator); + conduit.file("depositRecipient", depositRecipient); + vm.stopPrank(); + + gem.mint(address(conduit), gemAmount); + assertEq(gem.balanceOf(address(conduit)), gemAmount); + assertEq(depositAsset.balanceOf(address(conduit)), 0); + + vm.startPrank(mate); + conduit.depositIntoPool(); + assertEq(gem.balanceOf(address(conduit)), gemAmount); + assertEq(depositAsset.balanceOf(address(conduit)), gemAmount); + + assertEq(poolManager.values_address("transfer_currency"), address(depositAsset)); + assertEq(poolManager.values_bytes32("transfer_recipient"), depositRecipient); + assertEq(poolManager.values_uint128("transfer_amount"), uint128(gemAmount)); + } + + function testDepositIntoPoolOverflow(uint256 gemAmount) public { + vm.assume(gemAmount > type(uint128).max); + + vm.startPrank(operator); + conduit.file("depositRecipient", depositRecipient); + vm.stopPrank(); + + gem.mint(address(conduit), gemAmount); + + vm.startPrank(mate); + vm.expectRevert(bytes("AndromedaPaymentConduit/uint128-overflow")); + conduit.depositIntoPool(); + } + + function testClaimRedeem(address notMate, uint256 gemAmount, uint256 shareAmount) public { + vm.assume(mate != notMate); + + pool.setReturn("redeem", gemAmount); + pool.setReturn("maxRedeem", shareAmount); + depositAsset.mint(address(conduit), gemAmount); + + vm.expectRevert(bytes("AndromedaPaymentConduit/not-mate")); + vm.prank(notMate); + conduit.claimRedeem(); + + assertEq(depositAsset.balanceOf(address(conduit)), gemAmount); + + vm.startPrank(mate); + conduit.claimRedeem(); + + assertEq(depositAsset.balanceOf(address(conduit)), 0); + + assertEq(pool.values_uint256("redeem_shares"), shareAmount); + assertEq(pool.values_address("redeem_receiver"), address(conduit)); + assertEq(pool.values_address("redeem_owner"), address(conduit)); + } + + function testRepay() public { + // todo + } + + function testRepayBrokenLiquidityPool(uint256 jarRepayAmount, uint256 urnRepayAmount) public { + jarRepayAmount = bound(jarRepayAmount, 0, type(uint128).max); + urnRepayAmount = bound(urnRepayAmount, 0, type(uint128).max); + + conduit.file("withdrawal", withdrawal); + + // Liquidity Pool is broken (always reverting) + MockBrokenLiquidityPool brokenPool = new MockBrokenLiquidityPool(); + vm.prank(operator); + conduit.file("pool", address(brokenPool), address(poolManager)); + + // On-ramp to exchange agent + gem.mint(address(withdrawal), jarRepayAmount + urnRepayAmount); + + // Send to conduit + vm.prank(withdrawal); + gem.transfer(address(conduit), jarRepayAmount + urnRepayAmount); + + assertEq(gem.balanceOf(address(jar)), 0); + assertEq(gem.balanceOf(address(urn)), 0); + + // Repay to jar & urn + vm.startPrank(mate); + conduit.repayToJar(jarRepayAmount); + conduit.repayToUrn(urnRepayAmount); + + assertEq(gem.balanceOf(address(jar)), jarRepayAmount); + assertEq(gem.balanceOf(address(urn)), urnRepayAmount); + } + + function testAuthMint(address notMate, uint256 amount) public { + vm.assume(mate != notMate); + + vm.expectRevert(bytes("AndromedaPaymentConduit/not-mate")); + vm.prank(notMate); + conduit.authMint(amount); + + assertEq(depositAsset.balanceOf(address(conduit)), 0); + + vm.startPrank(mate); + conduit.authMint(amount); + + assertEq(depositAsset.balanceOf(address(conduit)), amount); + } + + function testAuthBurn(address notMate, uint256 mintAmount, uint256 burnAmount) public { + vm.assume(mate != notMate); + burnAmount = bound(burnAmount, 0, mintAmount); + + vm.startPrank(mate); + conduit.authMint(mintAmount); + vm.stopPrank(); + + vm.expectRevert(bytes("AndromedaPaymentConduit/not-mate")); + vm.prank(notMate); + conduit.authBurn(burnAmount); + + assertEq(depositAsset.balanceOf(address(conduit)), mintAmount); + + vm.startPrank(mate); + conduit.authBurn(burnAmount); + + assertEq(depositAsset.balanceOf(address(conduit)), mintAmount - burnAmount); + } } diff --git a/test/ERC20.t.sol b/test/ERC20.t.sol new file mode 100644 index 0000000..9cd4f56 --- /dev/null +++ b/test/ERC20.t.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import {ERC20} from "src/token/ERC20.sol"; +import "forge-std/Test.sol"; + +/// @author Modified from https://github.com/makerdao/xdomain-dss/blob/master/src/test/Dai.t.sol +contract ERC20Test is Test { + ERC20 token; + + event Transfer(address indexed from, address indexed to, uint256 amount); + event Approval(address indexed owner, address indexed spender, uint256 amount); + + function setUp() public { + token = new ERC20("Name", "SYMBOL", 18); + } + + function testPermissions(address anotherWard) public { + vm.assume(anotherWard != address(this)); + + token.rely(anotherWard); + assertEq(token.wards(anotherWard), 1); + token.deny(anotherWard); + assertEq(token.wards(anotherWard), 0); + } + + function testMint() public { + vm.expectEmit(true, true, true, true); + emit Transfer(address(0), address(0xBEEF), 1e18); + token.mint(address(0xBEEF), 1e18); + + assertEq(token.totalSupply(), 1e18); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testMintBadAddress() public { + vm.expectRevert("ERC20/invalid-address"); + token.mint(address(0), 1e18); + vm.expectRevert("ERC20/invalid-address"); + token.mint(address(token), 1e18); + } + + function testBurn() public { + token.mint(address(0xBEEF), 1e18); + token.rely(address(0xBEEF)); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(0xBEEF), address(0), 0.9e18); + vm.prank(address(0xBEEF)); + token.burn(address(0xBEEF), 0.9e18); + + assertEq(token.totalSupply(), 1e18 - 0.9e18); + assertEq(token.balanceOf(address(0xBEEF)), 0.1e18); + } + + function testBurnWithAllowance() public { + address from = address(0xABCD); + + token.mint(address(0xBEEF), 1e18); + token.rely(address(from)); + + vm.prank(address(from)); + vm.expectRevert(bytes("ERC20/insufficient-allowance")); + token.burn(address(0xBEEF), 0.9e18); + + vm.prank(address(0xBEEF)); + token.approve(address(from), 1e18); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(0xBEEF), address(0), 0.9e18); + vm.prank(address(from)); + token.burn(address(0xBEEF), 0.9e18); + } + + function testApprove() public { + vm.expectEmit(true, true, true, true); + emit Approval(address(this), address(0xBEEF), 1e18); + assertTrue(token.approve(address(0xBEEF), 1e18)); + + assertEq(token.allowance(address(this), address(0xBEEF)), 1e18); + } + + function testTransfer() public { + token.mint(address(this), 1e18); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(this), address(0xBEEF), 1e18); + assertTrue(token.transfer(address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testTransferBadAddress() public { + token.mint(address(this), 1e18); + + vm.expectRevert("ERC20/invalid-address"); + token.transfer(address(0), 1e18); + vm.expectRevert("ERC20/invalid-address"); + token.transfer(address(token), 1e18); + } + + function testTransferFrom() public { + address from = address(0xABCD); + + token.mint(from, 1e18); + + vm.prank(from); + token.approve(address(this), 1e18); + + vm.expectEmit(true, true, true, true); + emit Transfer(from, address(0xBEEF), 1e18); + assertTrue(token.transferFrom(from, address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(from, address(this)), 0); + + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testTransferFromBadAddress() public { + token.mint(address(this), 1e18); + + vm.expectRevert("ERC20/invalid-address"); + token.transferFrom(address(this), address(0), 1e18); + vm.expectRevert("ERC20/invalid-address"); + token.transferFrom(address(this), address(token), 1e18); + } + + function testInfiniteApproveTransferFrom() public { + address from = address(0xABCD); + + token.mint(from, 1e18); + + vm.prank(from); + vm.expectEmit(true, true, true, true); + emit Approval(from, address(this), type(uint256).max); + token.approve(address(this), type(uint256).max); + + vm.expectEmit(true, true, true, true); + emit Transfer(from, address(0xBEEF), 1e18); + assertTrue(token.transferFrom(from, address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(from, address(this)), type(uint256).max); + + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testTransferInsufficientBalance() public { + token.mint(address(this), 0.9e18); + vm.expectRevert("ERC20/insufficient-balance"); + token.transfer(address(0xBEEF), 1e18); + } + + function testTransferFromInsufficientAllowance() public { + address from = address(0xABCD); + + token.mint(from, 1e18); + + vm.prank(from); + token.approve(address(this), 0.9e18); + + vm.expectRevert("ERC20/insufficient-allowance"); + token.transferFrom(from, address(0xBEEF), 1e18); + } + + function testTransferFromInsufficientBalance() public { + address from = address(0xABCD); + + token.mint(from, 0.9e18); + + vm.prank(from); + token.approve(address(this), 1e18); + + vm.expectRevert("ERC20/insufficient-balance"); + token.transferFrom(from, address(0xBEEF), 1e18); + } + + function testMint(address to, uint256 amount) public { + if (to != address(0) && to != address(token)) { + vm.expectEmit(true, true, true, true); + emit Transfer(address(0), to, amount); + } else { + vm.expectRevert("ERC20/invalid-address"); + } + token.mint(to, amount); + + if (to != address(0) && to != address(token)) { + assertEq(token.totalSupply(), amount); + assertEq(token.balanceOf(to), amount); + } + } + + function testBurn(address from, uint256 mintAmount, uint256 burnAmount) public { + if (from == address(0) || from == address(token)) return; + + burnAmount = bound(burnAmount, 0, mintAmount); + + token.mint(from, mintAmount); + token.rely(from); + + vm.expectEmit(true, true, true, true); + emit Transfer(from, address(0), burnAmount); + vm.prank(from); + token.burn(from, burnAmount); + + assertEq(token.totalSupply(), mintAmount - burnAmount); + assertEq(token.balanceOf(from), mintAmount - burnAmount); + } + + function testApprove(address to, uint256 amount) public { + vm.expectEmit(true, true, true, true); + emit Approval(address(this), to, amount); + assertTrue(token.approve(to, amount)); + + assertEq(token.allowance(address(this), to), amount); + } + + function testTransfer(address to, uint256 amount) public { + if (to == address(0) || to == address(token)) return; + + token.mint(address(this), amount); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(this), to, amount); + assertTrue(token.transfer(to, amount)); + assertEq(token.totalSupply(), amount); + + if (address(this) == to) { + assertEq(token.balanceOf(address(this)), amount); + } else { + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(to), amount); + } + } + + function testTransferFrom(address to, uint256 approval, uint256 amount) public { + if (to == address(0) || to == address(token)) return; + + amount = bound(amount, 0, approval); + + address from = address(0xABCD); + + token.mint(from, amount); + + vm.prank(from); + token.approve(address(this), approval); + + vm.expectEmit(true, true, true, true); + emit Transfer(from, to, amount); + assertTrue(token.transferFrom(from, to, amount)); + assertEq(token.totalSupply(), amount); + + uint256 app = from == address(this) || approval == type(uint256).max ? approval : approval - amount; + assertEq(token.allowance(from, address(this)), app); + + if (from == to) { + assertEq(token.balanceOf(from), amount); + } else { + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(to), amount); + } + } + + function testBurnInsufficientBalance(address to, uint256 mintAmount, uint256 burnAmount) public { + if (to == address(0) || to == address(token)) return; + + if (mintAmount == type(uint256).max) mintAmount -= 1; + burnAmount = bound(burnAmount, mintAmount + 1, type(uint256).max); + + token.mint(to, mintAmount); + vm.expectRevert("ERC20/insufficient-balance"); + token.burn(to, burnAmount); + } + + function testTransferInsufficientBalance(address to, uint256 mintAmount, uint256 sendAmount) public { + if (to == address(0) || to == address(token)) return; + + if (mintAmount == type(uint256).max) mintAmount -= 1; + sendAmount = bound(sendAmount, mintAmount + 1, type(uint256).max); + + token.mint(address(this), mintAmount); + vm.expectRevert("ERC20/insufficient-balance"); + token.transfer(to, sendAmount); + } + + function testTransferFromInsufficientAllowance(address to, uint256 approval, uint256 amount) public { + if (to == address(0) || to == address(token)) return; + + if (approval == type(uint256).max) approval -= 1; + amount = bound(amount, approval + 1, type(uint256).max); + + address from = address(0xABCD); + + token.mint(from, amount); + + vm.prank(from); + token.approve(address(this), approval); + + vm.expectRevert("ERC20/insufficient-allowance"); + token.transferFrom(from, to, amount); + } + + function testTransferFromInsufficientBalance(address to, uint256 mintAmount, uint256 sendAmount) public { + if (to == address(0) || to == address(token)) return; + + if (mintAmount == type(uint256).max) mintAmount -= 1; + sendAmount = bound(sendAmount, mintAmount + 1, type(uint256).max); + + address from = address(0xABCD); + + token.mint(from, mintAmount); + + vm.prank(from); + token.approve(address(this), sendAmount); + + vm.expectRevert("ERC20/insufficient-balance"); + token.transferFrom(from, to, sendAmount); + } +} diff --git a/test/mocks/MockInputConduit.sol b/test/mocks/MockInputConduit.sol index 7cd56c4..d31b226 100644 --- a/test/mocks/MockInputConduit.sol +++ b/test/mocks/MockInputConduit.sol @@ -12,13 +12,15 @@ interface ERC20Like { contract MockInputConduit is Mock { ERC20Like gem; + address to; - constructor(address gem_) { + constructor(address gem_, address to_) { gem = ERC20Like(gem_); + to = to_; } function push() public { - gem.transfer(msg.sender, gem.balanceOf(address(this))); + gem.transfer(to, gem.balanceOf(address(this))); } function setPush(uint256 amount) public { diff --git a/test/mocks/MockLiquidityPool.sol b/test/mocks/MockLiquidityPool.sol index 022d1c4..19c7270 100644 --- a/test/mocks/MockLiquidityPool.sol +++ b/test/mocks/MockLiquidityPool.sol @@ -19,7 +19,7 @@ contract MockLiquidityPool is Mock { return 0; } - function deposit(uint256 assets, address receiver) public returns (uint256 shares) { + function deposit(uint256 assets, address receiver) public returns (uint256) { values_uint256["deposit_assets"] = assets; values_address["deposit_receiver"] = receiver; @@ -29,4 +29,28 @@ contract MockLiquidityPool is Mock { function maxDeposit(address /* owner */ ) public view returns (uint256 maxAssets) { maxAssets = values_uint256_return["maxDeposit"]; } + + function requestRedeem(uint256 shares, address receiver, address owner, bytes memory data) + public + returns (uint256) + { + values_uint256["requestRedeem_shares"] = shares; + values_address["requestRedeem_receiver"] = receiver; + values_address["requestRedeem_owner"] = owner; + values_bytes["requestRedeem_data"] = data; + + return 0; + } + + function maxRedeem(address /* owner */ ) public view returns (uint256 maxShares) { + maxShares = values_uint256_return["maxRedeem"]; + } + + function redeem(uint256 shares, address receiver, address owner) public returns (uint256) { + values_uint256["redeem_shares"] = shares; + values_address["redeem_receiver"] = receiver; + values_address["redeem_owner"] = owner; + + return values_uint256_return["redeem"]; + } } diff --git a/test/mocks/MockOutputConduit.sol b/test/mocks/MockOutputConduit.sol index 7e3b096..380d6f1 100644 --- a/test/mocks/MockOutputConduit.sol +++ b/test/mocks/MockOutputConduit.sol @@ -21,11 +21,13 @@ contract MockOutputConduit is Mock { gem.transfer(msg.sender, gem.balanceOf(address(this))); } - function hook(address psm) public pure { + function hook(address psm) public { + values_address["hook_psm"] = psm; return; } - function pick(address who) public pure { + function pick(address who) public { + values_address["pick_who"] = who; return; } diff --git a/test/mocks/MockPoolManager.sol b/test/mocks/MockPoolManager.sol new file mode 100644 index 0000000..40b7604 --- /dev/null +++ b/test/mocks/MockPoolManager.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import "forge-std/Test.sol"; +import "./Mock.sol"; + +contract MockPoolManager is Mock { + constructor() {} + + function transfer(address currency, bytes32 recipient, uint128 amount) external { + values_address["transfer_currency"] = currency; + values_bytes32["transfer_recipient"] = recipient; + values_uint128["transfer_amount"] = amount; + } +}