Whitepaper · 13
Performance Analysis
Measured gas of the reference implementation with zero, one and two hooks; what drives the cost; and deployment guidance.
Method
Numbers below are from scripts/gas-report-standard.js run on Hardhat's in-process network against the compiled reference implementation (solc 0.8.22, viaIR, optimizer.runs = 1 for ARexToken), September 2026. "Warm" means the same sender/receiver/id pair as the previous transfer (storage slots already touched in the block). Hooks: EligibilityHook (CHECK + ELIGIBILITY) and ExpiryHook (CHECK). Reproduce with:
npx hardhat run scripts/gas-report-standard.js
Results
xychart-beta
title "Warm transfer gas by number of hooks (thousands)"
x-axis ["no hooks", "Eligibility", "Eligibility + Expiry"]
y-axis "gas (k)" 0 --> 140
bar [66.8, 91.2, 119.6]Cost grows linearly with the number of hooks: each one is one external check call per id, plus canSend and canReceive for an eligibility hook. The table gives every operation.
| Operation | 0 hooks | 1 hook | 2 hooks |
|---|---|---|---|
createBatch (new id, mint 1000) | 264,096 | 283,831 | 299,396 |
safeTransferFrom (first, cold) | 80,687 | 108,266 | 136,682 |
safeTransferFrom (warm) | 66,790 | 91,166 | 119,582 |
transform split 1 → 2 (two new ids) | 564,422 | 595,707 | 636,858 |
setFrozenTokens | 54,188 | 54,188 | 54,188 |
forcedTransfer | 80,152 | 89,909 | 99,569 |
burn | 66,621 | 83,001 | 93,395 |
Warm transfer with exactly one hook of each kind (marginal cost of the rule):
xychart-beta horizontal
title "Marginal gas of one hook on a warm transfer (thousands)"
x-axis ["DeliveryHook", "Traceability (emit)", "EligibilityHook", "ExpiryHook", "SeasonalHook", "PerishabilityHook"]
y-axis "gas added (k)" 0 --> 35
bar [12.3, 17.4, 24.4, 25.2, 27.5, 31.0]TraceabilityHook in persisted mode adds 184.5k and is left out of the chart for scale.
| Hook | Gas | Δ vs. no hook |
|---|---|---|
| EligibilityHook | 91,166 | +24,376 |
| ExpiryHook | 92,023 | +25,233 |
| SeasonalHook | 94,324 | +27,534 |
| PerishabilityHook | 97,813 | +31,023 |
| DeliveryHook (no active delivery) | 79,076 | +12,286 |
| TraceabilityHook, emit-only | 84,152 | +17,362 |
| TraceabilityHook, persisted | 251,273 | +184,483 |
Company records on Base
CompanyFactsHook and RequireStageHook (reference release 2.0.0-alpha.9) were measured from transaction receipts with scripts/gas-company-facts.js and priced with Base mainnet values read on 3 November 2026 (block 53,443,324): base fee 0.005 gwei plus a 0.001 gwei tip, ETH at 2,250 USD.
| Operation | Gas | USD |
|---|---|---|
| Stage mark | 159,155 | 0.0021 |
| 50 stage marks in one transaction | 5,063,220 | 0.068 |
| Fact | 74,489 | 0.0010 |
| Daily anchor | 51,827 | 0.0007 |
| Carry of stages on a merge of 2 lots | +49,990 | |
| Carry of stages on a merge of 200 lots | +2,142,023 | |
RequireStageHook check on a delivery | +47,679 | |
| Registering a list of 6 stages | 771,350 | 0.010 |
On OP-stack chains such as Base the cost of a transaction is gasUsed × effectiveGasPrice + l1Fee, where l1Fee is the L1 data fee. That day it was below 0.00002 USD per operation, so the L2 execution above is almost all of the cost. The full data is in docs/gas/company-facts.json of arex-contracts.
What drives the cost
- A transfer with no hooks costs ~67k warm, i.e. ERC-1155 plus supply tracking plus the batch existence/status/frozen reads and the pipeline bookkeeping (data stash, lock). This is the floor.
- Each hook adds roughly 25–30k on a transfer: one
checkcall (view, cross-contract) plus, for eligibility hooks,canSendandcanReceivecalls. Cost is linear in the number of hooks and in the number of ids in a batch transfer. Probing every rule contract for every capability withsupportsInterfacebefore calling it would add several static calls per id; that overhead does not exist because flags are cached at registration. createBatchis dominated by storage: theBatchstruct (six words plus a string) and the ERC-1155 balance and supply slots. It is a one-time cost per lot.transformcreates new ids: a split into two pays two batch records plus two mints plus one burn, plus hook calls on every leg (OP_TRANSFORM). A split into n is roughlyn × createBatchplus a burn; merging n into one is roughlynburns plus one createBatch.- Enforcement is cheap: freezing is one slot; a forced transfer costs what a transfer costs plus the unfreeze write and the event.
transferCheck/canTransferare views: free off-chain, and the same code path as the transaction.
What the design buys
| Capability-probe design | A-REX Core (ARexToken) | |
|---|---|---|
| Transfer with n rule contracts | ids × modules × (supportsInterface probes + hook call) before any logic runs | ids × hooks × 1 call (+2 for eligibility hooks); flags cached at registration |
| Rejection | string revert (copied to memory, not indexable) | custom error with hook address and bytes32 code |
| Lineage | not available | ~280k per output batch |
| Freeze / forced transfer | not available | 54k / 80k |
The left column describes the design A-REX Core replaces (ADR-004, ADR-005); only the right column is measured.
Deployment guidance
- Cost in money is
gas × gas price × token price, and the last two change by orders of magnitude between chains and days. As an illustration only, at 0.01 gwei and 3,000 USD per ETH a warm transfer with two hooks (119,582 gas) costs about 0.0036 USD and acreateBatch(264,096 gas) about 0.008 USD; at 20 gwei on Ethereum mainnet the same operations cost about 7.2 and 15.8 USD. Measure on the target chain before committing to a unit economics model. - Batch transfers:
safeBatchTransferFromamortizes fixed costs; hook cost still scales with the number of ids. - Hook ordering: place high-rejection, cheap hooks first; a rejection stops the pipeline before more expensive hooks run.
- Number of hooks: three to five covers most issuers; each additional hook is a measurable per-transfer cost and a governance surface.
- Metadata: keep
urishort (a CID or a Digital Link); the string is stored on-chain. - Off-chain reads: use
transferCheckbefore submitting; a reverted transaction still costs gas.
Throughput
The token itself is not the bottleneck; the chain is. A single transfer is one transaction; a transformation with n outputs is one transaction with n mints. Indexing lineage requires reading BatchTransformed events, which are emitted once per operation with full quantity lists, so an indexer does not need to reconstruct anything from per-transfer events.
Contract size
ARexToken deploys at 24,213 bytes (limit 24,576), compiled with optimizer.runs = 1. The other contracts are small: ARexAttestations 2,708; hooks between 1,358 (ExpiryHook) and 6,315 (DeliveryHook), TraceabilityHook 4,076; extensions between 3,716 and 4,588. Adding functionality to the token itself is size-constrained by design — that is what hooks and companion contracts are for.