Skip to content

Commit

Permalink
OEV bot
Browse files Browse the repository at this point in the history
Update Liquidator contract

Add pure liquidation function for simulating profit, refactor

Convenience contract updates

OEV bot

Fix beacon-related issues

Fix profit calculation

Report fulfillment

Cleanup on failed liquidation attempts

Add bidding phase buffer and use placeBidWithExpiration

Fix incorrect profit calculation

Reduce diff

Update API3 contracts

Update Liquidator contract and related

Code review update

Update Api3ServerV1OevExtension address

Updated Compound fork deployed
  • Loading branch information
matejos committed Oct 9, 2024
1 parent dfc2002 commit 3599a2a
Show file tree
Hide file tree
Showing 19 changed files with 868 additions and 193 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ LIQUIDATOR_CONTRACT_ADDRESS=0x0000000000000000000000000000000000000000
MAX_BORROWER_DETAILS_MULTICALL=500
MAX_LOG_RANGE_BLOCKS=10000
MAX_POSITIONS_TO_LIQUIDATE=10
MAX_SIMULATE_LIQUIDATIONS_MULTICALL=50
MIN_POSITION_USD_E18=1
MIN_RPC_DELAY_MS=30
OEV_NETWORK_RPC_URL=https://oev.rpc.api3.org/http
OEV_PLACE_BID_TRANSACTION_TIMEOUT_MS=5000
OEV_POLL_AWARD_BID_DELAY_MS=5000
OEV_REPORT_FULFILLMENT_TIMEOUT_MS=30000
RESET_CURRENT_POSITIONS_FREQUENCY_MS=7200000
RESET_INTERESTING_POSITIONS_FREQUENCY_MS=600000
RPC_URL=https://base-rpc.publicnode.com
Expand Down
170 changes: 123 additions & 47 deletions contracts/Compound3Liquidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,48 @@ import { TransferHelper } from './uniswap/v3-periphery/contracts/libraries/Trans
import { IWETH9 } from './uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol';
import { ISwapRouter02 } from './uniswap/swap-router-contracts/contracts/interfaces/ISwapRouter02.sol';
import { IV3SwapRouter } from './uniswap/swap-router-contracts/contracts/interfaces/IV3SwapRouter.sol';
import { IApi3ServerV1OevExtension } from './api3-contracts/api3-server-v1/interfaces/IApi3ServerV1OevExtension.sol';
import { IApi3ServerV1OevExtensionPayOevBidCallback } from './api3-contracts/api3-server-v1/interfaces/IApi3ServerV1OevExtensionPayOevBidCallback.sol';

event AbsorbFailed(address indexed borrower);

contract Compound3Liquidator is Ownable, IUniswapV3SwapCallback {
contract Compound3Liquidator is Ownable, IUniswapV3SwapCallback, IApi3ServerV1OevExtensionPayOevBidCallback {
address public profitReceiver;
uint24 public constant DEFAULT_POOL_FEE = 500; // 0.05%
uint256 public constant DAPP_ID = 1;
uint256 public constant QUOTE_PRICE_SCALE = 1e18;
address public constant USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913;
address public constant WETH = 0x4200000000000000000000000000000000000006;
address public constant WSTETH = 0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452;
address public constant SWAP_ROUTER = 0x2626664c2603336E57B271c5C0b26F421741e481;
address public constant UNISWAP_FACTORY = 0x33128a8fC17869897dcE68Ed026d694621f6FDfD;
address public constant API3_SERVER_V1_OEV_EXTENSION = 0x6a6F4b90ac94Df292fAe521b24b94cE8E58EB91e;

mapping(address => mapping(address => uint24)) public uniswapPoolFees;

IComet public comet;
IWETH9 weth = IWETH9(WETH);
ISwapRouter02 swapRouter = ISwapRouter02(SWAP_ROUTER);
IApi3ServerV1OevExtension oevExtension = IApi3ServerV1OevExtension(API3_SERVER_V1_OEV_EXTENSION);

struct LiquidateParams {
address[] liquidatableAccounts;
uint256[] maxAmountsToPurchase;
uint256 liquidationThreshold;
}

struct PayBidAndUpdateFeedsAndLiquidateParams {
uint32 signedDataTimestampCutoff;
bytes signature;
uint256 bidAmount;
PayOevBidCallbackData payOevBidCallbackData;
}

struct PayOevBidCallbackData {
LiquidateParams liquidateParams;
bytes[][] signedDataArray;
}

struct SwapCallbackData {
PoolAddress.PoolKey poolKey;
address[] assets;
Expand Down Expand Up @@ -120,61 +137,45 @@ contract Compound3Liquidator is Ownable, IUniswapV3SwapCallback {
}
}

/// @notice This serves for simulating profit from liquidations via staticcall
function liquidate(LiquidateParams calldata params) external returns (uint256, uint256) {
address[] memory accountToAbsorb = new address[](1);
for (uint8 i; i < params.liquidatableAccounts.length; ++i) {
accountToAbsorb[0] = params.liquidatableAccounts[i];
uint256 ethBalanceBefore = address(this).balance;

try comet.absorb(msg.sender, accountToAbsorb) {} catch {
emit AbsorbFailed(accountToAbsorb[0]);
}
}
_liquidate(params);

uint8 numberOfAssets = comet.numAssets();
IComet.AssetInfo memory wethAsset;
address[] memory assets = new address[](numberOfAssets);
for (uint8 i; i < numberOfAssets; ++i) {
IComet.AssetInfo memory assetInfo = comet.getAssetInfo(i);
assets[i] = assetInfo.asset;
if (assetInfo.asset == WETH) {
wethAsset = assetInfo;
}
}
_withdrawWeth();

uint256 flashSwapAmount;
uint256[] memory assetBaseAmounts = new uint256[](assets.length);
for (uint8 i; i < assets.length; ++i) {
(, uint256 collateralBalanceInBase) = _purchasableBalanceOfAsset(assets[i], params.maxAmountsToPurchase[i]);
if (collateralBalanceInBase > params.liquidationThreshold) {
flashSwapAmount += collateralBalanceInBase;
assetBaseAmounts[i] = collateralBalanceInBase;
}
}

require(flashSwapAmount > 0, 'No collateral to buy.');

bool zeroForOne = WETH < USDC; // tokenIn < tokenOut
PoolAddress.PoolKey memory poolKey = _getFlashSwapPoolKey(USDC, WETH);
IUniswapV3Pool pool = _getFlashSwapPool(poolKey);
return _transferProfit(ethBalanceBefore);
}

pool.swap(
address(this),
zeroForOne,
-int256(flashSwapAmount),
zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1,
abi.encode(SwapCallbackData({ poolKey: poolKey, assets: assets, assetBaseAmounts: assetBaseAmounts }))
/// @notice Pays OEV bid, updates OEV data feeds, performs liquidations accounts and withdraws ETH to `profitReceiver`
function payBidAndUpdateFeedsAndLiquidate(
PayBidAndUpdateFeedsAndLiquidateParams calldata params
) external returns (uint256, uint256) {
uint256 ethBalanceBefore = address(this).balance;

oevExtension.payOevBid(
DAPP_ID,
params.signedDataTimestampCutoff,
params.signature,
params.bidAmount,
abi.encode(params.payOevBidCallbackData)
);

uint256 wethBalance = weth.balanceOf(address(this));
if (wethBalance > 0) {
weth.withdraw(wethBalance);
}
return _transferProfit(ethBalanceBefore);
}

uint256 profit = address(this).balance;
uint256 profitUsd = (profit * comet.getPrice(wethAsset.priceFeed)) / wethAsset.scale;
profitReceiver.call{ value: profit }('');
/// @notice Callback triggered by calling `payOevBid` on the OEV server extension. Updates data feeds,
/// liquidates accounts, and pays back the payment amount owed for the OEV bid.
function api3ServerV1OevExtensionPayOevBidCallback(uint256 amountOwed, bytes calldata _data) external override {
require(msg.sender == API3_SERVER_V1_OEV_EXTENSION, 'Unauthorized');

return (profit, profitUsd);
PayOevBidCallbackData memory data = abi.decode(_data, (PayOevBidCallbackData));
_updateDataFeeds(data.signedDataArray);
_liquidate(data.liquidateParams);

_withdrawWeth();
API3_SERVER_V1_OEV_EXTENSION.call{ value: amountOwed }('');
}

/// @notice Callback for flash swaps through Uniswap V3
Expand Down Expand Up @@ -219,6 +220,81 @@ contract Compound3Liquidator is Ownable, IUniswapV3SwapCallback {
weth.transfer(msg.sender, requiredReturnAmount);
}

function _liquidate(LiquidateParams memory params) internal {
address[] memory accountToAbsorb = new address[](1);
for (uint8 i; i < params.liquidatableAccounts.length; ++i) {
accountToAbsorb[0] = params.liquidatableAccounts[i];

try comet.absorb(msg.sender, accountToAbsorb) {} catch {
emit AbsorbFailed(accountToAbsorb[0]);
}
}

uint8 numberOfAssets = comet.numAssets();
address[] memory assets = new address[](numberOfAssets);
for (uint8 i; i < numberOfAssets; ++i) {
IComet.AssetInfo memory assetInfo = comet.getAssetInfo(i);
assets[i] = assetInfo.asset;
}

uint256 flashSwapAmount;
uint256[] memory assetBaseAmounts = new uint256[](assets.length);
for (uint8 i; i < assets.length; ++i) {
(, uint256 collateralBalanceInBase) = _purchasableBalanceOfAsset(assets[i], params.maxAmountsToPurchase[i]);
if (collateralBalanceInBase > params.liquidationThreshold) {
flashSwapAmount += collateralBalanceInBase;
assetBaseAmounts[i] = collateralBalanceInBase;
}
}

require(flashSwapAmount > 0, 'No collateral to buy.');

bool zeroForOne = WETH < USDC; // tokenIn < tokenOut
PoolAddress.PoolKey memory poolKey = _getFlashSwapPoolKey(USDC, WETH);
IUniswapV3Pool pool = _getFlashSwapPool(poolKey);

pool.swap(
address(this),
zeroForOne,
-int256(flashSwapAmount),
zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1,
abi.encode(SwapCallbackData({ poolKey: poolKey, assets: assets, assetBaseAmounts: assetBaseAmounts }))
);
}

function _updateDataFeeds(bytes[][] memory signedDataArray) internal {
uint256 len = signedDataArray.length;
for (uint256 i; i < len; ++i) {
oevExtension.updateDappOevDataFeed(DAPP_ID, signedDataArray[i]);
}
}

function _withdrawWeth() internal {
uint256 wethBalance = weth.balanceOf(address(this));
if (wethBalance > 0) {
weth.withdraw(wethBalance);
}
}

function _transferProfit(uint256 ethBalanceBefore) internal returns (uint256, uint256) {
uint256 ethBalanceAfter = address(this).balance;

uint8 numberOfAssets = comet.numAssets();
IComet.AssetInfo memory wethAsset;
for (uint8 i; i < numberOfAssets; ++i) {
IComet.AssetInfo memory assetInfo = comet.getAssetInfo(i);
if (assetInfo.asset == WETH) {
wethAsset = assetInfo;
}
}

uint256 profit = ethBalanceAfter - ethBalanceBefore;
uint256 profitUsd = (profit * comet.getPrice(wethAsset.priceFeed)) / wethAsset.scale;
profitReceiver.call{ value: profit }('');

return (profit, profitUsd);
}

function _purchasableBalanceOfAsset(
address asset,
uint256 maxCollateralToPurchase
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../../utils/interfaces/ISelfMulticall.sol";

interface IAccessControlRegistryAdminned is ISelfMulticall {
function accessControlRegistry() external view returns (address);

function adminRoleDescription() external view returns (string memory);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IAccessControlRegistryAdminned.sol";

interface IAccessControlRegistryAdminnedWithManager is
IAccessControlRegistryAdminned
{
function manager() external view returns (address);

function adminRole() external view returns (bytes32);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../../access/interfaces/IAccessControlRegistryAdminnedWithManager.sol";
import "../interfaces/IDataFeedServer.sol";

interface IApi3ServerV1OevExtension is
IAccessControlRegistryAdminnedWithManager,
IDataFeedServer
{
event Withdrew(address recipient, uint256 amount, address sender);

event PaidOevBid(
uint256 indexed dappId,
address indexed updater,
uint256 bidAmount,
uint256 signedDataTimestampCutoff,
address auctioneer
);

event UpdatedDappOevDataFeed(
uint256 indexed dappId,
address indexed updater,
bytes32 dataFeedId,
int224 updatedValue,
uint32 updatedTimestamp
);

function withdraw(address recipient, uint256 amount) external;

function payOevBid(
uint256 dappId,
uint32 signedDataTimestampCutoff,
bytes calldata signature,
uint256 bidAmount,
bytes calldata data
) external;

function updateDappOevDataFeed(
uint256 dappId,
bytes[] calldata signedData
)
external
returns (
bytes32 baseDataFeedId,
int224 updatedValue,
uint32 updatedTimestamp
);

function simulateDappOevDataFeedUpdate(
uint256 dappId,
bytes[] calldata signedData
)
external
returns (
bytes32 baseDataFeedId,
int224 updatedValue,
uint32 updatedTimestamp
);

function simulateExternalCall(
address target,
bytes calldata data
) external returns (bytes memory);

function oevDataFeed(
uint256 dappId,
bytes32 dataFeedId
) external view returns (int224 value, uint32 timestamp);

// solhint-disable-next-line func-name-mixedcase
function WITHDRAWER_ROLE_DESCRIPTION()
external
view
returns (string memory);

// solhint-disable-next-line func-name-mixedcase
function AUCTIONEER_ROLE_DESCRIPTION()
external
view
returns (string memory);

function withdrawerRole() external view returns (bytes32);

function auctioneerRole() external view returns (bytes32);

function api3ServerV1() external view returns (address);

function dappIdToLastPaidBid(
uint256 dappId
) external view returns (address updater, uint32 endTimestamp);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title Callback for IApi3ServerV1OevExtension#payOevBid
/// @notice Any contract that calls IApi3ServerV1OevExtension#payOevBid must implement this interface
interface IApi3ServerV1OevExtensionPayOevBidCallback {
/// @notice Called to `msg.sender` after granting the privilege to execute updates for the dApp from IApi3ServerV1OevExtension#payOevBid.
/// @dev In the implementation you must repay the server the tokens owed for the payment of the OEV bid.
/// The implementation is responsible to check that the caller of this method is the correct Api3ServerV1OevExtension.
/// @param amountOwed The amount of tokens owed to the server for the payment of the OEV bid
/// @param data Any data passed through by the caller via the IApi3ServerV1OevExtension#payOevBid call
function api3ServerV1OevExtensionPayOevBidCallback(
uint256 amountOwed,
bytes calldata data
) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../../utils/interfaces/IExtendedSelfMulticall.sol";

interface IDataFeedServer is IExtendedSelfMulticall {
event UpdatedBeaconWithSignedData(
bytes32 indexed beaconId,
int224 value,
uint32 timestamp
);

event UpdatedBeaconSetWithBeacons(
bytes32 indexed beaconSetId,
int224 value,
uint32 timestamp
);

function updateBeaconSetWithBeacons(
bytes32[] memory beaconIds
) external returns (bytes32 beaconSetId);
}
Loading

0 comments on commit 3599a2a

Please sign in to comment.