From 5821cb39ddb3cbff0a27fac953e709b80043aa2e Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Tue, 20 Feb 2024 10:01:05 +0100 Subject: [PATCH 01/12] Add unit tests --- src/Conduit.sol | 15 ++-- test/Conduit.t.sol | 116 ++++++++++++++++++++++++++++--- test/mocks/MockLiquidityPool.sol | 12 ++++ test/mocks/MockOutputConduit.sol | 6 +- test/mocks/MockPoolManager.sol | 15 ++++ 5 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 test/mocks/MockPoolManager.sol diff --git a/src/Conduit.sol b/src/Conduit.sol index 4dbf319..c337e7e 100644 --- a/src/Conduit.sol +++ b/src/Conduit.sol @@ -7,7 +7,6 @@ interface ERC20Like { 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 { @@ -65,7 +64,7 @@ contract Conduit { // Centrifuge pool ERC7540Like pool; PoolManagerLike poolManager; - bytes32 claimRecipient; + bytes32 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,7 +191,7 @@ contract Conduit { // Mint deposit tokens uint256 amount = gem.balanceOf(address(this)); depositAsset.mint(address(this), amount); - depositAsset.approve(address(pool), amount); + // Deposit in pool pool.requestDeposit(amount, address(this), address(this), ""); } @@ -223,9 +222,11 @@ 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 diff --git a/test/Conduit.t.sol b/test/Conduit.t.sol index 68cebdf..2d8cfed 100644 --- a/test/Conduit.t.sol +++ b/test/Conduit.t.sol @@ -5,6 +5,7 @@ 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 {MockPoolManager} from "test/mocks/MockPoolManager.sol"; import {Conduit} from "src/Conduit.sol"; import {ERC20} from "src/token/ERC20.sol"; @@ -14,22 +15,24 @@ contract ConduitTest is Test { ERC20 dai; ERC20 gem; - ERC20 depositToken; + ERC20 depositAsset; MockOutputConduit outputConduit; MockInputConduit urnConduit; MockInputConduit jarConduit; address operator; + 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)); @@ -37,20 +40,21 @@ contract ConduitTest is Test { 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"); @@ -75,14 +79,14 @@ 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(pool.values_uint256("requestDeposit_assets"), gemAmount); assertEq(pool.values_address("requestDeposit_receiver"), address(conduit)); @@ -102,4 +106,96 @@ contract ConduitTest is Test { assertEq(pool.values_uint256("deposit_assets"), gemAmount); assertEq(pool.values_address("deposit_receiver"), address(conduit)); } + + function testWithdrawFromPool(address notMate, address withdrawal, uint256 gemAmount) public { + vm.assume(mate != notMate); + vm.assume(withdrawal != address(0)); + + 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)); + } } diff --git a/test/mocks/MockLiquidityPool.sol b/test/mocks/MockLiquidityPool.sol index 022d1c4..69023b3 100644 --- a/test/mocks/MockLiquidityPool.sol +++ b/test/mocks/MockLiquidityPool.sol @@ -29,4 +29,16 @@ 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; + } } 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; + } +} From de2b130ed006ef92a02ec1450e2d77c7514b872d Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Tue, 20 Feb 2024 10:16:45 +0100 Subject: [PATCH 02/12] Add testRepayBrokenLiquidityPool --- src/Conduit.sol | 1 - test/Conduit.t.sol | 50 ++++++++++++++++++++++++++++++--- test/mocks/MockInputConduit.sol | 6 ++-- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/Conduit.sol b/src/Conduit.sol index c337e7e..742cec8 100644 --- a/src/Conduit.sol +++ b/src/Conduit.sol @@ -209,7 +209,6 @@ contract Conduit { uint256 amount = depositAsset.balanceOf(address(this)); depositAsset.burn(address(this), amount); - claimDeposit(); gem.transferFrom(address(this), withdrawal, amount); } diff --git a/test/Conduit.t.sol b/test/Conduit.t.sol index 2d8cfed..f3563d6 100644 --- a/test/Conduit.t.sol +++ b/test/Conduit.t.sol @@ -5,6 +5,7 @@ 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"; @@ -12,6 +13,8 @@ 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; @@ -22,6 +25,8 @@ contract ConduitTest is Test { MockInputConduit jarConduit; address operator; + address withdrawal = makeAddr("Withdrawal"); + bytes32 depositRecipient; MockLiquidityPool pool; MockPoolManager poolManager; @@ -35,8 +40,8 @@ contract ConduitTest is Test { 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)); @@ -107,9 +112,8 @@ contract ConduitTest is Test { assertEq(pool.values_address("deposit_receiver"), address(conduit)); } - function testWithdrawFromPool(address notMate, address withdrawal, uint256 gemAmount) public { + function testWithdrawFromPool(address notMate, uint256 gemAmount) public { vm.assume(mate != notMate); - vm.assume(withdrawal != address(0)); outputConduit.setPush(gemAmount); pool.setReturn("maxDeposit", gemAmount); @@ -198,4 +202,42 @@ contract ConduitTest is Test { assertEq(poolManager.values_bytes32("transfer_recipient"), depositRecipient); assertEq(poolManager.values_uint128("transfer_amount"), uint128(gemAmount)); } + + function testClaimRedeem() public { + // todo + } + + 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); + } } 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 { From a6462c7dd9497296c113f07fbf4c6c6db0ddab22 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Tue, 20 Feb 2024 10:17:38 +0100 Subject: [PATCH 03/12] Fix ci --- .github/workflows/ci.yml | 51 +++------------------------------------- 1 file changed, 3 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03f5d5b..bba9748 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ env: FOUNDRY_PROFILE: ci jobs: - test-unit: + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -19,21 +19,7 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 - name: Run tests - run: forge test --no-match-path "test/invariant/**/*.sol" - env: - FOUNDRY_PROFILE: ci - FORK_TESTS: false - - test-invariant: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - - - name: Run tests - run: forge test --match-path "test/invariant/**/*.sol" + run: forge test env: FOUNDRY_PROFILE: ci FORK_TESTS: false @@ -102,35 +88,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 From 9f694e0f9c6baa9c55588ed2ee3062368b91e131 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Tue, 20 Feb 2024 10:32:16 +0100 Subject: [PATCH 04/12] Separate unit & fork tests --- .github/workflows/ci.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bba9748..30db157 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ env: FOUNDRY_PROFILE: ci jobs: - test: + test-unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -19,11 +19,25 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 - name: Run tests - run: forge test + run: forge test --no-match-path "test/fork/**/*.sol" env: FOUNDRY_PROFILE: ci FORK_TESTS: false + # test-fork: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + + # - name: Install Foundry + # uses: foundry-rs/foundry-toolchain@v1 + + # - name: Run tests + # run: forge test --match-path "test/fork/**/*.sol" + # env: + # FOUNDRY_PROFILE: ci + # FORK_TESTS: false + lint: runs-on: ubuntu-latest steps: From 2076d8cc811d0bd0eedb23e1645615611f19d9f8 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Tue, 20 Feb 2024 10:38:38 +0100 Subject: [PATCH 05/12] Add ERC20 tests --- test/ERC20.t.sol | 296 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 test/ERC20.t.sol diff --git a/test/ERC20.t.sol b/test/ERC20.t.sol new file mode 100644 index 0000000..ed920e5 --- /dev/null +++ b/test/ERC20.t.sol @@ -0,0 +1,296 @@ +// 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 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 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); + } +} From d2dc988268ddb202cb4110635837eb682a59b791 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Fri, 23 Feb 2024 10:22:01 +0100 Subject: [PATCH 06/12] Add testClaimRedeem --- src/Conduit.sol | 6 +++--- test/Conduit.t.sol | 23 +++++++++++++++++++++-- test/mocks/MockLiquidityPool.sol | 14 +++++++++++++- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/Conduit.sol b/src/Conduit.sol index 742cec8..c144b45 100644 --- a/src/Conduit.sol +++ b/src/Conduit.sol @@ -230,10 +230,10 @@ contract Conduit { /// @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 diff --git a/test/Conduit.t.sol b/test/Conduit.t.sol index f3563d6..b8be3d6 100644 --- a/test/Conduit.t.sol +++ b/test/Conduit.t.sol @@ -203,8 +203,27 @@ contract ConduitTest is Test { assertEq(poolManager.values_uint128("transfer_amount"), uint128(gemAmount)); } - function testClaimRedeem() public { - // todo + 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 { diff --git a/test/mocks/MockLiquidityPool.sol b/test/mocks/MockLiquidityPool.sol index 69023b3..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; @@ -41,4 +41,16 @@ contract MockLiquidityPool is Mock { 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"]; + } } From 9952983da9a56180c7f932ab66dc4d57326c2035 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Fri, 23 Feb 2024 10:26:02 +0100 Subject: [PATCH 07/12] Add permission tests --- test/Conduit.t.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/Conduit.t.sol b/test/Conduit.t.sol index b8be3d6..85fb927 100644 --- a/test/Conduit.t.sol +++ b/test/Conduit.t.sol @@ -66,6 +66,25 @@ contract ConduitTest is Test { 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 testWardSetup(address notDeployer) public { vm.assume(address(this) != notDeployer); From eb83538b31a4025c73158c398f52d827edbe4c3c Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Fri, 23 Feb 2024 10:29:18 +0100 Subject: [PATCH 08/12] Add testFile --- src/Conduit.sol | 6 +++--- test/Conduit.t.sol | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/Conduit.sol b/src/Conduit.sol index c144b45..c3b5c88 100644 --- a/src/Conduit.sol +++ b/src/Conduit.sol @@ -62,9 +62,9 @@ contract Conduit { address public withdrawal; // Centrifuge pool - ERC7540Like pool; - PoolManagerLike poolManager; - bytes32 depositRecipient; + ERC7540Like public pool; + PoolManagerLike public poolManager; + bytes32 public depositRecipient; mapping(address => uint256) public wards; mapping(address => uint256) public can; diff --git a/test/Conduit.t.sol b/test/Conduit.t.sol index 85fb927..53436e9 100644 --- a/test/Conduit.t.sol +++ b/test/Conduit.t.sol @@ -85,6 +85,30 @@ contract ConduitTest is Test { 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); From f50d5764aff080040cd6e4f5b8008742f81bebe6 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Fri, 23 Feb 2024 10:36:46 +0100 Subject: [PATCH 09/12] Add auth method tests --- src/Conduit.sol | 2 +- test/Conduit.t.sol | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/Conduit.sol b/src/Conduit.sol index c3b5c88..3f5effc 100644 --- a/src/Conduit.sol +++ b/src/Conduit.sol @@ -260,7 +260,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 53436e9..80bfcd7 100644 --- a/test/Conduit.t.sol +++ b/test/Conduit.t.sol @@ -246,6 +246,20 @@ contract ConduitTest is Test { 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); @@ -302,4 +316,39 @@ contract ConduitTest is Test { 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); + } } From df35e2f0b744838db80a0001d3fb570126e413ca Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Fri, 23 Feb 2024 10:40:30 +0100 Subject: [PATCH 10/12] Add burn with allowance test --- test/ERC20.t.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/ERC20.t.sol b/test/ERC20.t.sol index ed920e5..e621fd9 100644 --- a/test/ERC20.t.sol +++ b/test/ERC20.t.sol @@ -44,6 +44,25 @@ contract ERC20Test is Test { 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); From b25579271fbf04fd5fa8b74e317927aa104fcc07 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Fri, 23 Feb 2024 10:41:54 +0100 Subject: [PATCH 11/12] Add permission test --- test/ERC20.t.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/ERC20.t.sol b/test/ERC20.t.sol index e621fd9..9cd4f56 100644 --- a/test/ERC20.t.sol +++ b/test/ERC20.t.sol @@ -15,6 +15,15 @@ contract ERC20Test is Test { 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); From fb2b270b301b792f81e7df761b92a37fdf409904 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Tue, 27 Feb 2024 19:43:04 +0100 Subject: [PATCH 12/12] Fix approve --- src/Conduit.sol | 3 ++- test/Conduit.t.sol | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Conduit.sol b/src/Conduit.sol index 3f5effc..ab4baf3 100644 --- a/src/Conduit.sol +++ b/src/Conduit.sol @@ -4,6 +4,7 @@ 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; @@ -36,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; @@ -193,6 +193,7 @@ contract Conduit { depositAsset.mint(address(this), amount); // Deposit in pool + depositAsset.approve(address(pool), amount); pool.requestDeposit(amount, address(this), address(this), ""); } diff --git a/test/Conduit.t.sol b/test/Conduit.t.sol index 80bfcd7..91fe825 100644 --- a/test/Conduit.t.sol +++ b/test/Conduit.t.sol @@ -135,6 +135,7 @@ contract ConduitTest is Test { assertEq(gem.balanceOf(address(outputConduit)), 0); assertEq(gem.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));