fix(simple-token): resolve CIP-0056 internal-review findings (56-1 – 56-6 + 56-Q1)#6
Open
amarzeppelin wants to merge 3 commits into
Open
fix(simple-token): resolve CIP-0056 internal-review findings (56-1 – 56-6 + 56-Q1)#6amarzeppelin wants to merge 3 commits into
amarzeppelin wants to merge 3 commits into
Conversation
…ing inputs Resolve two CIP-0056 internal-review findings in the simple-token reference. 56-4 (LOW/MEDIUM) direct-transfer receiver redirect: the preapproval used by the direct-transfer path comes from untrusted choice context and was not bound to the transfer's intended receiver, so a context supplying a preapproval for a different receiver silently redirected the funds. TransferPreapproval_Send now takes expectedReceiver and asserts it equals the preapproval's receiver; directTransfer passes transfer.receiver. 56-6 (MEDIUM) forged-holding supply inflation: archiveAndSumInputs trusted the shared Holding interface view of its inputs, so a foreign look-alike template advertising a spoofed view could inflate totalInput and mint a real backed holding for a fake amount. Inputs must now down-cast to SimpleHolding / LockedSimpleHolding; foreign templates are rejected. Tests: new SimpleToken/Test/ReviewFindings.daml (redirect rejection, forged- holding rejection, genuine-holding positive control); Negative.daml Test 28 updated for the new choice arg. daml test: 45/45 scripts ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…5), AllocationRequest scope (56-3) 56-1 (NOTE): SimpleHolding / LockedSimpleHolding now ensure admin == instrumentId.admin, so a holding cannot advertise an instrument it is not backed by. 56-5 (NOTE): the two-leg DvP test now settles both legs in a single transaction (was two separate submits), and a new test_dvpAtomicRollback proves that if one leg fails, neither settles. 56-3 (scope): SimpleAllocationRequest is documented as an unused standard-interface demonstration stub; the production-hardening checklist (settlement field ordering, non-empty/positive legs, senders == legs' senders, sender-scoped reject/withdraw) is recorded as [FUTURE]. No behaviour change. Tests: +test_holdingAdminMustMatchInstrumentAdmin, +test_dvpAtomicRollback. daml test: 47/47 ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(56-2, 56-Q1) 56-3a (defense-in-depth): TransferPreapproval_Send now asserts totalInput >= amount, mirroring the factory's invariant #18, so an integration calling the choice directly cannot mint a receiver holding it hasn't backed. SimpleTransferInstruction / SimpleAllocation are already self-backing (they derive totalInput from the fetched+archived locked holding), so no change there. 56-2 (scope): document that TransferPreapproval is open-by-default by design; per-sender scoping is an opt-in [FUTURE] extension, not a change to the base template. 56-Q1 (scope): document that atomic multi-leg DvP relies on a trusted executor batching the per-leg ExecuteTransfer into one transaction; there is no on-ledger settlement coordinator by design. Tests: +test_preapprovalTotalInputMustCoverAmount. daml test: 48/48 ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Resolves all findings from the CIP-0056 internal review of the
simple-tokenreference — twocorrectness fixes, three cheap hardenings, and three documented scope decisions. Every code change
has a regression test.
daml test→ 48/48 scripts ok (SDK 3.4.10).transfer.receiverensure admin == instrumentId.adminTransferPreapproval_SendtruststotalInputtotalInput >= amountSimpleAllocationRequestvalidation[FUTURE][FUTURE]Guided by the repo scope: protect the honest happy path and untrusted-actor exploits; harden reusable
primitives cheaply; document scope boundaries rather than gold-plate. Trusted roles (admin / executor /
relayer) are inside the trust model.
Correctness fixes
56-4 — direct-transfer receiver redirect (LOW/MEDIUM).
transferFactory_transferImplreads thepreapproval id from untrusted choice context and passed it to
directTransfer, which minted theholding for the preapproval's own
receiver— never checked againsttransfer.receiver. Whoeverassembled the context could redirect an honest transfer's funds. Fix:
TransferPreapproval_SendgainsexpectedReceiverand assertsreceiver == expectedReceiver;directTransferpassestransfer.receiver.56-6 — forged-holding supply inflation (MEDIUM).
archiveAndSumInputstrusted the sharedHoldinginterface view of its inputs; a foreign template with a spoofed view could inflate
totalInputandcause a real, backed holding to be minted for a fake amount. Fix: each input must down-cast
(
fromInterface) toSimpleHolding/LockedSimpleHolding; foreign templates are rejected. (Theregression test demonstrates a real unbacked mint on the pre-fix path — severity MEDIUM.)
Cheap hardenings
56-1 — holding ↔ instrument admin binding (NOTE).
ensure amount > 0.0 && admin == instrumentId.adminon both holding templates, so a holding can't advertise an instrument it isn't backed by. Distinct from
56-6 (a foreign look-alike never runs our
ensure; 56-6's gate covers that).56-5 — non-atomic DvP test (NOTE).
test_dvpTwoLegsexecuted the two legs in two separatetransactions. Now both
Allocation_ExecuteTransfers run in one submission (applicative*>), andtest_dvpAtomicRollbackproves that a failing leg rolls back the other (balances unchanged).56-3a —
TransferPreapproval_SendtruststotalInput(NOTE, defense-in-depth). UnlikeSimpleTransferInstruction/SimpleAllocation(which derivetotalInputfrom the fetched+archivedlocked holding, i.e. self-backing), this choice mints from a caller-supplied
totalInput. It now assertstotalInput >= amount, mirroring the factory's invariant #18, so a direct caller can't mint a receiverholding it hasn't backed. The residual (an admin-authorized caller lying about
totalInput) requires thetrusted registry's own signature and is out of the core threat model.
Documented scope decisions (no behaviour change)
56-3 —
SimpleAllocationRequest. Has zero references in the package; the real flow isAllocationFactory→SimpleAllocation. It ships only to witness the standardAllocationRequestinterface and has no-op choice bodies. Documented as a demo stub; production-hardening checklist recorded
[FUTURE].56-2 — preapproval sender scoping.
TransferPreapprovalis open-by-default by design (any vettedsender may pay the receiver). Per-sender scoping is a deliberate opt-in extension (
ScopedTransferPreapproval),[FUTURE]— the default is not changed.56-Q1 — settlement atomicity. Documented (module doc + tests): atomic multi-leg DvP relies on a
trusted executor batching each leg's
Allocation_ExecuteTransferinto one transaction; no on-ledgersettlement coordinator, by design.
Tests (
simple-token-test)New module
SimpleToken/Test/ReviewFindings.daml:test_directTransferReceiverRedirectRejected(56-4)test_forgedHoldingInputRejected(56-6) +test_genuineHoldingInputStillWorks(positive control)test_holdingAdminMustMatchInstrumentAdmin(56-1)test_preapprovalTotalInputMustCoverAmount(56-3a)SimpleToken/Test/Allocation.daml:test_dvpTwoLegsmade atomic; newtest_dvpAtomicRollback(56-5).SimpleToken/Test/Negative.daml: Test 28 updated for the newexpectedReceiverarg.daml test→ 48/48 scripts ok.How to review
Preapproval.daml—expectedReceiver+ receiver assert (56-4),totalInput >= amount(56-3a), open-by-default doc (56-2).Rules.daml—directTransferpasses receiver (56-4);archiveAndSumInputstemplate gate (56-6).Holding.daml— admin↔instrumentensure(56-1).Allocation.daml— trusted-executor atomicity doc (56-Q1).AllocationRequest.daml— demo-stub scope doc (56-3).simple-token-test/.../{ReviewFindings,Allocation,Negative}.daml— tests.Notes
TransferPreapproval_Sendgains a field (source-breaking for direct callers); the one in-tree caller andthe one test call-site are updated.
simple-tokenreference also lives in the sibling OZ repo(
canton-stablecoin⇄canton-token-template); the same change is opened there to keep the two byte-identical.🤖 Generated with Claude Code