Skip to content

Commit

Permalink
feat: default arbitrageur -> fixed arbitrageur
Browse files Browse the repository at this point in the history
  • Loading branch information
ts0yu authored Sep 17, 2024
1 parent 84d05ee commit a980fc4
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 113 deletions.
107 changes: 107 additions & 0 deletions contracts/utils/src/ArenaController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {LiquidExchange} from "./LiquidExchange.sol";
import {Fetcher} from "./Fetcher.sol";
import {FullMath} from "v4-core/libraries/FullMath.sol";
import {SqrtPriceMath} from "v4-core/libraries/SqrtPriceMath.sol";
import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";

contract ArenaController {
PoolManager immutable poolManager;
PoolModifyLiquidityTest immutable router;
PoolSwapTest immutable swapRouter;
LiquidExchange immutable lex;
Fetcher immutable fetcher;

Expand All @@ -21,6 +26,11 @@ contract ArenaController {

PoolKey public poolKey;

uint256 internal constant MAX_SWAP_FEE = 1e6;

uint160 public constant MIN_PRICE_LIMIT = TickMath.MIN_SQRT_PRICE + 1;
uint160 public constant MAX_PRICE_LIMIT = TickMath.MAX_SQRT_PRICE - 1;

struct Signal {
int24 currentTick;
uint160 sqrtPriceX96;
Expand All @@ -33,6 +43,7 @@ contract ArenaController {
constructor(uint256 fee, uint256 initialPrice) {
poolManager = new PoolManager(fee);
router = new PoolModifyLiquidityTest(poolManager);
swapRouter = new PoolSwapTest(poolManager);
fetcher = new Fetcher();

currency0 = new ArenaToken("currency0", "c0", 18);
Expand Down Expand Up @@ -69,6 +80,44 @@ contract ArenaController {
lex.swap(tokenIn, amountIn);
}

function equalizePrice() public {
require(currency0.approve(address(swapRouter), type(uint256).max), "Approval for currency0 failed");
require(currency1.approve(address(swapRouter), type(uint256).max), "Approval for currency1 failed");

(uint160 sqrtPriceX96, int24 tick,,) = fetcher.getSlot0(poolManager, fetcher.toId(poolKey));

uint256 uniswapPrice = FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, 1 << 192) * 1e18;
uint256 lexPrice = lex.price();

if (uniswapPrice > lexPrice) {
bool zeroForOne = true;

IPoolManager.SwapParams memory params = IPoolManager.SwapParams({
zeroForOne: zeroForOne,
amountSpecified: 1000000,
sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT // unlimited impact
});

PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});

swapRouter.swap(poolKey, params, testSettings, "");
} else if (uniswapPrice < lexPrice) {
bool zeroForOne = false;

IPoolManager.SwapParams memory params = IPoolManager.SwapParams({
zeroForOne: zeroForOne,
amountSpecified: 10000,
sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT // unlimited impact
});

PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});

swapRouter.swap(poolKey, params, testSettings, "");
}
}

function setPool(uint24 poolFee, int24 tickSpacing, IHooks hooks, uint160 sqrtPriceX96, bytes memory hookData)
public
{
Expand Down Expand Up @@ -101,4 +150,62 @@ contract ArenaController {

router.modifyLiquidity(poolKey, params, "");
}

function computeSwapStep(
uint160 sqrtPriceCurrentX96,
uint160 sqrtPriceTargetX96,
uint128 liquidity,
int256 amountRemaining,
uint24 feePips
) external pure returns (uint160 sqrtPriceNextX96, uint256 amountIn, uint256 amountOut, uint256 feeAmount) {
unchecked {
uint256 _feePips = feePips; // upcast once and cache
bool zeroForOne = sqrtPriceCurrentX96 >= sqrtPriceTargetX96;
bool exactIn = amountRemaining < 0;

if (exactIn) {
uint256 amountRemainingLessFee =
FullMath.mulDiv(uint256(-amountRemaining), MAX_SWAP_FEE - _feePips, MAX_SWAP_FEE);
amountIn = zeroForOne
? SqrtPriceMath.getAmount0Delta(sqrtPriceTargetX96, sqrtPriceCurrentX96, liquidity, true)
: SqrtPriceMath.getAmount1Delta(sqrtPriceCurrentX96, sqrtPriceTargetX96, liquidity, true);
if (amountRemainingLessFee >= amountIn) {
// `amountIn` is capped by the target price
sqrtPriceNextX96 = sqrtPriceTargetX96;
feeAmount = _feePips == MAX_SWAP_FEE
? amountIn // amountIn is always 0 here, as amountRemainingLessFee == 0 and amountRemainingLessFee >= amountIn
: FullMath.mulDivRoundingUp(amountIn, _feePips, MAX_SWAP_FEE - _feePips);
} else {
// exhaust the remaining amount
amountIn = amountRemainingLessFee;
sqrtPriceNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput(
sqrtPriceCurrentX96, liquidity, amountRemainingLessFee, zeroForOne
);
// we didn't reach the target, so take the remainder of the maximum input as fee
feeAmount = uint256(-amountRemaining) - amountIn;
}
amountOut = zeroForOne
? SqrtPriceMath.getAmount1Delta(sqrtPriceNextX96, sqrtPriceCurrentX96, liquidity, false)
: SqrtPriceMath.getAmount0Delta(sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity, false);
} else {
amountOut = zeroForOne
? SqrtPriceMath.getAmount1Delta(sqrtPriceTargetX96, sqrtPriceCurrentX96, liquidity, false)
: SqrtPriceMath.getAmount0Delta(sqrtPriceCurrentX96, sqrtPriceTargetX96, liquidity, false);
if (uint256(amountRemaining) >= amountOut) {
// `amountOut` is capped by the target price
sqrtPriceNextX96 = sqrtPriceTargetX96;
} else {
// cap the output amount to not exceed the remaining output amount
amountOut = uint256(amountRemaining);
sqrtPriceNextX96 =
SqrtPriceMath.getNextSqrtPriceFromOutput(sqrtPriceCurrentX96, liquidity, amountOut, zeroForOne);
}
amountIn = zeroForOne
? SqrtPriceMath.getAmount0Delta(sqrtPriceNextX96, sqrtPriceCurrentX96, liquidity, true)
: SqrtPriceMath.getAmount1Delta(sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity, true);
// `feePips` cannot be `MAX_SWAP_FEE` for exact out
feeAmount = FullMath.mulDivRoundingUp(amountIn, _feePips, MAX_SWAP_FEE - _feePips);
}
}
}
}
14 changes: 10 additions & 4 deletions src/arena.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::collections::HashMap;

use alloy::{primitives::Uint, providers::ProviderBuilder, signers::local::PrivateKeySigner};
use alloy::providers::WalletProvider;
use alloy::providers::Provider;
use alloy::{
primitives::Uint,
providers::{Provider, ProviderBuilder, WalletProvider},
signers::local::PrivateKeySigner,
};

use super::*;
use crate::{
config::Config,
Expand Down Expand Up @@ -46,7 +49,7 @@ impl<V> Arena<V> {
Uint::from(0),
Signed::try_from(2).unwrap(),
Address::default(),
Uint::from(79228162514264337593543950336_u128),
Uint::from(24028916059024274524587271040_u128),
Bytes::new(),
)
.send()
Expand Down Expand Up @@ -74,6 +77,7 @@ impl<V> Arena<V> {
signal.pool,
signal.fetcher,
self.feed.current_value(),
*controller.address(),
);

strategy
Expand All @@ -97,6 +101,7 @@ impl<V> Arena<V> {
signal.pool,
signal.fetcher,
self.feed.current_value(),
*controller.address(),
);

self.arbitrageur.init(&signal, admin_provider.clone()).await;
Expand Down Expand Up @@ -136,6 +141,7 @@ impl<V> Arena<V> {
signal.pool,
signal.fetcher,
self.feed.current_value(),
*controller.address(),
);

strategy
Expand Down
2 changes: 1 addition & 1 deletion src/artifacts/ArenaController.json

Large diffs are not rendered by default.

112 changes: 13 additions & 99 deletions src/engine/arbitrageur.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
use std::str::FromStr;

use async_trait::async_trait;
use rug::{ops::Pow, Float};

use super::*;
use crate::{
types::{
fetcher::Fetcher,
swap::{IPoolManager::SwapParams, PoolSwapTest, PoolSwapTest::TestSettings},
},
types::{controller::ArenaController, swap::PoolSwapTest},
AnvilProvider, Signal,
};

Expand All @@ -24,12 +18,12 @@ pub trait Arbitrageur {

/// Default implementation of an [`Arbitrageur`] that uses the closed-form optimal swap amount to determine the optimal arbitrage.
#[derive(Default)]
pub struct DefaultArbitrageur {
pub struct FixedArbitrageur {
swapper: Option<Address>,
}

#[async_trait]
impl Arbitrageur for DefaultArbitrageur {
impl Arbitrageur for FixedArbitrageur {
async fn init(&mut self, signal: &Signal, provider: AnvilProvider) {
let swapper = PoolSwapTest::deploy(provider.clone(), signal.manager)
.await
Expand All @@ -39,54 +33,16 @@ impl Arbitrageur for DefaultArbitrageur {
}

async fn arbitrage(&mut self, signal: &Signal, provider: AnvilProvider) {
// arbitrageur is always initialized before event loop starts, so unwrap should never fail.
let swapper = PoolSwapTest::new(self.swapper.unwrap(), provider.clone());

let base = Float::with_val(53, 1.0001);
let price = Float::with_val(53, signal.current_value);

let target_tick = price.log10() / base.log10();
let current_tick = Float::with_val(53, signal.tick.as_i64());

let (start, end) = (
current_tick.clone().min(&target_tick),
current_tick.clone().max(&target_tick),
);

let (a, b) = self
.get_tick_range_liquidity(
signal,
provider,
start.to_i32_saturating().unwrap(),
end.to_i32_saturating().unwrap(),
)
.await;

let k = a.clone() * b.clone();

// closed form optimal swap solution, ref: https://arxiv.org/pdf/1911.03380
let fee: u64 = signal.pool.fee.to_string().parse().unwrap();
let optimal_swap = Float::with_val(53, 0).max(&(a.clone() - (k / (fee * (a / b)))));

let zero_for_one = current_tick > target_tick;

let swap_params = SwapParams {
amountSpecified: Signed::from_str(&optimal_swap.to_string()).unwrap(),
zeroForOne: zero_for_one,
sqrtPriceLimitX96: signal.sqrt_price_x96,
};

let test_settings = TestSettings {
takeClaims: false,
settleUsingBurn: false,
};

swapper
.swap(
signal.pool.clone().into(),
swap_params,
test_settings,
Bytes::new(),
let controller = ArenaController::new(signal.controller, provider.clone());

controller
.equalizePrice()
.nonce(
provider
.clone()
.get_transaction_count(provider.clone().default_signer_address())
.await
.unwrap(),
)
.send()
.await
Expand All @@ -97,48 +53,6 @@ impl Arbitrageur for DefaultArbitrageur {
}
}

impl DefaultArbitrageur {
async fn get_tick_range_liquidity(
&self,
signal: &Signal,
provider: AnvilProvider,
start: i32,
end: i32,
) -> (Float, Float) {
let fetcher = Fetcher::new(signal.fetcher, provider.clone());

let mut liquidity_a = Float::with_val(53, 0);
let mut liquidity_b = Float::with_val(53, 0);

for tick in start..end {
let pool_id = fetcher
.toId(signal.pool.clone().into())
.call()
.await
.unwrap()
.poolId;

let tick_info = fetcher
.getTickInfo(
signal.manager,
pool_id,
Signed::from_str(&tick.to_string()).unwrap(),
)
.call()
.await
.unwrap();
let sqrt_price = Float::with_val(53, Float::with_val(53, 1.0001).pow(tick / 2));

let tick_liquidity = Float::with_val(53, tick_info.liquidityNet);

liquidity_a += tick_liquidity.clone() / sqrt_price.clone();
liquidity_b += tick_liquidity * sqrt_price;
}

(liquidity_a, liquidity_b)
}
}

/// No-op implementation of an [`Arbitrageur`] for custom usecases.
pub struct EmptyArbitrageur;

Expand Down
Loading

0 comments on commit a980fc4

Please sign in to comment.