Module 1, Burn Hour, is live. Module 4 drafts at 12:00 UTC. Read it

Module 1: Burn Hour

One UTC hour a day, drawn from a seed fixed by the block hash at deployment, sends the 40% mechanic share to BURN. The other 23 hours send it to STAKERS.

Status
Live
Sinks
Burn / Stakers
Code · BurnHour.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IHibouModule, SwapContext} from "../IHibouModule.sol";

/// @title Burn Hour (module 1)
/// @notice One UTC hour a day sends the whole mechanic share to BURN; the rest of the day it goes
///         to STAKERS. The hour is drawn per day from a seed fixed at deployment (the block hash).
contract BurnHour is IHibouModule {
    bytes32 public immutable seed;

    constructor() {
        seed = blockhash(block.number - 1);
    }

    function burnHourOf(uint256 day) public view returns (uint256) {
        return uint256(keccak256(abi.encode(seed, day))) % 24;
    }

    function split(SwapContext calldata ctx) external view returns (uint16[5] memory w) {
        uint256 day = ctx.timestamp / 1 days;
        uint256 hour = (ctx.timestamp % 1 days) / 1 hours;
        if (hour == burnHourOf(day)) w[0] = 10_000;
        else w[1] = 10_000;
    }
}
Tests
    function test_feeOnExactHibouOut() public {
        _genesis();
        swapRouter.swap{value: 1_000 ether}(
            key,
            SwapParams({zeroForOne: true, amountSpecified: 500 ether, sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1}),
            PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
            ""
        );
        assertGt(address(hook).balance, 0);
    }

    function test_feeOnExactEthOut() public {
        _genesis();
        swapRouter.swap(
            key,
            SwapParams({zeroForOne: false, amountSpecified: 1 ether, sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1}),
            PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
            ""
        );
        assertEq(address(hook).balance, 0.01 ether);
    }

    function test_defaultSplitWithoutModule() public {
        _buy(1 ether, alice);
        // no author: the 20% joins stakers; module 40% defaults to stakers
        assertEq(hook.stakersPending(), 0.006 ether);
        assertEq(hook.burnPending(), 0.004 ether);
    }

    function test_burnHourRoutes() public {
        _genesis();
        uint256 day = block.timestamp / 1 days;
        uint256 h = burnHour.burnHourOf(day);
        // move to a non-burn hour
        uint256 other = (h + 1) % 24;
        vm.warp(day * 1 days + other * 1 hours + 60);
        _buy(1 ether, alice);
        assertEq(hook.burnPending(), 0.004 ether);
        assertEq(hook.stakersPending(), 0.004 ether);
        // the burn hour
        vm.warp(day * 1 days + 1 days + burnHour.burnHourOf(day + 1) * 1 hours + 60);
        _buy(1 ether, alice);
        assertEq(hook.burnPending(), 0.004 ether + 0.008 ether);
    }

    function test_stockWeather() public {
        _genesis();
        feed.set(100e8, 110e8);
        SwapContext memory c = _ctx();
        uint16[5] memory w = stockWeather.split(c);
        assertEq(w[2], 10_000, "up -> LP donate");
        feed.set(110e8, 100e8);
        w = stockWeather.split(c);
        assertEq(w[0], 10_000, "down -> burn");
        feed.breakIt();
        w = stockWeather.split(c);
        assertEq(w[0], 10_000, "broken feed -> burn");
    }

    function test_executeBurn() public {
        _buy(10 ether, alice);
        uint256 pending = hook.burnPending();
        assertEq(pending, 0.04 ether);
        uint256 deadBefore = hibou.balanceOf(hook.DEAD());
        vm.expectRevert(HibouDispatcher.NotKeeper.selector);
        hook.executeBurn(0);
        vm.prank(agent);
        uint256 out = hook.executeBurn(1);
        assertGt(out, 0);
        assertEq(hibou.balanceOf(hook.DEAD()) - deadBefore, out);
        assertEq(hook.burnPending(), 0);
        assertEq(hook.ethBurned(), pending);
    }

    function test_burnSlippage() public {
        _buy(10 ether, alice);
        vm.prank(agent);
        vm.expectRevert();
        hook.executeBurn(type(uint256).max);
        assertEq(hook.burnPending(), 0.04 ether, "reverted burn keeps the pot");
    }

    function test_startersPassScreen() public view {
        this.screen(address(burnHour));
        this.screen(address(stockWeather));
        this.screen(address(topBuyer));
    }

    function test_submitIdeaBurns() public {
        vm.startPrank(alice);
        hibou.approve(address(registry), 1_000 ether);
        uint256 id = registry.submitIdea("Burn twice on Fridays");
        vm.stopPrank();
        assertEq(id, 1);
        assertEq(hibou.balanceOf(registry.DEAD()), 1_000 ether);
        vm.prank(agent);
        registry.recordIdea(bob, "Rebate the first buy of each hour");
        assertEq(registry.ideaCount(), 2);
    }
Fork report
Tests
3 passed, 0 failed
Cases
test_burnHourRoutes, test_fullVoteFlow, test_startersPassScreen
Fork
local · local tests only: this module reads no external contract
Source hash
0x860db76b9546412161109826a1b75467cf868c1d0007477219c362c41cb388c9
Report hash
0x586d54ae9b04b3e08cb32a9236b5fb4f770b66e4f25c574bad1ed6ce443721df
Generated
2026-10-27 05:02 UTC

Vote

Pre-approved at genesis with the other starter modules. Every module after these goes through a 24 h vote and a 24 h timelock.

Onchain

Contract
at deployment
Author
Genesis
Code hash
set by the registry at deployment
Reads
block time
Source
0x860db76b9546412161109826a1b75467cf868c1d0007477219c362c41cb388c9
How a module works
Submit an ideaFree on the site. The agent picks one a day.