Write a program
Three read-only functions. Deploy once, and anyone can write it on their units.
What a program can do
Say no, and answer one verb.
When units ask to move, the token asks their program. It answers yes or no: it can stop a send or a sell. Through act() it can also answer one verb, and the token carries it out: note something, sell the units for their owner, or hand them to whoever pays the price.
Programs are permissionless, like Uniswap v4 hooks. The token bounds every verb: a sale clears a floor price and caps the tip, and units received through a send can never be taken. This site only lists the programs it has reviewed.
The interface
Everything a program may look at arrives in one struct. Return the function's own selector for yes.
struct Ask { address token; // the ERC-555 token asking address owner; // who holds the units right now address caller; // who is calling the token address to; // where the units want to go bool toPool; // being sold into the pool uint256 units; // how much is asked to move uint256 amount; // how much the row holds right now uint256 initial; // its share of what was first programmed uint40 start; uint40 end; uint40 time; // the moment being asked about uint40 movedAt; uint256 price; // settled price, wei per unit int24 tick; int56 tickCumulative; bytes params; // the settings, never editable bytes32 note; uint256 lastPrice; } interface IProgram { function check(Ask calldata ask) external view returns (bytes4); function validate(Ask calldata ask) external view returns (bytes4); function describe(bytes calldata params) external view returns (string memory); }
Called read-only with 100k gas. A revert, out of gas or anything but the selector is a no. validate() is asked once, when the program is written, so a typo can't freeze units.
An example
Vesting in 27 lines. Compiled and tested against DIAL on a mainnet fork.
// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.26; import {IProgram, Ask} from "./IERC555.sol"; import {LibString} from "solady/utils/LibString.sol"; // Vesting: these units can't be sent or sold before a date. // Params: abi.encode(uint40 unlockAt). contract Vesting is IProgram { function check(Ask calldata ask) external pure returns (bytes4) { uint40 unlockAt = abi.decode(ask.params, (uint40)); return ask.time >= unlockAt ? IProgram.check.selector : bytes4(0); } function validate(Ask calldata ask) external pure returns (bytes4) { if (ask.params.length != 32) return bytes4(0); uint40 unlockAt = abi.decode(ask.params, (uint40)); // a date in the past is pointless, one past the end date is never reached bool ok = unlockAt > ask.time && unlockAt <= ask.end; return ok ? IProgram.validate.selector : bytes4(0); } function describe(bytes calldata params) external pure returns (string memory) { uint40 unlockAt = abi.decode(params, (uint40)); string memory at = LibString.toString(unlockAt); return string.concat("Can't be sent or sold before unix time ", at, "."); } }
IERC555.sol is CC0: copy it next to your contract.
Deploy it
Once, by anyone. It has no owner and no state.
forge create src/Vesting.sol:Vesting \ --rpc-url $RPC_URL --private-key $KEY --broadcast # the settings for "unlock on Jan 1 2027", as hex cast abi-encode "f(uint40)" 1798761600
- Deploy it. Any chain DIAL is on, any wallet.
- Encode its settings. They are typed once by whoever writes it and can never be edited.
- Write it on units from the Program page, under Custom. The fee is the same as for Stop loss and Loan: 1% of what the units are worth per year of term.