B402 Relayer Contract Update Fork
235 linhas
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
/**
* @title B402RelayerV2
* @title B402RelayerV2
* @notice Production-ready meta-transaction relayer for b402.ai
* @notice Production-ready meta-transaction relayer for b402.ai
* @dev Implements EIP-3009 transferWithAuthorization for gasless payments
* @dev Implements EIP-3009 transferWithAuthorization for gasless payments
*
*
* AUDIT FIXES:
* AUDIT FIXES:
* - Added ReentrancyGuard protection
* - Added ReentrancyGuard protection
* - Fixed validAfter check (now uses >=)
* - Fixed validAfter check (now uses >=)
* - Added token whitelist for security
* - Added token whitelist for security
* - Fixed cancelAuthorization TypeHash
* - Fixed cancelAuthorization TypeHash
* - Added pre-flight balance/allowance checks
* - Added pre-flight balance/allowance checks
* - Added emergency pause mechanism
* - Added emergency pause mechanism
*
*
* Works with USDT on BSC (0x55d398326f99059fF775485246999027B3197955)
* Works with USDT on BSC (0x55d398326f99059fF775485246999027B3197955)
* Requires user to approve this contract first: USDT.approve(relayer, amount)
* Requires user to approve this contract first: USDT.approve(relayer, amount)
*/
*/
contract B402RelayerV2 is EIP712, ReentrancyGuard {
contract B402RelayerV2 is EIP712, ReentrancyGuard {
using ECDSA for bytes32;
using ECDSA for bytes32;
// EIP-712 TypeHashes
// EIP-712 TypeHashes
bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = keccak256(
bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = keccak256(
"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
);
);
bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = keccak256(
bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = keccak256(
"CancelAuthorization(address authorizer,bytes32 nonce)"
"CancelAuthorization(address authorizer,bytes32 nonce)"
);
);
using ECDSA for bytes32;
struct ForwardRequest {
address from;
address to;
uint256 value;
uint256 gas;
uint256 nonce;
uint256 deadline;
bytes data;
}
bytes32 private constant TYPEHASH =
keccak256("ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,uint256 deadline,bytes data)");
mapping(address => uint256) private _nonces;
event MetaTransactionExecuted(
address indexed from,
address indexed to,
bytes data,
bool success
);
// Track used nonces (same as EIP-3009)
// Track used nonces (same as EIP-3009)
mapping(address => mapping(bytes32 => bool)) private _authorizationStates;
mapping(address => mapping(bytes32 => bool)) private _authorizationStates;
// Token whitelist for security
// Token whitelist for security
mapping(address => bool) public whitelistedTokens;
mapping(address => bool) public whitelistedTokens;
// Admin controls
// Admin controls
address public owner;
address public owner;
bool public paused;
bool public paused;
// Events (match EIP-3009)
// Events (match EIP-3009)
event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce);
event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce);
event AuthorizationCanceled(address indexed authorizer, bytes32 indexed nonce);
event AuthorizationCanceled(address indexed authorizer, bytes32 indexed nonce);
event TokenWhitelisted(address indexed token, bool status);
event TokenWhitelisted(address indexed token, bool status);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event Paused(address account);
event Paused(address account);
event Unpaused(address account);
event Unpaused(address account);
modifier onlyOwner() {
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
require(msg.sender == owner, "Not owner");
_;
_;
}
}
modifier whenNotPaused() {
modifier whenNotPaused() {
require(!paused, "Contract paused");
require(!paused, "Contract paused");
_;
_;
}
}
constructor() EIP712("B402", "1") {
constructor() EIP712("B402", "1") {
owner = msg.sender;
owner = msg.sender;
// Whitelist BSC USDT by default (mainnet)
// Whitelist BSC USDT by default (mainnet)
whitelistedTokens[0x55d398326f99059fF775485246999027B3197955] = true;
whitelistedTokens[0x55d398326f99059fF775485246999027B3197955] = true;
// Whitelist BSC Testnet USDT
whitelistedTokens[0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d] = true;
whitelistedTokens[0x337610d27c682E347C9cD60BD4b3b107C9d34dDd] = true;
}
}
/**
/**
* @notice Execute transfer with authorization (EIP-3009 compatible)
* @notice Execute transfer with authorization (EIP-3009 compatible)
* @param token Token contract address (must be whitelisted)
* @param token Token contract address (must be whitelisted)
* @param from Payer address
* @param from Payer address
* @param to Recipient address
* @param to Recipient address
* @param value Amount to transfer
* @param value Amount to transfer
* @param validAfter Timestamp after which authorization is valid
* @param validAfter Timestamp after which authorization is valid
* @param validBefore Timestamp before which authorization is valid
* @param validBefore Timestamp before which authorization is valid
* @param nonce Unique nonce
* @param nonce Unique nonce
* @param v Signature v
* @param v Signature v
* @param r Signature r
* @param r Signature r
* @param s Signature s
* @param s Signature s
*/
*/
function transferWithAuthorization(
function transferWithAuthorization(
address token,
address token,
address from,
address from,
address to,
address to,
uint256 value,
uint256 value,
uint256 validAfter,
uint256 validAfter,
uint256 validBefore,
uint256 validBefore,
bytes32 nonce,
bytes32 nonce,
uint8 v,
uint8 v,
bytes32 r,
bytes32 r,
bytes32 s
bytes32 s
) external nonReentrant whenNotPaused {
) external nonReentrant whenNotPaused {
// Security checks
// Security checks
require(whitelistedTokens[token], "Token not whitelisted");
require(whitelistedTokens[token], "Token not whitelisted");
require(to != address(0), "Invalid recipient");
require(to != address(0), "Invalid recipient");
require(value > 0, "Invalid amount");
require(value > 0, "Invalid amount");
// Timing validation (FIXED: now uses >= for validAfter)
// Timing validation (FIXED: now uses >= for validAfter)
require(block.timestamp >= validAfter, "Authorization not yet valid");
require(block.timestamp >= validAfter, "Authorization not yet valid");
require(block.timestamp < validBefore, "Authorization expired");
require(block.timestamp < validBefore, "Authorization expired");
// Nonce check
// Nonce check
require(!_authorizationStates[from][nonce], "Authorization already used");
require(!_authorizationStates[from][nonce], "Authorization already used");
// Verify signature using EIP-712
// Verify signature using EIP-712
bytes32 structHash = keccak256(
bytes32 structHash = keccak256(
abi.encode(
abi.encode(
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
from,
from,
to,
to,
value,
value,
validAfter,
validAfter,
validBefore,
validBefore,
nonce
nonce
)
)
);
);
bytes32 digest = _hashTypedDataV4(structHash);
bytes32 digest = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(digest, v, r, s);
address signer = ECDSA.recover(digest, v, r, s);
require(signer == from, "Invalid signature");
require(signer == from, "Invalid signature");
// Pre-flight checks (save gas on failures)
// Pre-flight checks (save gas on failures)
IERC20 tokenContract = IERC20(token);
IERC20 tokenContract = IERC20(token);
require(tokenContract.balanceOf(from) >= value, "Insufficient balance");
require(tokenContract.balanceOf(from) >= value, "Insufficient balance");
require(tokenContract.allowance(from, address(this)) >= value, "Insufficient allowance");
require(tokenContract.allowance(from, address(this)) >= value, "Insufficient allowance");
// Mark nonce as used BEFORE external call (CEI pattern)
// Mark nonce as used BEFORE external call (CEI pattern)
_authorizationStates[from][nonce] = true;
_authorizationStates[from][nonce] = true;
// Execute transfer (reentrancy protected)
// Execute transfer (reentrancy protected)
require(
require(
tokenContract.transferFrom(from, to, value),
tokenContract.transferFrom(from, to, value),
"Transfer failed"
"Transfer failed"
);
);
emit AuthorizationUsed(from, nonce);
emit AuthorizationUsed(from, nonce);
}
}
/**
/**
* @notice Returns the current nonce for a given address.
* @dev Nonces are used to prevent replay attacks.
* Each successful execution of a meta-transaction typically increments this value.
* @param from The address whose nonce is being queried.
* @return The current nonce value associated with the given address.
*/
function getNonce(address from) public view returns (uint256) {
return _nonces[from];
}
/**
* @notice Verifies whether a forwarded request and its signature are valid.
* @dev
* This function implements EIP-712 typed data verification.
* It hashes the ForwardRequest struct using the `_hashTypedDataV4` function,
* then recovers the signer’s address via ECDSA.
*
* The verification passes only if:
* 1. The recovered signer matches `req.from`
* 2. The provided `req.nonce` equals the stored `_nonces[req.from]`
*
* @param req The ForwardRequest struct containing the call details:
* - from: The address of the request initiator (signer)
* - to: The target contract to be called
* - value: The amount of ETH to send with the call
* - gas: The maximum gas allowed for execution
* - nonce: A unique number to prevent replay attacks
* - deadline: The expiration timestamp of the signature
* - data: The calldata to be executed on the target contract
* @param signature The EIP-712 signature generated by `req.from`
* @return True if the signature is valid and the nonce matches, otherwise false.
*/
function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) {
address signer = _hashTypedDataV4(
keccak256(abi.encode(
TYPEHASH,
req.from,
req.to,
req.value,
req.gas,
req.nonce,
req.deadline,
keccak256(req.data)
))
).recover(signature);
return _nonces[req.from] == req.nonce && signer == req.from;
}
/**
* @notice Executes a meta-transaction on behalf of the original signer.
* @dev
* This function validates the provided EIP-712 signature using `verify()`,
* then performs the requested call (`req.to.call{value: req.value, gas: req.gas}(req.data)`).
*
* It allows users to have their transactions executed by a relayer
* without directly paying gas, as long as the relayer submits a valid signed request.
*
* Commonly used in meta-transaction / trusted forwarder setups (EIP-2771-style).
*
* Requirements:
* - The signature must be valid (`verify(req, signature)` returns true).
* - The nonce must match the stored `_nonces[req.from]` and will typically be incremented after execution.
* - The deadline (if used) must not have expired.
*
* Emits events depending on implementation (e.g., MetaTransactionExecuted).
*
* @param req The ForwardRequest struct describing the call:
* - from: The address of the original signer
* - to: The destination contract to be called
* - value: The amount of ETH to forward
* - gas: The gas limit for the call
* - nonce: The unique number preventing replay
* - deadline: The timestamp until which the request is valid
* - data: The calldata to execute
* @param signature The EIP-712-compliant signature proving the request’s authenticity.
*
* @return success Boolean indicating whether the call succeeded.
* @return returndata The raw return data from the executed call.
*/
function execute(ForwardRequest calldata req, bytes calldata signature)
public
payable
returns (bool success, bytes memory returndata)
{
require(block.timestamp <= req.deadline, "ForwardRequest: expired");
require(verify(req, signature), "ForwardRequest: invalid signature");
_nonces[req.from]++;
(success, returndata) = req.to.call{gas: req.gas, value: req.value}(
abi.encodePacked(req.data, req.from)
);
require(gasleft() > req.gas / 63, "ForwardRequest: insufficient gas");
emit MetaTransactionExecuted(req.from, req.to, req.data, success);
return (success, returndata);
}
/**
* @notice Cancel authorization before it's used
* @notice Cancel authorization before it's used
* @param authorizer Address that signed the authorization
* @param authorizer Address that signed the authorization
* @param nonce Nonce to cancel
* @param nonce Nonce to cancel
* @param v Signature v
* @param v Signature v
* @param r Signature r
* @param r Signature r
* @param s Signature s
* @param s Signature s
*/
*/
function cancelAuthorization(
function cancelAuthorization(
address authorizer,
address authorizer,
bytes32 nonce,
bytes32 nonce,
uint8 v,
uint8 v,
bytes32 r,
bytes32 r,
bytes32 s
bytes32 s
) external {
) external {
require(!_authorizationStates[authorizer][nonce], "Authorization already used");
require(!_authorizationStates[authorizer][nonce], "Authorization already used");
// FIXED: Use proper EIP-712 TypeHash for cancellation
// FIXED: Use proper EIP-712 TypeHash for cancellation
bytes32 structHash = keccak256(
bytes32 structHash = keccak256(
abi.encode(
abi.encode(
CANCEL_AUTHORIZATION_TYPEHASH,
CANCEL_AUTHORIZATION_TYPEHASH,
authorizer,
authorizer,
nonce
nonce
)
)
);
);
bytes32 digest = _hashTypedDataV4(structHash);
bytes32 digest = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(digest, v, r, s);
address signer = ECDSA.recover(digest, v, r, s);
require(signer == authorizer, "Invalid signature");
require(signer == authorizer, "Invalid signature");
_authorizationStates[authorizer][nonce] = true;
_authorizationStates[authorizer][nonce] = true;
emit AuthorizationCanceled(authorizer, nonce);
emit AuthorizationCanceled(authorizer, nonce);
}
}
/**
/**
* @notice Check if authorization has been used
* @notice Check if authorization has been used
* @param authorizer Address that signed the authorization
* @param authorizer Address that signed the authorization
* @param nonce Nonce to check
* @param nonce Nonce to check
* @return True if nonce has been used
* @return True if nonce has been used
*/
*/
function authorizationState(address authorizer, bytes32 nonce) external view returns (bool) {
function authorizationState(address authorizer, bytes32 nonce) external view returns (bool) {
return _authorizationStates[authorizer][nonce];
return _authorizationStates[authorizer][nonce];
}
}
/**
/**
* @notice Add/remove token from whitelist (owner only)
* @notice Add/remove token from whitelist (owner only)
* @param token Token address
* @param token Token address
* @param status True to whitelist, false to remove
* @param status True to whitelist, false to remove
*/
*/
function setTokenWhitelist(address token, bool status) external onlyOwner {
function setTokenWhitelist(address token, bool status) external onlyOwner {
require(token != address(0), "Invalid token");
require(token != address(0), "Invalid token");
whitelistedTokens[token] = status;
whitelistedTokens[token] = status;
emit TokenWhitelisted(token, status);
emit TokenWhitelisted(token, status);
}
}
/**
/**
* @notice Pause contract (emergency only)
* @notice Pause contract (emergency only)
*/
*/
function pause() external onlyOwner {
function pause() external onlyOwner {
paused = true;
paused = true;
emit Paused(msg.sender);
emit Paused(msg.sender);
}
}
/**
/**
* @notice Unpause contract
* @notice Unpause contract
*/
*/
function unpause() external onlyOwner {
function unpause() external onlyOwner {
paused = false;
paused = false;
emit Unpaused(msg.sender);
emit Unpaused(msg.sender);
}
}
/**
/**
* @notice Transfer ownership
* @notice Transfer ownership
* @param newOwner New owner address
* @param newOwner New owner address
*/
*/
function transferOwnership(address newOwner) external onlyOwner {
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "Invalid address");
require(newOwner != address(0), "Invalid address");
address oldOwner = owner;
address oldOwner = owner;
owner = newOwner;
owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
emit OwnershipTransferred(oldOwner, newOwner);
}
}
/**
/**
* @notice Get domain separator (for off-chain signing)
* @notice Get domain separator (for off-chain signing)
* @return Domain separator hash
* @return Domain separator hash
*/
*/
function getDomainSeparator() external view returns (bytes32) {
function getDomainSeparator() external view returns (bytes32) {
return _domainSeparatorV4();
return _domainSeparatorV4();
}
}
}
}