From a02c50525b1ee4e7e1802382d8c157bd55101794 Mon Sep 17 00:00:00 2001 From: 0xchin Date: Fri, 6 Dec 2024 17:12:17 -0300 Subject: [PATCH] test: add payments and withdrawal tests --- package.json | 1 + test/unit/Payment.t.sol | 236 ++++++++++++++++++++++++++++++++++ test/unit/Withdrawal.t.sol | 240 +++++++++++++++++++++++++++++++++++ test/unit/helpers/Base.t.sol | 8 ++ 4 files changed, 485 insertions(+) create mode 100644 test/unit/Payment.t.sol create mode 100644 test/unit/Withdrawal.t.sol diff --git a/package.json b/package.json index cc74767..54a73d7 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "test:arbitrum-sepolia": "TESTING=true FORKED_NETWORK=arbitrum-sepolia FORK_BLOCK=98684306 forge test -vvv ", "test:base": "TESTING=true FORKED_NETWORK=base FORK_BLOCK=20648648 forge test -vvv ", "test:bnb": "TESTING=true FORKED_NETWORK=bnb FORK_BLOCK=44138162 forge test -vvv ", + "test:integration": "TESTING=true FORKED_NETWORK=mainnet FORK_BLOCK=18920905 forge test -vvv --match-contract Integration -vvv", "test:optimism": "TESTING=true FORKED_NETWORK=optimism FORK_BLOCK=121238511 forge test -vvv ", "test:optimism-sepolia": "TESTING=true FORKED_NETWORK=optimism-sepolia FORK_BLOCK=20113040 forge test -vvv ", "test:polygon": "TESTING=true FORKED_NETWORK=polygon FORK_BLOCK=62103393 forge test -vvv ", diff --git a/test/unit/Payment.t.sol b/test/unit/Payment.t.sol new file mode 100644 index 0000000..9f11ae9 --- /dev/null +++ b/test/unit/Payment.t.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +// Base test contract +import {UnitBase} from "./helpers/Base.t.sol"; + +// Grateful contract and related interfaces +import {Grateful, IGrateful} from "contracts/Grateful.sol"; +import {ERC20Mock} from "test/mocks/ERC20Mock.sol"; + +contract UnitPayment is UnitBase { + function test_paySuccessWithoutYield() public { + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + token.mint(user, amount); + vm.prank(user); + token.approve(address(grateful), amount); + + vm.prank(user); + vm.expectEmit(true, true, true, true); + emit IGrateful.PaymentProcessed(user, merchant, address(token), amount, false, paymentId); + grateful.pay(merchant, address(token), amount, paymentId, false); + + uint256 feeAmount = (amount * grateful.fee()) / 1e18; + uint256 expectedMerchantAmount = amount - feeAmount; + + assertEq(token.balanceOf(owner), feeAmount); + assertEq(token.balanceOf(merchant), expectedMerchantAmount); + } + + function test_paySuccessWithYield() public { + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + token.mint(user, amount); + vm.prank(user); + token.approve(address(grateful), amount); + + vm.prank(user); + vm.expectEmit(true, true, true, true); + emit IGrateful.PaymentProcessed(user, merchant, address(token), amount, true, paymentId); + grateful.pay(merchant, address(token), amount, paymentId, true); + + uint256 feeAmount = (amount * grateful.fee()) / 1e18; + uint256 amountAfterFee = amount - feeAmount; + + assertEq(token.balanceOf(owner), feeAmount); + assertEq(token.balanceOf(merchant), 0); + + uint256 merchantShares = grateful.shares(merchant, address(token)); + uint256 merchantDeposit = grateful.userDeposits(merchant, address(token)); + + assertGt(merchantShares, 0); + assertEq(merchantDeposit, amountAfterFee); + } + + function test_revertIfPayWithNonWhitelistedToken() public { + ERC20Mock nonWhitelistedToken = new ERC20Mock(); + + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + nonWhitelistedToken.mint(user, amount); + vm.prank(user); + nonWhitelistedToken.approve(address(grateful), amount); + + vm.prank(user); + vm.expectRevert(IGrateful.Grateful_TokenNotWhitelisted.selector); + grateful.pay(merchant, address(nonWhitelistedToken), amount, paymentId, false); + } + + function test_revertIfPayWithZeroAmount() public { + uint256 amount = 0; + uint256 paymentId = 1; + + vm.prank(user); + token.approve(address(grateful), amount); + + vm.prank(user); + vm.expectRevert(IGrateful.Grateful_InvalidAmount.selector); + grateful.pay(merchant, address(token), amount, paymentId, false); + } + + function test_revertIfPayWithInvalidMerchant() public { + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + token.approve(address(grateful), amount); + + vm.prank(user); + vm.expectRevert(IGrateful.Grateful_InvalidAddress.selector); + grateful.pay(address(0), address(token), amount, paymentId, false); + } + + function test_revertIfPayWithInsufficientAllowance() public { + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + token.mint(user, amount); + + vm.prank(user); + vm.expectRevert(); + grateful.pay(merchant, address(token), amount, paymentId, false); + } + + function test_revertIfPayWithInsufficientBalance() public { + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + token.approve(address(grateful), amount); + + vm.prank(user); + vm.expectRevert(); + grateful.pay(merchant, address(token), amount, paymentId, false); + } + + function test_revertIfPayWithInvalidTokenAddress() public { + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + vm.expectRevert(IGrateful.Grateful_TokenNotWhitelisted.selector); + grateful.pay(merchant, address(0), amount, paymentId, false); + } + + function test_payWithoutVaultYieldFundsTrue() public { + vm.prank(owner); + grateful.removeVault(address(token)); + + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + token.mint(user, amount); + vm.prank(user); + token.approve(address(grateful), amount); + + vm.prank(user); + vm.expectEmit(true, true, true, true); + emit IGrateful.PaymentProcessed(user, merchant, address(token), amount, true, paymentId); + grateful.pay(merchant, address(token), amount, paymentId, true); + + uint256 feeAmount = (amount * grateful.fee()) / 1e18; + uint256 amountAfterFee = amount - feeAmount; + + assertEq(token.balanceOf(owner), feeAmount); + assertEq(token.balanceOf(merchant), amountAfterFee); + + uint256 merchantShares = grateful.shares(merchant, address(token)); + uint256 merchantDeposit = grateful.userDeposits(merchant, address(token)); + + assertEq(merchantShares, 0); + assertEq(merchantDeposit, 0); + } + + function test_revertIfPayWithZeroAddressToken() public { + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + vm.expectRevert(IGrateful.Grateful_TokenNotWhitelisted.selector); + grateful.pay(merchant, address(0), amount, paymentId, false); + } + + function test_revertIfPayToZeroAddressMerchant() public { + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + token.mint(user, amount); + vm.prank(user); + token.approve(address(grateful), amount); + + vm.prank(user); + vm.expectRevert(IGrateful.Grateful_InvalidAddress.selector); + grateful.pay(address(0), address(token), amount, paymentId, false); + } + + function test_payWithCustomFee() public { + uint256 customFee = 0.02 ether; // 2% + vm.prank(owner); + grateful.setCustomFee(customFee, merchant); + + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + token.mint(user, amount); + vm.prank(user); + token.approve(address(grateful), amount); + + vm.prank(user); + grateful.pay(merchant, address(token), amount, paymentId, false); + + uint256 feeAmount = (amount * customFee) / 1e18; + uint256 expectedMerchantAmount = amount - feeAmount; + + assertEq(token.balanceOf(owner), feeAmount); + assertEq(token.balanceOf(merchant), expectedMerchantAmount); + } + + function test_payWithVaultNotSetAndYieldFundsTrue() public { + vm.prank(owner); + grateful.removeVault(address(token)); + + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + token.mint(user, amount); + vm.prank(user); + token.approve(address(grateful), amount); + + vm.prank(user); + grateful.pay(merchant, address(token), amount, paymentId, true); + + uint256 feeAmount = (amount * grateful.fee()) / 1e18; + uint256 amountAfterFee = amount - feeAmount; + + assertEq(token.balanceOf(owner), feeAmount); + assertEq(token.balanceOf(merchant), amountAfterFee); + + uint256 merchantShares = grateful.shares(merchant, address(token)); + uint256 merchantDeposit = grateful.userDeposits(merchant, address(token)); + + assertEq(merchantShares, 0); + assertEq(merchantDeposit, 0); + } +} diff --git a/test/unit/Withdrawal.t.sol b/test/unit/Withdrawal.t.sol new file mode 100644 index 0000000..9dc98ab --- /dev/null +++ b/test/unit/Withdrawal.t.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +// Base test contract +import {UnitBase} from "./helpers/Base.t.sol"; + +// Grateful contract and related interfaces +import {Grateful, IGrateful} from "contracts/Grateful.sol"; + +import {AaveV3Vault} from "contracts/vaults/AaveV3Vault.sol"; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ERC20Mock} from "test/mocks/ERC20Mock.sol"; +import {IPool, IRewardsController} from "yield-daddy/aave-v3/AaveV3ERC4626.sol"; + +contract UnitWithdrawal is UnitBase { + function test_withdrawFullSuccess() public { + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + token.mint(user, amount); + vm.prank(user); + token.approve(address(grateful), amount); + vm.prank(user); + grateful.pay(merchant, address(token), amount, paymentId, true); + + uint256 initialDeposit = grateful.userDeposits(merchant, address(token)); + + vm.prank(merchant); + vm.expectEmit(true, true, true, true); + emit IGrateful.Withdrawal(merchant, address(token), initialDeposit, 0); + grateful.withdraw(address(token)); + + uint256 finalMerchantBalance = token.balanceOf(merchant); + uint256 finalShares = grateful.shares(merchant, address(token)); + uint256 finalDeposit = grateful.userDeposits(merchant, address(token)); + + assertEq(finalShares, 0); + assertEq(finalDeposit, 0); + assertEq(finalMerchantBalance, initialDeposit); + } + + function test_withdrawPartialSuccess() public { + uint256 amount = 1000 ether; + uint256 paymentId = 1; + uint256 withdrawAmount = 500 ether; + uint256 tolerance = withdrawAmount / 10_000; // 0.01% precision loss tolerance + + vm.prank(user); + token.mint(user, amount); + vm.prank(user); + token.approve(address(grateful), amount); + vm.prank(user); + grateful.pay(merchant, address(token), amount, paymentId, true); + + uint256 initialShares = grateful.shares(merchant, address(token)); + uint256 initialDeposit = grateful.userDeposits(merchant, address(token)); + + vm.prank(merchant); + grateful.withdraw(address(token), withdrawAmount); + + uint256 finalMerchantBalance = token.balanceOf(merchant); + uint256 finalShares = grateful.shares(merchant, address(token)); + uint256 finalDeposit = grateful.userDeposits(merchant, address(token)); + + // Use assertApproxEqAbs to allow for small precision errors + assertApproxEqAbs(finalMerchantBalance, withdrawAmount, tolerance); + assertLt(finalShares, initialShares); + assertLt(finalDeposit, initialDeposit); + assertApproxEqAbs(finalDeposit, initialDeposit - withdrawAmount, tolerance); + } + + function test_withdrawMultipleFullSuccess() public { + (address token2, AaveV3Vault vault2) = _deployNewTokenAndVault(); + + uint256 amount = 1000 ether; + uint256 paymentId1 = 1; + uint256 paymentId2 = 2; + + vm.startPrank(user); + token.mint(user, amount); + token.approve(address(grateful), amount); + grateful.pay(merchant, address(token), amount, paymentId1, true); + + // mint new token + ERC20Mock(token2).mint(user, amount); + ERC20Mock(token2).approve(address(grateful), amount); + grateful.pay(merchant, token2, amount, paymentId2, true); + vm.stopPrank(); + + address[] memory tokens = new address[](2); + tokens[0] = address(token); + tokens[1] = token2; + + uint256 expectedMerchantBalanceToken1 = grateful.userDeposits(merchant, address(token)); + uint256 expectedMerchantBalanceToken2 = grateful.userDeposits(merchant, token2); + + vm.prank(merchant); + vm.expectEmit(true, true, true, true); + emit IGrateful.Withdrawal(merchant, address(token), expectedMerchantBalanceToken1, 0); + vm.expectEmit(true, true, true, true); + emit IGrateful.Withdrawal(merchant, token2, expectedMerchantBalanceToken2, 0); + grateful.withdrawMultiple(tokens); + + uint256 finalMerchantBalanceToken1 = token.balanceOf(merchant); + uint256 finalMerchantBalanceToken2 = ERC20Mock(token2).balanceOf(merchant); + + assertEq(finalMerchantBalanceToken1, expectedMerchantBalanceToken1); + assertEq(finalMerchantBalanceToken2, expectedMerchantBalanceToken2); + } + + /* function test_withdrawMultiplePartialSuccess() public { + address token2 = address(new ERC20Mock()); + vm.prank(owner); + grateful.addToken(token2); + + AaveV3Vault vault2 = new AaveV3Vault( + ERC20(token2), + ERC20(address(aToken)), + IPool(address(aavePool)), + owner, + IRewardsController(address(rewardsController)), + address(grateful) + ); + vm.prank(owner); + grateful.addVault(token2, address(vault2)); + + uint256 amount = 1000 ether; + uint256 paymentId1 = 1; + uint256 paymentId2 = 2; + + vm.prank(user); + ERC20Mock(token2).mint(user, amount); + + vm.startPrank(user); + token.mint(user, amount); + token.approve(address(grateful), amount); + grateful.pay(merchant, address(token), amount, paymentId1, true); + + ERC20Mock(token2).approve(address(grateful), amount); + grateful.pay(merchant, token2, amount, paymentId2, true); + vm.stopPrank(); + + address[] memory tokens = new address[](2); + tokens[0] = address(token); + tokens[1] = token2; + + uint256[] memory assets = new uint256[](2); + assets[0] = 500 ether; + assets[1] = 500 ether; + + vm.prank(merchant); + vm.expectEmit(true, true, true, true); + emit IGrateful.Withdrawal(merchant, address(token), assets[0], 0); + vm.expectEmit(true, true, true, true); + emit IGrateful.Withdrawal(merchant, token2, assets[1], 0); + grateful.withdrawMultiple(tokens, assets); + + uint256 finalMerchantBalanceToken1 = token.balanceOf(merchant); + uint256 finalMerchantBalanceToken2 = ERC20Mock(token2).balanceOf(merchant); + + assertEq(finalMerchantBalanceToken1, assets[0]); + assertEq(finalMerchantBalanceToken2, assets[1]); + } */ + + function test_revertIfWithdrawTokenNotWhitelisted() public { + address nonWhitelistedToken = address(new ERC20Mock()); + + vm.prank(merchant); + vm.expectRevert(IGrateful.Grateful_TokenNotWhitelisted.selector); + grateful.withdraw(nonWhitelistedToken); + } + + function test_revertIfWithdrawVaultNotSet() public { + vm.prank(owner); + grateful.removeVault(address(token)); + + vm.prank(merchant); + vm.expectRevert(IGrateful.Grateful_VaultNotSet.selector); + grateful.withdraw(address(token)); + } + + function test_revertIfWithdrawInvalidTokenAddress() public { + vm.prank(merchant); + vm.expectRevert(IGrateful.Grateful_TokenNotWhitelisted.selector); + grateful.withdraw(address(0)); + } + + function test_revertIfWithdrawInvalidAmount() public { + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + token.mint(user, amount); + vm.prank(user); + token.approve(address(grateful), amount); + vm.prank(user); + grateful.pay(merchant, address(token), amount, paymentId, true); + + vm.prank(merchant); + vm.expectRevert(IGrateful.Grateful_InvalidAmount.selector); + grateful.withdraw(address(token), 0); + } + + function test_revertIfWithdrawExceedsShares() public { + uint256 amount = 1000 ether; + uint256 paymentId = 1; + + vm.prank(user); + token.mint(user, amount); + vm.prank(user); + token.approve(address(grateful), amount); + vm.prank(user); + grateful.pay(merchant, address(token), amount, paymentId, true); + + vm.prank(merchant); + vm.expectRevert(); + grateful.withdraw(address(token), 2000 ether); + } + + function test_revertIfWithdrawMultipleMismatchedArrays() public { + address[] memory tokens = new address[](2); + tokens[0] = address(token); + tokens[1] = address(token); + + uint256[] memory assets = new uint256[](1); + assets[0] = 1000 ether; + + vm.prank(merchant); + vm.expectRevert(IGrateful.Grateful_MismatchedArrays.selector); + grateful.withdrawMultiple(tokens, assets); + } + + function test_revertIfWithdrawWithNoShares() public { + vm.prank(merchant); + vm.expectRevert(); + grateful.withdraw(address(token)); + } +} diff --git a/test/unit/helpers/Base.t.sol b/test/unit/helpers/Base.t.sol index 77f083f..1037cff 100644 --- a/test/unit/helpers/Base.t.sol +++ b/test/unit/helpers/Base.t.sol @@ -146,6 +146,14 @@ contract UnitBase is Test { // Set reserve AToken in the mock pool aavePool.setReserveAToken(newToken, address(aToken)); + // Add the new token to the Grateful contract + vm.startPrank(owner); + grateful.addToken(newToken); + + // Add the new AaveV3Vault to the Grateful contract + grateful.addVault(newToken, address(newVault)); + vm.stopPrank(); + return (newToken, newVault); } }