Security release. Fixes all confirmed Critical, High, and Medium findings from the
full-codebase audit (AUDIT.md). This is a breaking release — see the
"Breaking interface changes" section before upgrading. v2 stable state is not
upgrade-compatible from v1 in all modules; treat as a fresh deploy or migrate
deliberately (see "Migration").
Critical fixes
- C1 — EVM settlement now validates the payment recipient.
Gateway.settle
andSessions.openEvmSessionnow require the EIP-3009authorization.toto equal
the canister's own derived EVM address. Previously a payer could sign a
self-transfer (to= an address they control), pass signature verification, have
the canister pay gas to broadcast it, and still receive a valid receipt / funded
session — a complete payment bypass and (for sessions) a treasury-drain vector. - C2 / C3 — MCP server no longer exposes the controller signing key to the LLM.
The genericcalltool is restricted to a read-only/query allowlist;fetch_x402
enforces a URL allowlist (SSRF), per-call + cumulative spend caps, and explicit
confirmation before signing. (integrations/mcp.) - C4 — Service-marketplace escrow custody fixed.
ServiceRegistry.settleJob/
expireJobsnow pay the operator and refund the buyer from the platform recipient
account (where the payment actually lands) instead of an unfunded per-job
subaccount, so settlements/refunds no longer fail withInsufficientFunds. - C5 — EVM service-over-HTTP no longer traps after payment. The example passes
receipt.senderto the registry asTextinstead ofPrincipal.fromText(...),
which trapped for 0x EVM senders after the on-chain transfer had executed.
High fixes
- H1 — EVM settlement waits for on-chain confirmation (
eth_getTransactionReceipt,
status == 1) before issuing a receipt; reverted/never-mined transfers no longer
yield a "paid" receipt. NewPaymentResultvariant#settlementPending. - H2 —
EvmSenderreads the#Pendingchain nonce before every send (removed the
stale in-memory nonce cache that desynced againstEvmSigneron the shared address). - H3 —
EvmSenderno longer broadcasts with a fabricated ~0.1 gwei fee when fee
data is unavailable; it returns an error so the caller can retry. - H4 — Daily spending limit is reserved before the settlement await
(Policy.reserveCharge/reserveSessionOpen+releaseDaily), closing the
concurrent-charge bypass. - H5 —
ServiceRegistrysettlement uses a synchronous#Settlingreservation to
prevent double-settle/double-refund via racingconfirmJob. - H6 / H7 —
ContentStorerequires external-randomness seeding (no more
deterministic key from the public principal) and persists the key across upgrades. - H8 — Access grants are non-transferable:
verifyGrantnow takes the caller and
requirescaller == grant.grantee. - H9 / H10 — The HTTP x402 flow works end-to-end: paid GETs upgrade to update
context so the 402 nonce is persisted, and the 402 response now carries the server
nonce (ic402Nonce) for EVM clients to echo and bind the amount. - H11 / H12 / H13 — MCP
fetch_contentvalidates delivery targets (SSRF), and all
money-moving tools enforce spend caps + confirmation. - H14 — Prebuilt ledger/EVM-RPC WASMs are verified against pinned SHA-256 hashes
before deploy (scripts/prebuilt.sha256).
Medium fixes
- M1 / M8 — HMAC grant key uses the full 256-bit seed (was truncated to 64 bits).
- M2 —
ContentStoremixes a per-entry salt into key/nonce derivation, so
delete + re-put of the same id never reuses a (key, nonce) pair. - M5 —
NonceManagerenforces a hard cap (oldest unlocked nonces evicted),
bounding memory under 402-challenge spam. - M6 —
ServiceRegistryaddsresolveDisputeand auto-expires stuck
#Submitted/#Disputedjobs, so escrow can never be locked permanently. - M7 — Voucher signatures bind the verifying canister's principal (cross-canister
replay protection on Ed25519 key reuse). - M9 — Session deposits are counted against the daily limit once and credited back
on close (was double-counted: deposit + every voucher delta, never refunded). - M10 / M11 — Deploy scripts can't poison the source backup with testnet-patched
content, and backups are gitignored / mainnet markers re-verified after restore.
Breaking interface changes
Motoko library API (recompile required):
Gateway.verifyGrant(grant)→verifyGrant(caller : Principal, grant).Grants.verifyGrant(grant)→verifyGrant(caller : Principal, grant).Sessions.encodeVoucherPayload(sessionId, amount, sequence)→
encodeVoucherPayload(canisterId : Text, sessionId, amount, sequence).ServiceRegistry.submitRequest(buyer : Principal, …)→submitRequest(buyer : Text, …).ServiceRegistryno longer settles to an escrow subaccount; new
resolveDispute(jobId, refundBuyer : Bool).EvmSender.getFeeDatais internal and now returns?(Nat, Nat); new public
EvmSender.confirmTransaction(chainId, txHash, maxPolls).- New
Policymethods:reserveCharge,reserveSessionOpen,releaseDaily. ContentStorerequiresinitExternalSeed(...)(orstartTimers()) before any
write — writes trap until seeded.
Candid / wire / serialized formats (coordinate clients & upgrades):
PaymentResultgains#settlementPending;JobStatusgains#Settling
(exhaustive matchers must add these variants).- Voucher signed payload is now CBOR array(4)
[canisterId, sessionId, cumulativeAmount, sequence](was array(3)). Client and canister must be upgraded together; in-flight
vouchers are invalidated.@ic402/clientsignVouchergains acanisterIdargument
(handled automatically by the session handle). - The 402 response JSON now includes
ic402Nonceandexpiry; EVM-over-HTTP clients
must echoic402Noncein the X-PAYMENT payload (the@ic402/clientfetchX402
helper does this automatically). - Stable schemas changed:
StableContentStoreState(+masterKey,seedInitialized,
saltCounter),StableContentEntry(+salt). New fields are optional so the
upgrade decodes, but pre-v2 deterministic-key content is not decryptable under the
v2 required-seed model.
MCP server behavior (intended):
autoPaymentdefaults to off; money-moving/signing tools requireconfirm: true
and enforceperCallMaxAtomic/sessionMaxAtomiccaps; the genericcalltool is
read-only.
Dependencies
ic3.2.0 → 4.0.0 (major). Canister types moved from the top-levelmo:ic
module tomo:ic/Types;lib.moupdated accordingly (HttpResponse_now aliases
ICTypes.HttpRequestResult).ecdsa7.1.0 → 8.0.0 (major). No source changes required (mo:ecdsa/CurveAPI
compatible).sha20.1.14 → 0.2.1 (minor). No source changes required.- Toolchain:
moc1.3.0 → 1.9.0 (required byic@4.0.0/ecdsa@8.0.0, which
need moc ≥ 1.4.0). Integrators must build with moc ≥ 1.4.0.
Migration
- EVM identity unchanged: the canister's EVM address is the same (H2 reads the
pending nonce rather than changing derivation paths), so no re-funding is needed. - ContentStore: call
store.startTimers<system>()(orinitExternalSeed(await raw_rand())) once after deploy. Content stored under the pre-v2 deterministic key
must be re-uploaded. - Sessions/vouchers: upgrade
@ic402/clientand the canister together. - Service marketplace: the platform recipient account custodies funds; operator
payouts and buyer refunds now transfer from it.