2
2
pragma solidity ^ 0.8.22 ;
3
3
4
4
import "@openzeppelin/contracts/access/Ownable.sol " ;
5
- import "@openzeppelin/contracts/utils/Context.sol " ;
6
5
import "@openzeppelin/contracts/token/ERC20/IERC20.sol " ;
7
6
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol " ;
7
+ import { EIP712 } from "./utils/EIP712.sol " ;
8
+
8
9
import { SafeTransferLib } from "./lib/SafeTransferLib.sol " ;
10
+ import { ECDSA } from "./lib/ECDSA.sol " ;
9
11
10
12
/**
11
13
Requirements
@@ -18,13 +20,14 @@ import { SafeTransferLib } from "./lib/SafeTransferLib.sol";
18
20
- add operator role automating withdrawals
19
21
*/
20
22
21
- contract PaymentsGateway is Ownable , ReentrancyGuard {
22
- error PaymentsGatewayInvalidOperator ( address operator ) ;
23
- error PaymentsGatewayNotOwnerOrOperator ( address caller );
23
+ contract PaymentsGateway is EIP712 , Ownable , ReentrancyGuard {
24
+ using ECDSA for bytes32 ;
25
+
24
26
error PaymentsGatewayMismatchedValue (uint256 expected , uint256 actual );
25
27
error PaymentsGatewayInvalidAmount (uint256 amount );
26
28
error PaymentsGatewayVerificationFailed ();
27
29
error PaymentsGatewayFailedToForward ();
30
+ error PaymentsGatewayRequestExpired (uint256 expirationTimestamp );
28
31
29
32
event TransferStart (
30
33
bytes32 indexed clientId ,
@@ -64,52 +67,45 @@ contract PaymentsGateway is Ownable, ReentrancyGuard {
64
67
address payable payoutAddress;
65
68
uint256 feeBPS;
66
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
+ }
67
80
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
+ );
68
87
address private constant THIRDWEB_CLIENT_ID = 0x0000000000000000000000000000000000000000 ;
69
88
address private constant NATIVE_TOKEN_ADDRESS = 0x0000000000000000000000000000000000000000 ;
70
- address private _operator;
71
89
72
- constructor (address contractOwner , address initialOperator ) Ownable (contractOwner) {
73
- if (initialOperator == address (0 )) {
74
- revert PaymentsGatewayInvalidOperator (initialOperator);
75
- }
76
- _operator = initialOperator;
77
- emit OperatorChanged (address (0 ), initialOperator);
78
- }
90
+ /// @dev Mapping from pay request UID => whether the pay request is processed.
91
+ mapping (bytes32 => bool ) private processed;
79
92
80
- modifier onlyOwnerOrOperator () {
81
- if (msg .sender != owner () && msg .sender != _operator) {
82
- revert PaymentsGatewayNotOwnerOrOperator (msg .sender );
83
- }
84
- _;
85
- }
86
-
87
- function setOperator (address newOperator ) public onlyOwnerOrOperator {
88
- if (newOperator == address (0 )) {
89
- revert PaymentsGatewayInvalidOperator (newOperator);
90
- }
91
- emit OperatorChanged (_operator, newOperator);
92
- _operator = newOperator;
93
- }
94
-
95
- function getOperator () public view returns (address ) {
96
- return _operator;
97
- }
93
+ constructor (address contractOwner ) Ownable (contractOwner) {}
98
94
99
95
/* some bridges may refund need a way to get funds back to user */
100
96
function withdrawTo (
101
97
address tokenAddress ,
102
98
uint256 tokenAmount ,
103
99
address payable receiver
104
- ) public onlyOwnerOrOperator nonReentrant {
100
+ ) public onlyOwner nonReentrant {
105
101
if (_isTokenERC20 (tokenAddress)) {
106
102
SafeTransferLib.safeTransferFrom (tokenAddress, address (this ), receiver, tokenAmount);
107
103
} else {
108
104
SafeTransferLib.safeTransferETH (receiver, tokenAmount);
109
105
}
110
106
}
111
107
112
- function withdraw (address tokenAddress , uint256 tokenAmount ) external onlyOwnerOrOperator nonReentrant {
108
+ function withdraw (address tokenAddress , uint256 tokenAmount ) external onlyOwner nonReentrant {
113
109
withdrawTo (tokenAddress, tokenAmount, payable (msg .sender ));
114
110
}
115
111
@@ -158,61 +154,42 @@ contract PaymentsGateway is Ownable, ReentrancyGuard {
158
154
return totalFeeAmount;
159
155
}
160
156
157
+ function _domainNameAndVersion () internal pure override returns (string memory name , string memory version ) {
158
+ name = "PaymentsGateway " ;
159
+ version = "1 " ;
160
+ }
161
+
161
162
function _hashPayoutInfo (PayoutInfo[] calldata payouts ) private pure returns (bytes32 ) {
162
- bytes32 payoutHash = keccak256 ( abi.encodePacked ( " PayoutInfo " ) );
163
- for (uint256 i = 0 ; i < payouts.length ; ++ i ) {
164
- payoutHash = keccak256 (
165
- abi.encodePacked (payoutHash , payouts[i].clientId, payouts[i].payoutAddress, payouts[i].feeBPS)
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)
166
167
);
167
168
}
168
- return payoutHash ;
169
+ return keccak256 ( abi.encodePacked (payoutsHashes)) ;
169
170
}
170
171
171
- function _verifyTransferStart (
172
- bytes32 clientId ,
173
- bytes32 transactionId ,
174
- address tokenAddress ,
175
- uint256 tokenAmount ,
176
- PayoutInfo[] calldata payouts ,
177
- address payable forwardAddress ,
178
- bytes calldata data ,
179
- bytes calldata signature
180
- ) private returns (bool ) {
181
- bytes32 payoutsHash = _hashPayoutInfo (payouts);
182
- bytes32 hash = keccak256 (
183
- abi.encodePacked (clientId, transactionId, tokenAddress, tokenAmount, payoutsHash, forwardAddress, data)
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
+ )
184
186
);
185
187
186
- bytes32 ethSignedMsgHash = keccak256 (abi.encodePacked ("\x19Ethereum Signed Message:\n32 " , hash));
187
-
188
- (address recovered , bool valid ) = _recoverSigner (ethSignedMsgHash, signature);
189
-
190
- return valid && recovered == _operator;
191
- }
192
-
193
- function _recoverSigner (bytes32 ethSignedMsgHash , bytes memory signature ) public pure returns (address , bool ) {
194
- bytes32 r;
195
- bytes32 s;
196
- uint8 v;
188
+ bytes32 digest = _hashTypedData (structHash);
189
+ address recovered = digest.recover (signature);
190
+ bool valid = recovered == owner () && ! processed[req.transactionId];
197
191
198
- if (signature.length != 65 ) {
199
- return (address (0 ), false );
200
- }
201
-
202
- assembly {
203
- r := mload (add (signature, 0x20 ))
204
- s := mload (add (signature, 0x40 ))
205
- v := byte (0 , mload (add (signature, 0x60 )))
206
- }
207
-
208
- if (v < 27 ) {
209
- v += 27 ;
210
- }
211
-
212
- address recovered = ecrecover (ethSignedMsgHash, v, r, s);
213
- bool valid = (recovered != address (0 ));
214
-
215
- return (recovered, valid);
192
+ return valid;
216
193
}
217
194
218
195
/**
@@ -225,64 +202,53 @@ contract PaymentsGateway is Ownable, ReentrancyGuard {
225
202
3. distribute the fees to all the payees (thirdweb, developer, swap provider??)
226
203
4. forward the user funds to the swap provider (forwardAddress)
227
204
*/
228
- function startTransfer (
229
- bytes32 clientId ,
230
- bytes32 transactionId ,
231
- address tokenAddress ,
232
- uint256 tokenAmount ,
233
- PayoutInfo[] calldata payouts ,
234
- address payable forwardAddress ,
235
- bytes calldata data ,
236
- bytes calldata signature
237
- ) external payable nonReentrant {
205
+
206
+ function startTransfer (PayRequest calldata req , bytes calldata signature ) external payable nonReentrant {
238
207
// verify amount
239
- if (tokenAmount == 0 ) {
240
- revert PaymentsGatewayInvalidAmount (tokenAmount);
208
+ if (req.tokenAmount == 0 ) {
209
+ revert PaymentsGatewayInvalidAmount (req.tokenAmount);
210
+ }
211
+
212
+ // verify expiration timestamp
213
+ if (req.expirationTimestamp < block .timestamp ) {
214
+ revert PaymentsGatewayRequestExpired (req.expirationTimestamp);
241
215
}
242
216
243
217
// verify data
244
- if (
245
- ! _verifyTransferStart (
246
- clientId,
247
- transactionId,
248
- tokenAddress,
249
- tokenAmount,
250
- payouts,
251
- forwardAddress,
252
- data,
253
- signature
254
- )
255
- ) {
218
+ if (! _verifyTransferStart (req, signature)) {
256
219
revert PaymentsGatewayVerificationFailed ();
257
220
}
258
221
259
- if (_isTokenNative (tokenAddress)) {
260
- if (msg .value < tokenAmount) {
261
- revert PaymentsGatewayMismatchedValue (tokenAmount, msg .value );
222
+ if (_isTokenNative (req. tokenAddress)) {
223
+ if (msg .value < req. tokenAmount) {
224
+ revert PaymentsGatewayMismatchedValue (req. tokenAmount, msg .value );
262
225
}
263
226
}
264
227
228
+ // mark the pay request as processed
229
+ processed[req.transactionId] = true ;
230
+
265
231
// distribute fees
266
- uint256 totalFeeAmount = _distributeFees (tokenAddress, tokenAmount, payouts);
232
+ uint256 totalFeeAmount = _distributeFees (req. tokenAddress, req. tokenAmount, req. payouts);
267
233
268
234
// determine native value to send
269
235
uint256 sendValue = msg .value ; // includes bridge fee etc. (if any)
270
- if (_isTokenNative (tokenAddress)) {
236
+ if (_isTokenNative (req. tokenAddress)) {
271
237
sendValue = msg .value - totalFeeAmount;
272
238
273
- if (sendValue < tokenAmount) {
274
- revert PaymentsGatewayMismatchedValue (sendValue, tokenAmount);
239
+ if (sendValue < req. tokenAmount) {
240
+ revert PaymentsGatewayMismatchedValue (sendValue, req. tokenAmount);
275
241
}
276
242
}
277
243
278
- if (_isTokenERC20 (tokenAddress)) {
244
+ if (_isTokenERC20 (req. tokenAddress)) {
279
245
// pull user funds
280
- SafeTransferLib.safeTransferFrom (tokenAddress, msg .sender , address (this ), tokenAmount);
281
- SafeTransferLib.safeApprove (tokenAddress, forwardAddress, tokenAmount);
246
+ SafeTransferLib.safeTransferFrom (req. tokenAddress, msg .sender , address (this ), req. tokenAmount);
247
+ SafeTransferLib.safeApprove (req. tokenAddress, req. forwardAddress, req. tokenAmount);
282
248
}
283
249
284
250
{
285
- (bool success , bytes memory response ) = forwardAddress.call { value: sendValue }(data);
251
+ (bool success , bytes memory response ) = req. forwardAddress.call { value: sendValue }(req. data);
286
252
if (! success) {
287
253
// If there is return data, the delegate call reverted with a reason or a custom error, which we bubble up.
288
254
if (response.length > 0 ) {
@@ -296,7 +262,7 @@ contract PaymentsGateway is Ownable, ReentrancyGuard {
296
262
}
297
263
}
298
264
299
- emit TransferStart (clientId, msg .sender , transactionId, tokenAddress, tokenAmount);
265
+ emit TransferStart (req. clientId, msg .sender , req. transactionId, req. tokenAddress, req. tokenAmount);
300
266
}
301
267
302
268
/**
0 commit comments