diff --git a/package.json b/package.json index 501882e..793e234 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@babel/runtime": "^7.21.0", "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/providers": "^5.7.2", - "@orionprotocol/contracts": "1.9.0", + "@orionprotocol/contracts": "1.16.1", "bignumber.js": "^9.1.1", "bson-objectid": "^2.0.4", "buffer": "^6.0.3", diff --git a/src/utils/arrayHelpers.ts b/src/utils/arrayHelpers.ts new file mode 100644 index 0000000..fc583e2 --- /dev/null +++ b/src/utils/arrayHelpers.ts @@ -0,0 +1,29 @@ +declare global { + interface Array { + get(index: number): T; + last(): T + first(): T + } +} + +if (!Array.prototype.get) { + Array.prototype.get = function (this: T[], index: number): T { + const value = this.at(index); + if (value === undefined) { + throw new Error(`Element at index ${index} is undefined. Array: ${this}`) + } + return value + } +} + +if (!Array.prototype.last) { + Array.prototype.last = function (this: T[]): T { + return this.get(this.length - 1) + } +} + +if (!Array.prototype.first) { + Array.prototype.first = function (this: T[]): T { + return this.get(0) + } +} \ No newline at end of file diff --git a/src/utils/generateSwapCalldata.ts b/src/utils/generateSwapCalldata.ts new file mode 100644 index 0000000..b6ba623 --- /dev/null +++ b/src/utils/generateSwapCalldata.ts @@ -0,0 +1,293 @@ +import type { ExchangeWithGenericSwap } from '@orionprotocol/contracts/lib/ethers-v5/Exchange.js'; +import { UniswapV3Pool__factory, ERC20__factory, SwapExecutor__factory, CurveRegistry__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js'; +import { BigNumber, ethers } from 'ethers'; +import { concat, defaultAbiCoder, type BytesLike } from 'ethers/lib/utils.js'; + +type Factory = "UniswapV2" | "UniswapV3" | "Curve" | "OrionV2" | "OrionV3" + +export type SwapInfo = { + pool: string, + assetIn: string, + assetOut: string, + factory: Factory +} + +type CallParams = { + isMandatory?: boolean, + target?: string, + gaslimit?: BigNumber, + value?: BigNumber +} + +export default async function generateSwapCalldata( + amount: string, + minReturnAmount: string, + receiverAddress: string, + exchangeAddress: string, + executorAddress: string, + path: SwapInfo[], + weth: string, + curveRegistry: string, + provider: ethers.providers.JsonRpcProvider +): Promise<{ calldata: string, swapDescription: ExchangeWithGenericSwap.SwapDescriptionStruct }> { + if (path == undefined || path.length == 0) { + throw new Error(`Empty path`); + } + const factory = path.first().factory + + if (!path.every(e => e.factory === factory)) { + throw new Error(`Supporting only swaps with single factory`); + } + + const swapDescription: ExchangeWithGenericSwap.SwapDescriptionStruct = { + srcToken: path.first().assetIn, + dstToken: path.last().assetOut, + srcReceiver: executorAddress, + dstReceiver: receiverAddress, + amount: amount, + minReturnAmount: minReturnAmount, + flags: 0 + } + let calldata: string + switch (factory) { + case "OrionV2": { + swapDescription.srcReceiver = path.first().pool + calldata = await generateUni2Calls(exchangeAddress, path); + break; + } + case "UniswapV2": { + swapDescription.srcReceiver = path.first().pool + calldata = await generateUni2Calls(exchangeAddress, path); + break; + } + case "UniswapV3": { + calldata = await generateUni3Calls(amount, exchangeAddress, weth, path, provider) + break; + } + case "OrionV3": { + calldata = await generateOrion3Calls(amount, exchangeAddress, weth, path, provider) + break; + } + case "Curve": { + calldata = await generateCurveStableSwapCalls(amount, exchangeAddress, executorAddress, path, provider, curveRegistry); + break; + } + default: { + throw new Error(`Factory ${factory} is not supported`) + } + } + return { swapDescription, calldata } +} + + + +export async function generateUni2Calls( + exchangeAddress: string, + path: SwapInfo[] +) { + const executorInterface = SwapExecutor__factory.createInterface() + const calls: BytesLike[] = [] + if (path.length > 1) { + for (let i = 0; i < path.length - 1; ++i) { + const currentSwap = path.get(i) + const nextSwap = path.get(i + 1) + + const calldata = executorInterface.encodeFunctionData("swapUniV2", [ + currentSwap.pool, + currentSwap.assetIn, + currentSwap.assetOut, + defaultAbiCoder.encode(["uint256"], [concat(["0x03", nextSwap.pool])]), + ] + ) + calls.push(addCallParams(calldata)) + } + } + const lastSwap = path.last(); + const calldata = executorInterface.encodeFunctionData("swapUniV2", [ + lastSwap.pool, + lastSwap.assetIn, + lastSwap.assetOut, + defaultAbiCoder.encode(["uint256"], [concat(["0x03", exchangeAddress])]), + ]) + calls.push(addCallParams(calldata)) + + return generateCalls(calls) +} + +export async function generateUni3Calls( + amount: string, + exchangeAddress: string, + weth: string, + path: SwapInfo[], + provider: ethers.providers.JsonRpcProvider +) { + const encodedPools: BytesLike[] = [] + for (const swap of path) { + const pool = UniswapV3Pool__factory.connect(swap.pool, provider) + const token0 = await pool.token0() + const zeroForOne = token0 === swap.assetIn + const unwrapWETH = swap.assetOut === ethers.constants.AddressZero + if (unwrapWETH) { + swap.assetOut = weth + } + + let encodedPool = ethers.utils.solidityPack(["uint256"], [pool.address]) + encodedPool = ethers.utils.hexDataSlice(encodedPool, 1) + let firstByte = 0 + if (unwrapWETH) firstByte += 32 + if (!zeroForOne) firstByte += 128 + const encodedFirstByte = ethers.utils.solidityPack(["uint8"], [firstByte]) + encodedPool = ethers.utils.hexlify(ethers.utils.concat([encodedFirstByte, encodedPool])) + encodedPools.push(encodedPool) + } + const executorInterface = SwapExecutor__factory.createInterface() + let calldata = executorInterface.encodeFunctionData("uniswapV3SwapTo", [encodedPools, exchangeAddress, amount]) + calldata = addCallParams(calldata) + + return generateCalls([calldata]) +} + +export async function generateOrion3Calls( + amount: string, + exchangeAddress: string, + weth: string, + path: SwapInfo[], + provider: ethers.providers.JsonRpcProvider +) { + const encodedPools: BytesLike[] = [] + for (const swap of path) { + const pool = UniswapV3Pool__factory.connect(swap.pool, provider) + const token0 = await pool.token0() + const zeroForOne = token0 === swap.assetIn + const unwrapWETH = swap.assetOut === ethers.constants.AddressZero + if (unwrapWETH) { + swap.assetOut = weth + } + + let encodedPool = ethers.utils.solidityPack(["uint256"], [pool.address]) + encodedPool = ethers.utils.hexDataSlice(encodedPool, 1) + let firstByte = 0 + if (unwrapWETH) firstByte += 32 + if (!zeroForOne) firstByte += 128 + const encodedFirstByte = ethers.utils.solidityPack(["uint8"], [firstByte]) + encodedPool = ethers.utils.hexlify(ethers.utils.concat([encodedFirstByte, encodedPool])) + encodedPools.push(encodedPool) + } + const executorInterface = SwapExecutor__factory.createInterface() + let calldata = executorInterface.encodeFunctionData("orionV3SwapTo", [encodedPools, exchangeAddress, amount]) + calldata = addCallParams(calldata) + + return generateCalls([calldata]) +} + +export async function generateCurveStableSwapCalls( + amount: string, + exchangeAddress: string, + executorAddress: string, + path: SwapInfo[], + provider: ethers.providers.JsonRpcProvider, + curveRegistry: string +) { + if (path.length > 1) { + throw new Error("Supporting only single stable swap on curve") + } + const executorInterface = SwapExecutor__factory.createInterface() + const registry = CurveRegistry__factory.connect(curveRegistry, provider) + + const swap = path.first() + const firstToken = ERC20__factory.connect(swap.assetIn, provider) + const { pool, assetIn, assetOut } = swap + const [i, j,] = await registry.get_coin_indices(pool, assetIn, assetOut) + + const executorAllowance = await firstToken.allowance(executorAddress, swap.pool) + const calls: BytesLike[] = [] + if (executorAllowance.lt(amount)) { + const calldata = addCallParams( + executorInterface.encodeFunctionData("safeApprove", [ + swap.assetIn, + swap.pool, + ethers.constants.MaxUint256 + ]) + ) + calls.push(calldata) + } + let calldata = executorInterface.encodeFunctionData("curveSwapStableAmountIn", [ + pool, + assetOut, + i, + j, + amount, + 0, + exchangeAddress + ]) + + calldata = addCallParams(calldata) + calls.push(calldata) + + return generateCalls(calls) +} + +export function addCallParams( + calldata: BytesLike, + callParams?: CallParams +) { + let firstByte = 0 + if (callParams) { + if (callParams.value !== undefined) { + firstByte += 16 + const encodedValue = ethers.utils.solidityPack(["uint128"], [callParams.value]) + calldata = ethers.utils.hexlify(ethers.utils.concat([encodedValue, calldata])) + } + if (callParams.target !== undefined) { + firstByte += 32 + const encodedAddress = ethers.utils.solidityPack(["address"], [callParams.target]) + calldata = ethers.utils.hexlify(ethers.utils.concat([encodedAddress, calldata])) + } + if (callParams.gaslimit !== undefined) { + firstByte += 64 + const encodedGaslimit = ethers.utils.solidityPack(["uint32"], [callParams.gaslimit]) + calldata = ethers.utils.hexlify(ethers.utils.concat([encodedGaslimit, calldata])) + } + if (callParams.isMandatory !== undefined) firstByte += 128 + } + + const encodedFirstByte = ethers.utils.solidityPack(["uint8"], [firstByte]) + calldata = ethers.utils.hexlify(ethers.utils.concat([encodedFirstByte, calldata])) + return calldata +} + + +export async function generateCalls(calls: BytesLike[]) { + const executorInterface = SwapExecutor__factory.createInterface() + return "0x" + executorInterface.encodeFunctionData("func_70LYiww", [ethers.constants.AddressZero, calls]).slice(74) +} + +declare global { + interface Array { + get(index: number): T; + last(): T + first(): T + } +} + +if (!Array.prototype.get) { + Array.prototype.get = function (this: T[], index: number): T { + const value = this.at(index); + if (value === undefined) { + throw new Error(`Element at index ${index} is undefined. Array: ${this}`) + } + return value + } +} + +if (!Array.prototype.last) { + Array.prototype.last = function (this: T[]): T { + return this.get(this.length - 1) + } +} + +if (!Array.prototype.first) { + Array.prototype.first = function (this: T[]): T { + return this.get(0) + } +} \ No newline at end of file