1
- // SPDX-License-Identifier: UNLICENSED
1
+ // SPDX-License-Identifier: Apache-2.0
2
2
pragma solidity ^ 0.8.22 ;
3
3
4
+ /// @author thirdweb
5
+
4
6
import "@openzeppelin/contracts/access/Ownable.sol " ;
5
7
import "@openzeppelin/contracts/token/ERC20/IERC20.sol " ;
6
8
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol " ;
@@ -9,48 +11,80 @@ import { EIP712 } from "./utils/EIP712.sol";
9
11
import { SafeTransferLib } from "./lib/SafeTransferLib.sol " ;
10
12
import { ECDSA } from "./lib/ECDSA.sol " ;
11
13
12
- /**
13
- Requirements
14
- - easily change fee / payout structure per transaction
15
- - easily change provider per transaction
16
-
17
- TODO:
18
- - add receiver function
19
- - add thirdweb signer for tamperproofing
20
- - add operator role automating withdrawals
21
- */
22
-
23
14
contract PaymentsGateway is EIP712 , Ownable , ReentrancyGuard {
24
15
using ECDSA for bytes32 ;
25
16
26
- error PaymentsGatewayMismatchedValue (uint256 expected , uint256 actual );
27
- error PaymentsGatewayInvalidAmount (uint256 amount );
28
- error PaymentsGatewayVerificationFailed ();
29
- error PaymentsGatewayFailedToForward ();
30
- error PaymentsGatewayRequestExpired (uint256 expirationTimestamp );
17
+ /*///////////////////////////////////////////////////////////////
18
+ State, constants, structs
19
+ //////////////////////////////////////////////////////////////*/
20
+
21
+ bytes32 private constant PAYOUTINFO_TYPEHASH =
22
+ keccak256 ("PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS) " );
23
+ bytes32 private constant REQUEST_TYPEHASH =
24
+ keccak256 (
25
+ "PayRequest(bytes32 clientId,bytes32 transactionId,address tokenAddress,uint256 tokenAmount,uint256 expirationTimestamp,PayoutInfo[] payouts,address forwardAddress,bytes data)PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS) "
26
+ );
27
+ address private constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE ;
28
+
29
+ /// @dev Mapping from pay request UID => whether the pay request is processed.
30
+ mapping (bytes32 => bool ) private processed;
31
+
32
+ /**
33
+ * @notice Info of fee payout recipients.
34
+ *
35
+ * @param clientId ClientId of fee recipient
36
+ * @param payoutAddress Recipient address
37
+ * @param feeBPS The fee basis points to be charged. Max = 10000 (10000 = 100%, 1000 = 10%)
38
+ */
39
+ struct PayoutInfo {
40
+ bytes32 clientId;
41
+ address payable payoutAddress;
42
+ uint256 feeBPS;
43
+ }
44
+
45
+ /**
46
+ * @notice The body of a request to purchase tokens.
47
+ *
48
+ * @param clientId Thirdweb clientId for logging attribution data
49
+ * @param transactionId Acts as a uid and a key to lookup associated swap provider
50
+ * @param tokenAddress Address of the currency used for purchase
51
+ * @param tokenAmount Currency amount being sent
52
+ * @param expirationTimestamp The unix timestamp at which the request expires
53
+ * @param payouts Array of Payout struct - containing fee recipients' info
54
+ * @param forwardAddress Address of swap provider contract
55
+ * @param data Calldata for swap provider
56
+ */
57
+ struct PayRequest {
58
+ bytes32 clientId;
59
+ bytes32 transactionId;
60
+ address tokenAddress;
61
+ uint256 tokenAmount;
62
+ uint256 expirationTimestamp;
63
+ PayoutInfo[] payouts;
64
+ address payable forwardAddress;
65
+ bytes data;
66
+ }
67
+
68
+ /*///////////////////////////////////////////////////////////////
69
+ Events
70
+ //////////////////////////////////////////////////////////////*/
31
71
32
- event TransferStart (
72
+ event TokenPurchaseInitiated (
33
73
bytes32 indexed clientId ,
34
74
address indexed sender ,
35
75
bytes32 transactionId ,
36
76
address tokenAddress ,
37
77
uint256 tokenAmount
38
78
);
39
79
40
- event TransferEnd (
80
+ event TokenPurchaseCompleted (
41
81
bytes32 indexed clientId ,
42
82
address indexed receiver ,
43
83
bytes32 transactionId ,
44
84
address tokenAddress ,
45
85
uint256 tokenAmount
46
86
);
47
87
48
- /**
49
- Note: not sure if this is completely necessary
50
- estimate the gas on this and remove
51
- we could always combine transferFrom logs w/ this transaction
52
- where from=Address(this) => to != provider
53
- */
54
88
event FeePayout (
55
89
bytes32 indexed clientId ,
56
90
address indexed sender ,
@@ -60,39 +94,27 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
60
94
uint256 feeBPS
61
95
);
62
96
63
- event OperatorChanged (address indexed previousOperator , address indexed newOperator );
64
-
65
- struct PayoutInfo {
66
- bytes32 clientId;
67
- address payable payoutAddress;
68
- uint256 feeBPS;
69
- }
70
- struct PayRequest {
71
- bytes32 clientId;
72
- bytes32 transactionId;
73
- address tokenAddress;
74
- uint256 tokenAmount;
75
- uint256 expirationTimestamp;
76
- PayoutInfo[] payouts;
77
- address payable forwardAddress;
78
- bytes data;
79
- }
97
+ /*///////////////////////////////////////////////////////////////
98
+ Errors
99
+ //////////////////////////////////////////////////////////////*/
80
100
81
- bytes32 private constant PAYOUTINFO_TYPEHASH =
82
- keccak256 ("PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS) " );
83
- bytes32 private constant REQUEST_TYPEHASH =
84
- keccak256 (
85
- "PayRequest(bytes32 clientId,bytes32 transactionId,address tokenAddress,uint256 tokenAmount,uint256 expirationTimestamp,PayoutInfo[] payouts,address forwardAddress,bytes data)PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS) "
86
- );
87
- address private constant THIRDWEB_CLIENT_ID = 0x0000000000000000000000000000000000000000 ;
88
- address private constant NATIVE_TOKEN_ADDRESS = 0x0000000000000000000000000000000000000000 ;
101
+ error PaymentsGatewayMismatchedValue (uint256 expected , uint256 actual );
102
+ error PaymentsGatewayInvalidAmount (uint256 amount );
103
+ error PaymentsGatewayVerificationFailed ();
104
+ error PaymentsGatewayFailedToForward ();
105
+ error PaymentsGatewayRequestExpired (uint256 expirationTimestamp );
89
106
90
- /// @dev Mapping from pay request UID => whether the pay request is processed.
91
- mapping (bytes32 => bool ) private processed;
107
+ /*///////////////////////////////////////////////////////////////
108
+ Constructor
109
+ //////////////////////////////////////////////////////////////*/
92
110
93
111
constructor (address contractOwner ) Ownable (contractOwner) {}
94
112
95
- /* some bridges may refund need a way to get funds back to user */
113
+ /*///////////////////////////////////////////////////////////////
114
+ External / public functions
115
+ //////////////////////////////////////////////////////////////*/
116
+
117
+ /// @notice some bridges may refund need a way to get funds back to user
96
118
function withdrawTo (
97
119
address tokenAddress ,
98
120
uint256 tokenAmount ,
@@ -109,101 +131,19 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
109
131
withdrawTo (tokenAddress, tokenAmount, payable (msg .sender ));
110
132
}
111
133
112
- function _isTokenERC20 (address tokenAddress ) private pure returns (bool ) {
113
- return tokenAddress != NATIVE_TOKEN_ADDRESS;
114
- }
115
-
116
- function _isTokenNative (address tokenAddress ) private pure returns (bool ) {
117
- return tokenAddress == NATIVE_TOKEN_ADDRESS;
118
- }
119
-
120
- function _calculateFee (uint256 amount , uint256 feeBPS ) private pure returns (uint256 ) {
121
- uint256 feeAmount = (amount * feeBPS) / 10_000 ;
122
- return feeAmount;
123
- }
124
-
125
- function _distributeFees (
126
- address tokenAddress ,
127
- uint256 tokenAmount ,
128
- PayoutInfo[] calldata payouts
129
- ) private returns (uint256 ) {
130
- uint256 totalFeeAmount = 0 ;
131
-
132
- for (uint32 payeeIdx = 0 ; payeeIdx < payouts.length ; payeeIdx++ ) {
133
- uint256 feeAmount = _calculateFee (tokenAmount, payouts[payeeIdx].feeBPS);
134
- totalFeeAmount += feeAmount;
135
-
136
- emit FeePayout (
137
- payouts[payeeIdx].clientId,
138
- msg .sender ,
139
- payouts[payeeIdx].payoutAddress,
140
- tokenAddress,
141
- feeAmount,
142
- payouts[payeeIdx].feeBPS
143
- );
144
- if (_isTokenNative (tokenAddress)) {
145
- SafeTransferLib.safeTransferETH (payouts[payeeIdx].payoutAddress, feeAmount);
146
- } else {
147
- SafeTransferLib.safeTransferFrom (tokenAddress, msg .sender , payouts[payeeIdx].payoutAddress, feeAmount);
148
- }
149
- }
150
-
151
- if (totalFeeAmount > tokenAmount) {
152
- revert PaymentsGatewayMismatchedValue (totalFeeAmount, tokenAmount);
153
- }
154
- return totalFeeAmount;
155
- }
156
-
157
- function _domainNameAndVersion () internal pure override returns (string memory name , string memory version ) {
158
- name = "PaymentsGateway " ;
159
- version = "1 " ;
160
- }
161
-
162
- function _hashPayoutInfo (PayoutInfo[] calldata payouts ) private pure returns (bytes32 ) {
163
- bytes32 [] memory payoutsHashes = new bytes32 [](payouts.length );
164
- for (uint i = 0 ; i < payouts.length ; i++ ) {
165
- payoutsHashes[i] = keccak256 (
166
- abi.encode (PAYOUTINFO_TYPEHASH, payouts[i].clientId, payouts[i].payoutAddress, payouts[i].feeBPS)
167
- );
168
- }
169
- return keccak256 (abi.encodePacked (payoutsHashes));
170
- }
171
-
172
- function _verifyTransferStart (PayRequest calldata req , bytes calldata signature ) private view returns (bool ) {
173
- bytes32 payoutsHash = _hashPayoutInfo (req.payouts);
174
- bytes32 structHash = keccak256 (
175
- abi.encode (
176
- REQUEST_TYPEHASH,
177
- req.clientId,
178
- req.transactionId,
179
- req.tokenAddress,
180
- req.tokenAmount,
181
- req.expirationTimestamp,
182
- payoutsHash,
183
- req.forwardAddress,
184
- keccak256 (req.data)
185
- )
186
- );
187
-
188
- bytes32 digest = _hashTypedData (structHash);
189
- address recovered = digest.recover (signature);
190
- bool valid = recovered == owner () && ! processed[req.transactionId];
191
-
192
- return valid;
193
- }
194
-
195
134
/**
196
- The purpose of startTransfer is to be the entrypoint for all thirdweb pay swap / bridge
135
+ @notice
136
+ The purpose of initiateTokenPurchase is to be the entrypoint for all thirdweb pay swap / bridge
197
137
transactions. This function will allow us to standardize the logging and fee splitting across all providers.
198
138
199
139
Requirements:
200
140
1. Verify the parameters are the same parameters sent from thirdweb pay service by requiring a backend signature
201
141
2. Log transfer start allowing us to link onchain and offchain data
202
- 3. distribute the fees to all the payees (thirdweb, developer, swap provider?? )
142
+ 3. distribute the fees to all the payees (thirdweb, developer, swap provider (?) )
203
143
4. forward the user funds to the swap provider (forwardAddress)
204
144
*/
205
145
206
- function startTransfer (PayRequest calldata req , bytes calldata signature ) external payable nonReentrant {
146
+ function initiateTokenPurchase (PayRequest calldata req , bytes calldata signature ) external payable nonReentrant {
207
147
// verify amount
208
148
if (req.tokenAmount == 0 ) {
209
149
revert PaymentsGatewayInvalidAmount (req.tokenAmount);
@@ -262,20 +202,21 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
262
202
}
263
203
}
264
204
265
- emit TransferStart (req.clientId, msg .sender , req.transactionId, req.tokenAddress, req.tokenAmount);
205
+ emit TokenPurchaseInitiated (req.clientId, msg .sender , req.transactionId, req.tokenAddress, req.tokenAmount);
266
206
}
267
207
268
208
/**
269
- The purpose of endTransfer is to provide a forwarding contract call
270
- on the destination chain. For LiFi (swap provider), they can only guarantee the toAmount
209
+ @notice
210
+ The purpose of completeTokenPurchase is to provide a forwarding contract call
211
+ on the destination chain. For some swap providers, they can only guarantee the toAmount
271
212
if we use a contract call. This allows us to call the endTransfer function and forward the
272
213
funds to the end user.
273
214
274
215
Requirements:
275
216
1. Log the transfer end
276
217
2. forward the user funds
277
218
*/
278
- function endTransfer (
219
+ function completeTokenPurchase (
279
220
bytes32 clientId ,
280
221
bytes32 transactionId ,
281
222
address tokenAddress ,
@@ -299,6 +240,93 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
299
240
SafeTransferLib.safeTransferETH (receiverAddress, tokenAmount);
300
241
}
301
242
302
- emit TransferEnd (clientId, receiverAddress, transactionId, tokenAddress, tokenAmount);
243
+ emit TokenPurchaseCompleted (clientId, receiverAddress, transactionId, tokenAddress, tokenAmount);
244
+ }
245
+
246
+ /*///////////////////////////////////////////////////////////////
247
+ Internal functions
248
+ //////////////////////////////////////////////////////////////*/
249
+
250
+ function _domainNameAndVersion () internal pure override returns (string memory name , string memory version ) {
251
+ name = "PaymentsGateway " ;
252
+ version = "1 " ;
253
+ }
254
+
255
+ function _hashPayoutInfo (PayoutInfo[] calldata payouts ) private pure returns (bytes32 ) {
256
+ bytes32 [] memory payoutsHashes = new bytes32 [](payouts.length );
257
+ for (uint i = 0 ; i < payouts.length ; i++ ) {
258
+ payoutsHashes[i] = keccak256 (
259
+ abi.encode (PAYOUTINFO_TYPEHASH, payouts[i].clientId, payouts[i].payoutAddress, payouts[i].feeBPS)
260
+ );
261
+ }
262
+ return keccak256 (abi.encodePacked (payoutsHashes));
263
+ }
264
+
265
+ function _distributeFees (
266
+ address tokenAddress ,
267
+ uint256 tokenAmount ,
268
+ PayoutInfo[] calldata payouts
269
+ ) private returns (uint256 ) {
270
+ uint256 totalFeeAmount = 0 ;
271
+
272
+ for (uint32 payeeIdx = 0 ; payeeIdx < payouts.length ; payeeIdx++ ) {
273
+ uint256 feeAmount = _calculateFee (tokenAmount, payouts[payeeIdx].feeBPS);
274
+ totalFeeAmount += feeAmount;
275
+
276
+ emit FeePayout (
277
+ payouts[payeeIdx].clientId,
278
+ msg .sender ,
279
+ payouts[payeeIdx].payoutAddress,
280
+ tokenAddress,
281
+ feeAmount,
282
+ payouts[payeeIdx].feeBPS
283
+ );
284
+ if (_isTokenNative (tokenAddress)) {
285
+ SafeTransferLib.safeTransferETH (payouts[payeeIdx].payoutAddress, feeAmount);
286
+ } else {
287
+ SafeTransferLib.safeTransferFrom (tokenAddress, msg .sender , payouts[payeeIdx].payoutAddress, feeAmount);
288
+ }
289
+ }
290
+
291
+ if (totalFeeAmount > tokenAmount) {
292
+ revert PaymentsGatewayMismatchedValue (totalFeeAmount, tokenAmount);
293
+ }
294
+ return totalFeeAmount;
295
+ }
296
+
297
+ function _verifyTransferStart (PayRequest calldata req , bytes calldata signature ) private view returns (bool ) {
298
+ bytes32 payoutsHash = _hashPayoutInfo (req.payouts);
299
+ bytes32 structHash = keccak256 (
300
+ abi.encode (
301
+ REQUEST_TYPEHASH,
302
+ req.clientId,
303
+ req.transactionId,
304
+ req.tokenAddress,
305
+ req.tokenAmount,
306
+ req.expirationTimestamp,
307
+ payoutsHash,
308
+ req.forwardAddress,
309
+ keccak256 (req.data)
310
+ )
311
+ );
312
+
313
+ bytes32 digest = _hashTypedData (structHash);
314
+ address recovered = digest.recover (signature);
315
+ bool valid = recovered == owner () && ! processed[req.transactionId];
316
+
317
+ return valid;
318
+ }
319
+
320
+ function _isTokenERC20 (address tokenAddress ) private pure returns (bool ) {
321
+ return tokenAddress != NATIVE_TOKEN_ADDRESS;
322
+ }
323
+
324
+ function _isTokenNative (address tokenAddress ) private pure returns (bool ) {
325
+ return tokenAddress == NATIVE_TOKEN_ADDRESS;
326
+ }
327
+
328
+ function _calculateFee (uint256 amount , uint256 feeBPS ) private pure returns (uint256 ) {
329
+ uint256 feeAmount = (amount * feeBPS) / 10_000 ;
330
+ return feeAmount;
303
331
}
304
332
}
0 commit comments