Robinhood MainnetChain ID: 4663
Contract detail

SbrBuybackBurner

SbrBuybackBurner is a verified Robinhood Chain smart contract at 0x2606f2df…0d3f9d17 with 4 indexed transactions.

Address type
Smart contract
Transactions
4
First indexed block
#28464940
Latest indexed block
#28486902
Indexed token transfers
14
ERC-20 transfers
14
Emitted events
4
Verification
Verified
Compiler
v0.8.26+commit.8a97fa7a
Verification match
full
Creation transaction
0xa20139b5…b710af74
Created at block
#28464940
Language
solidity
EVM version
paris
License
mit
Compiler optimization
Enabled · 200 runs
Related activity

Transactions

Verified contract

Contract source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
}

interface IUniswapV3Pool {
    function token0() external view returns (address);
    function token1() external view returns (address);
    function slot0()
        external
        view
        returns (
            uint160 sqrtPriceX96,
            int24 tick,
            uint16 observationIndex,
            uint16 observationCardinality,
            uint16 observationCardinalityNext,
            uint8 feeProtocol,
            bool unlocked
        );
    function observations(uint256 index)
        external
        view
        returns (
            uint32 blockTimestamp,
            int56 tickCumulative,
            uint160 secondsPerLiquidityCumulativeX128,
            bool initialized
        );
    function observe(uint32[] calldata secondsAgos)
        external
        view
        returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s);
    function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external;
    function swap(
        address recipient,
        bool zeroForOne,
        int256 amountSpecified,
        uint160 sqrtPriceLimitX96,
        bytes calldata data
    ) external returns (int256 amount0, int256 amount1);
}

/**
 * @title SbrBuybackBurner
 * @notice Spends its WETH balance buying the launched token on its own Pons
 * pool and burns everything it receives, leaning into weakness by spending
 * more when the token is cheap against its own recent average.
 *
 * `execute` is deliberately permissionless, which rules out taking a minimum
 * output from the caller: anyone could then pass zero, sandwich the swap and
 * keep the difference. The contract therefore bounds its own execution, and
 * does so three ways.
 *
 * **It will not overpay.** Spot is compared against the pool's own
 * time-weighted average and the trade is refused when the price has run above
 * that average by more than the allowed band. The guard is deliberately
 * one-sided: its purpose is to avoid buying into a price someone has pushed
 * up, and a price *below* the average is not a hazard to a buyer — it is the
 * whole point. Ticks are compared as ticks, since a tick is a fixed 1.0001x
 * step and their difference already expresses the price ratio, which avoids
 * reproducing Uniswap's tick-to-price maths and the risk of hand-rolling it.
 *
 * **It spends in proportion to the discount.** At or above the average it
 * commits only `baseSpendBps` of what it holds, keeping the rest for a better
 * price; the deeper the discount the larger the share, reaching everything at
 * `fullSpendTicks`. Because the balance keeps accruing while only a fraction
 * leaves each time, the contract naturally builds a reserve during expensive
 * stretches and empties it into cheap ones. Note this never becomes "wait for
 * a dip": something is always bought, so a market that only ever rises cannot
 * leave the programme sitting idle.
 *
 * **It caps the damage of a single trade.** The pool is handed a
 * `sqrtPriceLimitX96`, so even a permitted trade cannot walk the price past a
 * fixed bound — the swap fills partially and the remainder waits.
 *
 * A fresh pool has almost no price history, and an average taken over a minute
 * is noise rather than signal. Rather than refusing to work until the pool has
 * matured, the contract reads how much history actually exists and uses the
 * longest window available to it. Below `minWindowForScaling` it skips the
 * discount logic entirely and simply spends what it has, so buybacks are
 * visible from the first minutes of trading; scaling then engages on its own
 * as the pool accumulates observations, with no intervention required.
 */
contract SbrBuybackBurner {
    address public constant BURN = 0x000000000000000000000000000000000000dEaD;
    uint16 public constant BPS = 10_000;

    /// Ceilings on how permissive governance may ever make execution.
    uint24 public constant MAX_TICK_DEVIATION_LIMIT = 1_000; // ~10%
    uint16 public constant MAX_PRICE_MOVE_LIMIT = 1_000; // 10% of sqrt price
    uint32 public constant MIN_TWAP_WINDOW = 300; // 5 minutes
    /// Shortest history that still defends against a same-block sandwich.
    uint32 public constant MIN_USABLE_WINDOW = 60;
    /// Governance may not starve the programme into inactivity.
    uint16 public constant MIN_BASE_SPEND_BPS = 500; // 5%

    IERC20 public immutable weth;
    /// The launched token. Neutral name for the same reason as the splitter:
    /// the getter's selector is public even when the source is not.
    IERC20 public immutable token;
    IUniswapV3Pool public immutable pool;
    /// True when WETH is token0, i.e. buying the token moves price downward.
    bool public immutable zeroForOne;

    address public governor;

    uint32 public twapWindow = 21_600; // 6 hours, trimmed to what the pool has
    uint24 public maxTickDeviation = 200; // ~2% above the average
    uint16 public maxPriceMoveBps = 200; // 2% of sqrt price per execution

    /// Share of the balance committed when the token is not cheap.
    uint16 public baseSpendBps = 2_500;
    /// Discount, in ticks, at which the whole balance is committed. ~1.5%.
    uint24 public fullSpendTicks = 150;
    /// History below which the discount logic is skipped as noise.
    uint32 public minWindowForScaling = 3_600;

    uint256 public maxSpendPerExecution = type(uint256).max;
    uint256 public minSpend;
    uint256 public cooldown;
    uint256 public lastExecuted;

    /// Running totals, so the record of what this contract has done is
    /// readable in one call rather than reconstructed from event logs.
    uint256 public buybackCount;
    uint256 public totalSpent;
    uint256 public totalBurned;

    bool private _swapping;

    event BoughtAndBurned(uint256 spent, uint256 burned, int24 discountTicks, uint32 window);
    event ParamsUpdated(
        uint32 twapWindow, uint24 maxTickDeviation, uint16 maxPriceMoveBps, uint256 maxSpendPerExecution
    );
    event ScalingUpdated(uint16 baseSpendBps, uint24 fullSpendTicks, uint32 minWindowForScaling);
    event LimitsUpdated(uint256 minSpend, uint256 cooldown);
    event GovernorTransferred(address previousGovernor, address newGovernor);

    error ZeroAddress();
    error NotGovernor();
    error NotPool();
    error NoSwapInProgress();
    error CoolingDown(uint256 readyAt);
    error BelowMinimum(uint256 amount, uint256 required);
    error PriceAboveBand(int24 spotTick, int24 twapTick, uint24 allowed);
    error NoPriceHistory(uint32 available);
    error NothingBought();
    error WindowTooShort(uint32 window);
    error DeviationTooHigh(uint24 deviation);
    error PriceMoveTooHigh(uint16 bps);
    error BaseSpendTooLow(uint16 bps);
    error BadScaling();
    error TransferFailed();

    modifier onlyGovernor() {
        if (msg.sender != governor) revert NotGovernor();
        _;
    }

    /**
     * @param minSpend_ smallest trade worth making, in the quote asset
     * @param cooldown_ minimum gap between trades; zero leaves pacing to
     *        `minSpend_`, which ties it to volume rather than to a clock
     * @dev Both are set here rather than left to a later governance call: the
     * timelock owns this contract from birth, so anything not fixed at
     * construction would run on its defaults until a proposal cleared.
     */
    constructor(
        address weth_,
        address sbr_,
        address pool_,
        address governor_,
        uint256 minSpend_,
        uint256 cooldown_
    ) {
        if (weth_ == address(0) || sbr_ == address(0) || pool_ == address(0) || governor_ == address(0)) {
            revert ZeroAddress();
        }
        weth = IERC20(weth_);
        token = IERC20(sbr_);
        pool = IUniswapV3Pool(pool_);
        governor = governor_;
        zeroForOne = IUniswapV3Pool(pool_).token0() == weth_;
        minSpend = minSpend_;
        cooldown = cooldown_;
    }

    /* ------------------------------------------------------------------ */
    /* Operations — permissionless                                         */
    /* ------------------------------------------------------------------ */

    /// @notice Buys the launched token with the WETH held here and burns it.
    function execute() external returns (uint256 spent, uint256 burned) {
        uint256 readyAt = lastExecuted + cooldown;
        if (lastExecuted != 0 && block.timestamp < readyAt) revert CoolingDown(readyAt);

        uint256 balance = weth.balanceOf(address(this));

        uint32 window = usableWindow();
        if (window < MIN_USABLE_WINDOW) revert NoPriceHistory(window);

        (uint160 sqrtPriceX96, int24 spotTick,,,,,) = pool.slot0();
        int24 twapTick = _twapTick(window);
        int24 discountTicks = _discountTicks(spotTick, twapTick);

        // One-sided on purpose: a price above its own average is the case
        // worth refusing, a price below it is the case worth acting on.
        if (discountTicks < 0 && uint24(-discountTicks) > maxTickDeviation) {
            revert PriceAboveBand(spotTick, twapTick, maxTickDeviation);
        }

        uint256 amountIn = (balance * spendFraction(discountTicks, window)) / BPS;
        if (amountIn > maxSpendPerExecution) amountIn = maxSpendPerExecution;
        // The floor applies to what is actually traded, so a deep discount —
        // which commits a larger share — clears it on a smaller balance.
        if (amountIn == 0 || amountIn < minSpend) revert BelowMinimum(amountIn, minSpend);

        // Buying the token pushes price against us, so the bound sits below
        // spot when WETH is token0 and above it when WETH is token1.
        uint160 limit = zeroForOne
            ? uint160((uint256(sqrtPriceX96) * (BPS - maxPriceMoveBps)) / BPS)
            : uint160((uint256(sqrtPriceX96) * (BPS + maxPriceMoveBps)) / BPS);

        lastExecuted = block.timestamp;

        _swapping = true;
        (int256 amount0, int256 amount1) =
            pool.swap(address(this), zeroForOne, int256(amountIn), limit, "");
        _swapping = false;

        // The pool reports what it took as positive and what it paid as negative.
        spent = uint256(zeroForOne ? amount0 : amount1);
        uint256 received = uint256(-(zeroForOne ? amount1 : amount0));
        if (received == 0) revert NothingBought();

        // Burn the whole balance rather than just this fill, so any dust left
        // by an earlier partial fill goes out with it.
        burned = token.balanceOf(address(this));
        _send(address(token), BURN, burned);

        unchecked {
            buybackCount += 1;
            totalSpent += spent;
            totalBurned += burned;
        }

        emit BoughtAndBurned(spent, burned, discountTicks, window);
    }

    /// @notice Pays the pool for the swap it is executing on our behalf.
    /// @dev Anyone can invoke this selector, so it must prove the call came
    /// from the pool *during* a swap we started — otherwise it is a free
    /// withdrawal of this contract's WETH.
    function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {
        if (msg.sender != address(pool)) revert NotPool();
        if (!_swapping) revert NoSwapInProgress();

        int256 owed = zeroForOne ? amount0Delta : amount1Delta;
        if (owed > 0) _send(address(weth), address(pool), uint256(owed));
    }

    /// @notice Fresh V3 pools store a single observation, which caps how far
    /// back the average can reach. Anyone may enlarge the buffer.
    function growObservationBuffer(uint16 next) external {
        pool.increaseObservationCardinalityNext(next);
    }

    /* ------------------------------------------------------------------ */
    /* Views                                                               */
    /* ------------------------------------------------------------------ */

    /// @notice Seconds of price history the pool can actually serve.
    function availableWindow() public view returns (uint32) {
        (,, uint16 index, uint16 cardinality,,,) = pool.slot0();
        if (cardinality == 0) return 0;

        // Once the ring has wrapped, the slot after the current index holds
        // the oldest observation; before that it is still uninitialised and
        // slot zero is the oldest.
        (uint32 oldest,,, bool initialized) = pool.observations((index + 1) % cardinality);
        if (!initialized) (oldest,,,) = pool.observations(0);

        uint32 nowTs = uint32(block.timestamp);
        return nowTs > oldest ? nowTs - oldest : 0;
    }

    /// @notice The window actually used: the target, trimmed to what exists.
    function usableWindow() public view returns (uint32) {
        uint32 available = availableWindow();
        return available < twapWindow ? available : twapWindow;
    }

    /// @notice Share of the balance this contract would commit right now.
    /// @dev Below `minWindowForScaling` the average is noise, so the discount
    /// is ignored and everything held is spent — which is also what keeps
    /// buybacks visible in the first minutes after a launch.
    function spendFraction(int24 discountTicks, uint32 window) public view returns (uint256) {
        if (window < minWindowForScaling) return BPS;
        if (discountTicks <= 0) return baseSpendBps;

        uint24 magnitude = uint24(discountTicks);
        if (magnitude >= fullSpendTicks) return BPS;

        uint256 span = BPS - baseSpendBps;
        return baseSpendBps + (span * magnitude) / fullSpendTicks;
    }

    /// @notice How far the token sits below its own average, in ticks, with
    /// the window that figure was measured over. Positive means cheap.
    /// @dev Exposed so the reserve can gate a deployment on price without
    /// standing up a second oracle: one reading, one set of assumptions.
    /// Reverts rather than returning zero when there is no history, so a caller
    /// cannot mistake "no reading" for "no discount".
    function discount() external view returns (int24 ticks, uint32 window) {
        window = usableWindow();
        if (window < MIN_USABLE_WINDOW) revert NoPriceHistory(window);

        (, int24 spotTick,,,,,) = pool.slot0();
        ticks = _discountTicks(spotTick, _twapTick(window));
    }

    /// @notice Whether `execute` would currently succeed, for keeper polling.
    function ready() external view returns (bool) {
        if (lastExecuted != 0 && block.timestamp < lastExecuted + cooldown) return false;

        uint32 window = usableWindow();
        if (window < MIN_USABLE_WINDOW) return false;

        (, int24 spotTick,,,,,) = pool.slot0();
        int24 discountTicks = _discountTicks(spotTick, _twapTick(window));
        if (discountTicks < 0 && uint24(-discountTicks) > maxTickDeviation) return false;

        uint256 amountIn = (weth.balanceOf(address(this)) * spendFraction(discountTicks, window)) / BPS;
        if (amountIn > maxSpendPerExecution) amountIn = maxSpendPerExecution;
        return amountIn != 0 && amountIn >= minSpend;
    }

    /// @notice The whole record in one read: how many buybacks have run, what
    /// they cost, what they destroyed, what is waiting and when the next may go.
    function stats()
        external
        view
        returns (
            uint256 count,
            uint256 spent,
            uint256 burned,
            uint256 pending,
            uint256 lastAt,
            uint256 nextAt
        )
    {
        count = buybackCount;
        spent = totalSpent;
        burned = totalBurned;
        pending = weth.balanceOf(address(this));
        lastAt = lastExecuted;
        nextAt = lastExecuted == 0 ? 0 : lastExecuted + cooldown;
    }

    /* ------------------------------------------------------------------ */
    /* Governance — bounded                                                */
    /* ------------------------------------------------------------------ */

    function setParams(
        uint32 twapWindow_,
        uint24 maxTickDeviation_,
        uint16 maxPriceMoveBps_,
        uint256 maxSpendPerExecution_
    ) external onlyGovernor {
        if (twapWindow_ < MIN_TWAP_WINDOW) revert WindowTooShort(twapWindow_);
        if (maxTickDeviation_ > MAX_TICK_DEVIATION_LIMIT) revert DeviationTooHigh(maxTickDeviation_);
        if (maxPriceMoveBps_ > MAX_PRICE_MOVE_LIMIT) revert PriceMoveTooHigh(maxPriceMoveBps_);

        twapWindow = twapWindow_;
        maxTickDeviation = maxTickDeviation_;
        maxPriceMoveBps = maxPriceMoveBps_;
        maxSpendPerExecution = maxSpendPerExecution_;

        emit ParamsUpdated(twapWindow_, maxTickDeviation_, maxPriceMoveBps_, maxSpendPerExecution_);
    }

    function setScaling(uint16 baseSpendBps_, uint24 fullSpendTicks_, uint32 minWindowForScaling_)
        external
        onlyGovernor
    {
        if (baseSpendBps_ < MIN_BASE_SPEND_BPS || baseSpendBps_ > BPS) revert BaseSpendTooLow(baseSpendBps_);
        // A zero discount target would divide by zero; a window under the
        // usable floor would mean scaling on numbers too short to mean anything.
        if (fullSpendTicks_ == 0 || minWindowForScaling_ < MIN_USABLE_WINDOW) revert BadScaling();

        baseSpendBps = baseSpendBps_;
        fullSpendTicks = fullSpendTicks_;
        minWindowForScaling = minWindowForScaling_;

        emit ScalingUpdated(baseSpendBps_, fullSpendTicks_, minWindowForScaling_);
    }

    function setLimits(uint256 minSpend_, uint256 cooldown_) external onlyGovernor {
        minSpend = minSpend_;
        cooldown = cooldown_;
        emit LimitsUpdated(minSpend_, cooldown_);
    }

    function transferGovernor(address newGovernor) external onlyGovernor {
        if (newGovernor == address(0)) revert ZeroAddress();
        emit GovernorTransferred(governor, newGovernor);
        governor = newGovernor;
    }

    /* ------------------------------------------------------------------ */
    /* Internals                                                           */
    /* ------------------------------------------------------------------ */

    /// @dev Positive when the token is cheaper than its average. A tick is the
    /// price of token1 in token0, so which direction means "cheap" depends on
    /// which side of the pair the quote asset sits.
    function _discountTicks(int24 spotTick, int24 twapTick) private view returns (int24) {
        return zeroForOne ? spotTick - twapTick : twapTick - spotTick;
    }

    /// @dev Arithmetic mean tick over `window`, rounded the way Uniswap's own
    /// OracleLibrary rounds it.
    function _twapTick(uint32 window) private view returns (int24) {
        uint32[] memory secondsAgos = new uint32[](2);
        secondsAgos[0] = window;
        secondsAgos[1] = 0;

        (int56[] memory tickCumulatives,) = pool.observe(secondsAgos);
        int56 delta = tickCumulatives[1] - tickCumulatives[0];
        int56 span = int56(uint56(window));

        int24 tick = int24(delta / span);
        if (delta < 0 && delta % span != 0) tick--;
        return tick;
    }

    function _send(address asset, address to, uint256 amount) private {
        (bool ok, bytes memory data) = asset.call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount));
        if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TransferFailed();
    }
}