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

Allow swapping bridged USDC.e to native USDC via new OpenGSN-enabled swap contract #488

Merged
merged 6 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions client/src/PublicRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,14 @@ export type PolygonTransactionInfo = {
*/
amount?: number,

/**
* The sender's nonce in the token contract, required when calling the
* contract function `swapWithApproval` for bridged USDC.e.
*/
approval?: {
tokenNonce: number,
},

/**
* The sender's nonce in the token contract, required when calling the
* contract function `transferWithPermit` for native USDC.
Expand Down
5 changes: 5 additions & 0 deletions src/assets/icons/usdc_dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/components/PolygonAddressInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class PolygonAddressInfo { // eslint-disable-line no-unused-vars
/**
* @param {string} address
* @param {string} [label]
* @param {'none' | 'usdc' | 'unknown'} [logo = 'none']
* @param {'none' | 'usdc' | 'usdc_dark' | 'unknown'} [logo = 'none']
*/
constructor(address, label, logo = 'none') {
this._address = address;
Expand Down Expand Up @@ -37,7 +37,7 @@ class PolygonAddressInfo { // eslint-disable-line no-unused-vars
$avatar.classList.add('unlabelled');
}
$el.appendChild($avatar);
} else if (this._logo === 'usdc' || this._logo === 'unknown') {
} else if (this._logo === 'usdc' || this._logo === 'usdc_dark' || this._logo === 'unknown') {
const $img = document.createElement('img');
$img.classList.add('logo');
$img.src = `../../assets/icons/${this._logo}.svg`;
Expand Down
2 changes: 2 additions & 0 deletions src/config/config.local.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ const CONFIG = { // eslint-disable-line no-unused-vars

NATIVE_USDC_CONTRACT_ADDRESS: '0x9999f7Fea5938fD3b1E26A12c3f2fb024e194f97',
NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '0x5D101A320547f8D640c44fDfe5d1f35224f00B8B', // v1

USDC_SWAP_CONTRACT_ADDRESS: '0x72e64Cff5cfFD4BFbC5b8d4fB081B33B9EE3e30e',
};
2 changes: 2 additions & 0 deletions src/config/config.mainnet.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ const CONFIG = { // eslint-disable-line no-unused-vars

NATIVE_USDC_CONTRACT_ADDRESS: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '0x3157d422cd1be13AC4a7cb00957ed717e648DFf2', // v1

USDC_SWAP_CONTRACT_ADDRESS: '',
};
2 changes: 2 additions & 0 deletions src/config/config.testnet.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ const CONFIG = { // eslint-disable-line no-unused-vars

NATIVE_USDC_CONTRACT_ADDRESS: '0x9999f7Fea5938fD3b1E26A12c3f2fb024e194f97',
NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '0x5D101A320547f8D640c44fDfe5d1f35224f00B8B', // v1

USDC_SWAP_CONTRACT_ADDRESS: '0x72e64Cff5cfFD4BFbC5b8d4fB081B33B9EE3e30e',
};
50 changes: 50 additions & 0 deletions src/lib/polygon/PolygonContractABIs.full.js.txt
Original file line number Diff line number Diff line change
Expand Up @@ -247,5 +247,55 @@ const PolygonContractABIsFull = { // eslint-disable-line no-unused-vars
'function withdrawToken(address token, uint256 amount, address target)',
'function wrappedChainToken() view returns (address)',
],

SWAP_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 registerSwapPool(address pool)",
"function registerToken(address token, address pool)",
"function registeredSwapPool(address) view returns (bool)",
"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 swap(address token, uint256 amount, address pool, uint256 targetAmount, uint256 fee)",
"function swapWithApproval(address token, uint256 amount, address pool, uint256 targetAmount, uint256 fee, uint256 approval, bytes32 sigR, bytes32 sigS, uint8 sigV)",
"function transferOwnership(address newOwner)",
"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 */
5 changes: 5 additions & 0 deletions src/lib/polygon/PolygonContractABIs.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,10 @@ const PolygonContractABIs = { // eslint-disable-line no-unused-vars
'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)',
],

SWAP_CONTRACT_ABI: [
'function swap(address token, uint256 amount, address pool, uint256 targetAmount, uint256 fee)',
'function swapWithApproval(address token, uint256 amount, address pool, uint256 targetAmount, uint256 fee, uint256 approval, bytes32 sigR, bytes32 sigS, uint8 sigV)',
],
};
/* eslint-enable max-len */
50 changes: 44 additions & 6 deletions src/request/sign-polygon-transaction/SignPolygonTransaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,24 @@ class SignPolygonTransaction {
const $sender = (this.$el.querySelector('.accounts .sender'));
if (request.description.name === 'refund') {
new PolygonAddressInfo(relayRequest.to, request.senderLabel, 'unknown').renderTo($sender);
} else if (request.description.name === 'swap' || request.description.name === 'swapWithApproval') {
new PolygonAddressInfo(relayRequest.from, 'USDC.e', 'usdc_dark').renderTo($sender);
} else {
new PolygonAddressInfo(relayRequest.from, request.keyLabel, 'usdc').renderTo($sender);
}

/** @type {HTMLLinkElement} */
const $recipient = (this.$el.querySelector('.accounts .recipient'));
const recipientAddress = /** @type {string} */ (request.description.args.target);
new PolygonAddressInfo(
recipientAddress,
request.description.name === 'refund' ? request.keyLabel : request.recipientLabel,
request.description.name === 'refund' ? 'usdc' : 'none',
).renderTo($recipient);
if (request.description.name === 'swap' || request.description.name === 'swapWithApproval') {
new PolygonAddressInfo(relayRequest.from, 'USDC', 'usdc').renderTo($recipient);
} else {
const recipientAddress = /** @type {string} */ (request.description.args.target);
new PolygonAddressInfo(
recipientAddress,
request.description.name === 'refund' ? request.keyLabel : request.recipientLabel,
request.description.name === 'refund' ? 'usdc' : 'none',
sisou marked this conversation as resolved.
Show resolved Hide resolved
).renderTo($recipient);
}

/** @type {HTMLDivElement} */
const $value = (this.$el.querySelector('#value'));
Expand Down Expand Up @@ -149,6 +155,38 @@ class SignPolygonTransaction {
]);
}

if (request.description.name === 'swapWithApproval') {
const { sigR, sigS, sigV } = await polygonKey.signUsdcApproval(
request.keyPath,
new ethers.Contract(
CONFIG.USDC_CONTRACT_ADDRESS,
PolygonContractABIs.USDC_CONTRACT_ABI,
sisou marked this conversation as resolved.
Show resolved Hide resolved
),
transferContract,
request.description.args.approval,
// Has been validated to be defined when function called is `swapWithApproval`
/** @type {{ tokenNonce: number }} */ (request.approval).tokenNonce,
request.request.from,
);

const swapContract = new ethers.Contract(
transferContract,
PolygonContractABIs.SWAP_CONTRACT_ABI,
);

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

const typedData = new OpenGSN.TypedRequestData(
CONFIG.POLYGON_CHAIN_ID,
transferContract,
Expand Down
41 changes: 35 additions & 6 deletions src/request/sign-polygon-transaction/SignPolygonTransactionApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) {
* KeyguardRequest.OpenGsnForwardRequest,
* PolygonTransferDescription
* | PolygonTransferWithPermitDescription
* | PolygonRefundDescription,
* | PolygonRefundDescription
* | PolygonSwapDescription
* | PolygonSwapWithApprovalDescription,
* ]}
*/
parseOpenGsnForwardRequest(request) {
Expand All @@ -68,7 +70,9 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) {
/**
* @type {PolygonTransferDescription
* | PolygonTransferWithPermitDescription
* | PolygonRefundDescription}
* | PolygonRefundDescription
* | PolygonSwapDescription
* | PolygonSwapWithApprovalDescription}
*/
let description;

Expand Down Expand Up @@ -106,19 +110,44 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) {
if (!['refund'].includes(description.name)) {
throw new Errors.InvalidRequestError('Requested Polygon contract method is invalid');
}
} else if (forwardRequest.to === CONFIG.USDC_SWAP_CONTRACT_ADDRESS) {
const usdcTransferContract = new ethers.Contract(
CONFIG.USDC_SWAP_CONTRACT_ADDRESS,
PolygonContractABIs.SWAP_CONTRACT_ABI,
);

/** @type {PolygonSwapDescription | PolygonSwapWithApprovalDescription} */
description = (usdcTransferContract.interface.parseTransaction({
data: forwardRequest.data,
value: forwardRequest.value,
}));

if (description.args.token !== CONFIG.USDC_CONTRACT_ADDRESS) {
throw new Errors.InvalidRequestError('Invalid USDC token contract in request data');
}

if (!['swap', 'swapWithApproval'].includes(description.name)) {
throw new Errors.InvalidRequestError('Requested Polygon contract method is invalid');
}
sisou marked this conversation as resolved.
Show resolved Hide resolved
} else {
throw new Errors.InvalidRequestError('request.to address is not allowed');
}

// Check that permit object exists when method is 'transferWithPermit', and unset for other methods.
if ((description.name === 'transferWithPermit') !== !!request.permit) {
throw new Errors.InvalidRequestError('`permit` object is only allowed for contract method '
+ '"transferWithPermit"');
}

// Check that amount exists when method is 'refund', and unset for other methods.
if ((description.name === 'refund') !== !!request.amount) {
throw new Errors.InvalidRequestError('`amount` is only allowed for contract method "refund"');
}

// Check that permit object exists when method is 'transferWithPermit', and unset for other methods.
if ((description.name === 'transferWithPermit') !== !!request.permit) {
throw new Errors.InvalidRequestError('`permit` object is only allowed for contract method '
+ '"transferWithPermit"');
// Check that approval object exists when method is 'swapWithApproval', and unset for other methods.
if ((description.name === 'swapWithApproval') !== !!request.approval) {
throw new Errors.InvalidRequestError('`approval` object is only allowed for contract method '
+ '"swapWithApproval"');
}

return [forwardRequest, description];
Expand Down
24 changes: 23 additions & 1 deletion types/Keyguard.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,26 @@ type PolygonRefundDescription = ethers.utils.TransactionDescription & {
readonly args: PolygonRefundArgs,
};

interface PolygonSwapArgs extends ReadonlyArray<any> {
sisou marked this conversation as resolved.
Show resolved Hide resolved
readonly token: string,
readonly amount: ethers.BigNumber,
readonly pool: string,
readonly targetAmount: ethers.BigNumber,
readonly fee: ethers.BigNumber,
}

type PolygonSwapDescription = ethers.utils.TransactionDescription & {
readonly name: 'swap',
readonly args: PolygonSwapArgs,
};

interface PolygonSwapWithApprovalArgs extends PolygonSwapArgs, PolygonUsdcApproval {}

type PolygonSwapWithApprovalDescription = ethers.utils.TransactionDescription & {
readonly name: 'swapWithApproval',
readonly args: PolygonSwapWithApprovalArgs,
};

type NimHtlcContents = {
refundAddress: string,
redeemAddress: string,
Expand Down Expand Up @@ -286,7 +306,9 @@ type Parsed<T extends KeyguardRequest.Request> =
KeyId2KeyInfo<KeyguardRequest.SignPolygonTransactionRequest>
& { description: PolygonTransferDescription
| PolygonTransferWithPermitDescription
| PolygonRefundDescription } :
| PolygonRefundDescription
| PolygonSwapDescription
| PolygonSwapWithApprovalDescription } :
T extends Is<T, KeyguardRequest.SignSwapRequestStandard> ?
KeyId2KeyInfo<ConstructSwap<KeyguardRequest.SignSwapRequestStandard>>
& { layout: KeyguardRequest.SignSwapRequestLayout } :
Expand Down
Loading