Add typst and tinymist to shell

Start writing notes

Notes on Uniswap V2's optimum

Fix error in formula

Test `computeAmountInt` using various deltas

Add `concurrency` to the default configuration file

Remove unused imports

Correctly propagate error

Allow dead code

Make the priority queue a real FIFO

Refactor: remove priority queue as stream and use channels

Increase buffer size

New `flashArbitrage` function

Comment with some ideas

Add pragma version

Refactor: decrease the amount of calls

Remove unused code

Re-enable tests

Remove comment

Process known pairs when started

Avoid re-allocating a new provider every time

Ignore `nixos.qcow2` file created by the VM

Add support for `aarch64-linux`

Add NixOS module and VM configuration

Add `itertools`

Add arbitrage opportunity detection

Implement `fallback` method for non standard callbacks

Add more logs

Fix sign error in optimum formula

Add deployment scripts and `agenix-shell` secrets

Bump cargo packages

Fix typo

Print out an error if processing a pair goes wrong

Add `actionlint` to formatters

Fix typo

Add TODO comment

Remove not relevant anymore comment

Big refactor

- process actions always in the correct order avoiding corner cases
- avoid using semaphores

New API key

Add `age` to dev shell

Used by Emacs' `agenix-mode` on my system

Fix parametric deploy scripts

Add `run-forge-tests` flake app

Remove fork URL from Solidity source

Remove `pairDir` argument

Add link to `ArbitrageManager`'s ABI

WIP
This commit is contained in:
Andrea Ciceri 2025-05-10 10:45:59 +02:00
parent 7a1e03ee7a
commit fb378c4931
No known key found for this signature in database
17 changed files with 1222 additions and 441 deletions

View file

@ -3,8 +3,10 @@ pragma solidity ^0.8.28;
import {IUniswapV2Pair} from "./IUniswapV2Pair.sol";
import {IERC20} from "./IERC20.sol";
import {IUniswapV2Callee} from "./IUniswapV2Callee.sol";
contract ArbitrageManager {
contract ArbitrageManager is IUniswapV2Callee {
uint256 constant f = 997;
function sqrt(uint256 x)
@ -54,36 +56,59 @@ contract ArbitrageManager {
returns(uint256)
{
uint256 k = f * X_B / 1000 + f ** 2 / 1000 * X_A / 1000;
uint256 phi = sqrt(f * X_A) * sqrt(Y_B * X_B / 1000 * Y_A);
uint256 phi = sqrt(f ** 2 * X_A * Y_B * X_B * Y_A / 1000**2);
uint256 psi = Y_A * X_B;
if (psi >= phi) return 0;
else return (phi - psi) / k;
}
function swap(address _pairA, address _pairB, uint256 amountIn, bool direction)
external
returns (uint256 amountOut)
function flashArbitrage(address firstPair, address secondPair, bool tokenDir)
public
returns (uint256 gain)
{
IUniswapV2Pair pairA = IUniswapV2Pair(_pairA);
IUniswapV2Pair pairB = IUniswapV2Pair(_pairB);
IUniswapV2Pair pairA = IUniswapV2Pair(firstPair);
IUniswapV2Pair pairB = IUniswapV2Pair(secondPair);
IERC20 tokenA = direction ? IERC20(pairA.token0()) : IERC20(pairA.token1());
IERC20 firstToken = tokenDir ? IERC20(pairA.token0()) : IERC20(pairA.token1());
IERC20 secondToken = tokenDir ? IERC20(pairA.token1()) : IERC20(pairA.token0());
(uint256 X_A, uint256 Y_A,) = pairA.getReserves();
(uint256 X_B, uint256 Y_B,) = pairB.getReserves();
// Transfer the input tokens from the sender to pairA
tokenA.transferFrom(msg.sender, address(pairA), amountIn);
uint256 amountIn = optimalIn(tokenDir ? X_B : X_A, tokenDir ? Y_B : Y_A, tokenDir ? X_A : X_B, tokenDir ? Y_A : Y_B);
uint256 firstAmountOut = getAmountOut(amountIn, tokenDir ? X_A : Y_A, tokenDir ? Y_A : X_A);
uint256 secondAmountOut = getAmountOut(firstAmountOut, tokenDir ? Y_B : X_B, tokenDir ? X_B : Y_B);
// Perform the first swap on pairA
(uint256 reserve0A, uint256 reserve1A,) = pairA.getReserves();
amountOut = getAmountOut(amountIn, direction ? reserve0A : reserve1A, direction ? reserve1A : reserve0A);
pairA.swap(direction ? 0 : amountOut, direction ? amountOut : 0, address(pairB), new bytes(0));
require(secondAmountOut > amountIn, "Not profitable");
// Perform the second swap on pairB
(uint256 reserve0B, uint256 reserve1B,) = pairB.getReserves();
amountOut = getAmountOut(amountOut, direction ? reserve1B : reserve0B, direction ? reserve0B : reserve1B);
pairB.swap(direction ? amountOut : 0, direction ? 0 : amountOut, msg.sender, new bytes(0));
bytes memory data = abi.encode(pairA, pairB, firstToken, secondToken, amountIn, secondAmountOut);
pairA.swap(tokenDir ? 0 : firstAmountOut, tokenDir ? firstAmountOut : 0, address(this), data);
uint256 profit = secondAmountOut - amountIn;
firstToken.transfer(msg.sender, profit);
return profit;
}
// Ensure that the arbitrage is profitable
require(amountOut > amountIn, "Arbitrage not profitable");
function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes memory data)
public
{
(address pairA, address pairB, address firstToken, address secondToken, uint256 amountIn, uint256 secondAmountOut) = abi.decode(data, (address, address, address, address, uint256, uint256));
bool tokenDir = amount0 == 0;
IERC20(secondToken).transfer(pairB, tokenDir ? amount1 : amount0);
IUniswapV2Pair(pairB).swap(tokenDir ? secondAmountOut : 0, tokenDir ? 0 : secondAmountOut, sender, new bytes(0));
IERC20(firstToken).transfer(pairA, amountIn);
}
fallback() external {
(
address sender,
uint256 amount0,
uint256 amount1,
bytes memory data
) = abi.decode(msg.data[4:], (address, uint256, uint256, bytes));
uniswapV2Call(sender, amount0, amount1, data);
}
}

View file

@ -0,0 +1,5 @@
pragma solidity ^0.8.28;
interface IUniswapV2Callee {
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external;
}

View file

@ -16,7 +16,6 @@ contract ArbitrageTest is Test {
IUniswapV2Pair sushiswapPair = IUniswapV2Pair(0x397FF1542f962076d0BFE58eA045FfA2d347ACa0);
function setUp() public {
mainnetFork = vm.createFork("https://eth-mainnet.g.alchemy.com/v2/kkDMaLVYpWQA0GsCYNFvAODnAxCCiamv"); // TODO use an env variable
vm.selectFork(mainnetFork);
vm.rollFork(22_147_269);
arbitrageManager = new ArbitrageManager();
@ -34,12 +33,13 @@ contract ArbitrageTest is Test {
n = 115792089237316195423570985008687907853269984665640564039457584007913129639935;
assertEq(340282366920938463463374607431768211456 - 1, arbitrageManager.sqrt(n)); // +-1 is an acceptable rounding error
}
function test_computeAmountIn() public {
function test_swapUsingOptimum() public {
(uint256 X_A, uint256 Y_A, ) = uniswapPair.getReserves(); // (USDT, WETH)
(uint256 X_B, uint256 Y_B, ) = sushiswapPair.getReserves(); // (USDT, WETH)
console.log("Uniswap pair reserves", X_A, Y_A);
console.log("Sushiswap pair reserves", X_B, Y_B);
console.log("Uniswap pair ratio", Y_A/X_A);
console.log("Sushiswap pair ratio", Y_B/X_B);
@ -53,6 +53,7 @@ contract ArbitrageTest is Test {
(X_A, Y_A, ) = uniswapPair.getReserves();
(X_B, Y_B, ) = sushiswapPair.getReserves();
console.log("Uniswap pair reserves", X_A, Y_A);
console.log("Sushiswap pair reserves", X_B, Y_B);
console.log("Uniswap pair ratio", Y_A/X_A);
console.log("Sushiswap pair ratio", Y_B/X_B);
@ -74,4 +75,47 @@ contract ArbitrageTest is Test {
console.log("Uniswap pair ratio", Y_A/X_A);
console.log("Sushiswap pair ratio", Y_B/X_B);
}
function computeGain(uint256 X_A, uint256 Y_A, uint256 X_B, uint256 Y_B, int256 delta)
internal view returns(uint256)
{
uint256 optimum = (delta > 0) ?
arbitrageManager.optimalIn(X_A, Y_A, X_B, Y_B) + uint256(delta)
: arbitrageManager.optimalIn(X_A, Y_A, X_B, Y_B) - uint256(-delta);
uint256 amountOut = arbitrageManager.getAmountOut(optimum, Y_A, X_A);
amountOut = arbitrageManager.getAmountOut(amountOut, X_B, Y_B);
return amountOut - optimum;
}
function test_computeOptimum() public view {
(uint256 X_A, uint256 Y_A, ) = uniswapPair.getReserves(); // (USDT, WETH)
(uint256 X_B, uint256 Y_B, ) = sushiswapPair.getReserves(); // (USDT, WETH)
Y_A -= Y_A / 5; // unbalancing the pair
// Using delta too low (~< 10**8) seems to produce a better gain,
// I *believe this has to do to some rounding, it should be neglibile
uint256[4] memory deltas = [uint256(0), uint256(10**8), uint256(10**9), uint256(10**10)];
uint256 gain = computeGain(X_A, Y_A, X_B, Y_B, 0);
for (uint256 i; i < deltas.length; i++) {
assertGe(gain, computeGain(X_A, Y_A, X_B, Y_B, int256(deltas[i])), "Computed optimum isnt't really optimal");
assertGe(gain, computeGain(X_A, Y_A, X_B, Y_B, -int256(deltas[i])), "Computed optimum isnt't really optimal");
}
}
function test_flashArbitrage () public {
uint256 initialWethBalance = weth.balanceOf(address(this));
console.log("initial weth balance", initialWethBalance);
(, uint256 Y_A, ) = uniswapPair.getReserves(); // (USDT, WETH)
uint256 unbalance = Y_A / 5;
vm.prank(address(uniswapPair)); // it works only for the next call
weth.transfer(address(0), unbalance);
uniswapPair.sync();
uint256 profit = arbitrageManager.flashArbitrage(address(uniswapPair), address(sushiswapPair), false);
console.log("profit", profit);
assertEq(initialWethBalance + profit, weth.balanceOf(address(this)), "There was no profit");
}
}