Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename functions, clean up #5

Merged
merged 5 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 172 additions & 144 deletions src/PaymentsGateway.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.22;

/// @author thirdweb

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
Expand All @@ -9,48 +11,80 @@ import { EIP712 } from "./utils/EIP712.sol";
import { SafeTransferLib } from "./lib/SafeTransferLib.sol";
import { ECDSA } from "./lib/ECDSA.sol";

/**
Requirements
- easily change fee / payout structure per transaction
- easily change provider per transaction

TODO:
- add receiver function
- add thirdweb signer for tamperproofing
- add operator role automating withdrawals
*/

contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
using ECDSA for bytes32;

error PaymentsGatewayMismatchedValue(uint256 expected, uint256 actual);
error PaymentsGatewayInvalidAmount(uint256 amount);
error PaymentsGatewayVerificationFailed();
error PaymentsGatewayFailedToForward();
error PaymentsGatewayRequestExpired(uint256 expirationTimestamp);
/*///////////////////////////////////////////////////////////////
State, constants, structs
//////////////////////////////////////////////////////////////*/

bytes32 private constant PAYOUTINFO_TYPEHASH =
keccak256("PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS)");
bytes32 private constant REQUEST_TYPEHASH =
keccak256(
"PayRequest(bytes32 clientId,bytes32 transactionId,address tokenAddress,uint256 tokenAmount,uint256 expirationTimestamp,PayoutInfo[] payouts,address forwardAddress,bytes data)PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS)"
);
address private constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

/// @dev Mapping from pay request UID => whether the pay request is processed.
mapping(bytes32 => bool) private processed;

/**
* @notice Info of fee payout recipients.
*
* @param clientId ClientId of fee recipient
* @param payoutAddress Recipient address
* @param feeBPS The fee basis points to be charged. Max = 10000 (10000 = 100%, 1000 = 10%)
*/
struct PayoutInfo {
bytes32 clientId;
address payable payoutAddress;
uint256 feeBPS;
}

/**
* @notice The body of a request to purchase tokens.
*
* @param clientId Thirdweb clientId for logging attribution data
* @param transactionId Acts as a uid and a key to lookup associated swap provider
* @param tokenAddress Address of the currency used for purchase
* @param tokenAmount Currency amount being sent
* @param expirationTimestamp The unix timestamp at which the request expires
* @param payouts Array of Payout struct - containing fee recipients' info
* @param forwardAddress Address of swap provider contract
* @param data Calldata for swap provider
*/
struct PayRequest {
bytes32 clientId;
bytes32 transactionId;
address tokenAddress;
uint256 tokenAmount;
uint256 expirationTimestamp;
PayoutInfo[] payouts;
address payable forwardAddress;
bytes data;
}

/*///////////////////////////////////////////////////////////////
Events
//////////////////////////////////////////////////////////////*/

event TransferStart(
event TokenPurchaseInitiated(
bytes32 indexed clientId,
address indexed sender,
bytes32 transactionId,
address tokenAddress,
uint256 tokenAmount
);

event TransferEnd(
event TokenPurchaseCompleted(
bytes32 indexed clientId,
address indexed receiver,
bytes32 transactionId,
address tokenAddress,
uint256 tokenAmount
);

/**
Note: not sure if this is completely necessary
estimate the gas on this and remove
we could always combine transferFrom logs w/ this transaction
where from=Address(this) => to != provider
*/
event FeePayout(
bytes32 indexed clientId,
address indexed sender,
Expand All @@ -60,39 +94,27 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
uint256 feeBPS
);

event OperatorChanged(address indexed previousOperator, address indexed newOperator);

struct PayoutInfo {
bytes32 clientId;
address payable payoutAddress;
uint256 feeBPS;
}
struct PayRequest {
bytes32 clientId;
bytes32 transactionId;
address tokenAddress;
uint256 tokenAmount;
uint256 expirationTimestamp;
PayoutInfo[] payouts;
address payable forwardAddress;
bytes data;
}
/*///////////////////////////////////////////////////////////////
Errors
//////////////////////////////////////////////////////////////*/

bytes32 private constant PAYOUTINFO_TYPEHASH =
keccak256("PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS)");
bytes32 private constant REQUEST_TYPEHASH =
keccak256(
"PayRequest(bytes32 clientId,bytes32 transactionId,address tokenAddress,uint256 tokenAmount,uint256 expirationTimestamp,PayoutInfo[] payouts,address forwardAddress,bytes data)PayoutInfo(bytes32 clientId,address payoutAddress,uint256 feeBPS)"
);
address private constant THIRDWEB_CLIENT_ID = 0x0000000000000000000000000000000000000000;
address private constant NATIVE_TOKEN_ADDRESS = 0x0000000000000000000000000000000000000000;
error PaymentsGatewayMismatchedValue(uint256 expected, uint256 actual);
error PaymentsGatewayInvalidAmount(uint256 amount);
error PaymentsGatewayVerificationFailed();
error PaymentsGatewayFailedToForward();
error PaymentsGatewayRequestExpired(uint256 expirationTimestamp);

/// @dev Mapping from pay request UID => whether the pay request is processed.
mapping(bytes32 => bool) private processed;
/*///////////////////////////////////////////////////////////////
Constructor
//////////////////////////////////////////////////////////////*/

constructor(address contractOwner) Ownable(contractOwner) {}

/* some bridges may refund need a way to get funds back to user */
/*///////////////////////////////////////////////////////////////
External / public functions
//////////////////////////////////////////////////////////////*/

/// @notice some bridges may refund need a way to get funds back to user
function withdrawTo(
address tokenAddress,
uint256 tokenAmount,
Expand All @@ -109,101 +131,19 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
withdrawTo(tokenAddress, tokenAmount, payable(msg.sender));
}

function _isTokenERC20(address tokenAddress) private pure returns (bool) {
return tokenAddress != NATIVE_TOKEN_ADDRESS;
}

function _isTokenNative(address tokenAddress) private pure returns (bool) {
return tokenAddress == NATIVE_TOKEN_ADDRESS;
}

function _calculateFee(uint256 amount, uint256 feeBPS) private pure returns (uint256) {
uint256 feeAmount = (amount * feeBPS) / 10_000;
return feeAmount;
}

function _distributeFees(
address tokenAddress,
uint256 tokenAmount,
PayoutInfo[] calldata payouts
) private returns (uint256) {
uint256 totalFeeAmount = 0;

for (uint32 payeeIdx = 0; payeeIdx < payouts.length; payeeIdx++) {
uint256 feeAmount = _calculateFee(tokenAmount, payouts[payeeIdx].feeBPS);
totalFeeAmount += feeAmount;

emit FeePayout(
payouts[payeeIdx].clientId,
msg.sender,
payouts[payeeIdx].payoutAddress,
tokenAddress,
feeAmount,
payouts[payeeIdx].feeBPS
);
if (_isTokenNative(tokenAddress)) {
SafeTransferLib.safeTransferETH(payouts[payeeIdx].payoutAddress, feeAmount);
} else {
SafeTransferLib.safeTransferFrom(tokenAddress, msg.sender, payouts[payeeIdx].payoutAddress, feeAmount);
}
}

if (totalFeeAmount > tokenAmount) {
revert PaymentsGatewayMismatchedValue(totalFeeAmount, tokenAmount);
}
return totalFeeAmount;
}

function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) {
name = "PaymentsGateway";
version = "1";
}

function _hashPayoutInfo(PayoutInfo[] calldata payouts) private pure returns (bytes32) {
bytes32[] memory payoutsHashes = new bytes32[](payouts.length);
for (uint i = 0; i < payouts.length; i++) {
payoutsHashes[i] = keccak256(
abi.encode(PAYOUTINFO_TYPEHASH, payouts[i].clientId, payouts[i].payoutAddress, payouts[i].feeBPS)
);
}
return keccak256(abi.encodePacked(payoutsHashes));
}

function _verifyTransferStart(PayRequest calldata req, bytes calldata signature) private view returns (bool) {
bytes32 payoutsHash = _hashPayoutInfo(req.payouts);
bytes32 structHash = keccak256(
abi.encode(
REQUEST_TYPEHASH,
req.clientId,
req.transactionId,
req.tokenAddress,
req.tokenAmount,
req.expirationTimestamp,
payoutsHash,
req.forwardAddress,
keccak256(req.data)
)
);

bytes32 digest = _hashTypedData(structHash);
address recovered = digest.recover(signature);
bool valid = recovered == owner() && !processed[req.transactionId];

return valid;
}

/**
The purpose of startTransfer is to be the entrypoint for all thirdweb pay swap / bridge
@notice
The purpose of initiateTokenPurchase is to be the entrypoint for all thirdweb pay swap / bridge
transactions. This function will allow us to standardize the logging and fee splitting across all providers.

Requirements:
1. Verify the parameters are the same parameters sent from thirdweb pay service by requiring a backend signature
2. Log transfer start allowing us to link onchain and offchain data
3. distribute the fees to all the payees (thirdweb, developer, swap provider??)
3. distribute the fees to all the payees (thirdweb, developer, swap provider (?))
4. forward the user funds to the swap provider (forwardAddress)
*/

function startTransfer(PayRequest calldata req, bytes calldata signature) external payable nonReentrant {
function initiateTokenPurchase(PayRequest calldata req, bytes calldata signature) external payable nonReentrant {
// verify amount
if (req.tokenAmount == 0) {
revert PaymentsGatewayInvalidAmount(req.tokenAmount);
Expand Down Expand Up @@ -262,20 +202,21 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
}
}

emit TransferStart(req.clientId, msg.sender, req.transactionId, req.tokenAddress, req.tokenAmount);
emit TokenPurchaseInitiated(req.clientId, msg.sender, req.transactionId, req.tokenAddress, req.tokenAmount);
}

/**
The purpose of endTransfer is to provide a forwarding contract call
on the destination chain. For LiFi (swap provider), they can only guarantee the toAmount
@notice
The purpose of completeTokenPurchase is to provide a forwarding contract call
on the destination chain. For some swap providers, they can only guarantee the toAmount
if we use a contract call. This allows us to call the endTransfer function and forward the
funds to the end user.

Requirements:
1. Log the transfer end
2. forward the user funds
*/
function endTransfer(
function completeTokenPurchase(
bytes32 clientId,
bytes32 transactionId,
address tokenAddress,
Expand All @@ -299,6 +240,93 @@ contract PaymentsGateway is EIP712, Ownable, ReentrancyGuard {
SafeTransferLib.safeTransferETH(receiverAddress, tokenAmount);
}

emit TransferEnd(clientId, receiverAddress, transactionId, tokenAddress, tokenAmount);
emit TokenPurchaseCompleted(clientId, receiverAddress, transactionId, tokenAddress, tokenAmount);
}

/*///////////////////////////////////////////////////////////////
Internal functions
//////////////////////////////////////////////////////////////*/

function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) {
name = "PaymentsGateway";
version = "1";
}

function _hashPayoutInfo(PayoutInfo[] calldata payouts) private pure returns (bytes32) {
bytes32[] memory payoutsHashes = new bytes32[](payouts.length);
for (uint i = 0; i < payouts.length; i++) {
payoutsHashes[i] = keccak256(
abi.encode(PAYOUTINFO_TYPEHASH, payouts[i].clientId, payouts[i].payoutAddress, payouts[i].feeBPS)
);
}
return keccak256(abi.encodePacked(payoutsHashes));
}

function _distributeFees(
address tokenAddress,
uint256 tokenAmount,
PayoutInfo[] calldata payouts
) private returns (uint256) {
uint256 totalFeeAmount = 0;

for (uint32 payeeIdx = 0; payeeIdx < payouts.length; payeeIdx++) {
uint256 feeAmount = _calculateFee(tokenAmount, payouts[payeeIdx].feeBPS);
totalFeeAmount += feeAmount;

emit FeePayout(
payouts[payeeIdx].clientId,
msg.sender,
payouts[payeeIdx].payoutAddress,
tokenAddress,
feeAmount,
payouts[payeeIdx].feeBPS
);
if (_isTokenNative(tokenAddress)) {
SafeTransferLib.safeTransferETH(payouts[payeeIdx].payoutAddress, feeAmount);
} else {
SafeTransferLib.safeTransferFrom(tokenAddress, msg.sender, payouts[payeeIdx].payoutAddress, feeAmount);
}
}

if (totalFeeAmount > tokenAmount) {
revert PaymentsGatewayMismatchedValue(totalFeeAmount, tokenAmount);
}
return totalFeeAmount;
}

function _verifyTransferStart(PayRequest calldata req, bytes calldata signature) private view returns (bool) {
bytes32 payoutsHash = _hashPayoutInfo(req.payouts);
bytes32 structHash = keccak256(
abi.encode(
REQUEST_TYPEHASH,
req.clientId,
req.transactionId,
req.tokenAddress,
req.tokenAmount,
req.expirationTimestamp,
payoutsHash,
req.forwardAddress,
keccak256(req.data)
)
);

bytes32 digest = _hashTypedData(structHash);
address recovered = digest.recover(signature);
bool valid = recovered == owner() && !processed[req.transactionId];

return valid;
}

function _isTokenERC20(address tokenAddress) private pure returns (bool) {
return tokenAddress != NATIVE_TOKEN_ADDRESS;
}

function _isTokenNative(address tokenAddress) private pure returns (bool) {
return tokenAddress == NATIVE_TOKEN_ADDRESS;
}

function _calculateFee(uint256 amount, uint256 feeBPS) private pure returns (uint256) {
uint256 feeAmount = (amount * feeBPS) / 10_000;
return feeAmount;
}
}
Loading
Loading