From cf7a1acadf7e7a047cf187c4b3e53e7767a7840b Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Thu, 15 May 2025 00:00:25 -0400 Subject: [PATCH 1/3] reclaim nonce after failure taking with atomic swap --- src/integration-tests/take-1inch.test.ts | 10 +++--- src/take.ts | 17 +++++---- src/transactions.ts | 44 ++++++++++++++++++++++-- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/integration-tests/take-1inch.test.ts b/src/integration-tests/take-1inch.test.ts index 8a3bb88..9fce8ed 100644 --- a/src/integration-tests/take-1inch.test.ts +++ b/src/integration-tests/take-1inch.test.ts @@ -275,14 +275,14 @@ describe('Take with 1inch Integration', () => { signer, config: { subgraphUrl: '', - oneInchRouters: { 1: '0x1111111254EEB25477B68fb85Ed929f73A960582' }, + oneInchRouters: { 31337: '0x1111111254EEB25477B68fb85Ed929f73A960582' }, connectorTokens: [], delayBetweenActions: 1, }, }) ); expect(liquidations.length).to.equal(1); - expect(liquidations[0].takeStrategy).to.equal(1); + expect(liquidations[0].isTakeable).to.equal(true); await takeLiquidation({ pool, @@ -298,7 +298,7 @@ describe('Take with 1inch Integration', () => { liquidation: liquidations[0], config: { dryRun: false, - oneInchRouters: { 1: '0x1111111254EEB25477B68fb85Ed929f73A960582' }, + oneInchRouters: { 31337: '0x1111111254EEB25477B68fb85Ed929f73A960582' }, connectorTokens: [], keeperTaker: keeperTakerAddress, delayBetweenActions: 1, @@ -371,7 +371,7 @@ describe('Take with 1inch Integration', () => { signer, config: { subgraphUrl: '', - oneInchRouters: { 1: '0x1111111254EEB25477B68fb85Ed929f73A960582' }, + oneInchRouters: { 31337: '0x1111111254EEB25477B68fb85Ed929f73A960582' }, connectorTokens: [], delayBetweenActions: 1, }, @@ -395,7 +395,7 @@ describe('Take with 1inch Integration', () => { liquidation: liquidations[0], config: { dryRun: false, - oneInchRouters: { 1: '0x1111111254EEB25477B68fb85Ed929f73A960582' }, + oneInchRouters: { 31337: '0x1111111254EEB25477B68fb85Ed929f73A960582' }, connectorTokens: [], keeperTaker: keeperTakerAddress, delayBetweenActions: 1, diff --git a/src/take.ts b/src/take.ts index 25cf899..fa645ad 100644 --- a/src/take.ts +++ b/src/take.ts @@ -3,7 +3,7 @@ import subgraph from './subgraph'; import { decimaledToWei, delay, RequireFields, weiToDecimaled } from './utils'; import { KeeperConfig, LiquiditySource, PoolConfig } from './config-types'; import { logger } from './logging'; -import { liquidationArbTake } from './transactions'; +import { liquidationArbTake, liquidationTakeWithAtomicSwap } from './transactions'; import { DexRouter } from './dex-router'; import { BigNumber, ethers } from 'ethers'; import { convertSwapApiResponseToDetailsBytes } from './1inch'; @@ -61,7 +61,7 @@ export async function handleTakes({ } } -interface LiquidationToTake { +export interface LiquidationToTake { borrower: string; hpbIndex: number; collateral: BigNumber; // WAD @@ -341,16 +341,15 @@ export async function takeLiquidation({ logger.debug( `Sending Take Tx - poolAddress: ${pool.poolAddress}, borrower: ${borrower}` ); - const tx = await keeperTaker.takeWithAtomicSwap( - pool.poolAddress, - liquidation.borrower, - liquidation.auctionPrice, - liquidation.collateral, + await liquidationTakeWithAtomicSwap( + keeperTaker, + signer, + pool, + liquidation, poolConfig.take.liquiditySource, dexRouter.getRouter(await signer.getChainId())!!, convertSwapApiResponseToDetailsBytes(swapData.data) - ); - await tx.wait(); + ) logger.info( `Take successful - poolAddress: ${pool.poolAddress}, borrower: ${borrower}` ); diff --git a/src/transactions.ts b/src/transactions.ts index 7be6f83..c52a0d1 100644 --- a/src/transactions.ts +++ b/src/transactions.ts @@ -1,4 +1,4 @@ -import { FungiblePool, Signer } from '@ajna-finance/sdk'; +import { createTransaction, FungiblePool, Signer } from '@ajna-finance/sdk'; import { removeQuoteToken, withdrawBonds, @@ -6,7 +6,7 @@ import { kick, bucketTake, } from '@ajna-finance/sdk/dist/contracts/pool'; -import { BigNumber } from 'ethers'; +import { BigNumber, BytesLike } from 'ethers'; import { MAX_FENWICK_INDEX, MAX_UINT_256 } from './constants'; import { NonceTracker } from './nonce'; import { Bucket } from '@ajna-finance/sdk/dist/classes/Bucket'; @@ -15,6 +15,9 @@ import { approve, } from '@ajna-finance/sdk/dist/contracts/erc20-pool'; import { Liquidation } from '@ajna-finance/sdk/dist/classes/Liquidation'; +import { AjnaKeeperTaker } from '../typechain-types'; +import { LiquiditySource } from './config-types'; +import { LiquidationToTake } from './take'; export async function poolWithdrawBonds(pool: FungiblePool, signer: Signer) { const address = await signer.getAddress(); @@ -150,3 +153,40 @@ export async function liquidationArbTake( throw error; } } + +export async function liquidationTakeWithAtomicSwap( + keeperTaker: AjnaKeeperTaker, + signer: Signer, + pool: FungiblePool, + liquidation: LiquidationToTake, + liquiditySource: LiquiditySource, + routerAddress: string, + swapDetails: BytesLike +) { + const address = await signer.getAddress(); + try { + const nonce = await NonceTracker.getNonce(signer); + const tx = await createTransaction( + keeperTaker, + { + methodName: 'takeWithAtomicSwap', + args: [ + pool.poolAddress, + liquidation.borrower, + liquidation.auctionPrice, + liquidation.collateral, + liquiditySource, + routerAddress, + swapDetails, + ], + }, + { + nonce: nonce.toString(), + } + ); + await tx.verifyAndSubmit(); + } catch (error) { + NonceTracker.resetNonce(signer, address); + throw error; + } +} From e6ec592e187b107693b6724c678a18abf8817710 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Thu, 28 Aug 2025 23:40:49 -0400 Subject: [PATCH 2/3] updated script to support non-18-decimal collateral and add recovery action --- scripts/query-1inch.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/scripts/query-1inch.ts b/scripts/query-1inch.ts index 8bb0d74..7dc7c07 100755 --- a/scripts/query-1inch.ts +++ b/scripts/query-1inch.ts @@ -7,7 +7,7 @@ import { promises as fs } from 'fs'; import { exit } from 'process'; import { configureAjna, readConfigFile } from "../src/config-types"; -import { approveErc20, getAllowanceOfErc20, transferErc20 } from '../src/erc20'; +import { approveErc20, getAllowanceOfErc20, getDecimalsErc20, transferErc20 } from '../src/erc20'; import { DexRouter } from '../src/dex-router'; import { getProviderAndSigner } from '../src/utils'; import { convertSwapApiResponseToDetailsBytes } from '../src/1inch'; @@ -30,7 +30,7 @@ const argv = yargs(process.argv.slice(2)) type: 'string', demandOption: true, describe: 'Action to perform', - choices: ['approve', 'deploy', 'quote', 'send', 'swap'], + choices: ['approve', 'deploy', 'quote', 'send', 'swap', 'recover'], }, amount: { type: 'number', @@ -72,13 +72,24 @@ async function main() { configureAjna(config.ajna); const ajna = new AjnaSDK(provider); const pool: FungiblePool = await ajna.fungiblePoolFactory.getPoolByAddress(poolConfig.address); - console.log('Found pool on chain', chainId, 'quoting', pool.collateralAddress, 'in', pool.quoteAddress) + + if (argv.action ==='recover' && pool && config.keeperTaker) { + const keeperTaker = AjnaKeeperTaker__factory.connect(config.keeperTaker, signer); + const tx = await keeperTaker.recover(pool.quoteAddress); + console.log('Recovery transaction hash:', tx.hash); + await tx.wait(); + console.log('Recovery transaction confirmed'); + + exit(0); + } + const dexRouter = new DexRouter(signer, { oneInchRouters: config?.oneInchRouters ?? {}, connectorTokens: config?.connectorTokens ?? [], }); - const amount = ethers.utils.parseEther(argv.amount!!.toString()); + const collateralDecimals = await getDecimalsErc20(signer, pool.collateralAddress); + const amount = ethers.utils.parseUnits(argv.amount!!.toString(), collateralDecimals); if (argv.action === 'approve' && pool && dexRouter) { // 1inch API will error out if approval not run before calling API @@ -108,6 +119,7 @@ async function main() { ); console.log('Quote:', quote); + // Sends collateral to AjnaKeeperTaker; useful for testing swap without an auction } else if (argv.action === 'send' && pool && config.keeperTaker) { try { console.log('Sending', amount.toString(), 'to keeperTaker at', config.keeperTaker); @@ -135,9 +147,9 @@ async function main() { convertSwapApiResponseToDetailsBytes(swapData.data), amount.mul(9).div(10), // 90% of the amount ); - console.log('Transaction hash:', tx.hash); + console.log('Swap transaction hash:', tx.hash); await tx.wait(); - console.log('Transaction confirmed'); + console.log('Swap transaction confirmed'); } } else { From 2be847edb1b041aa147b956da2eb1e4f0fbe1e47 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Sat, 30 Aug 2025 01:14:18 -0400 Subject: [PATCH 3/3] fix take issues with non-18-decimal collateral --- contracts/AjnaKeeperTaker.sol | 5 +++-- src/take.ts | 38 +++++++++++++++++++---------------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/contracts/AjnaKeeperTaker.sol b/contracts/AjnaKeeperTaker.sol index a5f0454..99d2698 100644 --- a/contracts/AjnaKeeperTaker.sol +++ b/contracts/AjnaKeeperTaker.sol @@ -99,8 +99,9 @@ contract AjnaKeeperTaker is IERC20Taker { } /// @dev Called by `Pool` to allow a taker to externally swap collateral for quote token. + /// @param collateral Amount of collateral received from the take, in token precision. /// @param data Determines where external liquidity should be sourced to swap collateral for quote token. - function atomicSwapCallback(uint256 collateralAmountWad, uint256, bytes calldata data) external override { + function atomicSwapCallback(uint256 collateral, uint256, bytes calldata data) external override { SwapData memory swapData = abi.decode(data, (SwapData)); // Ensure msg.sender is a valid Ajna pool and matches the pool in the data @@ -115,7 +116,7 @@ contract AjnaKeeperTaker is IERC20Taker { details.aggregationExecutor, details.swapDescription, details.opaqueData, - collateralAmountWad / pool.collateralScale() // convert WAD to token precision + collateral ); } else { revert UnsupportedLiquiditySource(); diff --git a/src/take.ts b/src/take.ts index fa645ad..1bb94e9 100644 --- a/src/take.ts +++ b/src/take.ts @@ -1,6 +1,6 @@ import { Signer, FungiblePool } from '@ajna-finance/sdk'; import subgraph from './subgraph'; -import { decimaledToWei, delay, RequireFields, weiToDecimaled } from './utils'; +import { decimaledToWei, delay, RequireFields, tokenChangeDecimals, weiToDecimaled } from './utils'; import { KeeperConfig, LiquiditySource, PoolConfig } from './config-types'; import { logger } from './logging'; import { liquidationArbTake, liquidationTakeWithAtomicSwap } from './transactions'; @@ -128,7 +128,7 @@ async function checkIfArbTakeable( async function checkIfTakeable( pool: FungiblePool, price: number, - collateral: BigNumber, + collateralWad: BigNumber, poolConfig: RequireFields, config: Pick, signer: Signer, @@ -142,9 +142,9 @@ async function checkIfTakeable( return { isTakeable: false }; } - if (!collateral.gt(0)) { + if (!collateralWad.gt(0)) { logger.debug( - `Invalid collateral amount: ${collateral.toString()} for pool ${pool.name}` + `Invalid collateral amount: ${collateralWad.toString()} for pool ${pool.name}` ); return { isTakeable: false }; } @@ -161,6 +161,13 @@ async function checkIfTakeable( // Pause between getting a quote for each liquidation to avoid 1inch rate limit await delay(config.delayBetweenActions); + // Convert to token precision for interacting with 1inch + const collateralDecimals = await getDecimalsErc20( + signer, + pool.collateralAddress + ); + const collateral = tokenChangeDecimals(collateralWad, 18, collateralDecimals); + const dexRouter = new DexRouter(signer, { oneInchRouters: oneInchRouters ?? {}, connectorTokens: connectorTokens ?? [], @@ -174,7 +181,7 @@ async function checkIfTakeable( if (!quoteResult.success) { logger.debug( - `No valid quote data for collateral ${ethers.utils.formatUnits(collateral, await getDecimalsErc20(signer, pool.collateralAddress))} in pool ${pool.name}: ${quoteResult.error}` + `No valid quote data for collateral ${ethers.utils.formatUnits(collateralWad, await getDecimalsErc20(signer, pool.collateralAddress))} in pool ${pool.name}: ${quoteResult.error}` ); return { isTakeable: false }; } @@ -182,24 +189,19 @@ async function checkIfTakeable( const amountOut = ethers.BigNumber.from(quoteResult.dstAmount); if (amountOut.isZero()) { logger.debug( - `Zero amountOut for collateral ${ethers.utils.formatUnits(collateral, await getDecimalsErc20(signer, pool.collateralAddress))} in pool ${pool.name}` + `Zero amountOut for collateral ${ethers.utils.formatUnits(collateralWad, await getDecimalsErc20(signer, pool.collateralAddress))} in pool ${pool.name}` ); return { isTakeable: false }; } - const collateralDecimals = await getDecimalsErc20( - signer, - pool.collateralAddress - ); + // Calculate market price from DEX and price at which auction is takeable based upon configuration const quoteDecimals = await getDecimalsErc20(signer, pool.quoteAddress); - - const collateralAmount = Number( - ethers.utils.formatUnits(collateral, collateralDecimals) - ); const quoteAmount = Number( ethers.utils.formatUnits(amountOut, quoteDecimals) ); - + const collateralAmount = Number( + ethers.utils.formatUnits(collateral, collateralDecimals) + ); const marketPrice = quoteAmount / collateralAmount; const takeablePrice = marketPrice * poolConfig.take.marketPriceFactor; @@ -323,13 +325,15 @@ export async function takeLiquidation({ // pause between getting the 1inch quote and requesting the swap to avoid 1inch rate limit await delay(config.delayBetweenActions); + const collateralDecimals = await getDecimalsErc20(signer, pool.collateralAddress); + const dexRouter = new DexRouter(signer, { oneInchRouters: config.oneInchRouters ?? {}, connectorTokens: config.connectorTokens ?? [], }); const swapData = await dexRouter.getSwapDataFromOneInch( await signer.getChainId(), - liquidation.collateral, + tokenChangeDecimals(liquidation.collateral, 18, collateralDecimals), pool.collateralAddress, pool.quoteAddress, 1, @@ -348,7 +352,7 @@ export async function takeLiquidation({ liquidation, poolConfig.take.liquiditySource, dexRouter.getRouter(await signer.getChainId())!!, - convertSwapApiResponseToDetailsBytes(swapData.data) + convertSwapApiResponseToDetailsBytes(swapData.data), ) logger.info( `Take successful - poolAddress: ${pool.poolAddress}, borrower: ${borrower}`