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:
parent
7a1e03ee7a
commit
fb378c4931
17 changed files with 1222 additions and 441 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
5
onchain/src/IUniswapV2Callee.sol
Normal file
5
onchain/src/IUniswapV2Callee.sol
Normal file
|
@ -0,0 +1,5 @@
|
|||
pragma solidity ^0.8.28;
|
||||
|
||||
interface IUniswapV2Callee {
|
||||
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external;
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue