|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity ^0.8.20; |
| 3 | + |
| 4 | +import {Test, console2} from "forge-std/Test.sol"; |
| 5 | +import {Exchange} from "../src/Exchange.sol"; |
| 6 | +import {Registry} from "../src/Registry.sol"; |
| 7 | +import {MockERC20} from "./mocks/MockERC20.sol"; |
| 8 | +import {MockERC4626} from "./mocks/MockERC4626.sol"; |
| 9 | + |
| 10 | +contract ExchangeTest is Test { |
| 11 | + Exchange public exchange; |
| 12 | + Registry public registry; |
| 13 | + MockERC20 public usdc; |
| 14 | + MockERC4626 public vault; |
| 15 | + |
| 16 | + address public owner; |
| 17 | + address public user; |
| 18 | + address public merchant; |
| 19 | + string constant UEN = "123456789A"; |
| 20 | + uint256 constant INITIAL_BALANCE = 1000000 * 10 ** 6; // 1M USDC |
| 21 | + uint256 constant DEFAULT_AMOUNT = 1000 * 10 ** 6; // 1000 USDC |
| 22 | + |
| 23 | + event Transfer(address indexed from, address indexed to, uint256 amount, string uen); |
| 24 | + event FeeUpdated(uint256 newFee); |
| 25 | + event FeeCollectorUpdated(address newFeeCollector); |
| 26 | + event FeesWithdrawn(address indexed to, uint256 amount); |
| 27 | + event VaultDeposit(address indexed merchant, uint256 assets, uint256 shares); |
| 28 | + event VaultWithdraw(address indexed merchant, uint256 assets, uint256 shares); |
| 29 | + |
| 30 | + function setUp() public { |
| 31 | + owner = makeAddr("owner"); |
| 32 | + user = makeAddr("user"); |
| 33 | + merchant = makeAddr("merchant"); |
| 34 | + |
| 35 | + vm.startPrank(owner); |
| 36 | + |
| 37 | + // Deploy mock contracts |
| 38 | + usdc = new MockERC20("USDC", "USDC", 6); |
| 39 | + vault = new MockERC4626(address(usdc), "Vault USDC", "vUSDC"); |
| 40 | + |
| 41 | + // Deploy registry and add merchant |
| 42 | + registry = new Registry(); |
| 43 | + registry.addMerchant( |
| 44 | + UEN, // _uen |
| 45 | + "Test Merchant", // _entity_name |
| 46 | + "Test Owner", // _owner_name |
| 47 | + merchant // _wallet_address |
| 48 | + ); |
| 49 | + |
| 50 | + // Deploy exchange |
| 51 | + exchange = new Exchange(address(registry), address(usdc), address(vault)); |
| 52 | + |
| 53 | + vm.stopPrank(); |
| 54 | + |
| 55 | + // Setup initial balances |
| 56 | + deal(address(usdc), user, INITIAL_BALANCE); |
| 57 | + } |
| 58 | + |
| 59 | + function test_TransferToMerchant() public { |
| 60 | + vm.startPrank(user); |
| 61 | + usdc.approve(address(exchange), DEFAULT_AMOUNT); |
| 62 | + |
| 63 | + uint256 feeAmount = (DEFAULT_AMOUNT * exchange.fee()) / 10000; |
| 64 | + uint256 merchantAmount = DEFAULT_AMOUNT - feeAmount; |
| 65 | + |
| 66 | + vm.expectEmit(true, true, false, true); |
| 67 | + emit Transfer(user, merchant, merchantAmount, UEN); |
| 68 | + |
| 69 | + exchange.transferToMerchant(UEN, DEFAULT_AMOUNT); |
| 70 | + |
| 71 | + assertEq(usdc.balanceOf(merchant), merchantAmount); |
| 72 | + assertEq(usdc.balanceOf(address(exchange)), feeAmount); |
| 73 | + vm.stopPrank(); |
| 74 | + } |
| 75 | + |
| 76 | + function test_TransferToVault() public { |
| 77 | + vm.startPrank(user); |
| 78 | + usdc.approve(address(exchange), DEFAULT_AMOUNT); |
| 79 | + |
| 80 | + uint256 feeAmount = (DEFAULT_AMOUNT * exchange.fee()) / 10000; |
| 81 | + uint256 vaultAmount = DEFAULT_AMOUNT - feeAmount; |
| 82 | + |
| 83 | + vm.expectEmit(true, true, false, true); |
| 84 | + emit Transfer(user, merchant, vaultAmount, UEN); |
| 85 | + vm.expectEmit(true, false, false, true); |
| 86 | + emit VaultDeposit(merchant, vaultAmount, vaultAmount); // Mock vault returns 1:1 shares |
| 87 | + |
| 88 | + exchange.transferToVault(UEN, DEFAULT_AMOUNT); |
| 89 | + |
| 90 | + assertEq(vault.balanceOf(merchant), vaultAmount); |
| 91 | + assertEq(usdc.balanceOf(address(exchange)), feeAmount); |
| 92 | + vm.stopPrank(); |
| 93 | + } |
| 94 | + |
| 95 | + function test_WithdrawToWallet() public { |
| 96 | + // First deposit to vault |
| 97 | + vm.startPrank(user); |
| 98 | + usdc.approve(address(exchange), DEFAULT_AMOUNT); |
| 99 | + exchange.transferToVault(UEN, DEFAULT_AMOUNT); |
| 100 | + vm.stopPrank(); |
| 101 | + |
| 102 | + uint256 feeAmount = (DEFAULT_AMOUNT * exchange.fee()) / 10000; |
| 103 | + uint256 vaultAmount = DEFAULT_AMOUNT - feeAmount; |
| 104 | + |
| 105 | + // Then withdraw |
| 106 | + vm.startPrank(merchant); |
| 107 | + vm.expectEmit(true, false, false, true); |
| 108 | + emit VaultWithdraw(merchant, vaultAmount, vaultAmount); |
| 109 | + |
| 110 | + exchange.withdrawToWallet(vaultAmount); |
| 111 | + |
| 112 | + assertEq(usdc.balanceOf(merchant), vaultAmount); |
| 113 | + assertEq(vault.balanceOf(merchant), 0); |
| 114 | + vm.stopPrank(); |
| 115 | + } |
| 116 | + |
| 117 | + function test_SetFee() public { |
| 118 | + uint256 newFee = 200; // 2% |
| 119 | + |
| 120 | + vm.prank(owner); |
| 121 | + vm.expectEmit(false, false, false, true); |
| 122 | + emit FeeUpdated(newFee); |
| 123 | + |
| 124 | + exchange.setFee(newFee); |
| 125 | + assertEq(exchange.fee(), newFee); |
| 126 | + } |
| 127 | + |
| 128 | + function test_SetFeeCollector() public { |
| 129 | + address newFeeCollector = makeAddr("newFeeCollector"); |
| 130 | + |
| 131 | + vm.prank(owner); |
| 132 | + vm.expectEmit(false, false, false, true); |
| 133 | + emit FeeCollectorUpdated(newFeeCollector); |
| 134 | + |
| 135 | + exchange.setFeeCollector(newFeeCollector); |
| 136 | + assertEq(exchange.feeCollector(), newFeeCollector); |
| 137 | + } |
| 138 | + |
| 139 | + function test_WithdrawFees() public { |
| 140 | + // First make a transfer to generate fees |
| 141 | + vm.startPrank(user); |
| 142 | + usdc.approve(address(exchange), DEFAULT_AMOUNT); |
| 143 | + exchange.transferToMerchant(UEN, DEFAULT_AMOUNT); |
| 144 | + vm.stopPrank(); |
| 145 | + |
| 146 | + uint256 feeAmount = (DEFAULT_AMOUNT * exchange.fee()) / 10000; |
| 147 | + address feeReceiver = makeAddr("feeReceiver"); |
| 148 | + |
| 149 | + vm.prank(owner); |
| 150 | + vm.expectEmit(true, false, false, true); |
| 151 | + emit FeesWithdrawn(feeReceiver, feeAmount); |
| 152 | + |
| 153 | + exchange.withdrawFees(feeReceiver, feeAmount); |
| 154 | + assertEq(usdc.balanceOf(feeReceiver), feeAmount); |
| 155 | + } |
| 156 | + |
| 157 | + function testFail_TransferToMerchant_InvalidUEN() public { |
| 158 | + vm.prank(user); |
| 159 | + exchange.transferToMerchant("INVALID_UEN", DEFAULT_AMOUNT); |
| 160 | + } |
| 161 | + |
| 162 | + function testFail_TransferToMerchant_ZeroAmount() public { |
| 163 | + vm.prank(user); |
| 164 | + exchange.transferToMerchant(UEN, 0); |
| 165 | + } |
| 166 | + |
| 167 | + function testFail_TransferToVault_InvalidUEN() public { |
| 168 | + vm.prank(user); |
| 169 | + exchange.transferToVault("INVALID_UEN", DEFAULT_AMOUNT); |
| 170 | + } |
| 171 | + |
| 172 | + function testFail_TransferToVault_ZeroAmount() public { |
| 173 | + vm.prank(user); |
| 174 | + exchange.transferToVault(UEN, 0); |
| 175 | + } |
| 176 | + |
| 177 | + function testFail_WithdrawToWallet_InsufficientShares() public { |
| 178 | + vm.prank(merchant); |
| 179 | + exchange.withdrawToWallet(1000); // No shares deposited |
| 180 | + } |
| 181 | + |
| 182 | + function testFail_SetFee_NotOwner() public { |
| 183 | + vm.prank(user); |
| 184 | + exchange.setFee(200); |
| 185 | + } |
| 186 | + |
| 187 | + function testFail_SetFee_TooHigh() public { |
| 188 | + vm.prank(owner); |
| 189 | + exchange.setFee(1001); // > 10% |
| 190 | + } |
| 191 | + |
| 192 | + function testFail_SetFeeCollector_NotOwner() public { |
| 193 | + vm.prank(user); |
| 194 | + exchange.setFeeCollector(address(1)); |
| 195 | + } |
| 196 | + |
| 197 | + function testFail_SetFeeCollector_ZeroAddress() public { |
| 198 | + vm.prank(owner); |
| 199 | + exchange.setFeeCollector(address(0)); |
| 200 | + } |
| 201 | + |
| 202 | + function testFail_WithdrawFees_NotOwner() public { |
| 203 | + vm.prank(user); |
| 204 | + exchange.withdrawFees(address(1), 100); |
| 205 | + } |
| 206 | + |
| 207 | + function testFail_WithdrawFees_ZeroAmount() public { |
| 208 | + vm.prank(owner); |
| 209 | + exchange.withdrawFees(address(1), 0); |
| 210 | + } |
| 211 | + |
| 212 | + function testFail_WithdrawFees_InsufficientBalance() public { |
| 213 | + vm.prank(owner); |
| 214 | + exchange.withdrawFees(address(1), INITIAL_BALANCE); |
| 215 | + } |
| 216 | +} |
0 commit comments