Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # exdiv · contracts and deployment
- one contract that is the erc 20, the erc 8056 surface, and the uniswap v4 hook. fee capture in native eth on both swap directions, a daily declare and pay cycle on utc bells, payment as a buyback through its own pool that retires what it buys and ticks the multiplier. no owner, no admin functions, no pause, no upgrade. plus the foundry deploy script that mines the hook address flags and initializes the canonical pool.
- copy each block into the stated path.
- ## 0. setup
- ```bash
- forge init exdiv && cd exdiv
- forge install uniswap/v4-core
- forge install uniswap/v4-periphery
- forge install OpenZeppelin/openzeppelin-contracts
- ```
- ## 1. foundry.toml
- ```toml
- [profile.default]
- src = "src"
- out = "out"
- libs = ["lib"]
- solc_version = "0.8.26"
- evm_version = "cancun"
- optimizer = true
- optimizer_runs = 800
- via_ir = true
- [rpc_endpoints]
- robinhood = "https://rpc.mainnet.chain.robinhood.com"
- robinhood_testnet = "https://rpc.testnet.chain.robinhood.com"
- ```
- ## 2. remappings.txt
- ```text
- @uniswap/v4-core/=lib/v4-core/
- @uniswap/v4-periphery/=lib/v4-periphery/
- @openzeppelin/=lib/openzeppelin-contracts/
- ```
- ## 3. src/Exdiv.sol
- ```solidity
- // SPDX-License-Identifier: MIT
- pragma solidity 0.8.26;
- /// exdiv
- ///
- /// a fixed raw supply erc 20 on robinhood chain that is simultaneously the
- /// uniswap v4 hook attached to its canonical exdiv/eth pool. every swap
- /// through that pool surrenders FEE_BPS of its native eth leg into an
- /// internal dividend reserve. once per day the reserve is declared at the
- /// close bell and paid at the next close bell: the hook buys exdiv through
- /// its own pool, burns what it bought, and ticks the erc 8056 ui multiplier
- /// so every holder's ui balance grows pro rata. raw balances never move.
- ///
- /// the ritual, mapped to mechanics:
- /// 21:00 utc declare reserve snapshot locked, pending tick published
- /// through newUIMultiplier and effectiveAt
- /// 13:30 utc ex informational bell between declaration and payment;
- /// the arithmetic defense against dividend capture is
- /// the round trip fee, see the whitepaper
- /// 21:00 utc pay buyback executes, tokens burn, multiplier ticks
- ///
- /// there is no owner. declare and pay are permissionless pokes. nothing in
- /// this contract can move reserve eth anywhere except into the buyback.
- import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
- import {BaseHook} from "@uniswap/v4-periphery/src/utils/BaseHook.sol";
- import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
- import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
- import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
- import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
- import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
- import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
- import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
- import {BeforeSwapDelta, toBeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
- import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
- import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
- contract Exdiv is ERC20, BaseHook {
- using CurrencyLibrary for Currency;
- using PoolIdLibrary for PoolKey;
- using StateLibrary for IPoolManager;
- // ===============================================================
- // constants: the whole rulebook, immutable at compile time
- // ===============================================================
- /// raw supply, minted once at construction to the deployer, who seeds
- /// the canonical pool. the contract itself never mints again.
- uint256 public constant RAW_SUPPLY = 100_000_000e18;
- /// fee taken on the native eth leg of every swap, in basis points.
- uint256 public constant FEE_BPS = 200; // 2 percent
- /// bells, as seconds after 00:00 utc.
- uint256 public constant DECLARE_UTC = 75_600; // 21:00
- uint256 public constant EX_UTC = 48_600; // 13:30
- /// slippage guard on the buyback: payment reverts if the pool's spot
- /// price has moved more than this many bps against the buy since
- /// declaration. anyone can poke pay again next block.
- uint256 public constant PAY_MAX_DRIFT_BPS = 500;
- /// erc 8056 scale.
- uint256 public constant ONE = 1e18;
- /// lp fee and tick spacing of the canonical pool. the hook fee is the
- /// economic fee; the pool's own lp fee is zero.
- uint24 public constant POOL_FEE = 0;
- int24 public constant TICK_SPACING = 60;
- // ===============================================================
- // state
- // ===============================================================
- /// erc 8056: current multiplier. starts at ONE, only ever steps up.
- uint256 private _uiMultiplier = ONE;
- /// cycle state. a declaration locks amount and publishes the pending
- /// tick; payment consumes it.
- uint256 public declaredReserve; // eth locked for the pending payment
- uint256 public declaredAt; // timestamp of the declaration
- uint160 public declaredSqrtPriceX96; // pool spot at declaration, for drift
- uint64 public payAt; // when the pending tick takes effect
- uint256 private _pendingMultiplier; // indicative next multiplier
- /// reserve accounting. everything the hook holds beyond declaredReserve
- /// is the accruing pool for the next cycle.
- uint256 public lifetimeCollected;
- uint256 public lifetimePaid;
- uint256 public cyclesPaid;
- /// the canonical pool, fixed at initialization, refused elsewhere.
- PoolKey public poolKey;
- bool public poolInitialized;
- /// reentrancy and self recognition during the buyback.
- bool private _paying;
- // ===============================================================
- // events
- // ===============================================================
- event Collected(uint256 amount, bool onBuySide);
- event Declared(uint256 reserve, uint256 pendingMultiplier, uint64 payAt);
- event Paid(uint256 ethIn, uint256 rawBought, uint256 newMultiplier);
- // ===============================================================
- // construction
- // ===============================================================
- constructor(IPoolManager manager)
- ERC20("exdiv", "exdiv")
- BaseHook(manager)
- {
- _mint(msg.sender, RAW_SUPPLY);
- }
- /// native eth arrives from poolManager.take during fee capture and
- /// from settlement during the buyback.
- receive() external payable {}
- // ===============================================================
- // erc 8056 · scaled ui amounts
- // ===============================================================
- /// current multiplier. ui units = raw balance * uiMultiplier / 1e18.
- function uiMultiplier() external view returns (uint256) {
- return _uiMultiplier;
- }
- /// pending multiplier between declaration and payment. indicative:
- /// the exact value is fixed by the buyback fill at the bell. zero
- /// when nothing is declared.
- function newUIMultiplier() external view returns (uint256) {
- return _pendingMultiplier;
- }
- /// when the pending multiplier takes effect. zero when nothing is
- /// declared.
- function effectiveAt() external view returns (uint64) {
- return payAt;
- }
- /// convenience: a holder's balance in ui units.
- function uiBalanceOf(address who) external view returns (uint256) {
- return (balanceOf(who) * _uiMultiplier) / ONE;
- }
- // ===============================================================
- // bells
- // ===============================================================
- function _dayStart(uint256 t) internal pure returns (uint256) {
- return t - (t % 86_400);
- }
- /// the most recent 21:00 utc at or before t.
- function lastCloseBell(uint256 t) public pure returns (uint256) {
- uint256 todaysBell = _dayStart(t) + DECLARE_UTC;
- return t >= todaysBell ? todaysBell : todaysBell - 86_400;
- }
- /// the next 21:00 utc strictly after t.
- function nextCloseBell(uint256 t) public pure returns (uint256) {
- return lastCloseBell(t) + 86_400;
- }
- // ===============================================================
- // cycle · declare
- // ===============================================================
- /// permissionless. valid once per close bell. locks everything the
- /// hook has accrued as the declared reserve and publishes the pending
- /// tick, payable at the next close bell.
- function declare() external {
- require(poolInitialized, "pool not initialized");
- require(payAt == 0, "already declared");
- uint256 bell = lastCloseBell(block.timestamp);
- require(declaredAt < bell, "this bell already declared");
- uint256 reserve = address(this).balance;
- require(reserve > 0, "nothing accrued");
- declaredReserve = reserve;
- declaredAt = block.timestamp;
- payAt = uint64(nextCloseBell(block.timestamp));
- (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey.toId());
- declaredSqrtPriceX96 = sqrtPriceX96;
- _pendingMultiplier = _quotePendingMultiplier(reserve, sqrtPriceX96);
- emit Declared(reserve, _pendingMultiplier, payAt);
- }
- /// indicative tick from spot: raw exdiv per eth at sqrtPriceX96, with
- /// the hook's own fee applied to the buyback input, then
- /// m' = m * supply / (supply - bought). currency0 is native eth, so
- /// price = (sqrtP / 2^96)^2 in token1 per token0.
- function _quotePendingMultiplier(uint256 reserve, uint160 sqrtPriceX96)
- internal
- view
- returns (uint256)
- {
- uint256 priceX192 = uint256(sqrtPriceX96) * uint256(sqrtPriceX96);
- uint256 ethAfterFee = reserve - ((reserve * FEE_BPS) / 10_000);
- uint256 estBought = (ethAfterFee * priceX192) >> 192;
- uint256 supply = totalSupply();
- if (estBought == 0 || estBought >= supply) return _uiMultiplier;
- return (_uiMultiplier * supply) / (supply - estBought);
- }
- // ===============================================================
- // cycle · pay
- // ===============================================================
- /// permissionless. valid at or after payAt. buys exdiv through the
- /// canonical pool with the declared reserve, burns the fill, ticks
- /// the multiplier by exact supply ratio, resets the cycle.
- function pay() external {
- require(payAt != 0 && block.timestamp >= payAt, "not payable yet");
- require(!_paying, "reentrant");
- _paying = true;
- // drift guard: spot must be within PAY_MAX_DRIFT_BPS of the
- // declaration spot. a failed guard leaves the declaration standing;
- // poke again next block.
- (uint160 spot,,,) = poolManager.getSlot0(poolKey.toId());
- uint256 hi = uint256(declaredSqrtPriceX96) * (10_000 + PAY_MAX_DRIFT_BPS) / 10_000;
- uint256 lo = uint256(declaredSqrtPriceX96) * (10_000 - PAY_MAX_DRIFT_BPS) / 10_000;
- require(uint256(spot) >= lo && uint256(spot) <= hi, "drift");
- uint256 supplyBefore = totalSupply();
- uint256 ethIn = declaredReserve;
- // the buyback: swap native eth (currency0) for exdiv (currency1),
- // exact input, no price limit beyond the drift guard above. the
- // unlock callback routes through poolManager.swap with this
- // contract as sender; beforeSwap recognizes _paying and takes no
- // fee on the hook's own flow.
- uint256 rawBought = _buyback(ethIn);
- require(rawBought > 0, "empty fill");
- // retire the fill and tick by the exact ratio, conserving total
- // ui units: (S - b) * m' = S * m.
- _burn(address(this), rawBought);
- uint256 supplyAfter = totalSupply();
- _uiMultiplier = (_uiMultiplier * supplyBefore) / supplyAfter;
- lifetimePaid += ethIn;
- cyclesPaid += 1;
- // reset the cycle. anything accrued between declaration and now
- // is already in the balance and rolls into the next declaration.
- declaredReserve = 0;
- declaredSqrtPriceX96 = 0;
- payAt = 0;
- _pendingMultiplier = 0;
- _paying = false;
- emit Paid(ethIn, rawBought, _uiMultiplier);
- }
- /// executes the swap inside the pool manager's unlock. verify the
- /// callback plumbing against the installed v4-periphery version: this
- /// uses the unlock pattern with a self dispatch.
- function _buyback(uint256 ethIn) internal returns (uint256 rawBought) {
- bytes memory result = poolManager.unlock(abi.encode(ethIn));
- rawBought = abi.decode(result, (uint256));
- }
- function unlockCallback(bytes calldata data)
- external
- returns (bytes memory)
- {
- require(msg.sender == address(poolManager), "not manager");
- uint256 ethIn = abi.decode(data, (uint256));
- SwapParams memory params = SwapParams({
- zeroForOne: true,
- amountSpecified: -int256(ethIn),
- sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
- });
- BalanceDelta delta = poolManager.swap(poolKey, params, "");
- // settle the eth we owe, take the exdiv we are owed.
- poolManager.sync(Currency.wrap(address(0)));
- poolManager.settle{value: ethIn}();
- int256 amt1 = int256(delta.amount1());
- require(amt1 > 0, "bad delta");
- poolManager.take(Currency.wrap(address(this)), address(this), uint256(amt1));
- return abi.encode(uint256(amt1));
- }
- // ===============================================================
- // hook · permissions and callbacks
- // ===============================================================
- function getHookPermissions()
- public
- pure
- override
- returns (Hooks.Permissions memory)
- {
- return Hooks.Permissions({
- beforeInitialize: true,
- afterInitialize: false,
- beforeAddLiquidity: false,
- afterAddLiquidity: false,
- beforeRemoveLiquidity: false,
- afterRemoveLiquidity: false,
- beforeSwap: true,
- afterSwap: true,
- beforeDonate: false,
- afterDonate: false,
- beforeSwapReturnDelta: true,
- afterSwapReturnDelta: true,
- afterAddLiquidityReturnDelta: false,
- afterRemoveLiquidityReturnDelta: false
- });
- }
- /// the hook attaches to exactly one pool: native eth against this
- /// token, POOL_FEE, TICK_SPACING. everything else is refused.
- function _beforeInitialize(address, PoolKey calldata key, uint160)
- internal
- override
- returns (bytes4)
- {
- require(!poolInitialized, "pool exists");
- require(Currency.unwrap(key.currency0) == address(0), "c0 not native");
- require(Currency.unwrap(key.currency1) == address(this), "c1 not exdiv");
- require(key.fee == POOL_FEE && key.tickSpacing == TICK_SPACING, "bad key");
- poolKey = key;
- poolInitialized = true;
- return BaseHook.beforeInitialize.selector;
- }
- /// fee on buys: exact input eth -> exdiv surrenders FEE_BPS of the
- /// input before the swap. the hook's own buyback is exempt.
- function _beforeSwap(
- address,
- PoolKey calldata,
- SwapParams calldata params,
- bytes calldata
- ) internal override returns (bytes4, BeforeSwapDelta, uint24) {
- if (_paying) {
- return (BaseHook.beforeSwap.selector, toBeforeSwapDelta(0, 0), 0);
- }
- // zeroForOne exact input: specified currency is native eth.
- if (params.zeroForOne && params.amountSpecified < 0) {
- uint256 inAmt = uint256(-params.amountSpecified);
- uint256 fee = (inAmt * FEE_BPS) / 10_000;
- if (fee > 0) {
- poolManager.take(Currency.wrap(address(0)), address(this), fee);
- lifetimeCollected += fee;
- emit Collected(fee, true);
- return (
- BaseHook.beforeSwap.selector,
- toBeforeSwapDelta(int128(int256(fee)), 0),
- 0
- );
- }
- }
- return (BaseHook.beforeSwap.selector, toBeforeSwapDelta(0, 0), 0);
- }
- /// fee on sells: exdiv -> eth surrenders FEE_BPS of the eth output
- /// after the swap. the hook's own buyback is exempt.
- function _afterSwap(
- address,
- PoolKey calldata,
- SwapParams calldata params,
- BalanceDelta delta,
- bytes calldata
- ) internal override returns (bytes4, int128) {
- if (_paying || params.zeroForOne) {
- return (BaseHook.afterSwap.selector, 0);
- }
- int256 ethOut = int256(delta.amount0());
- if (ethOut > 0) {
- uint256 fee = (uint256(ethOut) * FEE_BPS) / 10_000;
- if (fee > 0) {
- poolManager.take(Currency.wrap(address(0)), address(this), fee);
- lifetimeCollected += fee;
- emit Collected(fee, false);
- return (BaseHook.afterSwap.selector, int128(int256(fee)));
- }
- }
- return (BaseHook.afterSwap.selector, 0);
- }
- // ===============================================================
- // views for the site
- // ===============================================================
- /// everything accruing for the next cycle, on top of any declared
- /// reserve awaiting payment.
- function accruing() external view returns (uint256) {
- return address(this).balance - declaredReserve;
- }
- }
- ```
- ## 4. script/Deploy.s.sol
- ```solidity
- // SPDX-License-Identifier: MIT
- pragma solidity 0.8.26;
- import {Script, console2} from "forge-std/Script.sol";
- import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
- import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
- import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
- import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
- import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
- import {HookMiner} from "@uniswap/v4-periphery/src/utils/HookMiner.sol";
- import {Exdiv} from "../src/Exdiv.sol";
- /// deploys exdiv at a mined address whose low bits encode its hook
- /// permissions, then initializes the canonical pool. seed liquidity
- /// afterwards with the chain's position manager from the deployer, which
- /// received the full raw supply at construction.
- contract Deploy is Script {
- // uniswap v4 pool manager on robinhood chain. fill from the official
- // deployment addresses before running.
- address constant POOL_MANAGER = address(0);
- // create2 deployer proxy, canonical at this address on evm chains.
- address constant CREATE2_DEPLOYER = 0x4e59b44847b379578588920cA78FbF26c0B4956C;
- // starting price for pool initialization, sqrtPriceX96 for the chosen
- // eth per exdiv genesis price. fill deliberately.
- uint160 constant START_SQRT_PRICE_X96 = 0;
- function run() external {
- require(POOL_MANAGER != address(0), "fill POOL_MANAGER");
- require(START_SQRT_PRICE_X96 != 0, "fill START_SQRT_PRICE_X96");
- uint160 flags = uint160(
- Hooks.BEFORE_INITIALIZE_FLAG |
- Hooks.BEFORE_SWAP_FLAG |
- Hooks.AFTER_SWAP_FLAG |
- Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG |
- Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG
- );
- bytes memory constructorArgs = abi.encode(IPoolManager(POOL_MANAGER));
- (address hookAddress, bytes32 salt) = HookMiner.find(
- CREATE2_DEPLOYER,
- flags,
- type(Exdiv).creationCode,
- constructorArgs
- );
- vm.startBroadcast();
- Exdiv exdiv = new Exdiv{salt: salt}(IPoolManager(POOL_MANAGER));
- require(address(exdiv) == hookAddress, "mine mismatch");
- PoolKey memory key = PoolKey({
- currency0: Currency.wrap(address(0)),
- currency1: Currency.wrap(address(exdiv)),
- fee: exdiv.POOL_FEE(),
- tickSpacing: exdiv.TICK_SPACING(),
- hooks: IHooks(address(exdiv))
- });
- IPoolManager(POOL_MANAGER).initialize(key, START_SQRT_PRICE_X96);
- vm.stopBroadcast();
- console2.log("exdiv token + hook:", address(exdiv));
- console2.log("salt:", vm.toString(salt));
- console2.log("genesis block: read from the deploy receipt");
- }
- }
- ```
- ## 5. .env
- ```bash
- PRIVATE_KEY=0x_fill_me
- ```
- ## 6. deploy
- ```bash
- # testnet first. chain id 46630, faucet at faucet.testnet.chain.robinhood.com
- forge script script/Deploy.s.sol:Deploy \
- --rpc-url robinhood_testnet \
- --private-key $PRIVATE_KEY \
- --broadcast
- # mainnet, chain id 4663
- forge script script/Deploy.s.sol:Deploy \
- --rpc-url robinhood \
- --private-key $PRIVATE_KEY \
- --broadcast
- # verify on blockscout
- forge verify-contract <address> src/Exdiv.sol:Exdiv \
- --verifier blockscout \
- --verifier-url https://robinhoodchain.blockscout.com/api
- ```
- ## 7. after deploy
- - seed the canonical pool as full range liquidity from the deployer through the chain's v4 position manager. the deployer holds the entire raw supply; what goes into the pool and at what genesis price is the launch decision.
- - poke the first declare() after the first 21:00 utc with any accrued fees; pay() a day later. both are permissionless, so the site, a keeper, or anyone can call them.
- - fill the site config: TOKEN_ADDRESS and HOOK_ADDRESS are the same address, POOL_ID from the initialize receipt.
- ## 8. before real money
- two callback and type signatures move between v4-periphery releases: the BaseHook internal overrides and the unlock callback dispatch. compile against your installed commit and fix signatures where the compiler objects; the logic stands. then: a full fork test suite covering the drift guard, the fee deltas in both directions, and the multiplier ratio under repeated cycles, a testnet run through at least three real bells, and a second pair of eyes on the unlock flow before mainnet. the contract moves other people's money on a schedule; it earns the review.
Advertisement