Skip to content

Commit 82c7b7e

Browse files
committed
test: add donation handler test cases
1 parent 7288833 commit 82c7b7e

File tree

3 files changed

+247
-1
lines changed

3 files changed

+247
-1
lines changed

foundry.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ multiline_func_header = 'params_first_multi'
99
sort_imports = true
1010

1111
[profile.default]
12-
solc_version = '0.8.23'
12+
solc = "0.8.23"
1313
libs = ['node_modules', 'lib']
1414
optimizer_runs = 10_000
1515
via_ir = true

test/DonationHandler.t.sol

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.0;
3+
4+
import '../src/contracts/DonationHandler.sol';
5+
6+
import '@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol';
7+
import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
8+
import 'forge-std/Test.sol';
9+
10+
// Mock ERC20 token for testing
11+
contract MockERC20 is ERC20 {
12+
constructor() ERC20('MockToken', 'MTK') {
13+
_mint(msg.sender, 1_000_000 * 10 ** 18);
14+
}
15+
}
16+
17+
contract DonationHandlerTest is Test {
18+
DonationHandler public donationHandler;
19+
MockERC20 public mockToken;
20+
21+
address public owner;
22+
address public recipient1;
23+
address public recipient2;
24+
address public recipient3;
25+
26+
uint256 public constant INITIAL_BALANCE = 100 ether;
27+
uint256 public constant INITIAL_TOKEN_BALANCE = 1_000_000 * 10 ** 18;
28+
29+
event DonationMade(address indexed recipientAddress, uint256 amount, address indexed tokenAddress);
30+
31+
function setUp() public {
32+
// Deploy contracts
33+
donationHandler = new DonationHandler();
34+
donationHandler = DonationHandler(
35+
payable(address(new ERC1967Proxy(address(donationHandler), abi.encodeCall(DonationHandler.initialize, ()))))
36+
);
37+
mockToken = new MockERC20();
38+
39+
// accounts
40+
owner = address(this);
41+
recipient1 = makeAddr('recipient1');
42+
recipient2 = makeAddr('recipient2');
43+
recipient3 = makeAddr('recipient3');
44+
45+
// add funds to the owner address
46+
vm.deal(owner, INITIAL_BALANCE);
47+
48+
// Approve tokens in the contract
49+
mockToken.approve(address(donationHandler), type(uint256).max);
50+
}
51+
52+
// Helper functions
53+
function _setupMultipleRecipients()
54+
internal
55+
pure
56+
returns (address[] memory recipients, uint256[] memory amounts, bytes[] memory data)
57+
{
58+
recipients = new address[](3);
59+
recipients[0] = address(0x1);
60+
recipients[1] = address(0x2);
61+
recipients[2] = address(0x3);
62+
63+
amounts = new uint256[](3);
64+
amounts[0] = 1 ether;
65+
amounts[1] = 2 ether;
66+
amounts[2] = 3 ether;
67+
68+
data = new bytes[](3);
69+
data[0] = '';
70+
data[1] = '';
71+
data[2] = '';
72+
}
73+
74+
function _expectDonationEvent(address recipient, uint256 amount, address token) internal {
75+
vm.expectEmit(true, true, false, true);
76+
emit DonationMade(recipient, amount, token);
77+
}
78+
79+
// Helper function => recipient that reverts on ETH receive
80+
function _deployRevertingRecipient() internal returns (address) {
81+
bytes memory bytecode = hex'60806040523415600e57600080fd5b600080fdfe';
82+
address recipient;
83+
assembly {
84+
recipient := create(0, add(bytecode, 0x20), mload(bytecode))
85+
}
86+
return recipient;
87+
}
88+
89+
modifier whenMakingETHDonations() {
90+
vm.deal(address(this), 100 ether);
91+
_;
92+
}
93+
94+
modifier whenMakingERC20Donations() {
95+
mockToken.approve(address(donationHandler), type(uint256).max);
96+
_;
97+
}
98+
99+
function test_WhenMakingASingleETHDonation() external whenMakingETHDonations {
100+
uint256 donationAmount = 1 ether;
101+
bytes memory data = '';
102+
103+
uint256 recipientBalanceBefore = recipient1.balance;
104+
105+
_expectDonationEvent(recipient1, donationAmount, address(0));
106+
donationHandler.donateETH{value: donationAmount}(recipient1, donationAmount, data);
107+
108+
assertEq(
109+
recipient1.balance - recipientBalanceBefore,
110+
donationAmount,
111+
'Recipient balance should increase by donation amount'
112+
);
113+
// Test revert when sending incorrect amount
114+
vm.expectRevert('ETH transfer failed');
115+
donationHandler.donateETH{value: donationAmount - 0.1 ether}(recipient1, donationAmount, data);
116+
}
117+
118+
function test_WhenMakingMultipleETHDonations() external whenMakingETHDonations {
119+
(address[] memory recipients, uint256[] memory amounts, bytes[] memory data) = _setupMultipleRecipients();
120+
uint256 totalAmount = amounts[0] + amounts[1] + amounts[2];
121+
122+
// Initial balances
123+
uint256[] memory initialBalances = new uint256[](3);
124+
for (uint256 i = 0; i < recipients.length; i++) {
125+
initialBalances[i] = recipients[i].balance;
126+
_expectDonationEvent(recipients[i], amounts[i], address(0));
127+
}
128+
129+
donationHandler.donateManyETH{value: totalAmount}(totalAmount, recipients, amounts, data);
130+
131+
// Verify balances
132+
for (uint256 i = 0; i < recipients.length; i++) {
133+
assertEq(
134+
recipients[i].balance - initialBalances[i], amounts[i], 'Recipient balance should increase by donation amount'
135+
);
136+
}
137+
138+
// Test revert when msg.value doesn't match totalAmount
139+
vm.expectRevert('Incorrect ETH amount sent');
140+
donationHandler.donateManyETH{value: totalAmount - 0.1 ether}(totalAmount, recipients, amounts, data);
141+
142+
// Test revert with mismatched array lengths
143+
address[] memory shortRecipients = new address[](2);
144+
vm.expectRevert(DonationHandler.InvalidInput.selector);
145+
donationHandler.donateManyETH{value: totalAmount}(totalAmount, shortRecipients, amounts, data);
146+
}
147+
148+
function test_WhenMakingASingleERC20Donation() external whenMakingERC20Donations {
149+
uint256 donationAmount = 100 * 10 ** 18;
150+
bytes memory data = '';
151+
152+
uint256 recipientBalanceBefore = mockToken.balanceOf(recipient1);
153+
154+
_expectDonationEvent(recipient1, donationAmount, address(mockToken));
155+
donationHandler.donateERC20(address(mockToken), recipient1, donationAmount, data);
156+
157+
assertEq(
158+
mockToken.balanceOf(recipient1) - recipientBalanceBefore,
159+
donationAmount,
160+
'Recipient token balance should increase by donation amount'
161+
);
162+
163+
// TODO FIX REVERTS WITH CUSTOM ERRORS
164+
vm.expectRevert();
165+
donationHandler.donateERC20(address(0), recipient1, donationAmount, data);
166+
167+
vm.expectRevert();
168+
donationHandler.donateERC20(address(mockToken), address(0), donationAmount, data);
169+
170+
vm.expectRevert();
171+
donationHandler.donateERC20(address(mockToken), recipient1, 0, data);
172+
173+
// Test insufficient allowance
174+
mockToken.approve(address(donationHandler), donationAmount - 1);
175+
vm.expectRevert();
176+
donationHandler.donateERC20(address(mockToken), recipient1, donationAmount, data);
177+
}
178+
179+
function test_WhenMakingMultipleERC20Donations() external whenMakingERC20Donations {
180+
(address[] memory recipients, uint256[] memory amounts, bytes[] memory data) = _setupMultipleRecipients();
181+
uint256 totalAmount = amounts[0] + amounts[1] + amounts[2];
182+
183+
// Record initial balances
184+
uint256[] memory initialBalances = new uint256[](3);
185+
for (uint256 i = 0; i < recipients.length; i++) {
186+
initialBalances[i] = mockToken.balanceOf(recipients[i]);
187+
_expectDonationEvent(recipients[i], amounts[i], address(mockToken));
188+
}
189+
190+
donationHandler.donateManyERC20(address(mockToken), totalAmount, recipients, amounts, data);
191+
192+
// Verify balances
193+
for (uint256 i = 0; i < recipients.length; i++) {
194+
assertEq(
195+
mockToken.balanceOf(recipients[i]) - initialBalances[i],
196+
amounts[i],
197+
'Recipient token balance should increase by donation amount'
198+
);
199+
}
200+
201+
// Test revert with mismatched array lengths
202+
address[] memory shortRecipients = new address[](2);
203+
vm.expectRevert(DonationHandler.InvalidInput.selector);
204+
donationHandler.donateManyERC20(address(mockToken), totalAmount, shortRecipients, amounts, data);
205+
206+
// Test insufficient allowance
207+
mockToken.approve(address(donationHandler), totalAmount - 1);
208+
vm.expectRevert(DonationHandler.InsufficientAllowance.selector);
209+
donationHandler.donateManyERC20(address(mockToken), totalAmount, recipients, amounts, data);
210+
}
211+
212+
function test_RevertWhen_ReceivingDirectETHTransfers() external {
213+
vm.deal(address(this), 1 ether);
214+
vm.expectRevert();
215+
payable(address(donationHandler)).transfer(1 ether);
216+
}
217+
}

test/DonationHandler.tree

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
DonationHandler
2+
├── When initializing the contract
3+
│ ├── It should initialize with correct owner
4+
│ └── It should not be able to initialize twice
5+
├── When making ETH donations
6+
│ ├── When making a single ETH donation
7+
│ │ ├── It should successfully donate ETH to a recipient
8+
│ │ ├── It should emit DonationMade event
9+
│ │ └── It should revert if ETH transfer fails
10+
│ └── When making multiple ETH donations
11+
│ ├── It should successfully donate ETH to multiple recipients
12+
│ ├── It should emit DonationMade events for each recipient
13+
│ ├── It should revert if total amount doesn't match msg.value
14+
│ └── It should revert if input arrays have different lengths
15+
├── When making ERC20 donations
16+
│ ├── When making a single ERC20 donation
17+
│ │ ├── It should successfully transfer ERC20 tokens to recipient
18+
│ │ ├── It should emit DonationMade event
19+
│ │ ├── It should revert if token address is zero
20+
│ │ ├── It should revert if recipient address is zero
21+
│ │ ├── It should revert if amount is zero
22+
│ │ └── It should revert if allowance is insufficient
23+
│ └── When making multiple ERC20 donations
24+
│ ├── It should successfully transfer ERC20 tokens to multiple recipients
25+
│ ├── It should emit DonationMade events for each recipient
26+
│ ├── It should revert if input arrays have different lengths
27+
│ └── It should revert if allowance is insufficient
28+
└── When receiving direct ETH transfers
29+
└── It should revert

0 commit comments

Comments
 (0)