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 " ;
@@ -12,21 +14,70 @@ import { ECDSA } from "./lib/ECDSA.sol";
12
14
contract PaymentsGateway is EIP712 , Ownable , ReentrancyGuard {
13
15
using ECDSA for bytes32 ;
14
16
15
- error PaymentsGatewayMismatchedValue (uint256 expected , uint256 actual );
16
- error PaymentsGatewayInvalidAmount (uint256 amount );
17
- error PaymentsGatewayVerificationFailed ();
18
- error PaymentsGatewayFailedToForward ();
19
- 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;
20
31
21
- event TransferStart (
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
+ //////////////////////////////////////////////////////////////*/
71
+
72
+ event TokenPurchaseInitiated (
22
73
bytes32 indexed clientId ,
23
74
address indexed sender ,
24
75
bytes32 transactionId ,
25
76
address tokenAddress ,
26
77
uint256 tokenAmount
27
78
);
28
79
29
- event TransferEnd (
80
+ event TokenPurchaseCompleted (
30
81
bytes32 indexed clientId ,
31
82
address indexed receiver ,
32
83
bytes32 transactionId ,
@@ -43,38 +94,27 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
43
94
uint256 feeBPS
44
95
);
45
96
46
- event OperatorChanged (address indexed previousOperator , address indexed newOperator );
97
+ /*///////////////////////////////////////////////////////////////
98
+ Errors
99
+ //////////////////////////////////////////////////////////////*/
47
100
48
- struct PayoutInfo {
49
- bytes32 clientId;
50
- address payable payoutAddress;
51
- uint256 feeBPS;
52
- }
53
- struct PayRequest {
54
- bytes32 clientId;
55
- bytes32 transactionId;
56
- address tokenAddress;
57
- uint256 tokenAmount;
58
- uint256 expirationTimestamp;
59
- PayoutInfo[] payouts;
60
- address payable forwardAddress;
61
- bytes data;
62
- }
63
-
64
- bytes32 private constant PAYOUTINFO_TYPEHASH =
65
- keccak256 ("PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS) " );
66
- bytes32 private constant REQUEST_TYPEHASH =
67
- keccak256 (
68
- "PayRequest(bytes32 clientId,bytes32 transactionId,address tokenAddress,uint256 tokenAmount,uint256 expirationTimestamp,PayoutInfo[] payouts,address forwardAddress,bytes data)PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS) "
69
- );
70
- 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 );
71
106
72
- /// @dev Mapping from pay request UID => whether the pay request is processed.
73
- mapping (bytes32 => bool ) private processed;
107
+ /*///////////////////////////////////////////////////////////////
108
+ Constructor
109
+ //////////////////////////////////////////////////////////////*/
74
110
75
111
constructor (address contractOwner ) Ownable (contractOwner) {}
76
112
77
- /* 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
78
118
function withdrawTo (
79
119
address tokenAddress ,
80
120
uint256 tokenAmount ,
@@ -91,101 +131,19 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
91
131
withdrawTo (tokenAddress, tokenAmount, payable (msg .sender ));
92
132
}
93
133
94
- function _isTokenERC20 (address tokenAddress ) private pure returns (bool ) {
95
- return tokenAddress != NATIVE_TOKEN_ADDRESS;
96
- }
97
-
98
- function _isTokenNative (address tokenAddress ) private pure returns (bool ) {
99
- return tokenAddress == NATIVE_TOKEN_ADDRESS;
100
- }
101
-
102
- function _calculateFee (uint256 amount , uint256 feeBPS ) private pure returns (uint256 ) {
103
- uint256 feeAmount = (amount * feeBPS) / 10_000 ;
104
- return feeAmount;
105
- }
106
-
107
- function _distributeFees (
108
- address tokenAddress ,
109
- uint256 tokenAmount ,
110
- PayoutInfo[] calldata payouts
111
- ) private returns (uint256 ) {
112
- uint256 totalFeeAmount = 0 ;
113
-
114
- for (uint32 payeeIdx = 0 ; payeeIdx < payouts.length ; payeeIdx++ ) {
115
- uint256 feeAmount = _calculateFee (tokenAmount, payouts[payeeIdx].feeBPS);
116
- totalFeeAmount += feeAmount;
117
-
118
- emit FeePayout (
119
- payouts[payeeIdx].clientId,
120
- msg .sender ,
121
- payouts[payeeIdx].payoutAddress,
122
- tokenAddress,
123
- feeAmount,
124
- payouts[payeeIdx].feeBPS
125
- );
126
- if (_isTokenNative (tokenAddress)) {
127
- SafeTransferLib.safeTransferETH (payouts[payeeIdx].payoutAddress, feeAmount);
128
- } else {
129
- SafeTransferLib.safeTransferFrom (tokenAddress, msg .sender , payouts[payeeIdx].payoutAddress, feeAmount);
130
- }
131
- }
132
-
133
- if (totalFeeAmount > tokenAmount) {
134
- revert PaymentsGatewayMismatchedValue (totalFeeAmount, tokenAmount);
135
- }
136
- return totalFeeAmount;
137
- }
138
-
139
- function _domainNameAndVersion () internal pure override returns (string memory name , string memory version ) {
140
- name = "PaymentsGateway " ;
141
- version = "1 " ;
142
- }
143
-
144
- function _hashPayoutInfo (PayoutInfo[] calldata payouts ) private pure returns (bytes32 ) {
145
- bytes32 [] memory payoutsHashes = new bytes32 [](payouts.length );
146
- for (uint i = 0 ; i < payouts.length ; i++ ) {
147
- payoutsHashes[i] = keccak256 (
148
- abi.encode (PAYOUTINFO_TYPEHASH, payouts[i].clientId, payouts[i].payoutAddress, payouts[i].feeBPS)
149
- );
150
- }
151
- return keccak256 (abi.encodePacked (payoutsHashes));
152
- }
153
-
154
- function _verifyTransferStart (PayRequest calldata req , bytes calldata signature ) private view returns (bool ) {
155
- bytes32 payoutsHash = _hashPayoutInfo (req.payouts);
156
- bytes32 structHash = keccak256 (
157
- abi.encode (
158
- REQUEST_TYPEHASH,
159
- req.clientId,
160
- req.transactionId,
161
- req.tokenAddress,
162
- req.tokenAmount,
163
- req.expirationTimestamp,
164
- payoutsHash,
165
- req.forwardAddress,
166
- keccak256 (req.data)
167
- )
168
- );
169
-
170
- bytes32 digest = _hashTypedData (structHash);
171
- address recovered = digest.recover (signature);
172
- bool valid = recovered == owner () && ! processed[req.transactionId];
173
-
174
- return valid;
175
- }
176
-
177
134
/**
178
- The purpose of buyToken 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
179
137
transactions. This function will allow us to standardize the logging and fee splitting across all providers.
180
138
181
139
Requirements:
182
140
1. Verify the parameters are the same parameters sent from thirdweb pay service by requiring a backend signature
183
141
2. Log transfer start allowing us to link onchain and offchain data
184
- 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 (?) )
185
143
4. forward the user funds to the swap provider (forwardAddress)
186
144
*/
187
145
188
- function buyToken (PayRequest calldata req , bytes calldata signature ) external payable nonReentrant {
146
+ function initiateTokenPurchase (PayRequest calldata req , bytes calldata signature ) external payable nonReentrant {
189
147
// verify amount
190
148
if (req.tokenAmount == 0 ) {
191
149
revert PaymentsGatewayInvalidAmount (req.tokenAmount);
@@ -244,11 +202,12 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
244
202
}
245
203
}
246
204
247
- 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);
248
206
}
249
207
250
208
/**
251
- The purpose of endTransfer is to provide a forwarding contract call
209
+ @notice
210
+ The purpose of completeTokenPurchase is to provide a forwarding contract call
252
211
on the destination chain. For LiFi (swap provider), they can only guarantee the toAmount
253
212
if we use a contract call. This allows us to call the endTransfer function and forward the
254
213
funds to the end user.
@@ -257,7 +216,7 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
257
216
1. Log the transfer end
258
217
2. forward the user funds
259
218
*/
260
- function endTransfer (
219
+ function completeTokenPurchase (
261
220
bytes32 clientId ,
262
221
bytes32 transactionId ,
263
222
address tokenAddress ,
@@ -281,6 +240,93 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
281
240
SafeTransferLib.safeTransferETH (receiverAddress, tokenAmount);
282
241
}
283
242
284
- 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;
285
331
}
286
332
}
0 commit comments