Skip to content

Commit

Permalink
Add support for sending native USDC with transferWithPermit
Browse files Browse the repository at this point in the history
  • Loading branch information
sisou committed Nov 17, 2023
1 parent b52988d commit fb42269
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 7 deletions.
8 changes: 8 additions & 0 deletions client/src/PublicRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,14 @@ export type PolygonTransactionInfo = {
approval?: {
tokenNonce: number,
},

/**
* The sender's nonce in the token contract, required when calling the
* contract function `transferWithPermit`.
*/
permit?: {
tokenNonce: number,
},
};

export type SignPolygonTransactionRequest = Omit<SimpleRequest, 'keyLabel'> & PolygonTransactionInfo & {
Expand Down
3 changes: 3 additions & 0 deletions src/config/config.local.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ const CONFIG = { // eslint-disable-line no-unused-vars
USDC_CONTRACT_ADDRESS: '0x0FA8781a83E46826621b3BC094Ea2A0212e71B23',
USDC_TRANSFER_CONTRACT_ADDRESS: '0x2805f3187dcDfa424EFA8c55Db6012Cf08Fa6eEc', // v3
USDC_HTLC_CONTRACT_ADDRESS: '0x2EB7cd7791b947A25d629219ead941fCd8f364BF',

NATIVE_USDC_CONTRACT_ADDRESS: '0x9999f7Fea5938fD3b1E26A12c3f2fb024e194f97',
NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '0x5D101A320547f8D640c44fDfe5d1f35224f00B8B', // v1
};
3 changes: 3 additions & 0 deletions src/config/config.mainnet.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ const CONFIG = { // eslint-disable-line no-unused-vars
USDC_CONTRACT_ADDRESS: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
USDC_TRANSFER_CONTRACT_ADDRESS: '0x98E69a6927747339d5E543586FC0262112eBe4BD',
USDC_HTLC_CONTRACT_ADDRESS: '0xF615bD7EA00C4Cc7F39Faad0895dB5f40891359f',

NATIVE_USDC_CONTRACT_ADDRESS: '',
NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '', // v1
};
3 changes: 3 additions & 0 deletions src/config/config.testnet.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ const CONFIG = { // eslint-disable-line no-unused-vars
USDC_CONTRACT_ADDRESS: '0x0FA8781a83E46826621b3BC094Ea2A0212e71B23',
USDC_TRANSFER_CONTRACT_ADDRESS: '0x2805f3187dcDfa424EFA8c55Db6012Cf08Fa6eEc',
USDC_HTLC_CONTRACT_ADDRESS: '0x2EB7cd7791b947A25d629219ead941fCd8f364BF',

NATIVE_USDC_CONTRACT_ADDRESS: '0x9999f7Fea5938fD3b1E26A12c3f2fb024e194f97',
NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '0x5D101A320547f8D640c44fDfe5d1f35224f00B8B', // v1
};
118 changes: 118 additions & 0 deletions src/lib/polygon/PolygonContractABIs.full.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,5 +177,123 @@ const PolygonContractABIsFull = { // eslint-disable-line no-unused-vars
'function withdrawToken(address token, uint256 amount, address target)',
'function wrappedChainToken() view returns (address)',
],

NATIVE_USDC_CONTRACT_ABI: [
'event Approval(address indexed owner, address indexed spender, uint256 value)',
'event AuthorizationCanceled(address indexed authorizer, bytes32 indexed nonce)',
'event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)',
'event Blacklisted(address indexed _account)',
'event BlacklisterChanged(address indexed newBlacklister)',
'event Burn(address indexed burner, uint256 amount)',
'event MasterMinterChanged(address indexed newMasterMinter)',
'event Mint(address indexed minter, address indexed to, uint256 amount)',
'event MinterConfigured(address indexed minter, uint256 minterAllowedAmount)',
'event MinterRemoved(address indexed oldMinter)',
'event OwnershipTransferred(address previousOwner, address newOwner)',
'event Pause()',
'event PauserChanged(address indexed newAddress)',
'event RescuerChanged(address indexed newRescuer)',
'event Transfer(address indexed from, address indexed to, uint256 value)',
'event UnBlacklisted(address indexed _account)',
'event Unpause()',
'function CANCEL_AUTHORIZATION_TYPEHASH() view returns (bytes32)',
'function DOMAIN_SEPARATOR() view returns (bytes32)',
'function PERMIT_TYPEHASH() view returns (bytes32)',
'function RECEIVE_WITH_AUTHORIZATION_TYPEHASH() view returns (bytes32)',
'function TRANSFER_WITH_AUTHORIZATION_TYPEHASH() view returns (bytes32)',
'function allowance(address owner, address spender) view returns (uint256)',
'function approve(address spender, uint256 value) returns (bool)',
'function authorizationState(address authorizer, bytes32 nonce) view returns (bool)',
'function balanceOf(address account) view returns (uint256)',
'function blacklist(address _account)',
'function blacklister() view returns (address)',
'function burn(uint256 _amount)',
'function cancelAuthorization(address authorizer, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)',
'function configureMinter(address minter, uint256 minterAllowedAmount) returns (bool)',
'function currency() view returns (string)',
'function decimals() view returns (uint8)',
'function decreaseAllowance(address spender, uint256 decrement) returns (bool)',
'function increaseAllowance(address spender, uint256 increment) returns (bool)',
'function initialize(string tokenName, string tokenSymbol, string tokenCurrency, uint8 tokenDecimals, address newMasterMinter, address newPauser, address newBlacklister, address newOwner)',
'function initializeV2(string newName)',
'function initializeV2_1(address lostAndFound)',
'function isBlacklisted(address _account) view returns (bool)',
'function isMinter(address account) view returns (bool)',
'function masterMinter() view returns (address)',
'function mint(address _to, uint256 _amount) returns (bool)',
'function minterAllowance(address minter) view returns (uint256)',
'function name() view returns (string)',
'function nonces(address owner) view returns (uint256)',
'function owner() view returns (address)',
'function pause()',
'function paused() view returns (bool)',
'function pauser() view returns (address)',
'function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)',
'function receiveWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)',
'function removeMinter(address minter) returns (bool)',
'function rescueERC20(address tokenContract, address to, uint256 amount)',
'function rescuer() view returns (address)',
'function symbol() view returns (string)',
'function totalSupply() view returns (uint256)',
'function transfer(address to, uint256 value) returns (bool)',
'function transferFrom(address from, address to, uint256 value) returns (bool)',
'function transferOwnership(address newOwner)',
'function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)',
'function unBlacklist(address _account)',
'function unpause()',
'function updateBlacklister(address _newBlacklister)',
'function updateMasterMinter(address _newMasterMinter)',
'function updatePauser(address _newPauser)',
'function updateRescuer(address newRescuer)',
'function version() view returns (string)',
],

NATIVE_USDC_TRANSFER_CONTRACT_ABI: [
'constructor()',
'event DomainRegistered(bytes32 indexed domainSeparator, bytes domainValue)',
'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)',
'event RequestTypeRegistered(bytes32 indexed typeHash, string typeStr)',
'function EIP712_DOMAIN_TYPE() view returns (string)',
'function deposits(address) view returns (uint256)',
'function domains(bytes32) view returns (bool)',
'function execute(tuple(address from, address to, uint256 value, uint256 gas, uint256 nonce, bytes data, uint256 validUntil) request, bytes32 domainSeparator, bytes32 requestTypeHash, bytes suffixData, bytes signature) payable returns (bool success, bytes ret)',
'function getGasAndDataLimits() view returns (tuple(uint256 acceptanceBudget, uint256 preRelayedCallGasLimit, uint256 postRelayedCallGasLimit, uint256 calldataSizeLimit) limits)',
'function getHubAddr() view returns (address)',
'function getMinimumRelayFee(tuple(uint256 gasPrice, uint256 pctRelayFee, uint256 baseRelayFee, address relayWorker, address paymaster, address forwarder, bytes paymasterData, uint256 clientId) relayData) view returns (uint256 amount)',
'function getNonce(address from) view returns (uint256)',
'function getRelayHubDeposit() view returns (uint256)',
'function getRequiredRelayFee(tuple(uint256 gasPrice, uint256 pctRelayFee, uint256 baseRelayFee, address relayWorker, address paymaster, address forwarder, bytes paymasterData, uint256 clientId) relayData, bytes4 methodId) view returns (uint256 amount)',
'function getRequiredRelayGas(bytes4 methodId) view returns (uint256 gas)',
'function isTrustedForwarder(address forwarder) view returns (bool)',
'function owner() view returns (address)',
'function postRelayedCall(bytes context, bool success, uint256 gasUseWithoutPost, tuple(uint256 gasPrice, uint256 pctRelayFee, uint256 baseRelayFee, address relayWorker, address paymaster, address forwarder, bytes paymasterData, uint256 clientId) relayData)',
'function preRelayedCall(tuple(tuple(address from, address to, uint256 value, uint256 gas, uint256 nonce, bytes data, uint256 validUntil) request, tuple(uint256 gasPrice, uint256 pctRelayFee, uint256 baseRelayFee, address relayWorker, address paymaster, address forwarder, bytes paymasterData, uint256 clientId) relayData) relayRequest, bytes signature, bytes approvalData, uint256 maxPossibleGas) returns (bytes context, bool revertOnRecipientRevert)',
'function registerDomainSeparator(string name, string version)',
'function registerRequestType(string typeName, string typeSuffix)',
'function registerToken(address token, address pool)',
'function registeredTokenPool(address) view returns (address)',
'function registeredTokenPoolFee(address token) view returns (uint24 fee)',
'function renounceOwnership()',
'function requiredRelayGas() view returns (uint256)',
'function setGasAndDataLimits(tuple(uint256 acceptanceBudget, uint256 preRelayedCallGasLimit, uint256 postRelayedCallGasLimit, uint256 calldataSizeLimit) limits)',
'function setMaxRequiredRelayGas(uint256 gas)',
'function setRelayHub(address hub)',
'function setRequiredRelayGas(bytes4 methodId, uint256 gas)',
'function setWrappedChainToken(address _wrappedChainToken)',
'function transfer(address token, uint256 amount, address target, uint256 fee)',
'function transferOwnership(address newOwner)',
'function transferWithPermit(address token, uint256 amount, address target, uint256 fee, uint256 value, bytes32 sigR, bytes32 sigS, uint8 sigV)',
'function trustedForwarder() view returns (address forwarder)',
'function typeHashes(bytes32) view returns (bool)',
'function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes _data)',
'function unregisterToken(address token)',
'function verify(tuple(address from, address to, uint256 value, uint256 gas, uint256 nonce, bytes data, uint256 validUntil) forwardRequest, bytes32 domainSeparator, bytes32 requestTypeHash, bytes suffixData, bytes signature) view',
'function versionPaymaster() view returns (string)',
'function versionRecipient() view returns (string)',
'function withdraw(uint256 amount, address target)',
'function withdrawRelayHubDeposit(uint256 amount, address target)',
'function withdrawToken(address token, uint256 amount, address target)',
'function wrappedChainToken() view returns (address)',
],
};
/* eslint-enable max-len */
7 changes: 7 additions & 0 deletions src/lib/polygon/PolygonContractABIs.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,12 @@ const PolygonContractABIs = { // eslint-disable-line no-unused-vars
'function redeemWithSecretInData(bytes32 id, address target, uint256 fee)',
'function refund(bytes32 id, address target, uint256 fee)',
],

NATIVE_USDC_CONTRACT_ABI: [],

NATIVE_USDC_TRANSFER_CONTRACT_ABI: [
'function transfer(address token, uint256 amount, address target, uint256 fee)',
'function transferWithPermit(address token, uint256 amount, address target, uint256 fee, uint256 value, bytes32 sigR, bytes32 sigS, uint8 sigV)',
],
};
/* eslint-enable max-len */
49 changes: 49 additions & 0 deletions src/lib/polygon/PolygonKey.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,55 @@ class PolygonKey { // eslint-disable-line no-unused-vars
return { sigR, sigS, sigV };
}

/**
* @param {string} path
* @param {string} forwarderContractAddress
* @param {ethers.BigNumber} approvalAmount
* @param {number} tokenNonce
* @param {string} ownerAddress
* @returns {Promise<{sigR: string, sigS: string, sigV: number}>}
*/
async signUsdcPermit(path, forwarderContractAddress, approvalAmount, tokenNonce, ownerAddress) {
// TODO: Make the domain parameters configurable in the request?
const domain = {
name: 'USD Coin', // This is currently the same for testnet and mainnet
version: '2', // This is currently the same for testnet and mainnet
verifyingContract: CONFIG.NATIVE_USDC_CONTRACT_ADDRESS,
chainId: CONFIG.POLYGON_CHAIN_ID,
};

const types = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};

const message = {
owner: ownerAddress,
spender: forwarderContractAddress,
value: approvalAmount,
nonce: tokenNonce,
deadline: ethers.constants.MaxUint256,
};

const signature = await this.signTypedData(
path,
domain,
types,
message,
);

const sigR = signature.slice(0, 66); // 0x prefix plus 32 bytes = 66 characters
const sigS = `0x${signature.slice(66, 130)}`; // 32 bytes = 64 characters
const sigV = parseInt(signature.slice(130, 132), 16); // last byte = 2 characters

return { sigR, sigS, sigV };
}

/**
* @param {string} path
* @param {Uint8Array} message - A byte array
Expand Down
39 changes: 36 additions & 3 deletions src/request/sign-polygon-transaction/SignPolygonTransaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,26 @@ class SignPolygonTransaction {

const polygonKey = new PolygonKey(key);

let transferContract = '';

if (request.description.name === 'transferWithApproval') {
transferContract = CONFIG.USDC_TRANSFER_CONTRACT_ADDRESS;

const { sigR, sigS, sigV } = await polygonKey.signUsdcApproval(
request.keyPath,
new ethers.Contract(
CONFIG.USDC_CONTRACT_ADDRESS,
PolygonContractABIs.USDC_CONTRACT_ABI,
),
CONFIG.USDC_TRANSFER_CONTRACT_ADDRESS,
transferContract,
request.description.args.approval,
// Has been validated to be defined when function called is `transferWithApproval`
/** @type {{ tokenNonce: number }} */ (request.approval).tokenNonce,
request.request.from,
);

const usdcTransfer = new ethers.Contract(
CONFIG.USDC_TRANSFER_CONTRACT_ADDRESS,
transferContract,
PolygonContractABIs.USDC_TRANSFER_CONTRACT_ABI,
);

Expand All @@ -148,11 +152,40 @@ class SignPolygonTransaction {
]);
}

if (request.description.name === 'transferWithPermit') {
transferContract = CONFIG.NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS;

const { sigR, sigS, sigV } = await polygonKey.signUsdcPermit(
request.keyPath,
transferContract,
request.description.args.value,
// Has been validated to be defined when function called is `transferWithPermit`
/** @type {{ tokenNonce: number }} */ (request.permit).tokenNonce,
request.request.from,
);

const nativeUsdcTransfer = new ethers.Contract(
transferContract,
PolygonContractABIs.NATIVE_USDC_TRANSFER_CONTRACT_ABI,
);

request.request.data = nativeUsdcTransfer.interface.encodeFunctionData(request.description.name, [
/* address token */ request.description.args.token,
/* uint256 amount */ request.description.args.amount,
/* address target */ request.description.args.target,
/* uint256 fee */ request.description.args.fee,
/* uint256 value */ request.description.args.value,
/* bytes32 sigR */ sigR,
/* bytes32 sigS */ sigS,
/* uint8 sigV */ sigV,
]);
}

const typedData = new OpenGSN.TypedRequestData(
CONFIG.POLYGON_CHAIN_ID,
request.description.name === 'refund'
? CONFIG.USDC_HTLC_CONTRACT_ADDRESS
: CONFIG.USDC_TRANSFER_CONTRACT_ADDRESS,
: transferContract,
{
request: request.request,
relayData: request.relayData,
Expand Down
Loading

0 comments on commit fb42269

Please sign in to comment.