@@ -3,9 +3,11 @@ pragma solidity ^0.8.22;
3
3
4
4
/// @author thirdweb
5
5
6
+ import { EIP712 } from "lib/solady/src/utils/EIP712.sol " ;
6
7
import { SafeTransferLib } from "lib/solady/src/utils/SafeTransferLib.sol " ;
7
8
import { ReentrancyGuard } from "lib/solady/src/utils/ReentrancyGuard.sol " ;
8
- import { Ownable } from "lib/solady/src/auth/Ownable.sol " ;
9
+ import { ECDSA } from "lib/solady/src/utils/ECDSA.sol " ;
10
+ import { OwnableRoles } from "lib/solady/src/auth/OwnableRoles.sol " ;
9
11
import { UUPSUpgradeable } from "lib/solady/src/utils/UUPSUpgradeable.sol " ;
10
12
import { Initializable } from "lib/solady/src/utils/Initializable.sol " ;
11
13
@@ -35,13 +37,34 @@ library UniversalBridgeStorage {
35
37
}
36
38
}
37
39
38
- contract UniversalBridgeV1 is Initializable , UUPSUpgradeable , Ownable , ReentrancyGuard {
40
+ contract UniversalBridgeV1 is EIP712 , Initializable , UUPSUpgradeable , OwnableRoles , ReentrancyGuard {
41
+ using ECDSA for bytes32 ;
42
+
39
43
/*///////////////////////////////////////////////////////////////
40
44
State, constants, structs
41
45
//////////////////////////////////////////////////////////////*/
42
46
43
47
address private constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE ;
44
48
uint256 private constant MAX_PROTOCOL_FEE_BPS = 300 ; // 3%
49
+ uint256 private constant _OPERATOR_ROLE = 1 << 0 ;
50
+
51
+ struct TransactionRequest {
52
+ bytes32 transactionId;
53
+ address tokenAddress;
54
+ uint256 tokenAmount;
55
+ address payable forwardAddress;
56
+ address payable spenderAddress;
57
+ uint256 expirationTimestamp;
58
+ address payable developerFeeRecipient;
59
+ uint256 developerFeeBps;
60
+ bytes callData;
61
+ bytes extraData;
62
+ }
63
+
64
+ bytes32 private constant TRANSACTION_REQUEST_TYPEHASH =
65
+ keccak256 (
66
+ "TransactionRequest(bytes32 transactionId,address tokenAddress,uint256 tokenAmount,address forwardAddress,address spenderAddress,uint256 expirationTimestamp,address developerFeeRecipient,uint256 developerFeeBps,bytes callData,bytes extraData) "
67
+ );
45
68
46
69
/*///////////////////////////////////////////////////////////////
47
70
Events
@@ -69,17 +92,22 @@ contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, Reentranc
69
92
error UniversalBridgeZeroAddress ();
70
93
error UniversalBridgePaused ();
71
94
error UniversalBridgeRestrictedAddress ();
95
+ error UniversalBridgeVerificationFailed ();
96
+ error UniversalBridgeRequestExpired (uint256 expirationTimestamp );
97
+ error UniversalBridgeTransactionAlreadyProcessed ();
72
98
73
99
constructor () {
74
100
_disableInitializers ();
75
101
}
76
102
77
103
function initialize (
78
104
address _owner ,
105
+ address _operator ,
79
106
address payable _protocolFeeRecipient ,
80
107
uint256 _protocolFeeBps
81
108
) external initializer {
82
109
_initializeOwner (_owner);
110
+ _grantRoles (_operator, _OPERATOR_ROLE);
83
111
_setProtocolFeeInfo (_protocolFeeRecipient, _protocolFeeBps);
84
112
}
85
113
@@ -136,69 +164,71 @@ contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, Reentranc
136
164
transactions. This function will allow us to standardize the logging and fee splitting across all providers.
137
165
*/
138
166
function initiateTransaction (
139
- bytes32 transactionId ,
140
- address tokenAddress ,
141
- uint256 tokenAmount ,
142
- address payable forwardAddress ,
143
- address payable spenderAddress ,
144
- address payable developerFeeRecipient ,
145
- uint256 developerFeeBps ,
146
- bytes calldata callData ,
147
- bytes calldata extraData
167
+ TransactionRequest calldata req ,
168
+ bytes calldata signature
148
169
) external payable nonReentrant onlyProxy {
170
+ // verify req
171
+ if (! _verifyTransactionReq (req, signature)) {
172
+ revert UniversalBridgeVerificationFailed ();
173
+ }
174
+ // mark the pay request as processed
175
+ _universalBridgeStorage ().processed[req.transactionId] = true ;
176
+
149
177
if (_universalBridgeStorage ().isPaused) {
150
178
revert UniversalBridgePaused ();
151
179
}
152
180
153
181
if (
154
- _universalBridgeStorage ().isRestricted[forwardAddress] ||
155
- _universalBridgeStorage ().isRestricted[tokenAddress]
182
+ _universalBridgeStorage ().isRestricted[req. forwardAddress] ||
183
+ _universalBridgeStorage ().isRestricted[req. tokenAddress]
156
184
) {
157
185
revert UniversalBridgeRestrictedAddress ();
158
186
}
159
187
160
188
// verify amount
161
- if (tokenAmount == 0 ) {
162
- revert UniversalBridgeInvalidAmount (tokenAmount);
189
+ if (req. tokenAmount == 0 ) {
190
+ revert UniversalBridgeInvalidAmount (req. tokenAmount);
163
191
}
164
192
165
- // mark the pay request as processed
166
- _universalBridgeStorage ().processed[transactionId] = true ;
167
-
168
193
uint256 sendValue = msg .value ; // includes bridge fee etc. (if any)
169
194
170
195
// distribute fees
171
- uint256 totalFeeAmount = _distributeFees (tokenAddress, tokenAmount, developerFeeRecipient, developerFeeBps);
196
+ uint256 totalFeeAmount = _distributeFees (
197
+ req.tokenAddress,
198
+ req.tokenAmount,
199
+ req.developerFeeRecipient,
200
+ req.developerFeeBps
201
+ );
172
202
173
- if (_isNativeToken (tokenAddress)) {
203
+ if (_isNativeToken (req. tokenAddress)) {
174
204
sendValue = msg .value - totalFeeAmount;
175
205
176
- if (sendValue < tokenAmount) {
177
- revert UniversalBridgeMismatchedValue (tokenAmount, sendValue);
206
+ if (sendValue < req. tokenAmount) {
207
+ revert UniversalBridgeMismatchedValue (req. tokenAmount, sendValue);
178
208
}
179
- _call (forwardAddress, sendValue, callData); // calldata empty for direct transfer
180
- } else if (callData.length == 0 ) {
209
+ _call (req. forwardAddress, sendValue, req. callData); // calldata empty for direct transfer
210
+ } else if (req. callData.length == 0 ) {
181
211
if (msg .value != 0 ) {
182
212
revert UniversalBridgeMsgValueNotZero ();
183
213
}
184
- SafeTransferLib.safeTransferFrom (tokenAddress, msg .sender , forwardAddress, tokenAmount);
214
+ SafeTransferLib.safeTransferFrom (req. tokenAddress, msg .sender , req. forwardAddress, req. tokenAmount);
185
215
} else {
186
216
// pull user funds
187
- SafeTransferLib.safeTransferFrom (tokenAddress, msg .sender , address (this ), tokenAmount);
217
+ SafeTransferLib.safeTransferFrom (req. tokenAddress, msg .sender , address (this ), req. tokenAmount);
188
218
189
219
// approve to spender address and call forward address -- both will be same in most cases
190
- SafeTransferLib.safeApprove (tokenAddress, spenderAddress, tokenAmount);
191
- _call (forwardAddress, sendValue, callData);
220
+ SafeTransferLib.safeApprove (req. tokenAddress, req. spenderAddress, req. tokenAmount);
221
+ _call (req. forwardAddress, sendValue, req. callData);
192
222
}
193
223
194
224
emit TransactionInitiated (
195
225
msg .sender ,
196
- transactionId,
197
- tokenAddress,
198
- tokenAmount,
199
- developerFeeRecipient,
200
- developerFeeBps,
201
- extraData
226
+ req. transactionId,
227
+ req. tokenAddress,
228
+ req. tokenAmount,
229
+ req. developerFeeRecipient,
230
+ req. developerFeeBps,
231
+ req. extraData
202
232
);
203
233
}
204
234
@@ -221,6 +251,43 @@ contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, Reentranc
221
251
Internal functions
222
252
//////////////////////////////////////////////////////////////*/
223
253
254
+ function _verifyTransactionReq (
255
+ TransactionRequest calldata req ,
256
+ bytes calldata signature
257
+ ) private view returns (bool ) {
258
+ if (req.expirationTimestamp < block .timestamp ) {
259
+ revert UniversalBridgeRequestExpired (req.expirationTimestamp);
260
+ }
261
+
262
+ bool processed = _universalBridgeStorage ().processed[req.transactionId];
263
+
264
+ if (processed) {
265
+ revert UniversalBridgeTransactionAlreadyProcessed ();
266
+ }
267
+
268
+ bytes32 structHash = keccak256 (
269
+ abi.encode (
270
+ TRANSACTION_REQUEST_TYPEHASH,
271
+ req.transactionId,
272
+ req.tokenAddress,
273
+ req.tokenAmount,
274
+ req.forwardAddress,
275
+ req.spenderAddress,
276
+ req.expirationTimestamp,
277
+ req.developerFeeRecipient,
278
+ req.developerFeeBps,
279
+ keccak256 (req.callData),
280
+ keccak256 (req.extraData)
281
+ )
282
+ );
283
+
284
+ bytes32 digest = _hashTypedData (structHash);
285
+ address recovered = digest.recover (signature);
286
+ bool valid = hasAllRoles (recovered, _OPERATOR_ROLE);
287
+
288
+ return valid;
289
+ }
290
+
224
291
function _distributeFees (
225
292
address tokenAddress ,
226
293
uint256 tokenAmount ,
@@ -255,6 +322,11 @@ contract UniversalBridgeV1 is Initializable, UUPSUpgradeable, Ownable, Reentranc
255
322
return totalFeeAmount;
256
323
}
257
324
325
+ function _domainNameAndVersion () internal pure override returns (string memory name , string memory version ) {
326
+ name = "UniversalBridgeV1 " ;
327
+ version = "1 " ;
328
+ }
329
+
258
330
function _setProtocolFeeInfo (address payable feeRecipient , uint256 feeBps ) internal {
259
331
if (feeRecipient == address (0 )) {
260
332
revert UniversalBridgeZeroAddress ();
0 commit comments