TokenID format
Every option leg is an ERC-1155 token whose id is its specification. Nothing about an option is stored in a registry or looked up off-chain: unpack the id and you have the underlying, the strike, the expiry, the type, the kind and the side.
Layout
The identifier occupies 124 bits, carried inside a standard uint256 ERC-1155 id. Fields, most significant first:
| Field | Bit range | Width | Contents |
|---|---|---|---|
| Pricefeed hash | 60–123 | 64 | First 8 bytes of keccak256(feed_address) |
| Maturity | 36–59 | 24 | (year − 2000) << 16 \| month << 8 \| day |
| Strike | 12–35 | 24 | Packed exponential strike code |
| Option type | 8–11 | 4 | 0 = Call, 1 = Put |
| Option kind | 4–7 | 4 | 1 = European, 0 = American |
| Direction | 0–3 | 4 | 0 = long (buyer), 1 = short (seller) |
The remaining 132 high bits must be zero, and the contract checks this on every operation. An id with anything set above bit 123 is rejected rather than truncated, so a malformed id can never be mistaken for a valid one.
Note that type, kind and direction each occupy a nibble, not a byte. They are passed as uint8 arguments for convenience, but a value above 15 would run into the neighbouring field, so the packing function rejects it outright instead of masking it away.
Field encodings
Pricefeed
The underlying is identified by the first 64 bits of the keccak hash of its Chainlink aggregator address, not by the address itself — 160 bits would not fit alongside everything else. The contract keeps the reverse mapping, so a hash resolves back to a feed:
priceFeedMapping(uint64 pricefeedHash) returns (address feed)
Collisions across 64 bits are not a practical concern for the number of feeds a protocol registers, and a feed must be explicitly registered before its hash means anything.
Maturity
A packed calendar date, not a timestamp. 2026-07-17 encodes as:
year: 2026 − 2000 = 26 = 0x1A
month: 7 = 0x07
day: 17 = 0x11
──────────────
maturity = 0x1A0711 = 1705745
The timestamp is derived when needed. Storing the date rather than the timestamp keeps the field to 24 bits and makes ids readable by eye.
Strike
Four significant digits plus a base-10 exponent, in 24 bits:
| Sub-field | Bits (within the 24) | Contents |
|---|---|---|
| Mantissa | 8–23 | Four significant digits |
| Sign | 4–7 | 0 = scale down, 1 = scale up |
| Exponent | 0–3 | Power of ten |
Decoding, against an 18-decimal anchor:
sign == 0 → value = mantissa × 10^(15 − exponent)
sign == 1 → value = mantissa × 10^(15 + exponent)
This is what lets 248 codes span 0.000001 to 10,000,000. For example 0x1C8012 — mantissa 0x1C80 = 7296, sign 1, exponent 2 — decodes to 729.6.
A worked example
A long European call on a registered feed, strike 729.6, expiring 2026-07-17:
pricefeed hash 0xE775A8F829551E8F (64 bits)
maturity 0x1A0711 (2026-07-17)
strike 0x1C8012 (729.6)
option type 0x0 (Call)
option kind 0x1 (European)
direction 0x0 (long)
tokenId = 0xE775A8F829551E8F1A07111C8012010
124 bits · 31 hex digits
Every field lands on a clean nibble boundary — 16 + 6 + 6 + 1 + 1 + 1 = 31 hex digits — so a hex id can be read field by field without any bit shifting.
The last nibble is the side
The final hex digit is the direction. …010 is the long leg; …011 is its short counterpart. If you are about to trade and want one sanity check, check that digit.
Deriving the counterpart leg
The two legs of an option differ only in the direction field, so flipping it gives you the other side:
TokenIdLib.invert_direction_for_tokenid(uint256 id) returns (uint256)
This is what minting uses: you name one leg, and the contract mints that id together with its inverse.
Constructing and validating
// Build an id from parameters
TokenIdLib.construct_token_id(
pricefeed_hash, maturity, strike, option_type, option_kind, direction
) returns (uint256);
// Unpack an existing id
TokenIdLib.get_pricefeed_hash(id);
TokenIdLib.get_maturity(id);
TokenIdLib.get_strike(id);
TokenIdLib.get_option_type(id);
TokenIdLib.get_option_kind(id);
TokenIdLib.get_direction(id);
// Is every field on the grid, and are the reserved bits clear?
ISFOptions(0xf0022aC3…).if_token_id_exists(id) returns (bool);
A well-formed id is not necessarily a valid one: the fields must also exist on the grid, and the feed must be registered. if_token_id_exists checks all of it in a single view call, and is the same validation the contract applies internally on every mint and exercise.
Why it's built this way
Packing the specification into the id means:
- Pools are addressable without a registry. A pair's address derives from
(option contract, tokenId), so the pool for any option can be computed before it exists. - Metadata needs no server. The contract generates a token's name and attributes by unpacking its own id — there is no URL that has to stay alive for the options' lifetime.
- Over 100,000 instruments live in one contract. No per-option deployment, and any ERC-1155-aware wallet or protocol can hold them.
Next
- The options grid — the valid values for each field
- Contracts & addresses — deriving a pool address from an id
- Minting options — creating a token at a given id