Skip to content

Commit f2db1bf

Browse files
authored
Merge pull request #5 from thirdweb-dev/yash/rename
Rename functions, clean up
2 parents 8bf4dc6 + 165fd7f commit f2db1bf

File tree

4 files changed

+222
-176
lines changed

4 files changed

+222
-176
lines changed

src/PaymentsGateway.sol

Lines changed: 172 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
// SPDX-License-Identifier: UNLICENSED
1+
// SPDX-License-Identifier: Apache-2.0
22
pragma solidity ^0.8.22;
33

4+
/// @author thirdweb
5+
46
import "@openzeppelin/contracts/access/Ownable.sol";
57
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
68
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
@@ -9,48 +11,80 @@ import { EIP712 } from "./utils/EIP712.sol";
911
import { SafeTransferLib } from "./lib/SafeTransferLib.sol";
1012
import { ECDSA } from "./lib/ECDSA.sol";
1113

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-
2314
contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
2415
using ECDSA for bytes32;
2516

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+
//////////////////////////////////////////////////////////////*/
3171

32-
event TransferStart(
72+
event TokenPurchaseInitiated(
3373
bytes32 indexed clientId,
3474
address indexed sender,
3575
bytes32 transactionId,
3676
address tokenAddress,
3777
uint256 tokenAmount
3878
);
3979

40-
event TransferEnd(
80+
event TokenPurchaseCompleted(
4181
bytes32 indexed clientId,
4282
address indexed receiver,
4383
bytes32 transactionId,
4484
address tokenAddress,
4585
uint256 tokenAmount
4686
);
4787

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-
*/
5488
event FeePayout(
5589
bytes32 indexed clientId,
5690
address indexed sender,
@@ -60,39 +94,27 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
6094
uint256 feeBPS
6195
);
6296

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+
//////////////////////////////////////////////////////////////*/
80100

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);
89106

90-
/// @dev Mapping from pay request UID => whether the pay request is processed.
91-
mapping(bytes32 => bool) private processed;
107+
/*///////////////////////////////////////////////////////////////
108+
Constructor
109+
//////////////////////////////////////////////////////////////*/
92110

93111
constructor(address contractOwner) Ownable(contractOwner) {}
94112

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
96118
function withdrawTo(
97119
address tokenAddress,
98120
uint256 tokenAmount,
@@ -109,101 +131,19 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
109131
withdrawTo(tokenAddress, tokenAmount, payable(msg.sender));
110132
}
111133

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-
195134
/**
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
197137
transactions. This function will allow us to standardize the logging and fee splitting across all providers.
198138
199139
Requirements:
200140
1. Verify the parameters are the same parameters sent from thirdweb pay service by requiring a backend signature
201141
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 (?))
203143
4. forward the user funds to the swap provider (forwardAddress)
204144
*/
205145

206-
function startTransfer(PayRequest calldata req, bytes calldata signature) external payable nonReentrant {
146+
function initiateTokenPurchase(PayRequest calldata req, bytes calldata signature) external payable nonReentrant {
207147
// verify amount
208148
if (req.tokenAmount == 0) {
209149
revert PaymentsGatewayInvalidAmount(req.tokenAmount);
@@ -262,20 +202,21 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
262202
}
263203
}
264204

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);
266206
}
267207

268208
/**
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
271212
if we use a contract call. This allows us to call the endTransfer function and forward the
272213
funds to the end user.
273214
274215
Requirements:
275216
1. Log the transfer end
276217
2. forward the user funds
277218
*/
278-
function endTransfer(
219+
function completeTokenPurchase(
279220
bytes32 clientId,
280221
bytes32 transactionId,
281222
address tokenAddress,
@@ -299,6 +240,93 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
299240
SafeTransferLib.safeTransferETH(receiverAddress, tokenAmount);
300241
}
301242

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;
303331
}
304332
}

0 commit comments

Comments
 (0)