-
Notifications
You must be signed in to change notification settings - Fork 4
/
CrowdinvestingERC2771.t.sol
248 lines (203 loc) · 10.7 KB
/
CrowdinvestingERC2771.t.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.23;
import "../lib/forge-std/src/Test.sol";
import "../contracts/factories/TokenProxyFactory.sol";
import "../contracts/factories/CrowdinvestingCloneFactory.sol";
import "../contracts/FeeSettings.sol";
import "./resources/FakePaymentToken.sol";
import "./resources/ERC2771Helper.sol";
import "./resources/CloneCreators.sol";
import "@opengsn/contracts/src/forwarder/Forwarder.sol"; // chose specific version to avoid import error: yarn add @opengsn/[email protected]
contract CrowdinvestingTest is Test {
using ECDSA for bytes32; // for verify with var.recover()
CrowdinvestingCloneFactory fundraisingFactory;
Crowdinvesting crowdinvesting;
AllowList list;
FeeSettings feeSettings;
Token token;
FakePaymentToken paymentToken;
//Forwarder trustedForwarder;
ERC2771Helper ERC2771helper;
CrowdinvestingInitializerArguments arguments;
address public constant trustedForwarder = 0x9109709EcFA91A80626FF3989D68f67F5B1dD129;
address public constant admin = 0x0109709eCFa91a80626FF3989D68f67f5b1dD120;
address public constant mintAllower = 0x2109709EcFa91a80626Ff3989d68F67F5B1Dd122;
address public constant minter = 0x3109709ECfA91A80626fF3989D68f67F5B1Dd123;
address public constant owner = 0x6109709EcFA91A80626FF3989d68f67F5b1dd126;
address public constant receiver = 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127;
address public constant paymentTokenProvider = 0x8109709ecfa91a80626fF3989d68f67F5B1dD128;
address public constant sender = 0x9109709EcFA91A80626FF3989D68f67F5B1dD129;
// DO NOT USE IN PRODUCTION! Key was generated online for testing only.
uint256 public constant buyerPrivateKey = 0x3c69254ad72222e3ddf37667b8173dd773bdbdfd93d4af1d192815ff0662de5f;
address public buyer; // = 0x38d6703d37988C644D6d31551e9af6dcB762E618;
uint8 public constant paymentTokenDecimals = 6;
uint256 public constant paymentTokenAmount = 1000 * 10 ** paymentTokenDecimals;
uint256 public constant price = 7 * 10 ** paymentTokenDecimals; // 7 payment tokens per token
uint256 public constant maxAmountOfTokenToBeSold = 20 * 10 ** 18; // 20 token
uint256 public constant maxAmountPerBuyer = maxAmountOfTokenToBeSold / 2; // 10 token
uint256 public constant minAmountPerBuyer = maxAmountOfTokenToBeSold / 200; // 0.1 token
uint256 tokenBuyAmount;
uint256 costInPaymentToken;
uint32 tokenFeeNumerator = 100;
uint32 paymentTokenFeeNumerator = 200;
function setUp() public {
buyer = vm.addr(buyerPrivateKey);
// set up currency
vm.prank(paymentTokenProvider);
paymentToken = new FakePaymentToken(paymentTokenAmount, paymentTokenDecimals); // 1000 tokens with 6 decimals
// transfer currency to buyer
vm.prank(paymentTokenProvider);
paymentToken.transfer(buyer, paymentTokenAmount);
assertTrue(paymentToken.balanceOf(buyer) == paymentTokenAmount);
list = createAllowList(trustedForwarder, owner);
vm.prank(owner);
list.set(address(paymentToken), TRUSTED_CURRENCY);
Fees memory fees = Fees(tokenFeeNumerator, paymentTokenFeeNumerator, paymentTokenFeeNumerator, 0);
feeSettings = createFeeSettings(trustedForwarder, address(this), fees, admin, admin, admin);
Token implementation = new Token(trustedForwarder);
TokenProxyFactory tokenFactory = new TokenProxyFactory(address(implementation));
token = Token(
tokenFactory.createTokenProxy(0, trustedForwarder, feeSettings, admin, list, 0x0, "TESTTOKEN", "TEST")
);
ERC2771helper = new ERC2771Helper();
tokenBuyAmount = 5 * 10 ** token.decimals();
costInPaymentToken = (tokenBuyAmount * price) / 10 ** 18;
arguments = CrowdinvestingInitializerArguments(
address(this),
payable(receiver),
minAmountPerBuyer,
maxAmountPerBuyer,
price,
0,
0,
maxAmountOfTokenToBeSold,
paymentToken,
token,
0,
address(0)
);
}
function buyWithERC2771(Forwarder forwarder) public {
vm.prank(owner);
fundraisingFactory = new CrowdinvestingCloneFactory(address(new Crowdinvesting(address(forwarder))));
crowdinvesting = Crowdinvesting(fundraisingFactory.createCrowdinvestingClone(0, address(forwarder), arguments));
// allow crowdinvesting contract to mint
bytes32 roleMintAllower = token.MINTALLOWER_ROLE();
vm.prank(admin);
token.grantRole(roleMintAllower, mintAllower);
vm.prank(mintAllower);
token.increaseMintingAllowance(address(crowdinvesting), maxAmountOfTokenToBeSold);
// give crowdinvesting contract allowance
vm.prank(buyer);
paymentToken.approve(address(crowdinvesting), paymentTokenAmount);
assert(costInPaymentToken == 35 * 10 ** paymentTokenDecimals); // 35 payment tokens, manually calculated
// register domain and request type
bytes32 domainSeparator = ERC2771helper.registerDomain(
forwarder,
Strings.toHexString(uint256(uint160(address(crowdinvesting))), 20),
"1"
);
bytes32 requestType = ERC2771helper.registerRequestType(forwarder, "buy", "address buyer,uint256 amount");
/*
create data and signature for execution
*/
// // https://github.com/foundry-rs/foundry/issues/3330
// // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol
// bytes32 digest = ECDSA.toTypedDataHash(domainSeparator, keccak256(payload));
// (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKey, digest);
// todo: get nonce from forwarder
// build request
bytes memory payload = abi.encodeWithSelector(
crowdinvesting.buy.selector,
tokenBuyAmount,
type(uint256).max,
buyer
);
IForwarder.ForwardRequest memory request = IForwarder.ForwardRequest({
from: buyer,
to: address(crowdinvesting),
value: 0,
gas: 1000000,
nonce: forwarder.getNonce(buyer),
data: payload,
validUntil: 0
});
bytes memory suffixData = "0";
// pack and hash request
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
domainSeparator,
keccak256(forwarder._getEncoded(request, requestType, suffixData))
)
);
// sign request
//bytes memory signature
(uint8 v, bytes32 r, bytes32 s) = vm.sign(buyerPrivateKey, digest);
bytes memory signature = abi.encodePacked(r, s, v); // https://docs.openzeppelin.com/contracts/2.x/utilities
require(digest.recover(signature) == request.from, "FWD: signature mismatch");
// // encode buy call and sign it https://book.getfoundry.sh/cheatcodes/sign
// bytes memory buyCallData = abi.encodeWithSignature("buy(uint256)", type(uint256).max, tokenBuyAmount);
/*
execute request and check results
*/
vm.prank(buyer);
assertEq(token.balanceOf(buyer), 0, "buyer has tokens before");
assertEq(paymentToken.balanceOf(receiver), 0, "receiver has payment tokens before");
assertEq(paymentToken.balanceOf(address(crowdinvesting)), 0, "crowdinvesting has payment tokens before");
assertEq(token.balanceOf(address(crowdinvesting)), 0, "crowdinvesting has tokens before");
assertEq(token.balanceOf(receiver), 0, "receiver has tokens before");
assertEq(token.balanceOf(address(forwarder)), 0, "forwarder has tokens before");
assertTrue(crowdinvesting.tokensSold() == 0, "tokens sold before");
assertTrue(crowdinvesting.tokensBought(buyer) == 0, "tokens bought before");
//assertTrue(vm.getNonce(buyer) == 0); // it seems forge does not increase nonces with prank
console.log("Token balance of buyer before: ", token.balanceOf(buyer));
console.log("eth balance of buyer ", buyer.balance);
// send call through forwarder contract
uint256 gasBefore = gasleft();
forwarder.execute(request, domainSeparator, requestType, suffixData, signature);
// vm.prank(buyer);
// crowdinvesting.buy(tokenBuyAmount);
console.log("Gas used: ", gasBefore - gasleft());
// investor receives as many tokens as they paid for
assertTrue(token.balanceOf(buyer) == tokenBuyAmount, "buyer has tokens after");
// but fee collector receives additional tokens
assertTrue(
token.balanceOf(feeSettings.feeCollector()) == tokenBuyAmount / tokenFeeNumerator,
"fee collector has tokens after"
);
// receiver receives payment tokens after fee has been deducted
assertEq(
paymentToken.balanceOf(receiver),
costInPaymentToken - (costInPaymentToken * paymentTokenFeeNumerator) / feeSettings.FEE_DENOMINATOR(),
"receiver has payment tokens after"
);
// fee collector receives fee in payment tokens
assertEq(
paymentToken.balanceOf(feeSettings.feeCollector()),
(costInPaymentToken * paymentTokenFeeNumerator) / feeSettings.FEE_DENOMINATOR(),
"fee collector has payment tokens after"
);
assertEq(paymentToken.balanceOf(address(crowdinvesting)), 0, "crowdinvesting has payment tokens after");
assertEq(token.balanceOf(address(crowdinvesting)), 0, "crowdinvesting has tokens after");
assertEq(token.balanceOf(receiver), 0, "receiver has tokens after");
assertEq(token.balanceOf(address(forwarder)), 0, "forwarder has tokens after");
assertTrue(crowdinvesting.tokensSold() == tokenBuyAmount, "tokens sold after");
assertTrue(crowdinvesting.tokensBought(buyer) == tokenBuyAmount, "tokens bought after");
//assertTrue(vm.getNonce(buyer) == 0);
console.log("paymentToken balance of receiver after: ", paymentToken.balanceOf(receiver));
console.log("Token balance of buyer after: ", token.balanceOf(buyer));
/*
try to execute request again (must fail)
*/
vm.expectRevert("FWD: nonce mismatch");
forwarder.execute(request, domainSeparator, requestType, suffixData, signature);
}
function testBuyWithLocalForwarder() public {
buyWithERC2771(new Forwarder());
}
function testBuyWithMainnetGSNForwarder() public {
// uses deployed forwarder on mainnet with fork. https://docs-v2.opengsn.org/networks/ethereum/mainnet.html
buyWithERC2771(Forwarder(payable(0xAa3E82b4c4093b4bA13Cb5714382C99ADBf750cA)));
}
}