UNPKG

@kaiachain/ethers-ext

Version:
311 lines (310 loc) 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isGaslessSwap = exports.isGaslessApprove = exports.getSwapTx = exports.getApproveTx = exports.getAmountIn = exports.getMinAmountOut = exports.getAmountRepay = exports.getGaslessSwapRouter = void 0; const ethers_1 = require("ethers"); const GaslessSwapRouter_js_1 = require("./abi/GaslessSwapRouter.js"); const Registry_js_1 = require("./abi/Registry.js"); const txutil_js_1 = require("./txutil.js"); // GaslessSwapRouterAddress registry key // https://github.com/kaiachain/kaia/blob/v2.0.0/contracts/contracts/system_contracts/multicall/MultiCallContract.sol#L140 const GASLESS_SWAP_ROUTER_NAME = "GaslessSwapRouter"; const REGISTRY_ADDRESS = "0x0000000000000000000000000000000000000401"; // Using fixed constants to simplify amountRepay calculation. const GAS_LIMIT_LEND = 21000; const GAS_LIMIT_APPROVE = 100000; const GAS_LIMIT_SWAP = 500000; /** * Get the gasless swap router for the specified chain * @param provider The ethers provider * @param chainId The chain ID * @param address Override the address of the gasless swap router (optional) * @returns The gasless swap router contract */ async function getGaslessSwapRouter(provider, address) { let contractAddress; if (!address) { // Read from the KIP-149 registry. const registry = new ethers_1.ethers.Contract(REGISTRY_ADDRESS, Registry_js_1.RegistryAbi, provider); const addr = await registry.getActiveAddr(GASLESS_SWAP_ROUTER_NAME); if (addr === undefined || addr === null || addr === ethers_1.ethers.ZeroAddress) { throw new Error("GaslessSwapRouter not found in the registry"); } contractAddress = addr; } else { // Use the custom address. contractAddress = address; } return new ethers_1.ethers.Contract(contractAddress, GaslessSwapRouter_js_1.GaslessSwapRouterAbi.abi, provider); } exports.getGaslessSwapRouter = getGaslessSwapRouter; /** * Calculate the amount to repay based on whether approval is required and gas price * @param approveRequired Whether approval transaction is required * @param gasPrice Gas price in gkei * @returns The amount to repay */ function getAmountRepay(approveRequired, gasPrice) { const gasPriceBN = BigInt(gasPrice); const lendTxGas = BigInt(GAS_LIMIT_LEND); const approveTxGas = approveRequired ? BigInt(GAS_LIMIT_APPROVE) : BigInt(0); const swapTxGas = BigInt(GAS_LIMIT_SWAP); const R1 = gasPriceBN * lendTxGas; const R2 = gasPriceBN * approveTxGas; const R3 = gasPriceBN * swapTxGas; return R1 + R2 + R3; } exports.getAmountRepay = getAmountRepay; /** * Calculate the minimum amount out based on amount to repay, app transaction fee, and commission rate * @param amountRepay The amount to repay * @param appTxFee The application transaction fee * @param commissionRateBps The commission rate in basis points (e.g., 1000 = 10%) * @returns The minimum amount out */ function getMinAmountOut(amountRepay, appTxFee, commissionRateBps) { const appTxFeeBN = BigInt(appTxFee); const amountRepayBN = BigInt(amountRepay); const commissionRateBpsBN = BigInt(commissionRateBps); const denominator = BigInt(10000); if (commissionRateBpsBN < 0 || commissionRateBpsBN >= 10000) { throw new Error("Commission rate must be between 0 and 9999 basis points"); } // minAmountOut = appTxFee / (1 - commissionRate) + amountRepay // i.e. (amountOut - amountRepay) * (1 - commissionRate) >= appTxFee // because the swap output has to be enough to repay and pay the commission. const adjustedFee = appTxFeeBN * denominator / (denominator - commissionRateBpsBN); return adjustedFee + amountRepayBN; } exports.getMinAmountOut = getMinAmountOut; /** * Calculate the amount in based on minimum amount out and slippage * @param router The gasless swap router contract * @param tokenAddress The token address * @param minAmountOut The minimum amount out * @param slippageBps The slippage basis point (e.g., 50 basis points = 0.5%) * @returns The amount in */ async function getAmountIn(router, tokenAddress, minAmountOut, slippageBps) { const minAmountOutBN = BigInt(minAmountOut); const slippageBN = BigInt(slippageBps); const denominator = BigInt(10000); // minAmountIn = DEX.getAmountIn(tokenAddress, minAmountOut * (1 + slippage)) // Have DEX calculate the required input to produce minAmountOut plus slippage. const withSlippage = minAmountOutBN * (denominator + slippageBN) / denominator; const amountIn = await router.getAmountIn(tokenAddress, withSlippage); return BigInt(amountIn); } exports.getAmountIn = getAmountIn; /** * Generate a approve transaction * @param provider The ethers provider * @param fromAddress The sender address * @param tokenAddress The token address * @param routerAddress The router address * @param amount The amount to approve * @returns The approve transaction */ async function getApproveTx(provider, fromAddress, tokenAddress, routerAddress, gasPrice) { const tokenInterface = new ethers_1.ethers.Interface([ "function approve(address spender, uint256 amount) external returns (bool)" ]); const data = tokenInterface.encodeFunctionData("approve", [ routerAddress, ethers_1.MaxUint256.toString() ]); const nonce = await provider.getTransactionCount(fromAddress); return { type: 0, from: fromAddress, to: tokenAddress, nonce: nonce, gasLimit: GAS_LIMIT_APPROVE, gasPrice: gasPrice, value: 0n, data: data, }; } exports.getApproveTx = getApproveTx; /** * Generate an swap transaction * @param provider The ethers provider * @param fromAddress The sender address * @param tokenAddress The token address to swap * @param amountIn The amount to swap * @param minAmountOut The minimum amount out * @param amountRepay The amount to repay * @param isSingle Whether this is a single transaction (default: true) * @param deadline The deadline in seconds (default: 1800) * @returns The swap transaction */ async function getSwapTx(provider, fromAddress, tokenAddress, routerAddress, amountIn, minAmountOut, amountRepay, gasPrice, approveRequired = false, deadlineBuffer = 1800) { const latestBlock = await provider.getBlock("latest"); if (!latestBlock) { throw new Error("Failed to get latest block"); } const deadlineTimestamp = BigInt(latestBlock.timestamp) + BigInt(deadlineBuffer); let nonce = await provider.getTransactionCount(fromAddress); if (approveRequired) { nonce += 1; } const routerInterface = new ethers_1.ethers.Interface(GaslessSwapRouter_js_1.GaslessSwapRouterAbi.abi); const data = routerInterface.encodeFunctionData("swapForGas", [ tokenAddress, amountIn, minAmountOut, amountRepay, deadlineTimestamp ]); // Construct the transaction object const tx = { type: 0, from: fromAddress, to: routerAddress, nonce: nonce, gasLimit: GAS_LIMIT_SWAP, gasPrice: gasPrice, value: 0n, data: data, }; return tx; } exports.getSwapTx = getSwapTx; /** * Check if a transaction is a gasless approve transaction * @param provider The ethers provider * @param tx The transaction * @returns True if the transaction is a gasless approve transaction, false otherwise */ async function isGaslessApprove(provider, router, transactionOrRLP) { const routerAddress = await router.getAddress(); const tx = await (0, txutil_js_1.getTransactionRequest)(transactionOrRLP); if (!tx.from || !tx.to || !tx.nonce || !tx.data) { return { ok: false, error: "Invalid transaction" }; } // A1: GaslessApproveTx.to is a whitelisted ERC-20 token. const isTokenSupported = await router.isTokenSupported(tx.to); if (!isTokenSupported) { return { ok: false, error: "A1: Token not supported" }; } // A2: GaslessApproveTx.data is approve(spender, amount). let spender; let amount; try { const tokenInterface = new ethers_1.ethers.Interface([ "function approve(address spender, uint256 amount) external returns (bool)" ]); [spender, amount] = tokenInterface.decodeFunctionData("approve", tx.data); } catch (error) { return { ok: false, error: "A2: Invalid data" }; } // A3: spender is a whitelisted GaslessSwapRouter. if (spender.toLowerCase() !== routerAddress.toLowerCase()) { return { ok: false, error: "A3: Invalid spender" }; } // A4: amount is MaxUint256. if (BigInt(amount) !== ethers_1.MaxUint256) { return { ok: false, error: "A4: Invalid amount" }; } // A5: nonce is getNonce(tx.from). const expectedNonce = await provider.getTransactionCount(tx.from); if (BigInt(tx.nonce) !== BigInt(expectedNonce)) { return { ok: false, error: "A5: Invalid nonce" }; } return { ok: true }; } exports.isGaslessApprove = isGaslessApprove; /** * Check if transactions form a valid gasless swap * @param approveTxOrNull The approve transaction or null if not needed * @param transactionOrRLP The swap transaction * @param chainId The chain ID * @param provider The ethers provider * @returns True if the transactions form a valid gasless swap, false otherwise */ async function isGaslessSwap(provider, router, approveTxOrNull, transactionOrRLP) { const routerAddress = await router.getAddress(); const swapTx = await (0, txutil_js_1.getTransactionRequest)(transactionOrRLP); if (!swapTx.from || !swapTx.to || !swapTx.nonce || !swapTx.data) { return { ok: false, error: "Invalid transaction" }; } // S1: GaslessSwapTx.to is a whitelisted GaslessSwapRouter. if (swapTx.to.toLowerCase() !== routerAddress.toLowerCase()) { return { ok: false, error: "S1: Invalid router address" }; } // S2. GaslessSwapTx.data is swapForGas(token, amountIn, minAmountOut, amountRepay, deadline). let token; let amountIn; let minAmountOut; let amountRepay; let deadline; try { const routerInterface = new ethers_1.ethers.Interface(GaslessSwapRouter_js_1.GaslessSwapRouterAbi.abi); [token, amountIn, minAmountOut, amountRepay, deadline] = routerInterface.decodeFunctionData("swapForGas", swapTx.data); } catch (error) { return { ok: false, error: "S2: Invalid data" }; } // S3. token is a whitelisted ERC20 token. const isTokenSupported = await router.isTokenSupported(token); if (!isTokenSupported) { return { ok: false, error: "S3: Token not supported" }; } const senderNonce = await provider.getTransactionCount(swapTx.from); if (approveTxOrNull) { const isApprove = await isGaslessApprove(provider, router, approveTxOrNull); if (!isApprove.ok) { return isApprove; } const approveTx = await (0, txutil_js_1.getTransactionRequest)(approveTxOrNull); if (!approveTx.from || !approveTx.to || !approveTx.nonce || !approveTx.data) { return { ok: false, error: "Invalid transaction" }; } // SP1: GaslessApproveTx.to=token. if (!approveTx.to || approveTx.to.toLowerCase() !== token.toLowerCase()) { return { ok: false, error: "SP1: Invalid token" }; } // SP2: GaslessApproveTx.data.amount>=amountIn. let approveSpender; let approveAmount; try { const tokenInterface = new ethers_1.ethers.Interface([ "function approve(address spender, uint256 amount) external returns (bool)" ]); [approveSpender, approveAmount] = tokenInterface.decodeFunctionData("approve", approveTx.data); } catch (error) { return { ok: false, error: "A2: Invalid data" }; } if (approveAmount < amountIn) { return { ok: false, error: "SP2: Invalid amount" }; } // SP3: GaslessApproveTx.nonce+1 = tx.nonce = getNonce(tx.from)+1. if (approveTx.nonce !== senderNonce) { return { ok: false, error: "SP3: Invalid nonce" }; } if (swapTx.nonce !== senderNonce + 1) { return { ok: false, error: "SP3: Invalid nonce" }; } // SP4: amountRepay = CalcRepayAmount(GaslessApproveTx, GaslessSwapTx). const expectedAmountRepay = getAmountRepay(true, Number(swapTx.gasPrice)); if (BigInt(amountRepay) !== BigInt(expectedAmountRepay)) { console.log("amountRepay", amountRepay, expectedAmountRepay); return { ok: false, error: "SP4: Invalid amount repay" }; } return { ok: true }; } else { // SP3: tx.nonce = getNonce(tx.from). if (swapTx.nonce !== senderNonce) { return { ok: false, error: "SP3: Invalid nonce" }; } // SP4: amountRepay = CalcRepayAmount(GaslessSwapTx). const expectedAmountRepay = getAmountRepay(false, Number(swapTx.gasPrice)); if (BigInt(amountRepay) !== BigInt(expectedAmountRepay)) { console.log("amountRepay", amountRepay, expectedAmountRepay); return { ok: false, error: "SP4: Invalid amount repay" }; } return { ok: true }; } } exports.isGaslessSwap = isGaslessSwap;