dial
Ethereum

Write a program

Three read-only functions. Deploy once, and anyone can write it on their units.

Functions3
Gas per answer100k
Can actYes
Fee1%a year

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.

VestingNo send and no sell before a date. Team and investor units stay put, in plain sight.
One addressThese units may only go to one wallet, a cold wallet or a multisig.
Daily limitNo more than N units leave per day, read from the row's own numbers.
Never to the poolFree between wallets, never sold. A gift that stays a gift.

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
  1. Deploy it. Any chain DIAL is on, any wallet.
  2. Encode its settings. They are typed once by whoever writes it and can never be edited.
  3. 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.
@dial_555