diff --git a/package.json b/package.json index cb67fcb9..b4e16a95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aperture_finance/uniswap-v3-automation-sdk", - "version": "1.12.0", + "version": "1.13.0", "description": "SDK for Aperture's Uniswap V3 automation platform", "author": "Aperture Finance ", "license": "MIT", diff --git a/test/hardhat/hardhat.test.ts b/test/hardhat/hardhat.test.ts index f590ec89..91b726ac 100644 --- a/test/hardhat/hardhat.test.ts +++ b/test/hardhat/hardhat.test.ts @@ -74,6 +74,8 @@ import { PositionDetails, checkPositionApprovalStatus, computeOperatorApprovalSlot, + estimateRebalanceGas, + estimateReinvestGas, generateAccessList, generatePriceConditionFromTokenValueProportion, generateTypedDataForPermit, @@ -124,6 +126,108 @@ async function resetFork(testClient: TestClient) { }); } +describe('Estimate gas tests', function () { + async function estimateRebalanceGasWithFrom(from: Address | undefined) { + const blockNumber = 17975698n; + const publicClient = createPublicClient({ + chain: mainnet, + transport: http( + `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, + ), + }); + const token0 = WBTC_ADDRESS; + const token1 = WETH_ADDRESS; + const fee = FeeAmount.MEDIUM; + const amount0Desired = 100000000n; + const amount1Desired = 1000000000000000000n; + const pool = await getPool( + token0, + token1, + fee, + chainId, + undefined, + blockNumber, + ); + const mintParams = { + token0: token0 as Address, + token1: token1 as Address, + fee, + tickLower: nearestUsableTick( + pool.tickCurrent - 10 * pool.tickSpacing, + pool.tickSpacing, + ), + tickUpper: nearestUsableTick( + pool.tickCurrent + 10 * pool.tickSpacing, + pool.tickSpacing, + ), + amount0Desired, + amount1Desired, + amount0Min: BigInt(0), + amount1Min: BigInt(0), + recipient: eoa as Address, + deadline: BigInt(Math.floor(Date.now() / 1000 + 60 * 30)), + }; + const gas = await estimateRebalanceGas( + chainId, + publicClient, + from, + eoa, + mintParams, + 4n, + undefined, + undefined, + blockNumber, + ); + return gas; + } + + async function estimateReinvestGasWithFrom(from: Address | undefined) { + const blockNumber = 17975698n; + const publicClient = createPublicClient({ + chain: mainnet, + transport: http( + `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, + ), + }); + const amount0Desired = 100000n; + const amount1Desired = 1000000000000000n; + const gas = await estimateReinvestGas( + chainId, + publicClient, + from, + eoa, + 4n, + BigInt(Math.floor(Date.now() / 1000 + 60 * 30)), + amount0Desired, + amount1Desired, + BigInt(0), + '0x', + blockNumber, + ); + return gas; + } + + it('Test estimateRebalanceGas with owner', async function () { + const gas = await estimateRebalanceGasWithFrom(eoa); + expect(gas).to.equal(775010n); + }); + + it('Test estimateRebalanceGas with whale', async function () { + const gas = await estimateRebalanceGasWithFrom(undefined); + expect(gas).to.equal(777510n); + }); + + it('Test estimateReinvestGas with owner', async function () { + const gas = await estimateReinvestGasWithFrom(eoa); + expect(gas).to.equal(528206n); + }); + + it('Test estimateReinvestGas with whale', async function () { + const gas = await estimateReinvestGasWithFrom(undefined); + expect(gas).to.equal(528206n); + }); +}); + describe('State overrides tests', function () { it('Test computeOperatorApprovalSlot', async function () { const testClient = await hre.viem.getTestClient(); diff --git a/viem/automan.ts b/viem/automan.ts index 968ab882..9c7172e6 100644 --- a/viem/automan.ts +++ b/viem/automan.ts @@ -11,6 +11,7 @@ import { getContract, hexToSignature, } from 'viem'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { getChainInfo } from '../chain'; import { ApertureSupportedChainId, PermitInfo } from '../interfaces'; @@ -20,10 +21,12 @@ import { } from '../typechain-types'; import { GetAbiFunctionParamsTypes } from './generics'; import { + estimateGasWithOverrides, getERC20Overrides, getNPMApprovalOverrides, staticCallWithOverrides, tryStaticCallWithOverrides, + updateIsControllerOverrides, } from './overrides'; export type AutomanActionName = @@ -447,3 +450,78 @@ export async function simulateRebalance( functionName: 'rebalance', }); } + +export async function estimateRebalanceGas( + chainId: ApertureSupportedChainId, + publicClient: PublicClient, + from: Address | undefined, + owner: Address, + mintParams: MintParams, + tokenId: bigint, + feeBips = BigInt(0), + swapData: Hex = '0x', + blockNumber?: bigint, +): Promise { + checkTicks(mintParams); + const data = getAutomanRebalanceCalldata( + mintParams, + tokenId, + feeBips, + undefined, + swapData, + ); + const overrides = getNPMApprovalOverrides(chainId, owner); + if (from === undefined) { + const privateKey = generatePrivateKey(); + const account = privateKeyToAccount(privateKey); + from = account.address; + } + updateIsControllerOverrides(overrides, chainId, from); + return await estimateGasWithOverrides( + from, + getChainInfo(chainId).aperture_uniswap_v3_automan, + data, + overrides, + publicClient, + blockNumber, + ); +} + +export async function estimateReinvestGas( + chainId: ApertureSupportedChainId, + publicClient: PublicClient, + from: Address | undefined, + owner: Address, + tokenId: bigint, + deadline: bigint, + amount0Min = BigInt(0), + amount1Min = BigInt(0), + feeBips = BigInt(0), + swapData: Hex = '0x', + blockNumber?: bigint, +): Promise { + const data = getAutomanReinvestCalldata( + tokenId, + deadline, + amount0Min, + amount1Min, + feeBips, + undefined, + swapData, + ); + const overrides = getNPMApprovalOverrides(chainId, owner); + if (from === undefined) { + const privateKey = generatePrivateKey(); + const account = privateKeyToAccount(privateKey); + from = account.address; + } + updateIsControllerOverrides(overrides, chainId, from); + return await estimateGasWithOverrides( + from, + getChainInfo(chainId).aperture_uniswap_v3_automan, + data, + overrides, + publicClient, + blockNumber, + ); +} diff --git a/viem/overrides.ts b/viem/overrides.ts index 8cb479aa..967891a2 100644 --- a/viem/overrides.ts +++ b/viem/overrides.ts @@ -6,6 +6,7 @@ import { RpcTransactionRequest, encodeAbiParameters, encodeFunctionData, + hexToBigInt, keccak256, parseAbiParameters, toHex, @@ -49,6 +50,20 @@ export function computeOperatorApprovalSlot( ); } +/** + * Compute the storage slot for the isController in UniV3Automan. + * @param from The address of controller. + * @returns The storage slot. + */ +export function computeIsControllerSlot(from: Address): Hex { + return keccak256( + encodeAbiParameters(parseAbiParameters('address, bytes32'), [ + from, + encodeAbiParameters(parseAbiParameters('uint256'), [2n]), + ]), + ); +} + export function getNPMApprovalOverrides( chainId: ApertureSupportedChainId, owner: Address, @@ -67,6 +82,22 @@ export function getNPMApprovalOverrides( }; } +export function updateIsControllerOverrides( + overrides: StateOverrides, + chainId: ApertureSupportedChainId, + from: Address, +) { + const { aperture_uniswap_v3_automan } = getChainInfo(chainId); + overrides[aperture_uniswap_v3_automan] = { + stateDiff: { + [computeIsControllerSlot(from)]: encodeAbiParameters( + parseAbiParameters('bool'), + [true], + ), + }, + }; +} + export function getAutomanWhitelistOverrides( chainId: ApertureSupportedChainId, routerToWhitelist: Address, @@ -217,6 +248,36 @@ export async function staticCallWithOverrides( })) as Hex; } +/** + * Estimate Gas of a contract call with the given state overrides. + * @param tx The transaction request. + * @param overrides The state overrides. + * @param publicClient A JSON RPC provider that supports `eth_estimateGas` with state overrides. + * @param blockNumber Optional block number to use for the call. + */ +export async function estimateGasWithOverrides( + from: Address, + to: Address, + data: Hex, + overrides: StateOverrides, + publicClient: PublicClient, + blockNumber?: bigint, +): Promise { + const tx = { + from, + to, + data, + }; + return hexToBigInt( + (await publicClient.request({ + method: 'eth_estimateGas', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + params: [tx, blockNumber ? toHex(blockNumber) : 'latest', overrides], + })) as Hex, + ); +} + /** * Try to call a contract with the given state overrides. If the call fails, fall back to a regular call. * @param from The sender address.