OrderPayments.t.sol
1 // SPDX-FileCopyrightText: 2024 Mass Labs 2 // 3 // SPDX-License-Identifier: GPL-3.0-or-later 4 5 pragma solidity ^0.8.19; 6 7 import {Test} from "forge-std/Test.sol"; 8 import {ERC20Mock, ERC20} from "openzeppelin/contracts/mocks/token/ERC20Mock.sol"; 9 10 import {OrderPaymentsFactory, OrderPaymentBinding, OrderPayment, ETH} from "../src/OrderPayments.sol"; 11 12 contract OrderPaymentsFactoryTest is Test { 13 OrderPaymentsFactory factory; 14 ERC20Mock mockToken; 15 address payable merchant; 16 address payable customer; 17 18 OrderPaymentBinding binding; 19 20 function setUp() public { 21 factory = new OrderPaymentsFactory(); 22 mockToken = new ERC20Mock(); 23 merchant = payable(makeAddr("merchant")); 24 customer = payable(makeAddr("customer")); 25 26 binding = OrderPaymentBinding({ 27 chainId: 1, 28 shopId: 123, 29 orderId: 456, 30 receivingAddress: merchant 31 }); 32 } 33 34 function test_getSalt() public view { 35 bytes32 salt = factory.getSalt(binding); 36 bytes32 expectedSalt = keccak256(abi.encode(binding)); 37 assertEq(salt, expectedSalt); 38 } 39 40 function test_getSalt_DifferentBindingsProduceDifferentSalts() public view { 41 OrderPaymentBinding memory binding2 = OrderPaymentBinding({ 42 chainId: 1, 43 shopId: 123, 44 orderId: 457, // different order ID 45 receivingAddress: merchant 46 }); 47 48 bytes32 salt1 = factory.getSalt(binding); 49 bytes32 salt2 = factory.getSalt(binding2); 50 assertTrue(salt1 != salt2); 51 } 52 53 function test_getOrderPaymentAddress() public { 54 address predictedAddress = factory.getOrderPaymentAddress(binding); 55 56 // Deploy the contract and verify the address matches 57 factory.deployOrderPayment(binding); 58 59 // The predicted address should match the actual deployed address 60 assertTrue(predictedAddress.code.length > 0); 61 } 62 63 function test_deployOrderPayment() public { 64 address predictedAddress = factory.getOrderPaymentAddress(binding); 65 66 // Deploy the contract 67 factory.deployOrderPayment(binding); 68 69 // Verify the contract was deployed 70 assertTrue(predictedAddress.code.length > 0); 71 72 // Verify the contract is an OrderPayment instance 73 OrderPayment orderPayment = OrderPayment(predictedAddress); 74 // Contract should exist and be callable 75 vm.expectCall(predictedAddress, abi.encodeWithSelector(OrderPayment.sweepEth.selector)); 76 orderPayment.sweepEth(""); 77 } 78 79 function test_deployOrderPayment_CannotDeployTwice() public { 80 // Deploy once 81 factory.deployOrderPayment(binding); 82 83 // Try to deploy again - should revert 84 vm.expectRevert(); 85 factory.deployOrderPayment(binding); 86 } 87 88 function test_deployOrderPayment_DifferentBindingsDeployToDifferentAddresses() public { 89 OrderPaymentBinding memory binding2 = OrderPaymentBinding({ 90 chainId: 1, 91 shopId: 123, 92 orderId: 457, 93 receivingAddress: merchant 94 }); 95 96 address addr1 = factory.getOrderPaymentAddress(binding); 97 address addr2 = factory.getOrderPaymentAddress(binding2); 98 99 assertTrue(addr1 != addr2); 100 101 factory.deployOrderPayment(binding); 102 factory.deployOrderPayment(binding2); 103 104 assertTrue(addr1.code.length > 0); 105 assertTrue(addr2.code.length > 0); 106 } 107 } 108 109 contract OrderPaymentTest is Test { 110 OrderPayment orderPayment; 111 ERC20Mock mockToken; 112 address payable merchant; 113 address payable customer; 114 115 function setUp() public { 116 merchant = payable(makeAddr("merchant")); 117 customer = payable(makeAddr("customer")); 118 orderPayment = new OrderPayment(merchant); 119 mockToken = new ERC20Mock(); 120 121 // Fund the customer for testing 122 vm.deal(customer, 10 ether); 123 mockToken.mint(customer, 1000 ether); 124 } 125 126 function test_constructor() public { 127 OrderPayment newOrderPayment = new OrderPayment(merchant); 128 // Contract should be deployed successfully 129 assertTrue(address(newOrderPayment) != address(0)); 130 } 131 132 function test_sweepEth() public { 133 uint256 amount = 1 ether; 134 135 // Fund the order payment contract 136 vm.deal(address(orderPayment), amount); 137 138 uint256 merchantBalanceBefore = merchant.balance; 139 uint256 contractBalanceBefore = address(orderPayment).balance; 140 141 assertEq(contractBalanceBefore, amount); 142 143 // Sweep ETH 144 orderPayment.sweepEth(""); 145 146 uint256 merchantBalanceAfter = merchant.balance; 147 uint256 contractBalanceAfter = address(orderPayment).balance; 148 149 assertEq(contractBalanceAfter, 0); 150 assertEq(merchantBalanceAfter, merchantBalanceBefore + amount); 151 } 152 153 function test_sweepEth_EmptyContract() public { 154 uint256 merchantBalanceBefore = merchant.balance; 155 156 // Contract has no ETH 157 assertEq(address(orderPayment).balance, 0); 158 159 // Sweep should not revert but also not change balances 160 orderPayment.sweepEth(""); 161 162 assertEq(address(orderPayment).balance, 0); 163 assertEq(merchant.balance, merchantBalanceBefore); 164 } 165 166 function test_sweepERC20() public { 167 uint256 amount = 100 ether; 168 169 // Transfer tokens to the order payment contract 170 vm.prank(customer); 171 bool success = mockToken.transfer(address(orderPayment), amount); 172 assertTrue(success); 173 174 uint256 merchantBalanceBefore = mockToken.balanceOf(merchant); 175 uint256 contractBalanceBefore = mockToken.balanceOf(address(orderPayment)); 176 177 assertEq(contractBalanceBefore, amount); 178 179 // Sweep ERC20 180 orderPayment.sweepErc20(mockToken, ""); 181 182 uint256 merchantBalanceAfter = mockToken.balanceOf(merchant); 183 uint256 contractBalanceAfter = mockToken.balanceOf(address(orderPayment)); 184 185 assertEq(contractBalanceAfter, 0); 186 assertEq(merchantBalanceAfter, merchantBalanceBefore + amount); 187 } 188 189 function test_sweepERC20_EmptyContract() public { 190 uint256 merchantBalanceBefore = mockToken.balanceOf(merchant); 191 192 // Contract has no tokens 193 assertEq(mockToken.balanceOf(address(orderPayment)), 0); 194 195 // Sweep should not revert but also not change balances 196 orderPayment.sweepErc20(mockToken, ""); 197 198 assertEq(mockToken.balanceOf(address(orderPayment)), 0); 199 assertEq(mockToken.balanceOf(merchant), merchantBalanceBefore); 200 } 201 202 function test_sweep_WithETHAddress() public { 203 uint256 amount = 2 ether; 204 205 // Fund the contract with ETH 206 vm.deal(address(orderPayment), amount); 207 208 uint256 merchantBalanceBefore = merchant.balance; 209 210 // Create a mock ERC20 with ETH address (address(0)) 211 ERC20 ethToken = ERC20(ETH); 212 213 // This should call sweepEth internally 214 orderPayment.sweep(ethToken, ""); 215 216 uint256 merchantBalanceAfter = merchant.balance; 217 218 assertEq(address(orderPayment).balance, 0); 219 assertEq(merchantBalanceAfter, merchantBalanceBefore + amount); 220 } 221 222 function test_sweepERC20_CanBeCalledByAnyone() public { 223 uint256 amount = 100 ether; 224 225 // Transfer tokens to the order payment contract 226 vm.prank(customer); 227 assertTrue(mockToken.transfer(address(orderPayment), amount)); 228 229 // Random address calls sweep 230 address randomUser = makeAddr("random"); 231 vm.prank(randomUser); 232 orderPayment.sweepErc20(mockToken, ""); 233 234 // Tokens should still go to merchant 235 assertEq(mockToken.balanceOf(merchant), amount); 236 assertEq(mockToken.balanceOf(address(orderPayment)), 0); 237 } 238 239 function test_sweepEth_CanBeCalledByAnyone() public { 240 uint256 amount = 1 ether; 241 242 // Fund the order payment contract 243 vm.deal(address(orderPayment), amount); 244 245 // Random address calls sweep 246 address randomUser = makeAddr("random"); 247 vm.prank(randomUser); 248 orderPayment.sweepEth(""); 249 250 // ETH should still go to merchant 251 assertEq(merchant.balance, amount); 252 assertEq(address(orderPayment).balance, 0); 253 } 254 255 function test_fuzz_sweepEth(uint256 amount) public { 256 // Bound the amount to reasonable values 257 amount = bound(amount, 0, 100 ether); 258 259 // Fund the contract 260 vm.deal(address(orderPayment), amount); 261 262 uint256 merchantBalanceBefore = merchant.balance; 263 264 orderPayment.sweepEth(""); 265 266 assertEq(address(orderPayment).balance, 0); 267 assertEq(merchant.balance, merchantBalanceBefore + amount); 268 } 269 270 function test_fuzz_sweepERC20(uint256 amount) public { 271 // Bound the amount to reasonable values 272 amount = bound(amount, 0, type(uint128).max); 273 274 // Mint tokens to customer and transfer to contract 275 mockToken.mint(customer, amount); 276 vm.prank(customer); 277 assertTrue(mockToken.transfer(address(orderPayment), amount)); 278 279 uint256 merchantBalanceBefore = mockToken.balanceOf(merchant); 280 281 orderPayment.sweepErc20(mockToken, ""); 282 283 assertEq(mockToken.balanceOf(address(orderPayment)), 0); 284 assertEq(mockToken.balanceOf(merchant), merchantBalanceBefore + amount); 285 } 286 } 287 288 contract OrderPaymentsIntegrationTest is Test { 289 OrderPaymentsFactory factory; 290 ERC20Mock mockToken; 291 address payable merchant; 292 address payable customer; 293 294 function setUp() public { 295 factory = new OrderPaymentsFactory(); 296 mockToken = new ERC20Mock(); 297 merchant = payable(makeAddr("merchant")); 298 customer = payable(makeAddr("customer")); 299 300 // Fund the customer 301 vm.deal(customer, 10 ether); 302 mockToken.mint(customer, 1000 ether); 303 } 304 305 function test_fullWorkflow() public { 306 // Create binding 307 OrderPaymentBinding memory binding = OrderPaymentBinding({ 308 chainId: 1, 309 shopId: 123, 310 orderId: 456, 311 receivingAddress: merchant 312 }); 313 314 // Get predicted address 315 address predictedAddress = factory.getOrderPaymentAddress(binding); 316 317 // Customer sends ETH and tokens to the predicted address 318 vm.startPrank(customer); 319 payable(predictedAddress).transfer(1 ether); 320 assertTrue(mockToken.transfer(predictedAddress, 100 ether)); 321 vm.stopPrank(); 322 323 // Verify funds are at the predicted address 324 assertEq(predictedAddress.balance, 1 ether); 325 assertEq(mockToken.balanceOf(predictedAddress), 100 ether); 326 327 // Deploy the contract 328 factory.deployOrderPayment(binding); 329 330 // Get the order payment instance 331 OrderPayment orderPayment = OrderPayment(predictedAddress); 332 333 // Sweep funds 334 uint256 merchantEthBefore = merchant.balance; 335 uint256 merchantTokensBefore = mockToken.balanceOf(merchant); 336 337 orderPayment.sweepEth(""); 338 orderPayment.sweepErc20(mockToken, ""); 339 340 // Verify funds were swept to merchant 341 assertEq(merchant.balance, merchantEthBefore + 1 ether); 342 assertEq(mockToken.balanceOf(merchant), merchantTokensBefore + 100 ether); 343 assertEq(predictedAddress.balance, 0); 344 assertEq(mockToken.balanceOf(predictedAddress), 0); 345 } 346 347 function test_multipleOrders() public { 348 // Create multiple bindings 349 OrderPaymentBinding memory binding1 = OrderPaymentBinding({ 350 chainId: 1, 351 shopId: 123, 352 orderId: 456, 353 receivingAddress: merchant 354 }); 355 356 OrderPaymentBinding memory binding2 = OrderPaymentBinding({ 357 chainId: 1, 358 shopId: 123, 359 orderId: 457, 360 receivingAddress: merchant 361 }); 362 363 // Get predicted addresses 364 address addr1 = factory.getOrderPaymentAddress(binding1); 365 address addr2 = factory.getOrderPaymentAddress(binding2); 366 367 // Verify addresses are different 368 assertTrue(addr1 != addr2); 369 370 // Send different amounts to each 371 vm.startPrank(customer); 372 payable(addr1).transfer(1 ether); 373 payable(addr2).transfer(2 ether); 374 assertTrue(mockToken.transfer(addr1, 100 ether)); 375 assertTrue(mockToken.transfer(addr2, 200 ether)); 376 vm.stopPrank(); 377 378 // Deploy contracts 379 factory.deployOrderPayment(binding1); 380 factory.deployOrderPayment(binding2); 381 382 // Sweep funds from both 383 uint256 merchantEthBefore = merchant.balance; 384 uint256 merchantTokensBefore = mockToken.balanceOf(merchant); 385 386 OrderPayment(addr1).sweepEth(""); 387 OrderPayment(addr1).sweepErc20(mockToken, ""); 388 OrderPayment(addr2).sweepEth(""); 389 OrderPayment(addr2).sweepErc20(mockToken, ""); 390 391 // Verify total funds were swept 392 assertEq(merchant.balance, merchantEthBefore + 3 ether); 393 assertEq(mockToken.balanceOf(merchant), merchantTokensBefore + 300 ether); 394 } 395 }