Last active
February 1, 2023 15:19
-
-
Save onmax/e701e8de4a50000dc70c368e01d8f0b5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: GPL-3.0 | |
pragma solidity ^0.8.0; | |
import "@uniswap/swap-router-contracts/contracts/interfaces/IV3SwapRouter.sol"; | |
import "./interfaces/IERC20Meta.sol"; | |
import "./interfaces/IWrappedChainToken.sol"; | |
import "./BaseCombinedGsnHandler.sol"; | |
abstract contract BaseERC20MetaHandler is BaseCombinedGsnHandler { | |
IV3SwapRouter public swapRouter; | |
IWrappedChainToken public wrappedChainToken; | |
uint256 public preApprovedGasDiscount = 0; | |
mapping(IERC20Meta => uint24) public registeredTokenPoolFee; | |
uint256 private registeredTokenCount = 0; | |
mapping(address => uint256) internal nonces; | |
struct FeeInformation { | |
IERC20Meta token; | |
uint256 fee; | |
uint256 chainTokenFee; | |
} | |
struct ApprovalRequestData { | |
IERC20Meta token; | |
uint256 approval; | |
bytes32 sigR; | |
bytes32 sigS; | |
uint8 sigV; | |
} | |
modifier onlyRegisteredToken(IERC20Meta token) { | |
require(registeredTokenPoolFee[token] > 0, "Base: token not registered"); | |
_; | |
} | |
modifier onlyWithSwapRouter() { | |
require(address(swapRouter) != address(0), "Base: no swap router"); | |
_; | |
} | |
modifier onlyWithWrappedChainToken() { | |
require(address(wrappedChainToken) != address(0), "Base: no wrapped chain token"); | |
_; | |
} | |
function setSwapRouter(IV3SwapRouter _swapRouter) public onlyOwner { | |
require(registeredTokenCount == 0, "Base: tokens registered"); | |
swapRouter = _swapRouter; | |
} | |
function setWrappedChainToken(IWrappedChainToken _wrappedChainToken) public onlyOwner { | |
require(registeredTokenCount == 0, "Base: tokens registered"); | |
wrappedChainToken = _wrappedChainToken; | |
require(wrappedChainToken.approve(owner(), type(uint256).max), "Base: owner approval failed"); | |
} | |
function registerToken(IERC20Meta token, uint24 poolFee) public onlyOwner onlyWithSwapRouter { | |
require(address(swapRouter) != address(0), "Base: No swap router defined"); | |
require(poolFee > 0, "Base: No pool fee defined"); | |
require(registeredTokenPoolFee[token] == 0, "Base: token already registered"); | |
require(token.approve(address(swapRouter), type(uint256).max), "Base: swap approval failed"); | |
require(token.approve(owner(), type(uint256).max), "Base: owner approval failed"); | |
registeredTokenPoolFee[token] = poolFee; | |
registeredTokenCount = registeredTokenCount + 1; | |
} | |
function unregisterToken(IERC20Meta token) public onlyOwner onlyWithSwapRouter onlyRegisteredToken(token) { | |
delete registeredTokenPoolFee[token]; | |
token.approve(address(swapRouter), 0); | |
registeredTokenCount = registeredTokenCount - 1; | |
} | |
function updatePreApprovedGasDiscount(uint256 _preApprovedGasDiscount) public onlyOwner { | |
preApprovedGasDiscount = _preApprovedGasDiscount; | |
} | |
function retrieveFeeInternal(address userAddress, FeeInformation memory feeInformation) internal { | |
if (feeInformation.fee > 0) { | |
require(feeInformation.token.transferFrom(userAddress, address(this), feeInformation.fee), "Base: Fee transfer failed"); | |
deductFeeInternal(feeInformation); | |
} else { | |
require(feeInformation.chainTokenFee == 0, "Base: Fee too low"); | |
} | |
} | |
function deductFeeInternal(FeeInformation memory feeInformation) internal { | |
if (feeInformation.chainTokenFee > 0) { | |
IV3SwapRouter.ExactOutputSingleParams memory params = IV3SwapRouter.ExactOutputSingleParams({ | |
tokenIn : address(feeInformation.token), | |
tokenOut : address(wrappedChainToken), | |
fee : registeredTokenPoolFee[feeInformation.token], | |
recipient : address(this), | |
amountOut : feeInformation.chainTokenFee, | |
amountInMaximum : feeInformation.fee, | |
sqrtPriceLimitX96 : 0 | |
}); | |
swapRouter.exactOutputSingle(params); | |
} | |
} | |
function finishFeeInternal(FeeInformation memory feeInformation) internal { | |
if (feeInformation.chainTokenFee > 0) { | |
wrappedChainToken.withdraw(feeInformation.chainTokenFee); | |
relayHub.depositFor{value : feeInformation.chainTokenFee}(address(this)); | |
} | |
} | |
function executeApproveInternal(address userAddress, ApprovalRequestData memory approvalRequestData) internal { | |
bytes memory functionSignature = abi.encodeCall(IERC20.approve, (address(this), approvalRequestData.approval)); | |
approvalRequestData.token.executeMetaTransaction(userAddress, functionSignature, approvalRequestData.sigR, approvalRequestData.sigS, approvalRequestData.sigV); | |
} | |
function approveIfRequiredInternal(address userAddress, ApprovalRequestData memory approvalRequestData, uint256 required) internal { | |
if (approvalRequestData.token.allowance(userAddress, address(this)) < required) { | |
executeApproveInternal(userAddress, approvalRequestData); | |
} | |
} | |
function decodeFeeInformationInternal(bytes memory data, uint tokenIndex, uint feeIndex, uint chainTokenFeeIndex) internal pure returns(FeeInformation memory feeInformation) { | |
feeInformation = FeeInformation({ | |
token : IERC20Meta(address(uint160(GsnUtils.getParam(data, tokenIndex)))), | |
fee : uint256(GsnUtils.getParam(data, feeIndex)), | |
chainTokenFee : uint256(GsnUtils.getParam(data, chainTokenFeeIndex)) | |
}); | |
} | |
function decodeFeeInformationKnownTokenInternal(bytes memory data, IERC20Meta token, uint feeIndex, uint chainTokenFeeIndex) internal pure returns(FeeInformation memory feeInformation) { | |
feeInformation = FeeInformation({ | |
token : token, | |
fee : uint256(GsnUtils.getParam(data, feeIndex)), | |
chainTokenFee : uint256(GsnUtils.getParam(data, chainTokenFeeIndex)) | |
}); | |
} | |
function decodeApprovalRequestDataInternal(bytes memory data, uint tokenIndex, uint approvalIndex, uint sigIndex) internal pure returns(ApprovalRequestData memory approvalRequestData) { | |
approvalRequestData = ApprovalRequestData({ | |
token : IERC20Meta(address(uint160(GsnUtils.getParam(data, tokenIndex)))), | |
approval : uint256(GsnUtils.getParam(data, approvalIndex)), | |
sigR : bytes32(GsnUtils.getParam(data, sigIndex)), | |
sigS : bytes32(GsnUtils.getParam(data, sigIndex + 1)), | |
sigV : uint8(GsnUtils.getParam(data, sigIndex + 2)) | |
}); | |
} | |
function getNonce(address from) public override view returns (uint256) { | |
return nonces[from]; | |
} | |
function withdraw(uint amount, address payable target) public onlyOwner { | |
target.transfer(amount); | |
} | |
// solhint-disable-next-line no-empty-blocks | |
receive() external virtual payable {} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: GPL-3.0 | |
pragma solidity ^0.8.0; | |
import "@uniswap/swap-router-contracts/contracts/interfaces/IV3SwapRouter.sol"; | |
import "./BaseERC20MetaHandler.sol"; | |
/** | |
* @title An OpenGSN-powered meta transaction handler for ERC20 tokens | |
* @notice This contract allows sending ERC20 tokens without having to acquire the chains native token first. | |
* The network fee is paid using the ERC20 token, which is converted automatically using UniSwap. | |
*/ | |
contract ERC20MetaHandler is BaseERC20MetaHandler { | |
function transferPrivate(address userAddress, TransferRequestData memory requestData) private { | |
require(requestData.token.transferFrom(userAddress, requestData.target, requestData.amount), "Meta: Value transfer failed"); | |
} | |
function checkTransferPrivate(address userAddress, TransferRequestData memory requestData, FeeInformation memory feeInformation) private view onlyWithSwapRouter onlyWithWrappedChainToken onlyRegisteredToken(requestData.token) { | |
require(requestData.token.balanceOf(userAddress) >= requestData.amount + feeInformation.fee, "Meta: balance too low"); | |
} | |
function checkTransferWithApproval(address userAddress, TransferRequestData memory requestData, FeeInformation memory feeInformation, ApprovalRequestData memory approvalRequestData) internal view { | |
checkTransferPrivate(userAddress, requestData, feeInformation); | |
if (requestData.token.allowance(userAddress, address(this)) < requestData.amount + feeInformation.fee) { | |
require(approvalRequestData.approval >= requestData.amount + feeInformation.fee, "Meta: approval too low"); | |
} | |
} | |
function approvePrivate(address userAddress, TransferRequestData memory requestData, FeeInformation memory feeInformation, ApprovalRequestData memory approvalRequestData) private { | |
approveIfRequiredInternal(userAddress, approvalRequestData, requestData.amount + feeInformation.fee); | |
} | |
/** | |
* @notice Transfers `amount` of `token` to `target`. Includes an approval for `approval` `token`. | |
*/ | |
function transferWithApproval(IERC20Meta token, uint256 amount, address target, uint256 fee, uint256 chainTokenFee, uint256 approval, bytes32 sigR, bytes32 sigS, uint8 sigV) public { | |
TransferRequestData memory requestData = TransferRequestData(token, amount, target); | |
FeeInformation memory feeInformation = FeeInformation(token, fee, chainTokenFee); | |
ApprovalRequestData memory approvalRequestData = ApprovalRequestData(token, approval, sigR, sigS, sigV); | |
checkTransferWithApproval(msg.sender, requestData, feeInformation, approvalRequestData); | |
approvePrivate(msg.sender, requestData, feeInformation, approvalRequestData); | |
retrieveFeeInternal(msg.sender, feeInformation); | |
transferPrivate(msg.sender, requestData); | |
finishFeeInternal(feeInformation); | |
} | |
function checkTransfer(address userAddress, TransferRequestData memory requestData, FeeInformation memory feeInformation) internal view { | |
checkTransferPrivate(userAddress, requestData, feeInformation); | |
require(requestData.token.allowance(userAddress, address(this)) >= requestData.amount + feeInformation.fee, "Meta: allowance too low"); | |
} | |
/** | |
* @notice Transfers `amount` of `token` to `target`. | |
*/ | |
function transfer(IERC20Meta token, uint256 amount, address target, uint256 fee, uint256 chainTokenFee) public { | |
TransferRequestData memory requestData = TransferRequestData(token, amount, target); | |
FeeInformation memory feeInformation = FeeInformation(token, fee, chainTokenFee); | |
checkTransfer(msg.sender, requestData, feeInformation); | |
retrieveFeeInternal(msg.sender, feeInformation); | |
transferPrivate(msg.sender, requestData); | |
finishFeeInternal(feeInformation); | |
} | |
struct TransferRequestData { | |
IERC20Meta token; | |
uint256 amount; | |
address target; | |
} | |
function decodeTransferRequestDataPrivate(bytes memory data, uint tokenIndex, uint amountIndex, uint targetIndex) private pure returns (TransferRequestData memory requestData) { | |
requestData = TransferRequestData({ | |
token : IERC20Meta(address(uint160(GsnUtils.getParam(data, tokenIndex)))), | |
amount : uint256(GsnUtils.getParam(data, amountIndex)), | |
target : address(uint160(GsnUtils.getParam(data, targetIndex))) | |
}); | |
} | |
function decodeRequestDataPrivate(bytes memory data) private pure returns (bytes4 methodId, TransferRequestData memory requestData, FeeInformation memory feeInformation) { | |
methodId = GsnUtils.getMethodSig(data); | |
if (methodId == this.transfer.selector || methodId == this.transferWithApproval.selector) { | |
requestData = decodeTransferRequestDataPrivate(data, 0, 1, 2); | |
feeInformation = decodeFeeInformationInternal(data, 0, 3, 4); | |
} else { | |
require(false, "Meta: unsupported method"); | |
} | |
} | |
function verifyCallPrivate(IForwarder.ForwardRequest memory request, GsnTypes.RelayData calldata relayData, bytes memory signature, bytes memory approvalData) private view returns (bytes4 methodId, TransferRequestData memory requestData, FeeInformation memory feeInformation, ApprovalRequestData memory approvalRequestData) { | |
(approvalData); | |
bytes memory suffixData = abi.encode(GsnEip712Library.hashRelayData(relayData)); | |
bytes32 _domainSeparator = GsnEip712Library.domainSeparator(relayData.forwarder); | |
verifyInternal(request, _domainSeparator, GsnEip712Library.RELAY_REQUEST_TYPEHASH, suffixData, signature); | |
(methodId, requestData, feeInformation) = decodeRequestDataPrivate(request.data); | |
if (methodId == this.transfer.selector) { | |
require(feeInformation.chainTokenFee >= relayHub.calculateCharge(requiredRelayGas() - preApprovedGasDiscount, relayData), "Meta: fee too low"); | |
checkTransfer(request.from, requestData, feeInformation); | |
} else if (methodId == this.transferWithApproval.selector) { | |
require(feeInformation.chainTokenFee >= relayHub.calculateCharge(requiredRelayGas(), relayData), "Meta: fee too low"); | |
approvalRequestData = decodeApprovalRequestDataInternal(request.data, 0, 5, 6); | |
checkTransferWithApproval(request.from, requestData, feeInformation, approvalRequestData); | |
} | |
} | |
function preRelayedCall(GsnTypes.RelayRequest calldata relayRequest, bytes calldata signature, bytes calldata approvalData, uint256 maxPossibleGas) external override virtual onlyRelayHub onlyToSelf(relayRequest) returns (bytes memory context, bool revertOnRecipientRevert) { | |
(relayRequest, signature, approvalData, maxPossibleGas); | |
require(relayRequest.request.nonce == getNonce(relayRequest.request.from), "Meta: Invalid nonce"); | |
(bytes4 methodId, TransferRequestData memory requestData, FeeInformation memory feeInformation, ApprovalRequestData memory approvalRequestData) = verifyCallPrivate(relayRequest.request, relayRequest.relayData, signature, approvalData); | |
(methodId); | |
if (approvalRequestData.approval != 0) { | |
approvePrivate(relayRequest.request.from, requestData, feeInformation, approvalRequestData); | |
} | |
retrieveFeeInternal(relayRequest.request.from, feeInformation); | |
context = abi.encode(relayRequest.request, signature); | |
revertOnRecipientRevert = true; | |
} | |
function execute(ForwardRequest calldata request, bytes32 domainSeparator, bytes32 requestTypeHash, bytes calldata suffixData, bytes calldata signature) public override payable returns (bool success, bytes memory ret) { | |
(request, domainSeparator, requestTypeHash, suffixData, signature); | |
verifyInternal(request, domainSeparator, requestTypeHash, suffixData, signature); | |
require(request.nonce == getNonce(request.from), "Meta: Invalid nonce"); | |
(bytes4 methodId, TransferRequestData memory requestData, FeeInformation memory feeInformation) = decodeRequestDataPrivate(request.data); | |
(methodId, feeInformation); | |
nonces[request.from] = nonces[request.from] + 1; | |
transferPrivate(request.from, requestData); | |
success = true; | |
ret = ""; | |
} | |
function postRelayedCall(bytes calldata context, bool success, uint256 gasUseWithoutPost, GsnTypes.RelayData calldata relayData) external override virtual onlyRelayHub { | |
(context, success, gasUseWithoutPost, relayData); | |
(IForwarder.ForwardRequest memory request, bytes memory signature) = abi.decode(context, (IForwarder.ForwardRequest, bytes)); | |
bytes memory suffixData = abi.encode(GsnEip712Library.hashRelayData(relayData)); | |
bytes32 _domainSeparator = GsnEip712Library.domainSeparator(relayData.forwarder); | |
verifyInternal(request, _domainSeparator, GsnEip712Library.RELAY_REQUEST_TYPEHASH, suffixData, signature); | |
require(request.to == address(this), "POST: illegal request.to"); | |
(bytes4 methodId, TransferRequestData memory requestData, FeeInformation memory feeInformation) = decodeRequestDataPrivate(request.data); | |
(methodId, requestData); | |
finishFeeInternal(feeInformation); | |
} | |
function versionRecipient() external override virtual view returns (string memory) { | |
return "2.2.6+opengsn.recipient.erc20meta.handler"; | |
} | |
function versionPaymaster() external override virtual view returns (string memory) { | |
return "2.2.6+opengsn.paymaster.erc20meta.handler"; | |
} | |
constructor() { | |
registerRequestTypeInternal(string(abi.encodePacked("ForwardRequest(", GsnEip712Library.GENERIC_PARAMS, ")"))); | |
registerRequestTypeInternal(GsnEip712Library.RELAY_REQUEST_NAME, GsnEip712Library.RELAY_REQUEST_SUFFIX); | |
registerDomainSeparatorInternal("GSN Relayed Transaction", "2"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment