Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions contracts/AjnaKeeperTaker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down
24 changes: 18 additions & 6 deletions scripts/query-1inch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions src/integration-tests/take-1inch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -371,7 +371,7 @@ describe('Take with 1inch Integration', () => {
signer,
config: {
subgraphUrl: '',
oneInchRouters: { 1: '0x1111111254EEB25477B68fb85Ed929f73A960582' },
oneInchRouters: { 31337: '0x1111111254EEB25477B68fb85Ed929f73A960582' },
connectorTokens: [],
delayBetweenActions: 1,
},
Expand All @@ -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,
Expand Down
55 changes: 29 additions & 26 deletions src/take.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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 } from './transactions';
import { liquidationArbTake, liquidationTakeWithAtomicSwap } from './transactions';
import { DexRouter } from './dex-router';
import { BigNumber, ethers } from 'ethers';
import { convertSwapApiResponseToDetailsBytes } from './1inch';
Expand Down Expand Up @@ -61,7 +61,7 @@ export async function handleTakes({
}
}

interface LiquidationToTake {
export interface LiquidationToTake {
borrower: string;
hpbIndex: number;
collateral: BigNumber; // WAD
Expand Down Expand Up @@ -128,7 +128,7 @@ async function checkIfArbTakeable(
async function checkIfTakeable(
pool: FungiblePool,
price: number,
collateral: BigNumber,
collateralWad: BigNumber,
poolConfig: RequireFields<PoolConfig, 'take'>,
config: Pick<KeeperConfig, 'delayBetweenActions'>,
signer: Signer,
Expand All @@ -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 };
}
Expand All @@ -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 ?? [],
Expand All @@ -174,32 +181,27 @@ 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 };
}

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;

Expand Down Expand Up @@ -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,
Expand All @@ -341,16 +345,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();
convertSwapApiResponseToDetailsBytes(swapData.data),
)
logger.info(
`Take successful - poolAddress: ${pool.poolAddress}, borrower: ${borrower}`
);
Expand Down
44 changes: 42 additions & 2 deletions src/transactions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { FungiblePool, Signer } from '@ajna-finance/sdk';
import { createTransaction, FungiblePool, Signer } from '@ajna-finance/sdk';
import {
removeQuoteToken,
withdrawBonds,
quoteTokenScale,
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';
Expand All @@ -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();
Expand Down Expand Up @@ -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;
}
}