Skip to content

Commit

Permalink
Switch to native USDC HTLC for swaps
Browse files Browse the repository at this point in the history
  • Loading branch information
sisou committed Mar 4, 2024
1 parent 5a7d9ff commit 679a900
Show file tree
Hide file tree
Showing 15 changed files with 239 additions and 130 deletions.
19 changes: 13 additions & 6 deletions client/src/PublicRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,11 @@ export type SignSwapRequestCommon = SimpleRequest & {
refundKeyPath: string, // To validate that we own the HTLC script's refund address
}>
) | (
{type: 'USDC'}
& Omit<PolygonTransactionInfo, 'amount'>
{type: 'USDC_MATIC'}
& Omit<PolygonTransactionInfo,
| 'approval' // HTLC opening for native USDC uses `permit`, not `approval`
| 'amount' // Not used for HTLC opening - only for redeem and refund
>
) | (
{type: 'EUR'}
& {
Expand Down Expand Up @@ -320,8 +323,12 @@ export type SignSwapRequestCommon = SimpleRequest & {
output: BitcoinTransactionChangeOutput,
}
) | (
{type: 'USDC'}
& Omit<PolygonTransactionInfo, 'approval' | 'amount'>
{type: 'USDC_MATIC'}
& Omit<PolygonTransactionInfo,
| 'approval' // Not needed for redeeming
| 'permit' // Not needed for redeeming
| 'amount' // Overwritten from optional to required
>
& {
amount: number,
}
Expand Down Expand Up @@ -401,7 +408,7 @@ export type SignSwapTransactionsRequest = {
type: 'BTC',
htlcScript: Uint8Array,
} | {
type: 'USDC',
type: 'USDC_MATIC',
htlcData: string,
} | {
type: 'EUR',
Expand All @@ -419,7 +426,7 @@ export type SignSwapTransactionsRequest = {
transactionHash: string,
outputIndex: number;
} | {
type: 'USDC',
type: 'USDC_MATIC',
hash: string,
timeout: number,
htlcId: string,
Expand Down
6 changes: 3 additions & 3 deletions src/components/BalanceDistributionBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/* global CryptoUtils */

/** @typedef {{address: string, balance: number, active: boolean, newBalance: number}} Segment */
/** @typedef {'NIM' | 'BTC' | 'USDC' | 'EUR'} Asset */
/** @typedef {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} Asset */

class BalanceDistributionBar { // eslint-disable-line no-unused-vars
/**
Expand Down Expand Up @@ -34,7 +34,7 @@ class BalanceDistributionBar { // eslint-disable-line no-unused-vars
newBalance: CryptoUtils.unitsToCoins(leftAsset, segment.newBalance) * leftFiatRate,
backgroundClass: leftAsset === 'NIM'
? LoginFileConfig[IqonHash.getBackgroundColorIndex(segment.address)].className
: leftAsset.toLowerCase(),
: CryptoUtils.assetToCurrency(leftAsset),
active: segment.active,
}));

Expand All @@ -43,7 +43,7 @@ class BalanceDistributionBar { // eslint-disable-line no-unused-vars
newBalance: CryptoUtils.unitsToCoins(rightAsset, segment.newBalance) * rightFiatRate,
backgroundClass: rightAsset === 'NIM'
? LoginFileConfig[IqonHash.getBackgroundColorIndex(segment.address)].className
: rightAsset.toLowerCase(),
: CryptoUtils.assetToCurrency(rightAsset),
active: segment.active,
}));

Expand Down
12 changes: 6 additions & 6 deletions src/components/SwapFeesTooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,17 @@ class SwapFeesTooltip { // eslint-disable-line no-unused-vars
}

// Show USDC fees next
if (fundTx.type === 'USDC' || redeemTx.type === 'USDC') {
const myFee = fundTx.type === 'USDC'
if (fundTx.type === 'USDC_MATIC' || redeemTx.type === 'USDC_MATIC') {
const myFee = fundTx.type === 'USDC_MATIC'
? fundTx.description.args.fee.toNumber()
: redeemTx.type === 'USDC'
: redeemTx.type === 'USDC_MATIC'
? redeemTx.description.args.fee.toNumber()
: 0;

const theirFee = fundTx.type === 'USDC' ? fundFees.redeeming : redeemFees.funding;
const theirFee = fundTx.type === 'USDC_MATIC' ? fundFees.redeeming : redeemFees.funding;

const fiatRate = fundTx.type === 'USDC' ? fundingFiatRate : redeemingFiatRate;
const fiatFee = CryptoUtils.unitsToCoins('USDC', myFee + theirFee) * fiatRate;
const fiatRate = fundTx.type === 'USDC_MATIC' ? fundingFiatRate : redeemingFiatRate;
const fiatFee = CryptoUtils.unitsToCoins('USDC_MATIC', myFee + theirFee) * fiatRate;

const rows = this._createUsdcLine(fiatFee, fiatCurrency);
this.$tooltip.appendChild(rows[0]);
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 @@ -18,10 +18,12 @@ const CONFIG = { // eslint-disable-line no-unused-vars

POLYGON_CHAIN_ID: 80001,
BRIDGED_USDC_CONTRACT_ADDRESS: '0x0FA8781a83E46826621b3BC094Ea2A0212e71B23',
/** @deprecated */
BRIDGED_USDC_HTLC_CONTRACT_ADDRESS: '0x2EB7cd7791b947A25d629219ead941fCd8f364BF',

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

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

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

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

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

USDC_SWAP_CONTRACT_ADDRESS: '0xf4a619F6561CeE543BDa9BBA0cAC68758B607714',
};
56 changes: 56 additions & 0 deletions src/lib/polygon/PolygonContractABIs.full.js.txt
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,62 @@ const PolygonContractABIsFull = { // eslint-disable-line no-unused-vars
'function wrappedChainToken() view returns (address)',
],

NATIVE_USDC_HTLC_CONTRACT_ABI: [
'constructor()',
'event DomainRegistered(bytes32 indexed domainSeparator, bytes domainValue)',
'event Open(bytes32 indexed id, address token, uint256 amount, address recipient, bytes32 hash, uint256 timeout)',
'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)',
'event Redeem(bytes32 indexed id, bytes32 secret)',
'event Refund(bytes32 indexed id)',
'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 htlcs(bytes32) view returns (address token, uint256 amount, address refund, address recipient, bytes32 hash, uint256 timeout)',
'function isTrustedForwarder(address forwarder) view returns (bool)',
'function open(bytes32 id, address token, uint256 amount, address refundAddress, address recipientAddress, bytes32 hash, uint256 timeout, uint256 fee)',
'function openWithPermit(bytes32 id, address token, uint256 amount, address refundAddress, address recipientAddress, bytes32 hash, uint256 timeout, uint256 fee, uint256 value, bytes32 sigR, bytes32 sigS, uint8 sigV)',
'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 redeem(bytes32 id, address target, bytes32 secret, uint256 fee)',
'function redeemWithSecretInData(bytes32 id, address target, uint256 fee)',
'function refund(bytes32 id, address target, uint256 fee)',
'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 relayWithoutGsn(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, address relay)',
'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 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)',
],

SWAP_CONTRACT_ABI: [
"constructor()",
"event DomainRegistered(bytes32 indexed domainSeparator, bytes domainValue)",
Expand Down
12 changes: 8 additions & 4 deletions src/lib/polygon/PolygonContractABIs.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ const PolygonContractABIs = { // eslint-disable-line no-unused-vars
],

BRIDGED_USDC_HTLC_CONTRACT_ABI: [
'function open(bytes32 id, address token, uint256 amount, address refundAddress, address recipientAddress, bytes32 hash, uint256 timeout, uint256 fee)',
'function openWithApproval(bytes32 id, address token, uint256 amount, address refundAddress, address recipientAddress, bytes32 hash, uint256 timeout, uint256 fee, uint256 approval, bytes32 sigR, bytes32 sigS, uint8 sigV)',
'function redeem(bytes32 id, address target, bytes32 secret, uint256 fee)',
'function redeemWithSecretInData(bytes32 id, address target, uint256 fee)',
'function refund(bytes32 id, address target, uint256 fee)',
],

Expand All @@ -19,6 +15,14 @@ const PolygonContractABIs = { // eslint-disable-line no-unused-vars
'function transferWithPermit(address token, uint256 amount, address target, uint256 fee, uint256 value, bytes32 sigR, bytes32 sigS, uint8 sigV)',
],

NATIVE_USDC_HTLC_CONTRACT_ABI: [
'function open(bytes32 id, address token, uint256 amount, address refundAddress, address recipientAddress, bytes32 hash, uint256 timeout, uint256 fee)',
'function openWithPermit(bytes32 id, address token, uint256 amount, address refundAddress, address recipientAddress, bytes32 hash, uint256 timeout, uint256 fee, uint256 value, bytes32 sigR, bytes32 sigS, uint8 sigV)',
'function redeem(bytes32 id, address target, bytes32 secret, uint256 fee)',
'function redeemWithSecretInData(bytes32 id, address target, uint256 fee)',
'function refund(bytes32 id, address target, uint256 fee)',
],

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)',
Expand Down
22 changes: 18 additions & 4 deletions src/lib/swap/CryptoUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,45 @@

class CryptoUtils { // eslint-disable-line no-unused-vars
/**
* @param {'NIM' | 'BTC' | 'USDC' | 'EUR'} asset
* @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} asset
* @param {number} units
* @returns {number}
*/
static unitsToCoins(asset, units) {
switch (asset) {
case 'NIM': return Nimiq.Policy.lunasToCoins(units);
case 'BTC': return BitcoinUtils.satoshisToCoins(units);
case 'USDC': return PolygonUtils.unitsToCoins(units);
case 'USDC_MATIC': return PolygonUtils.unitsToCoins(units);
case 'EUR': return EuroUtils.centsToCoins(units);
default: throw new Error(`Invalid asset ${asset}`);
}
}

/**
* @param {'NIM' | 'BTC' | 'USDC' | 'EUR'} asset
* @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} asset
* @returns {number}
*/
static assetDecimals(asset) {
switch (asset) {
case 'NIM': return Math.log10(Nimiq.Policy.LUNAS_PER_COIN);
case 'BTC': return Math.log10(BitcoinConstants.SATOSHIS_PER_COIN);
case 'USDC': return Math.log10(PolygonConstants.UNITS_PER_COIN);
case 'USDC_MATIC': return Math.log10(PolygonConstants.UNITS_PER_COIN);
case 'EUR': return Math.log10(EuroConstants.CENTS_PER_COIN);
default: throw new Error(`Invalid asset ${asset}`);
}
}

/**
* @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} asset
* @returns {'nim' | 'btc' | 'usdc' | 'eur'}
*/
static assetToCurrency(asset) {
switch (asset) {
case 'NIM': return 'nim';
case 'BTC': return 'btc';
case 'USDC_MATIC': return 'usdc';
case 'EUR': return 'eur';
default: throw new Error(`Invalid asset ${asset}`);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,14 @@ class SignPolygonTransaction {
]);
}

if (request.description.name === 'refund') {
const derivedAddress = polygonKey.deriveAddress(request.keyPath);
if (request.description.args.target !== derivedAddress) {
reject(new Errors.InvalidRequestError('Refund target does not match derived address'));
return;
}
}

const typedData = new OpenGSN.TypedRequestData(
CONFIG.POLYGON_CHAIN_ID,
transferContract,
Expand Down
22 changes: 20 additions & 2 deletions src/request/sign-polygon-transaction/SignPolygonTransactionApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,21 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) {
if (!['transfer', 'transferWithPermit'].includes(description.name)) {
throw new Errors.InvalidRequestError('Requested Polygon contract method is invalid');
}
} else if (forwardRequest.to === CONFIG.NATIVE_USDC_HTLC_CONTRACT_ADDRESS) {
const usdcHtlcContract = new ethers.Contract(
CONFIG.NATIVE_USDC_HTLC_CONTRACT_ADDRESS,
PolygonContractABIs.NATIVE_USDC_HTLC_CONTRACT_ABI,
);

/** @type {PolygonRefundDescription} */
description = (usdcHtlcContract.interface.parseTransaction({
data: forwardRequest.data,
value: forwardRequest.value,
}));

if (!['refund'].includes(description.name)) {
throw new Errors.InvalidRequestError('Requested Polygon contract method is invalid');
}
} else if (forwardRequest.to === CONFIG.BRIDGED_USDC_HTLC_CONTRACT_ADDRESS) {
const usdcHtlcContract = new ethers.Contract(
CONFIG.BRIDGED_USDC_HTLC_CONTRACT_ADDRESS,
Expand Down Expand Up @@ -137,9 +152,12 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) {
const targetAmount = /** @type {PolygonSwapDescription | PolygonSwapWithApprovalDescription} */ (description) // eslint-disable-line max-len
.args
.targetAmount;
if (targetAmount.lt(inputAmount.mul(99).div(100))) {
// Allow 1% slippage for swaps on Polygon mainnet, but up to 5% for testnet
const maxTargetAmountSlippage = CONFIG.POLYGON_CHAIN_ID === 137 ? 1 : 5;
const minTargetAmount = inputAmount.mul(100 - maxTargetAmountSlippage).div(100);
if (targetAmount.lt(minTargetAmount)) {
throw new Errors.InvalidRequestError(
'Requested Polygon swap `targetAmount` more than 1% lower than the input `amount`',
'Requested USDC swap `targetAmount` is too low',
);
}
} else {
Expand Down
Loading

0 comments on commit 679a900

Please sign in to comment.