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 |
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,136 bytes (limit 24,576), compiled with optimizer.runs = 1. The other contracts are small: ARexAttestations 2,708; hooks between 1,358 (ExpiryHook) and 6,215 (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.