Robinhood MainnetChain ID: 4663
Contract detail

LiquidSniperAuctionV2

LiquidSniperAuctionV2 is a verified Robinhood Chain smart contract at 0x1cb194f2…b10efeea with 0 indexed transactions.

Address type
Smart contract
Transactions
0
History coverage
No indexed activity found
Native balance
0 ETH
Balance observed at
Block #35602995
Emitted events
1
Verification
Verified
Compiler
v0.8.28+commit.7893614a
Verification match
partial
Creation transaction
0x292547d7…def2ab06
Created at block
#930555
Language
solidity
EVM version
cancun
License
none
Compiler optimization
Enabled · 20000 runs
Verified contract

Contract source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ILiquid} from "../interfaces/ILiquid.sol";
import {ILiquidFeeLocker} from "../interfaces/ILiquidFeeLocker.sol";

import {ILiquidHookV2} from "../hooks/interfaces/ILiquidHookV2.sol";
import {ILiquidLpLocker} from "../interfaces/ILiquidLpLocker.sol";
import {ILiquidMevModule} from "../interfaces/ILiquidMevModule.sol";

import {ILiquidMevDescendingFees} from "./interfaces/ILiquidMevDescendingFees.sol";
import {ILiquidSniperAuctionV0} from "./interfaces/ILiquidSniperAuctionV0.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";

/*
 .--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--.
/ .. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \
\ \/\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ \/ /
 \/ /`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'\/ /
 / /\  ````````````````````````````````````````````````````````````````````````````````````  / /\
/ /\ \ ```````````````````````````````````````````````````````````````````````````````````` / /\ \
\ \/ / ```````::::::::``:::````````````:::`````::::````:::`:::````:::`::::::::::`:::::::::` \ \/ /
 \/ /  `````:+:````:+:`:+:``````````:+:`:+:```:+:+:```:+:`:+:```:+:``:+:````````:+:````:+:`  \/ /
 / /\  ````+:+````````+:+`````````+:+```+:+``:+:+:+``+:+`+:+``+:+```+:+````````+:+````+:+``  / /\
/ /\ \ ```+#+````````+#+````````+#++:++#++:`+#+`+:+`+#+`+#++:++````+#++:++#```+#++:++#:```` / /\ \
\ \/ / ``+#+````````+#+````````+#+`````+#+`+#+``+#+#+#`+#+``+#+```+#+````````+#+````+#+```` \ \/ /
 \/ /  `#+#````#+#`#+#````````#+#`````#+#`#+#```#+#+#`#+#```#+#``#+#````````#+#````#+#`````  \/ /
 / /\  `########``##########`###`````###`###````####`###````###`##########`###````###``````  / /\
/ /\ \ ```````````````````````````````````````````````````````````````````````````````````` / /\ \
\ \/ / ```````````````````````````````````````````````````````````````````````````````````` \ \/ /
 \/ /  ````````````````````````````````````````````````````````````````````````````````````  \/ /
 / /\.--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--..--./ /\
/ /\ \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \.. \/\ \
\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `'\ `' /
 `--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'`--'
*/

contract LiquidSniperAuctionV2 is
    ReentrancyGuard,
    ILiquidSniperAuctionV0,
    ILiquidMevDescendingFees,
    Ownable
{
    string public constant PROTOCOL = "Liquid Protocol";

    // gas peg and block number for a pool's auction
    mapping(PoolId => uint256 gasPeg) public gasPeg;
    mapping(PoolId => uint256 nextAuctionBlock) public nextAuctionBlock;
    // round of the auction
    mapping(PoolId => uint256 round) public round;

    // block between deployment and first auction
    uint256 public blocksBetweenDeploymentAndFirstAuction;

    // blocks between recurrent auction
    uint256 public blocksBetweenAuction;

    // max rounds of auction
    uint256 public maxRounds;

    // payment amount per gas unit difference
    uint256 public paymentPerGasUnit;

    // factory's portion of the payment
    uint256 public constant FACTORY_PORTION = 2000;
    uint256 public constant BPS = 10_000;

    // descending fee config
    mapping(PoolId poolId => FeeConfig feeConfig) public feeConfig;
    mapping(PoolId poolId => uint256 poolDecayStartTime) public poolDecayStartTime;

    // variable to have decay start at end of auction
    mapping(PoolId poolId => uint256 auctionTimestamp) internal auctionTimestamp;

    address public immutable weth;

    ILiquid public immutable liquidFactory;
    ILiquidFeeLocker public immutable feeLocker;

    constructor(address owner_, address _liquidFactory, address _feeLocker, address _weth)
        Ownable(owner_)
    {
        liquidFactory = ILiquid(_liquidFactory);
        feeLocker = ILiquidFeeLocker(_feeLocker);
        weth = _weth;

        blocksBetweenDeploymentAndFirstAuction = 2;
        blocksBetweenAuction = 2;
        maxRounds = 5;
        paymentPerGasUnit = 0.0001 ether;
    }

    modifier onlyHook(PoolKey calldata poolKey) {
        if (msg.sender != address(poolKey.hooks)) {
            revert OnlyHook();
        }
        _;
    }

    function setBlocksBetweenDeploymentAndFirstAuction(uint256 _blocksBetweenDeploymentAndFirstAuction)
        external
        onlyOwner
    {
        uint256 oldBlocksBetweenDeploymentAndFirstAuction = blocksBetweenDeploymentAndFirstAuction;
        blocksBetweenDeploymentAndFirstAuction = _blocksBetweenDeploymentAndFirstAuction;

        emit SetBlocksBetweenDeploymentAndFirstAuction(
            oldBlocksBetweenDeploymentAndFirstAuction, blocksBetweenDeploymentAndFirstAuction
        );
    }

    function setBlocksBetweenAuction(uint256 _blocksBetweenAuction) external onlyOwner {
        uint256 oldBlocksBetweenAuction = blocksBetweenAuction;
        blocksBetweenAuction = _blocksBetweenAuction;

        emit SetBlocksBetweenAuction(oldBlocksBetweenAuction, blocksBetweenAuction);
    }

    function setPaymentPerGasUnit(uint256 _paymentPerGasUnit) external onlyOwner {
        uint256 oldPaymentPerGasUnit = paymentPerGasUnit;
        paymentPerGasUnit = _paymentPerGasUnit;

        emit SetPaymentPerGasUnit(oldPaymentPerGasUnit, paymentPerGasUnit);
    }

    function setMaxRounds(uint256 _maxRounds) external onlyOwner {
        uint256 oldMaxRounds = maxRounds;
        maxRounds = _maxRounds;

        emit SetMaxRounds(oldMaxRounds, maxRounds);
    }

    // returns the the LP fee that will be used for the next swap or
    // zero if the decay period is over
    function getFee(PoolId poolId) external view returns (uint24) {
        // if the pool is not initialized, return zero
        if (gasPeg[poolId] == 0) {
            return 0;
        }

        // if the decay period has not started, return the starting fee
        if (poolDecayStartTime[poolId] == 0) {
            return feeConfig[poolId].startingFee;
        }

        // check if the decay period is over
        if (block.timestamp > poolDecayStartTime[poolId] + feeConfig[poolId].secondsToDecay) {
            // decay period is over, return zero
            return 0;
        }

        return _calculateFee(poolId);
    }

    function _validateFeeConfig(PoolKey calldata poolKey, FeeConfig memory feeConfigData)
        internal
        view
    {
        // ensure the seconds to decay is greater than zero
        if (feeConfigData.secondsToDecay == 0) {
            revert TimeDecayMustBeGreaterThanZero();
        }

        // ensure the starting fee is not zero
        if (feeConfigData.startingFee == 0) {
            revert StartingFeeMustBeGreaterThanZero();
        }

        // ensure the starting fee is greater than the ending fee
        if (feeConfigData.startingFee < feeConfigData.endingFee) {
            revert StartingFeeMustBeGreaterThanEndingFee();
        }

        // ensure that the associated hook is a LiquidHookV2
        if (!ILiquidHookV2(address(poolKey.hooks))
                .supportsInterface(type(ILiquidHookV2).interfaceId)) {
            revert OnlyLiquidHookV2();
        }

        // ensure the starting fee is not greater than the max mev LP fee
        if (feeConfigData.startingFee > ILiquidHookV2(address(poolKey.hooks)).MAX_MEV_LP_FEE()) {
            revert StartingFeeGreaterThanMaxLpFee();
        }

        // ensure the max time length is not longer than the max auction length
        if (
            feeConfigData.secondsToDecay
                > ILiquidHookV2(address(poolKey.hooks)).MAX_MEV_MODULE_DELAY()
        ) {
            revert TimeDecayLongerThanMaxMevDelay();
        }
    }

    // initialize the mev module for a specific pool, called by the hook
    function initialize(PoolKey calldata poolKey, bytes calldata descendingFeeConfig)
        external
        nonReentrant
        onlyHook(poolKey)
    {
        PoolId poolId = poolKey.toId();

        // check if the pool is already initialized
        if (gasPeg[poolId] != 0) {
            revert PoolAlreadyInitialized();
        }

        // get the first round's gas peg
        gasPeg[poolId] = _getBaseAuctionGasPeg(blocksBetweenDeploymentAndFirstAuction);

        // track the block number for the auction to be ran in
        nextAuctionBlock[poolId] = block.number + blocksBetweenDeploymentAndFirstAuction;

        // set the round to 1
        round[poolId] = 1;

        emit AuctionInitialized(poolId, gasPeg[poolId], nextAuctionBlock[poolId], round[poolId]);

        // initialize the descending fee config
        ILiquidMevDescendingFees.FeeConfig memory feeConfigData =
            abi.decode(descendingFeeConfig, (ILiquidMevDescendingFees.FeeConfig));

        // validate the descending fee config
        _validateFeeConfig(poolKey, feeConfigData);

        feeConfig[poolId] = feeConfigData;

        emit FeeConfigSet(
            poolId, feeConfigData.startingFee, feeConfigData.endingFee, feeConfigData.secondsToDecay
        );

        // set the auction timestamp to the current block timestamp
        auctionTimestamp[poolId] = block.timestamp;
    }

    function _getBaseAuctionGasPeg(uint256 _blocksBetweenAuction) internal view returns (uint256) {
        // Assuming that the sequencer is running vanilla EIP-1559, gas prices can increase
        // by max 12.5% per block if the previous block was full. To enable a clean signal
        // for the auction, we peg the starting auction's gas price to tx.base_gas *
        // (1.125 ^ (_blocksBetweenAuction))
        //
        // This ensures that the lowest gas price signal can accommodate the highest shift
        // in the gas price
        return block.basefee * (1125 ** _blocksBetweenAuction) / (1000 ** _blocksBetweenAuction);
    }

    // pull payment from the payee, the price is a multiple of tx's gas price
    // minus the gas peg
    function _pullPayment(PoolId poolId, bytes calldata auctionData)
        internal
        returns (uint256 paymentAmount)
    {
        (address payee) = abi.decode(auctionData, (address));

        // calculate the expected payment for the given gas price
        int256 gasSignal = int256(tx.gasprice) - int256(gasPeg[poolId]);

        // shouldn't be negative
        if (gasSignal < 0) {
            revert GasSignalNegative();
        }

        // calculate the expected payment for the given swap params
        paymentAmount = uint256(gasSignal) * paymentPerGasUnit;

        // pull payment from the payee
        SafeERC20.safeTransferFrom(IERC20(weth), payee, address(this), paymentAmount);

        emit AuctionWon(poolId, payee, paymentAmount, round[poolId]);
    }

    function _sendPayment(PoolKey calldata poolKey, bool liquidIsToken0, uint256 paymentAmount)
        internal
    {
        if (paymentAmount == 0) {
            return;
        }

        // determine factory vs lp payment split
        uint256 factoryPayment = paymentAmount * FACTORY_PORTION / BPS;
        uint256 lpPayment = paymentAmount - factoryPayment;

        // send factory's portion
        SafeERC20.safeTransfer(IERC20(weth), address(liquidFactory), factoryPayment);

        address liquid = liquidIsToken0
            ? Currency.unwrap(poolKey.currency0)
            : Currency.unwrap(poolKey.currency1);

        // grab locker address from factory
        address lpLocker = liquidFactory.tokenDeploymentInfo(liquid).locker;

        // get reward info from the locker
        ILiquidLpLocker.TokenRewardInfo memory tokenRewardInfo =
            ILiquidLpLocker(lpLocker).tokenRewards(liquid);

        // get the reward recipients and their splits
        uint256[] memory rewardsSplit = new uint256[](tokenRewardInfo.rewardBps.length);
        uint256 rewardTotal = 0;

        for (uint256 i = 0; i < tokenRewardInfo.rewardBps.length - 1; i++) {
            rewardsSplit[i] = tokenRewardInfo.rewardBps[i] * lpPayment / BPS;
            rewardTotal += rewardsSplit[i];
        }
        rewardsSplit[tokenRewardInfo.rewardBps.length - 1] = lpPayment - rewardTotal;

        // distribute the rewards
        SafeERC20.forceApprove(IERC20(weth), address(feeLocker), lpPayment);
        for (uint256 i = 0; i < tokenRewardInfo.rewardBps.length; i++) {
            feeLocker.storeFees(tokenRewardInfo.rewardRecipients[i], weth, rewardsSplit[i]);
        }

        emit AuctionRewardsTransferred(poolKey.toId(), lpPayment, factoryPayment);
    }

    function _prepareNextRound(PoolId poolId) internal {
        // bump round and record the auction timestamp
        round[poolId] = round[poolId] + 1;
        auctionTimestamp[poolId] = block.timestamp;

        // check if max rounds have been reached, if so,
        // trigger the start of the decay logic
        if (round[poolId] > maxRounds) {
            poolDecayStartTime[poolId] = block.timestamp;
            emit AuctionEnded(poolId);
            return;
        }

        // setup other variables for the next round
        gasPeg[poolId] = _getBaseAuctionGasPeg(blocksBetweenAuction);
        nextAuctionBlock[poolId] = block.number + blocksBetweenAuction;

        emit AuctionReset(poolId, gasPeg[poolId], nextAuctionBlock[poolId], round[poolId]);
    }

    function _calculateFee(PoolId poolId) internal view returns (uint24) {
        // how much time has passed since pool creation
        uint256 timeDecay =
            feeConfig[poolId].secondsToDecay - (block.timestamp - (poolDecayStartTime[poolId]));
        uint256 feeRange = feeConfig[poolId].startingFee - feeConfig[poolId].endingFee;

        // Parabolic decay: fee = endingFee + feeRange * (timeDecay / timeToDecay)²
        uint256 normalizedTime = (timeDecay * 1e18) / feeConfig[poolId].secondsToDecay; // Scale for precision
        uint256 squaredTime = (normalizedTime * normalizedTime) / 1e18;
        uint256 decayAmount = (feeRange * squaredTime) / 1e18;

        return uint24(feeConfig[poolId].endingFee + decayAmount);
    }

    function _setLpFee(PoolKey calldata poolKey, uint24 lpFee) internal {
        // call back into the hook to update the fee for the swap
        ILiquidHookV2(msg.sender).mevModuleSetFee(poolKey, lpFee);
    }

    function _handleFeeDecay(PoolKey calldata poolKey) internal returns (bool disableMevModule) {
        PoolId poolId = poolKey.toId();

        // check if the decay period is over
        if (block.timestamp > poolDecayStartTime[poolId] + feeConfig[poolId].secondsToDecay) {
            // decay period is over, disable the mev module
            emit DecayPeriodOver(poolId);
            return true;
        }

        // decay period is not over, set the LP fee
        _setLpFee(poolKey, _calculateFee(poolId));

        // mev module is still active
        return false;
    }

    // the hook calls this function in it's _beforeSwap logic
    function beforeSwap(
        PoolKey calldata poolKey,
        IPoolManager.SwapParams calldata,
        bool liquidIsToken0,
        bytes calldata auctionData // expected to be address paying
    ) external nonReentrant onlyHook(poolKey) returns (bool disableMevModule) {
        // check if the auction is ready to be ran or if we need to trigger the decay logic
        if (poolDecayStartTime[poolKey.toId()] != 0) {
            // decay period is active, allow the decay logic to handle setting the LP fee
            return _handleFeeDecay(poolKey);
        } else if (block.number < nextAuctionBlock[poolKey.toId()]) {
            // auction block not reached yet
            revert NotAuctionBlock();
        } else if (block.number > nextAuctionBlock[poolKey.toId()]) {
            // auction block has passed, trigger the decay logic start
            emit AuctionExpired(poolKey.toId(), round[poolKey.toId()]);

            // note: the decay period starts at the last targeted auction block's timestamp
            poolDecayStartTime[poolKey.toId()] = auctionTimestamp[poolKey.toId()];
            return _handleFeeDecay(poolKey);
        }
        // block == nextAuctionBlock, run the auction logic

        // pull payment from the payee
        uint256 paymentAmount = _pullPayment(poolKey.toId(), auctionData);

        // send payment to fee recipients
        _sendPayment(poolKey, liquidIsToken0, paymentAmount);

        // set the LP fee to the starting fee
        _setLpFee(poolKey, feeConfig[poolKey.toId()].startingFee);

        // setup auction for next round
        _prepareNextRound(poolKey.toId());

        // mev module is still active
        return false;
    }

    // implements the ILiquidMevModule interface
    function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
        return interfaceId == type(ILiquidMevModule).interfaceId;
    }
}