Skip to content

Commit

Permalink
Merge pull request #484 from nimiq/native-usdc
Browse files Browse the repository at this point in the history
Support sending new native USDC
  • Loading branch information
sisou authored Jan 5, 2024
2 parents 6955d98 + 926582c commit 35d13b3
Show file tree
Hide file tree
Showing 10 changed files with 489 additions and 189 deletions.
10 changes: 9 additions & 1 deletion client/src/PublicRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,19 @@ export type PolygonTransactionInfo = {

/**
* The sender's nonce in the token contract, required when calling the
* contract function `transferWithApproval`.
* contract function `transferWithApproval` 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.
*/
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: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
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
};
299 changes: 299 additions & 0 deletions src/lib/polygon/PolygonContractABIs.full.js.txt

Large diffs are not rendered by default.

168 changes: 7 additions & 161 deletions src/lib/polygon/PolygonContractABIs.js

Large diffs are not rendered by default.

61 changes: 57 additions & 4 deletions src/lib/polygon/PolygonKey.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,52 @@ class PolygonKey { // eslint-disable-line no-unused-vars
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 this._signatureToParts(signature);
}

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,
);

return this._signatureToParts(signature);
}

/**
Expand Down Expand Up @@ -122,6 +163,18 @@ class PolygonKey { // eslint-disable-line no-unused-vars
return this._key;
}

/**
* @param {string} signature
* @returns {{sigR: string, sigS: string, sigV: number}}
*/
_signatureToParts(signature) {
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 };
}

/**
* @type {string}
*/
Expand Down
40 changes: 35 additions & 5 deletions src/request/sign-polygon-transaction/SignPolygonTransaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,25 @@ class SignPolygonTransaction {

const polygonKey = new PolygonKey(key);

// Has been validated to be an approved transfer contract address
const transferContract = request.request.to;

if (request.description.name === 'transferWithApproval') {
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 +151,38 @@ class SignPolygonTransaction {
]);
}

if (request.description.name === 'transferWithPermit') {
const { sigR, sigS, sigV } = await polygonKey.signUsdcPermit(
request.keyPath,
transferContract,
// `value` is the permit approval amount - the transaction value is called `amount`
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,
// `value` is the permit approval amount - the transaction value is called `amount` (above)
/* 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
76 changes: 58 additions & 18 deletions src/request/sign-polygon-transaction/SignPolygonTransactionApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) {
parsedRequest.keyInfo = await this.parseKeyId(request.keyId);
parsedRequest.keyLabel = /** @type {string} */ (this.parseLabel(request.keyLabel, false, 'keyLabel'));
parsedRequest.keyPath = this.parsePolygonPath(request.keyPath, 'keyPath');
[parsedRequest.request, parsedRequest.description] = this.parseOpenGsnForwardRequest(
request,
['transfer', 'transferWithApproval', 'refund'],
);
[parsedRequest.request, parsedRequest.description] = this.parseOpenGsnForwardRequest(request);
parsedRequest.relayData = this.parseOpenGsnRelayData(request.relayData);
parsedRequest.senderLabel = this.parseLabel(request.senderLabel); // Used for HTLC refunds
parsedRequest.recipientLabel = this.parseLabel(request.recipientLabel);
Expand All @@ -41,6 +38,15 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) {
),
};
}
if (request.permit !== undefined) {
parsedRequest.permit = {
tokenNonce: this.parsePositiveInteger(
request.permit.tokenNonce,
true,
'permit.tokenNonce',
),
};
}

return parsedRequest;
}
Expand All @@ -49,16 +55,23 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) {
/**
*
* @param {KeyguardRequest.PolygonTransactionInfo} request
* @param {Array<'transfer' | 'transferWithApproval' | 'refund'>} allowedMethods
* @returns {[
* KeyguardRequest.OpenGsnForwardRequest,
* PolygonTransferDescription | PolygonTransferWithApprovalDescription | PolygonRefundDescription,
* PolygonTransferDescription
* | PolygonTransferWithApprovalDescription
* | PolygonTransferWithPermitDescription
* | PolygonRefundDescription,
* ]}
*/
parseOpenGsnForwardRequest(request, allowedMethods) {
parseOpenGsnForwardRequest(request) {
const forwardRequest = this.parseOpenGsnForwardRequestRoot(request.request);

/** @type {PolygonTransferDescription | PolygonTransferWithApprovalDescription | PolygonRefundDescription} */
/**
* @type {PolygonTransferDescription
* | PolygonTransferWithApprovalDescription
* | PolygonTransferWithPermitDescription
* | PolygonRefundDescription}
*/
let description;

if (forwardRequest.to === CONFIG.USDC_TRANSFER_CONTRACT_ADDRESS) {
Expand All @@ -72,6 +85,33 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) {
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 (!['transfer', 'transferWithApproval'].includes(description.name)) {
throw new Errors.InvalidRequestError('Requested Polygon contract method is invalid');
}
} else if (forwardRequest.to === CONFIG.NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS) {
const nativeUsdcTransferContract = new ethers.Contract(
CONFIG.NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS,
PolygonContractABIs.NATIVE_USDC_TRANSFER_CONTRACT_ABI,
);

/** @type {PolygonTransferDescription | PolygonTransferWithPermitDescription} */
description = (nativeUsdcTransferContract.interface.parseTransaction({
data: forwardRequest.data,
value: forwardRequest.value,
}));

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

if (!['transfer', 'transferWithPermit'].includes(description.name)) {
throw new Errors.InvalidRequestError('Requested Polygon contract method is invalid');
}
} else if (forwardRequest.to === CONFIG.USDC_HTLC_CONTRACT_ADDRESS) {
const usdcHtlcContract = new ethers.Contract(
CONFIG.USDC_HTLC_CONTRACT_ADDRESS,
Expand All @@ -83,18 +123,12 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) {
data: forwardRequest.data,
value: forwardRequest.value,
}));
} else {
throw new Errors.InvalidRequestError('request.to address is not allowed');
}

if (!allowedMethods.includes(description.name)) {
throw new Errors.InvalidRequestError('Requested Polygon contract method is invalid');
}

if (description.name === 'transfer' || description.name === 'transferWithApproval') {
if (description.args.token !== CONFIG.USDC_CONTRACT_ADDRESS) {
throw new Errors.InvalidRequestError('Invalid USDC token contract in request data');
if (!['refund'].includes(description.name)) {
throw new Errors.InvalidRequestError('Requested Polygon contract method is invalid');
}
} else {
throw new Errors.InvalidRequestError('request.to address is not allowed');
}

// Check that amount exists when method is 'refund', and unset for other methods.
Expand All @@ -108,6 +142,12 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) {
+ '"transferWithApproval"');
}

// 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"');
}

return [forwardRequest, description];
}

Expand Down
15 changes: 15 additions & 0 deletions types/Keyguard.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ interface PolygonUsdcApproval {
readonly sigV: ethers.BigNumber,
}

interface PolygonUsdcPermit {
readonly value: ethers.BigNumber, // amount to be approved
readonly sigR: string,
readonly sigS: string,
readonly sigV: ethers.BigNumber,
}

interface PolygonTransferArgs extends ReadonlyArray<any> {
readonly token: string,
readonly amount: ethers.BigNumber,
Expand All @@ -81,6 +88,13 @@ type PolygonTransferWithApprovalDescription = ethers.utils.TransactionDescriptio
readonly args: PolygonTransferWithApprovalArgs,
};

interface PolygonTransferWithPermitArgs extends PolygonTransferArgs, PolygonUsdcPermit {}

type PolygonTransferWithPermitDescription = ethers.utils.TransactionDescription & {
readonly name: 'transferWithPermit',
readonly args: PolygonTransferWithPermitArgs,
};

interface PolygonOpenArgs extends ReadonlyArray<any> {
readonly id: string,
readonly token: string,
Expand Down Expand Up @@ -279,6 +293,7 @@ type Parsed<T extends KeyguardRequest.Request> =
KeyId2KeyInfo<KeyguardRequest.SignPolygonTransactionRequest>
& { description: PolygonTransferDescription
| PolygonTransferWithApprovalDescription
| PolygonTransferWithPermitDescription
| PolygonRefundDescription } :
T extends Is<T, KeyguardRequest.SignSwapRequestStandard> ?
KeyId2KeyInfo<ConstructSwap<KeyguardRequest.SignSwapRequestStandard>>
Expand Down

0 comments on commit 35d13b3

Please sign in to comment.