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();
}
}