Skip to content

Commit e4f6c6f

Browse files
committed
feat: added unit test and updated readme
1 parent 1b912f2 commit e4f6c6f

File tree

4 files changed

+330
-0
lines changed

4 files changed

+330
-0
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,9 @@ forge script script/deploy/Exchange.s.sol:DeployExchange --rpc-url <PRC_URL> --b
2828
```
2929
forge script script/deploy/Vault.s.sol:DeployVault --rpc-url <PRC_URL> --broadcast --private-key <PRIVATE_KEY>
3030
```
31+
32+
## Tests
33+
34+
```
35+
forge test -vv
36+
```

test/Exchange.t.sol

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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+
}

test/mocks/MockERC20.sol

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
6+
contract MockERC20 is ERC20 {
7+
uint8 private _decimals;
8+
9+
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {
10+
_decimals = decimals_;
11+
}
12+
13+
function decimals() public view virtual override returns (uint8) {
14+
return _decimals;
15+
}
16+
17+
function mint(address account, uint256 amount) public {
18+
_mint(account, amount);
19+
}
20+
}

test/mocks/MockERC4626.sol

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6+
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
7+
8+
contract MockERC4626 is ERC20, IERC4626 {
9+
IERC20 private _asset;
10+
11+
constructor(address asset, string memory name, string memory symbol) ERC20(name, symbol) {
12+
_asset = IERC20(asset);
13+
}
14+
15+
function asset() external view returns (address) {
16+
return address(_asset);
17+
}
18+
19+
function totalAssets() external view returns (uint256) {
20+
return _asset.balanceOf(address(this));
21+
}
22+
23+
function convertToShares(uint256 assets) external pure returns (uint256) {
24+
return assets; // 1:1 conversion for simplicity
25+
}
26+
27+
function convertToAssets(uint256 shares) external pure returns (uint256) {
28+
return shares; // 1:1 conversion for simplicity
29+
}
30+
31+
function maxDeposit(address) external pure returns (uint256) {
32+
return type(uint256).max;
33+
}
34+
35+
function previewDeposit(uint256 assets) external pure returns (uint256) {
36+
return assets;
37+
}
38+
39+
function deposit(uint256 assets, address receiver) external returns (uint256) {
40+
_asset.transferFrom(msg.sender, address(this), assets);
41+
_mint(receiver, assets);
42+
return assets;
43+
}
44+
45+
function maxMint(address) external pure returns (uint256) {
46+
return type(uint256).max;
47+
}
48+
49+
function previewMint(uint256 shares) external pure returns (uint256) {
50+
return shares;
51+
}
52+
53+
function mint(uint256 shares, address receiver) external returns (uint256) {
54+
_asset.transferFrom(msg.sender, address(this), shares);
55+
_mint(receiver, shares);
56+
return shares;
57+
}
58+
59+
function maxWithdraw(address owner) external view returns (uint256) {
60+
return balanceOf(owner);
61+
}
62+
63+
function previewWithdraw(uint256 assets) external pure returns (uint256) {
64+
return assets;
65+
}
66+
67+
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256) {
68+
require(balanceOf(owner) >= assets, "Insufficient balance");
69+
_burn(owner, assets);
70+
_asset.transfer(receiver, assets);
71+
return assets;
72+
}
73+
74+
function maxRedeem(address owner) external view returns (uint256) {
75+
return balanceOf(owner);
76+
}
77+
78+
function previewRedeem(uint256 shares) external pure returns (uint256) {
79+
return shares;
80+
}
81+
82+
function redeem(uint256 shares, address receiver, address owner) external returns (uint256) {
83+
require(balanceOf(owner) >= shares, "Insufficient balance");
84+
_burn(owner, shares);
85+
_asset.transfer(receiver, shares);
86+
return shares;
87+
}
88+
}

0 commit comments

Comments
 (0)