What happened
The contract asked the wrong question.
TrustedVolumes is an off-chain order book with on-chain settlement. Makers sign orders off-chain. Takers bring those signed orders on-chain to execute. The settlement contract verifies the maker’s signature before moving funds.
The signature verification looked up a per-principal
allowed-signers map: _allowedSigners[key_1][key_2],
where the presence of key_2 under
key_1 means “key_2 is an authorized signer
for key_1.” The bug was in which address played the role
of key_1.
Per SlowMist’s root-cause breakdown, quoted in the rekt.news writeup:
“Signature validation checks
_allowedSigners[msg.sender][signer]using caller (taker) instead of order’s maker as key…”
The contract asked “is signer an allowed
signer for msg.sender?” where
msg.sender is the taker executing the settlement.
It should have asked “is signer an allowed
signer for the order’s maker?” The question is
structurally identical; the lookup subject is wrong.
The second leg made the first leg drainable.
registerAllowedOrderSigner was a permissionless
entry point — any address could register any key as an
allowed signer for itself. So the attack required no privileged
access and no prior position:
-
Call
registerAllowedOrderSignerto register the attacker’s own key as an allowed signer for the attacker account. - Sign a fake order with that key, authorizing transfers from a victim maker’s balance.
-
Call settle with the attacker as
msg.sender. The contract looked up_allowedSigners[attacker][attacker_key], found a match, and processed the transfer.
The victim maker never signed anything. The contract asked the wrong question.
The pattern
Authentication used as authorization.
The contract had two distinct functions: a registration
function (“tell me which key you control —
authentication”) and a settlement function (“this
key is allowed to spend this maker’s balance —
authorization”). The settlement function keyed its
authorization lookup on msg.sender, which is
correct for authentication (“am I who I say I am?”)
and wrong for authorization (“am I allowed to act on this
resource owned by someone else?”). Authentication answers
“who are you”; authorization answers “are you
allowed to do this thing to this resource.” The contract
substituted the first for the second.
This is the pattern the access_control invariant
class is built to catch: an unauthorized principal causing a
state change reserved for an authorized one. The state change
here is a maker’s balance moving without the
maker’s genuine authorization.
The property
What the invariant asserts.
The property the settlement contract should have enforced is:
No caller can cause a maker’s balance to move unless the maker’s own key — not the caller’s key — has signed the order.
Expressed as a stateful test property:
-
Set up the contract with a known maker
Mand a fresh attacker keypairAthatMhas never authorized. -
Generate call sequences including
registerAllowedOrderSigner(attacker_key)fromAandsettle(order_from_M)fromA. -
After every transition, check: did
M’s balance decrease withoutM’s key appearing in the set of authorized signers forM?
The violating sequence is short. A stateful fuzzer that carries
an attacker keypair in its call space — one the
authorized principal never granted anything to — is
designed to find this shape quickly.
registerAllowedOrderSigner is available in the
callspace; settle is available in the callspace;
the attacker keypair is the unauthorized subject. The fuzzer
picks both up, sequences them, and the property fires.
banteg’s Foundry repro is exploit-shaped: reconstruct the exact attacker call sequence and replay it against block 25039669. That is a valuable artifact — it confirms the attack surface on the real pre-exploit contract. Our framing is orthogonal. State the property the contract should have enforced. Show a reference where the property holds on the clean variant (zero markers) and fires on a planted twin where the constraint is removed (at least one marker). Wire both legs in CI. The property fires before you get to the replay step.
CI-verified evidence
Map to CI-verified reference pairs.
Three reference artifacts carry an access_control
reference pair verified in CI today. All three are CI-verified
per the build squad’s proof register. The two incidents
are EVM; the reference pairs are Anchor/Solana and
Cairo/Starknet — the property is rail-agnostic, and we
note the gap on the Solidity rail honestly at the end.
cf-invariants-anchor — Anchor / Solana,
access_control class
(admin_ref / admin_ref_planted).
The clean variant holds a vault with Anchor’s
has_one = depositor constraint and PDA seeds
derived from depositor.key(). The planted variant
drops both constraints — Anchor will then accept any
account owned by the program in the vault slot regardless of the
signer. The access_control invariant fuzzes with a fresh
attacker keypair every round; any successful unauthorized call
sets a sticky flag the invariant asserts against. Clean = 0
markers, planted ≥ 1 marker. CI-verified, commit
aee4136. Repo:
github.com/caliperforge/cf-invariants-anchor.
The shape of what the planted admin_ref drops is
structurally equivalent to what TrustedVolumes’s
settlement function did not check: the planted version omits the
binding between “the account slot accepted” and
“the authorized principal.” The invariant then finds
the call sequence that violates the property, same as the
TrustedVolumes shape. The bug class is the same; the language
and runtime are different.
cf-invariants-jito — Anchor / Solana,
admin_gating class on the real Jito
tip-distribution program. The planted twin drops the
UpdateConfig::auth(&ctx)? call from
update_config, so any signer (not just the recorded
Config.authority) can rewrite the Config’s
authority field. This is the same invariant class: an
unauthorized principal causing a state change reserved for an
authorized one. The invariant
invariant_update_config_requires_authority fires on
the planted variant and stays silent on the clean. Clean = 0,
planted ≥ 1, CI-verified at commit e683c5a.
Repo:
github.com/caliperforge/cf-invariants-jito.
The Jito reference harnessed a real on-chain program’s access-control gate, not a synthetic example. That is the same framing the TrustedVolumes invariant would need: harness the real settlement contract’s settler → verify the signer-validation logic → plant the wrong-principal lookup → show the invariant fires.
cf-invariants — Cairo / Starknet, 12-class
matrix. The 12-class reference suite includes
governance and multisig under the access-control / monotonicity
pattern. Twelve reference contracts × twelve invariant
classes, each with a planted bug, each compile-twinned to a
clean variant via a clean scarb feature, each
asserting planted ≥ 1 AND clean = 0
in CI. PR #2 squash-merged 2026-06-07 at commit
003e33c, CI run 27075213962 26/26
green. All twelve references deployed and source-verified on
Starknet Sepolia. Repo:
github.com/caliperforge/cf-invariants.
Developer cookbook (public since 2026-06-11, PR #3
squash-merged at 7f94b08):
github.com/caliperforge/cf-invariants/tree/main/docs/cookbook.
Design
What a TrustedVolumes-specific invariant harness would look like.
The exercise is design rather than a shipped harness.
TrustedVolumes is an EVM contract; our CI-verified
access_control reference pairs are Anchor/Solana and Cairo. A
TrustedVolumes-specific twin would live on the Solidity/Foundry
rail, and a Solidity-rail access_control bundle
does not exist in the current toolkit (hyperevm-safety v0.1
ships six HyperCore-boundary classes: oracle staleness, oracle
deviation, decimals scaling, gas DoS, solvency window, broken
Chainlink adapter — none access_control-shaped).
The invariant design is clear regardless:
- Identify the principal whose authorization gates settlement: the order’s maker, not the caller.
-
Express the property: after any call to
settle, if the maker’s balance decreased, the signer must be present in_allowedSigners[maker], not_allowedSigners[msg.sender]. -
Test setup: one clean maker
M, one attackerA. IncluderegisterAllowedOrderSignerandsettlein the call space. IncludeA’s keypair as the unauthorized signer. -
Assert after every transition: no decrease in
M’s balance withoutM-keyed authorization.
The violating sequence in step 4 is exactly the attack sequence. A stateful test reaches it without being told the exact sequence; it only needs to know the property and the callspace.
This is “invariant design, not yet harnessed”
framing. A Solidity-rail access_control reference
pair does not exist today, and we will not stretch v0.1’s
bundles to claim shape they do not have. The design above is
offered as a reference for teams building Solidity-rail
harnesses against this class.
Calibration
What this writeup does not claim.
- We did not discover this bug. SlowMist and CertiK identified the root cause, cited in the rekt.news writeup. banteg published the runnable reproduction.
- We do not claim our tooling would have automatically generated the right invariant from the ABI. The AI suggester proposes candidates; an operator reviews; the harness verifies. The framing we use elsewhere — “assistive, human-reviewed suggester” — applies here.
- We do not claim audit equivalence. Stateful invariant testing is one layer in a defense-in-depth pre-deploy pipeline.
- We do not claim a Solidity-rail access_control reference pair exists. It does not. Our CI-verified pairs are Anchor/Solana (cf-invariants-anchor, cf-invariants-jito) and Cairo (cf-invariants). We will say so on every EVM-adjacent piece until it ships.
- We did not replay this in CI against TrustedVolumes specifically. The design above is the invariant that would have caught the class; the reference pairs are the closest CI-verified evidence we have.
- We do not claim this writeup covers every TrustedVolumes bug. The exploit chained four bugs (the wrong-principal lookup we focus on, plus permissionless registration, taker-as-from with unlimited proxy approvals, and a broken replay guard). The QuillAudits and banteg sources below carry the full mechanism.
Sources
Primary post-mortems and CaliperForge artifacts.
- rekt.news writeup (article body, includes SlowMist root-cause quote and CertiK first-flag attribution): rekt.news/trustedvolumes-rekt
- banteg’s runnable Foundry reproduction (reconstructed Solidity + D2 flow diagram + on-chain evidence, confirmed against a fork of block 25039669): gist.github.com/banteg/3475d43a80fb6e0ab81f2fa549b88c1b
- QuillAudits hack analysis (fuller four-bug breakdown — wrong-principal fillOrder check + permissionless registration + taker-as-from-address with unlimited proxy approvals + broken replay guard): quillaudits.com/blog/hack-analysis/trustedvolumes-rfq-hack
CaliperForge artifacts cited:
-
cf-invariants-anchor
— Anchor / Solana,
access_controlreference pair, CI-verifiedaee4136. -
cf-invariants-jito
— Anchor / Solana,
admin_gatingclass on real Jito tip-distribution, CI-verifiede683c5a. -
cf-invariants
— Cairo / Starknet, 12-class matrix, CI-verified
003e33c, run27075213962. -
cf-invariants developer cookbook
— see also, public since 2026-06-11,
7f94b08.