SLICE draft interface · v0.3
01 — OVERVIEW

What the hook does

SLICE is a Uniswap v4 hook holding resting instructions of the form fill this amount in N pieces, at most one piece per block. The pieces are executed against the pool's own liquidity, inside the swap of whichever address happens to trade next.

It occupies one hook permission: beforeSwap, with beforeSwapReturnDelta enabled so the slice can settle its own token deltas without a second transaction.

// hook permissions
beforeInitialize      false
afterInitialize       false
beforeAddLiquidity    false
afterAddLiquidity     false
beforeRemoveLiquidity false
afterRemoveLiquidity  false
beforeSwap            true
afterSwap             false
beforeDonate          false
afterDonate           false

// return-delta flags
beforeSwapReturnDelta true
afterSwapReturnDelta  false

Because the address of a v4 hook encodes its permissions in the low bits, the deployment address is mined, not chosen. That's worth knowing before you assume a vanity address means anything here.

02 — LIFECYCLE

How one order runs

Four states, and only two of them involve you.

REGISTERED  →  FILLING  →  FILLED
                   ↓
               CANCELLED

Registration

You call registerOrder. The hook pulls amountIn from your address via transferFrom, so an ERC-20 approval to the hook is required first. The order gets an id, and lastBlock is set to the current block so the first slice cannot execute in the same block as registration.

Carrying

On every swap the pool runs beforeSwap. The hook looks up the head of that pool's order queue and asks two questions: is block.number > order.lastBlock, and does the order have slices left. If both are yes, it executes exactly one slice, updates lastBlock, then returns a delta so the pool settles both the slice and the incoming swap together.

If either answer is no, the hook returns a zero delta and the swap proceeds untouched. The added gas in that case is a storage read and a comparison.

Completion or cancellation

When done == slices, the accumulated output is claimable by the owner. Cancellation returns the unfilled input and the output accumulated so far, in the same call.

One order per pool, at the headRight now the queue is FIFO and only the head order is eligible per block. Multiple concurrent orders on the same pool therefore fill sequentially, not in parallel — which is not what the launch video shows, and the video is the thing that's wrong. Parallel fills are on the list; they make the per-swap gas unpredictable, which is why they aren't in v0.3.
03 — INTERFACE

Draft external functions

interface ISlice {

    /// @notice Register a resting order against a v4 pool.
    /// @param poolId      The v4 pool id.
    /// @param zeroForOne  True to sell token0 for token1.
    /// @param amountIn    Total input, pulled on call. Must be >= slices.
    /// @param slices      Number of pieces. 2 <= slices <= MAX_SLICES.
    /// @return id         Order id.
    function registerOrder(
        bytes32 poolId,
        bool    zeroForOne,
        uint256 amountIn,
        uint16  slices
    ) external returns (uint256 id);

    /// @notice Cancel and sweep. Returns unfilled input and accrued output.
    function cancelOrder(uint256 id) external returns (uint256 refundIn, uint256 refundOut);

    /// @notice Claim output of a completed order.
    function claim(uint256 id) external returns (uint256 amountOut);

    /// @notice Read an order.
    function getOrder(uint256 id) external view returns (Order memory);

    /// @notice Order ids currently resting on a pool, in fill order.
    function queueOf(bytes32 poolId) external view returns (uint256[] memory);
}
selectorsignature
0x4ce2d438registerOrder(bytes32,bool,uint256,uint16)
0x514fcac7cancelOrder(uint256)
0xd09ef241getOrder(uint256)

These are keccak-256 of the signatures as written above. If a parameter type changes, the selector changes, and anything encoding by hand — including the site's register panel — breaks silently. That's the main reason this page exists.

04 — STORAGE

Order struct

struct Order {
    address owner;        // 160
    uint48  lastBlock;    // 48   last block a slice executed in
    uint16  slices;       // 16   total pieces
    uint16  done;         // 16   pieces executed
    bool    zeroForOne;   // 8    ──────── slot 0 (248 bits used)

    bytes32 poolId;       //      slot 1

    uint128 amountIn;     // 128
    uint128 filledIn;     // 128  ──────── slot 2

    uint128 receivedOut;  // 128
    uint128 minOutTotal;  // 128  ──────── slot 3
}

Four slots. The packing matters more than usual here: the carrying path reads this struct on every swap that touches the pool, including swaps where no slice is due, so a fifth slot is a tax paid by everyone else.

05 — EVENTS

What an indexer needs

event OrderRegistered(
    uint256 indexed id,
    address indexed owner,
    bytes32 indexed poolId,
    bool zeroForOne, uint256 amountIn, uint16 slices
);

event SliceFilled(
    uint256 indexed id,
    address indexed carrier,
    uint16 index, uint256 amountIn, uint256 amountOut, uint256 rebate
);

event OrderCancelled(uint256 indexed id, uint256 refundIn, uint256 refundOut);
event OrderClaimed  (uint256 indexed id, uint256 amountOut);

carrier is indexed on purpose: it's the only way anyone can prove after the fact how much of the rebate pool went to which addresses, which is the number that decides whether the incentive was calibrated correctly or not.

06 — PARAMETERS

Limits and constants

namedraft valuenotes
MIN_SLICES2One slice is just a swap with extra steps.
MAX_SLICES256Bounded by uint16 anyway; 256 keeps worst-case queue scans sane.
MIN_SLICE_NOTIONALunsetNeeds a floor, or dust slices cost more gas than they move. Depends on the chain.
BLOCK_GAP1Minimum blocks between slices. Fixed at 1 in v0.3.
REBATE_BPSunsetShare of the slice's fee paid to the carrier. The open question.
minOutTotalper orderSlippage bound on the whole order, not per slice.
Why the bound is on the totalA per-slice minimum would let anyone stall your order by making one slice unfillable at the moment they choose. A total bound can only fail once, at the end, and it fails into cancellation rather than into limbo.
07 — FOR SWAPPERS

You don't have to do anything

There is no integration. If you swap on a pool with SLICE attached and a slice is due, your transaction carries it and you receive the rebate on that slice. No approval to the hook, no extra call, no registration.

What changes for you:

  • Your gas goes up by the cost of executing one extra swap on the same pool, in the frames where a slice is due.
  • You receive REBATE_BPS of the fee accrued on that slice's notional.
  • The price you get is the price after the slice executed, because the slice runs first.

That last point is the honest one. Carrying a slice moves the pool slightly against you before your own swap prices. The rebate has to be worth more than that movement, or carrying is a tax dressed as a reward — which is exactly what makes REBATE_BPS the hardest number in the design.

08 — ACCOUNTING

Where the tokens sit

Input is held by the hook from registration. Output accrues to the hook and is credited to the order, claimable by the owner. The hook holds no protocol balance of its own; every token in it belongs to an open or unclaimed order.

slice notional   = amountIn / slices                  // integer division
last slice       = amountIn - filledIn                // absorbs the remainder
carrier rebate   = sliceFee * REBATE_BPS / 10_000
owner receives   = receivedOut                        // net of the rebate

The remainder from integer division goes into the final slice rather than being distributed, so the order always fills exactly and never leaves dust behind.

09 — FAILURE MODES

What breaks, and what happens then

conditionbehaviour
No swaps on the poolOrder stalls indefinitely. Funds remain withdrawable via cancelOrder. This is the design's real cost and there is no fallback keeper.
Slice would breach minOutTotalSlice reverts inside the hook, caught, order marked stalled. The carrier's own swap still goes through.
Pool liquidity removedSlices stop filling. Cancellation still returns input and accrued output.
Owner cancels mid-fillUnfilled input and accrued output both returned in one call. No partial-claim state.
Two orders, same poolFIFO. Head order fills first. See the note in 02.

A hook that reverts takes the swap down with it, so every path the carrying logic can take has to either succeed or fail closed without touching the incoming swap. That constraint is doing more work in this design than anything else on the page.

10 — SECURITY

Known and unresolved

Slice visibility

A resting order is public state. Anyone can read that a slice is due, in which direction, and for how much. On a chain with a public mempool that is a per-slice sandwich target. The per-slice prize is small by construction — that's the point of slicing — but it is not zero, and a searcher can sit on every slice of a large order. Unresolved.

Griefing the queue

Registering a dust order to sit at the head of a pool's queue would delay everyone behind it. MIN_SLICE_NOTIONAL is the intended defence and it is currently unset, which means the defence does not exist yet.

Reentrancy surface

The carrying path calls back into the pool manager from inside beforeSwap. Everything runs under v4's lock, but this is the part that gets read line by line in an audit, and it should be.

Not audited

No audit, no testnet deployment, no bug bounty. Do not approve tokens to any address claiming to be this hook.

11 — SPEC CHANGES

What moved, and why

versionchange
v0.3Slippage bound moved from per-slice to per-order. A per-slice bound was stallable by anyone. Cheapest fix to the nastiest griefing vector found so far.
v0.2slices narrowed from uint256 to uint16 to fit slot 0. Changed the selector, which broke the front end for a day.
v0.2Dropped the per-slice keeper reward paid from the order. Replaced with a share of the slice's own fee, so an order never subsidises its own execution out of principal.
v0.1First sketch. Parallel fills across concurrent orders, since removed — unbounded gas on the carrying swap.
12 — GLOSSARY
sliceOne piece of a registered order. amountIn / slices, with the remainder in the last.
carrierThe address whose swap executed a slice. Receives the rebate. Did not opt in.
resting orderAn order registered and not yet filled or cancelled. Public state.
arb recoveryHow much of a slice's price impact arbitrage undoes before the next slice. The model's key assumption; not a measured value.
stalledAn order that cannot fill — no traffic, no liquidity, or a breached bound. Always cancellable.
SLICE — one order. many blocks. site writing draft v0.3 · not deployed