From ac971f08a2ae353537d3a74fa67f3fd75d503beb Mon Sep 17 00:00:00 2001 From: Bengt Lofgren Date: Thu, 16 Jul 2026 05:19:49 -0700 Subject: [PATCH] Revert "canton: revert guardianGovernance fee-pin functional changes (re-review) (#45)" This reverts commit a1f225e943d81f955c110f267cee2fdd13ab439a. --- canton/README.md | 22 +++- canton/core/daml/Wormhole/Core/Fees.daml | 5 +- canton/core/daml/Wormhole/Core/Setup.daml | 21 ++-- canton/core/daml/Wormhole/Core/State.daml | 88 ++++++++++---- canton/test/daml/Test/TestCore.daml | 8 +- canton/test/daml/Test/TestFees.daml | 114 +++++++++++++++++- .../test/daml/Test/TestGuardianObserver.daml | 2 +- 7 files changed, 213 insertions(+), 47 deletions(-) diff --git a/canton/README.md b/canton/README.md index e504f8462b..55f78b98a6 100644 --- a/canton/README.md +++ b/canton/README.md @@ -210,9 +210,12 @@ consumer's trust anchor, is stable across rotation). Two things follow: the real governance party, so any `CoreState` whose `guardianGovernance` field is the known anchor is **authentic by construction**, however its cid was obtained — consumers authenticate the payload, never the resolution path - (§4.7). An operator puppet could create a `CoreState` under a _different_ - governance party — but its payload names the wrong parties and no consumer - trusts it. + (§4.7). **Pinning `operator` is weaker than pinning `guardianGovernance`:** a + compromised operator (a single hot key) can co-sign a `CoreState` whose + `operator` is genuine but whose `guardianGovernance`/`feeRecipient` it + controls, so an `operator`-only pin accepts a shadow `CoreState`. For the fee + path — where `guardianGovernance` _is_ the fee-custody anchor — only the + `guardianGovernance` pin suffices (see below). - **Governance stays low-friction.** `guardianGovernance` actively signs only at **genesis**; each `SubmitGovernanceVAA` inherits its authority from the archived contract (as the `Emitter` owner does through `PublishMessage`), so @@ -398,12 +401,19 @@ with Canton Coin or any standard token, chosen at genesis an app publishes on a user's behalf (NTT §10); `payer` co-controls the publish, so it cannot be a third party who did not authorize spending its funds. Registries charge their `registrationFee`/`claimFee` the same way, skipping the - `CoreState` entirely at fee 0. + `CoreState` entirely at fee 0. Before charging, every call site pins the + `CoreState` by exercising `GetGuardianGovernance` against the + `guardianGovernance` committed on the emitter/registry — **the fee-custody + anchor, not merely the `operator`** — so a forged `CoreState` cannot redirect + the fee (see the custody bullet). - **Custody is governance-controlled**: `feeRecipient` is the `guardianGovernance` threshold party, so accrued fees move only via the k-of-n guardian ceremony — the operator never touches them (the Canton analog of "fees - sit in the bridge contract"). A `TransferFees` VAA mints an on-ledger - `FeeWithdrawalAuthorization` (signed by + sit in the bridge contract"). This holds **even against a compromised operator + hot key**: each charge pins `guardianGovernance` (unforgeable by a single + operator key) rather than `operator`, so an operator co-signing a shadow + `CoreState` under a throwaway `feeRecipient` is rejected before any funds move. + A `TransferFees` VAA mints an on-ledger `FeeWithdrawalAuthorization` (signed by guardianGovernance) recording the amount, the VAA's 32-byte recipient, and the VAA hash — the artifact the guardians' custody policy matches when threshold-signing the outbound transfer. "Only per authorization" is enforced diff --git a/canton/core/daml/Wormhole/Core/Fees.daml b/canton/core/daml/Wormhole/Core/Fees.daml index fdda2676e8..3b3b0748b6 100644 --- a/canton/core/daml/Wormhole/Core/Fees.daml +++ b/canton/core/daml/Wormhole/Core/Fees.daml @@ -10,7 +10,10 @@ -- (guardianGovernance). The receiver's authority is inherited from -- 'CoreState' (a signatory), so every charge routes through -- 'ChargeMessageFee'/'ChargeFee' there, and a foreign sender can never be a --- controller — blocking theft structurally. +-- controller — blocking theft structurally, PROVIDED 'CoreState' is +-- authentic. Callers pin 'GetGuardianGovernance' against their own committed +-- anchor before charging, so a compromised operator can't redirect fees via +-- a shadow CoreState under a gg it controls. -- -- Allocation views are forgeable; authenticity is the instrument admin's -- signature on the allocation, checked in 'chargeFee'. diff --git a/canton/core/daml/Wormhole/Core/Setup.daml b/canton/core/daml/Wormhole/Core/Setup.daml index 86ab16d48c..74eb5b7cda 100644 --- a/canton/core/daml/Wormhole/Core/Setup.daml +++ b/canton/core/daml/Wormhole/Core/Setup.daml @@ -58,15 +58,18 @@ mkInitialCoreState operator guardianGovernance govChain govContract initialGuard consumedGovernance = Set.empty -- | The emitter-id registry the operator creates alongside the 'CoreState' at --- setup (sequential ids). Carries @guardianObserver@, threaded into each --- 'Emitter'. -mkInitialEmitterRegistry : Party -> Party -> Int -> EmitterRegistry -mkInitialEmitterRegistry operator guardianObserver registrationFee = - EmitterRegistry with operator, guardianObserver, registrationFee, nextId = 0 +-- setup (sequential ids). Carries @guardianGovernance@ (the fee-custody anchor, +-- pinned by 'RegisterEmitter') and @guardianObserver@, both threaded into each +-- 'Emitter'. Pass the same @guardianGovernance@ the 'CoreState' was created with. +mkInitialEmitterRegistry : Party -> Party -> Party -> Int -> EmitterRegistry +mkInitialEmitterRegistry operator guardianGovernance guardianObserver registrationFee = + EmitterRegistry with operator, guardianGovernance, guardianObserver, registrationFee, nextId = 0 -- | The replay-root registry (the stateless claim anchor) the operator creates -- alongside the 'CoreState' at setup; hands a 'ReplayNode' trie root to any --- consumer that claims one (see 'Wormhole.Core.Replay'). -mkInitialReplayRootRegistry : Party -> Int -> ReplayRootRegistry -mkInitialReplayRootRegistry operator claimFee = - ReplayRootRegistry with operator, claimFee +-- consumer that claims one (see 'Wormhole.Core.Replay'). Carries +-- @guardianGovernance@ (the fee-custody anchor, pinned by 'ClaimReplayRoot'); +-- pass the same one the 'CoreState' was created with. +mkInitialReplayRootRegistry : Party -> Party -> Int -> ReplayRootRegistry +mkInitialReplayRootRegistry operator guardianGovernance claimFee = + ReplayRootRegistry with operator, guardianGovernance, claimFee diff --git a/canton/core/daml/Wormhole/Core/State.daml b/canton/core/daml/Wormhole/Core/State.daml index 8e70c1b0d6..dcbdff1392 100644 --- a/canton/core/daml/Wormhole/Core/State.daml +++ b/canton/core/daml/Wormhole/Core/State.daml @@ -15,7 +15,11 @@ -- (README §4.7). Authenticity is checked on the PAYLOAD, never the resolution -- path: signatories cannot be forged, so a 'CoreState' whose @guardianGovernance@ -- is the known anchor is authentic by construction — a compromised @operator@ --- cannot fabricate one. +-- cannot fabricate one. The fee-custody anchor is pinned this way: 'PublishMessage', +-- 'RegisterEmitter', and 'ClaimReplayRoot' exercise 'GetGuardianGovernance' and +-- compare it against the @guardianGovernance@ committed on the emitter/registry +-- before charging. Pinning @guardianGovernance@ (not @operator@) is what holds +-- against a compromised operator, which alone does not determine @feeRecipient@. -- -- Two invariants: -- @@ -162,6 +166,21 @@ template CoreState controller reader do pure operator + -- | Read the @guardianGovernance@ anchor off an authentic 'CoreState' — a + -- STRONGER pin than 'GetOperator' for consumers that must resist a compromised + -- operator, not just third-party forgery. A 'CoreState' needs only some + -- (operator, gg) signature pair, so a compromised operator key can co-sign a + -- forged one under a throwaway gg it controls and pass an operator-pin; + -- pinning @guardianGovernance@ (the guardians' k-of-n external party, + -- unforgeable by a single operator key) closes that. Used by + -- 'Wormhole.Ntt.Manager.Receive' and the fee-charging pins ('PublishMessage', + -- 'RegisterEmitter', 'ClaimReplayRoot'). + nonconsuming choice GetGuardianGovernance : Party + with + reader : Party + controller reader + do pure guardianGovernance + -- | Charge the governance-set per-publish fee ('messageFee'). Lives here, -- not on 'Emitter': the fee amount is only knowable from this contract, and -- 'Allocation_ExecuteTransfer' needs the fee recipient's authority (sender ∧ @@ -351,11 +370,13 @@ emitterAddressFor registrar owner emitterId = -- the owner, whose signature it lacks (see 'Emitter'). template EmitterRegistry with - operator : Party - guardianObserver : Party -- threaded into each new Emitter by RegisterEmitter - registrationFee : Int -- onboarding fee, 10^-10 feeInstrument units; - -- operator-set at creation, 0 for free - nextId : Int + operator : Party + guardianGovernance : Party -- CoreState fee-custody anchor; pinned (not a + -- signatory) in RegisterEmitter's fee charge + guardianObserver : Party -- threaded into each new Emitter by RegisterEmitter + registrationFee : Int -- onboarding fee, 10^-10 feeInstrument units; + -- operator-set at creation, 0 for free + nextId : Int where signatory operator @@ -375,12 +396,16 @@ template EmitterRegistry controller requester do -- Zero-fee registrations never touch the CoreState; fee'd ones charge - -- through it, pinning its operator so a fabricated CoreState cannot - -- waive or redirect the fee. + -- through it. The gg pin runs BEFORE the charge: pinning + -- @guardianGovernance@ (not just @operator@, which a forged CoreState + -- would match) rejects a shadow CoreState under a rogue gg, so the + -- registration fee cannot be redirected. case (registrationFee > 0, coreStateCid) of (False, _) -> pure () (True, None) -> abort "registration fee due: pass the CoreState" (True, Some csCid) -> do + csGg <- exercise csCid GetGuardianGovernance with reader = requester + assertMsg "core state guardianGovernance mismatch" (csGg == guardianGovernance) csOperator <- exercise csCid ChargeFee with payer = requester fee = registrationFee @@ -389,6 +414,7 @@ template EmitterRegistry reg' <- create this with nextId = nextId + 1 emitter <- create Emitter with operator + guardianGovernance owner = requester guardianObserver emitterId = nextId @@ -415,9 +441,11 @@ template EmitterRegistry -- grows. template ReplayRootRegistry with - operator : Party - claimFee : Int -- onboarding fee, 10^-10 feeInstrument units; - -- operator-set at creation, 0 for free + operator : Party + guardianGovernance : Party -- CoreState fee-custody anchor; pinned (not a + -- signatory) in ClaimReplayRoot's fee charge + claimFee : Int -- onboarding fee, 10^-10 feeInstrument units; + -- operator-set at creation, 0 for free where signatory operator @@ -429,13 +457,17 @@ template ReplayRootRegistry feeAllocation : Optional (ContractId Allocation, ExtraArgs) controller consumer do - -- Zero-fee claims never touch the CoreState; fee'd ones charge - -- through it, pinning its operator so a fabricated CoreState cannot - -- waive or redirect the fee (see README §4.2). + -- Zero-fee claims never touch the CoreState; fee'd ones charge through + -- it. The gg pin runs BEFORE the charge: pinning @guardianGovernance@ + -- (not just @operator@, which a forged CoreState would match) rejects a + -- shadow CoreState under a rogue gg, so the claim fee cannot be + -- redirected (see README §4.2). case (claimFee > 0, coreStateCid) of (False, _) -> pure () (True, None) -> abort "claim fee due: pass the CoreState" (True, Some csCid) -> do + csGg <- exercise csCid GetGuardianGovernance with reader = consumer + assertMsg "core state guardianGovernance mismatch" (csGg == guardianGovernance) csOperator <- exercise csCid ChargeFee with payer = consumer fee = claimFee @@ -454,12 +486,14 @@ template ReplayRootRegistry -- authority). Mirrors a Sui @EmitterCap@. template Emitter with - operator : Party - owner : Party - guardianObserver : Party -- read-only guardian observer - emitterId : Int -- registry-allocated - emitterAddress : Bytes32 -- emitterAddressFor operator owner emitterId, set at creation - sequence : Int -- next sequence to assign + operator : Party + guardianGovernance : Party -- CoreState fee-custody anchor; pinned (not a + -- signatory) in PublishMessage's fee charge + owner : Party + guardianObserver : Party -- read-only guardian observer + emitterId : Int -- registry-allocated + emitterAddress : Bytes32 -- emitterAddressFor operator owner emitterId, set at creation + sequence : Int -- next sequence to assign where -- Owner co-signs: only a transaction with the owner's authority can create a -- contract whose derived address is the owner's — the invariant guardians @@ -477,10 +511,10 @@ template Emitter -- directly. Mirrors @Implementation.publishMessage@. -- -- The 'messageFee' is charged through the referenced 'CoreState' (EVM parity: - -- the current fee is read on every call), whose operator is pinned so a fake - -- CoreState cannot waive the fee. Publishers attach the CoreState disclosure - -- to every publish (cacheable between governance actions, which invalidate - -- the cid transiently — + -- the current fee is read on every call), whose @guardianGovernance@ and + -- operator are pinned so a fake CoreState cannot redirect or waive it. + -- Publishers attach the CoreState disclosure to every publish (cacheable + -- between governance actions, which invalidate the cid transiently — -- re-resolve and retry); at fee 0 pass @feeAllocation = None@, but the -- CoreState reference is still required since the fee amount is only knowable -- from it. @@ -501,6 +535,12 @@ template Emitter feeAllocation : Optional (ContractId Allocation, ExtraArgs) controller owner, payer do + -- Pin the fee-custody anchor BEFORE charging: @guardianGovernance@ (not + -- just @operator@, which a forged CoreState would match) determines where + -- the fee lands, so this check rejects a shadow CoreState under a rogue + -- gg before the message fee can be redirected. + csGg <- exercise coreStateCid GetGuardianGovernance with reader = payer + assertMsg "core state guardianGovernance mismatch" (csGg == guardianGovernance) csOperator <- exercise coreStateCid ChargeMessageFee with payer feeAllocation diff --git a/canton/test/daml/Test/TestCore.daml b/canton/test/daml/Test/TestCore.daml index 744aa8a9aa..6c7700b37c 100644 --- a/canton/test/daml/Test/TestCore.daml +++ b/canton/test/daml/Test/TestCore.daml @@ -54,8 +54,8 @@ setupWithInstrument mInstrument = do createCmd (mkInitialCoreState operator guardianGovernance solanaGovernanceChainId defaultGovernanceContract [devnetGuardian] guardianObserver feeInstrument guardianGovernance) - _ <- submit operator do createCmd (mkInitialEmitterRegistry operator guardianObserver 0) - _ <- submit operator do createCmd (mkInitialReplayRootRegistry operator 0) + _ <- submit operator do createCmd (mkInitialEmitterRegistry operator guardianGovernance guardianObserver 0) + _ <- submit operator do createCmd (mkInitialReplayRootRegistry operator guardianGovernance 0) pure (operator, guardianObserver, csId) -- | Register an emitter for @owner@, the production way: the requester submits @@ -168,14 +168,14 @@ testImpersonationBlocked = do -- (a) Operator-alone forgery of Alice's address is impossible. submitMustFail operator do createCmd Emitter with - operator, owner = alice + operator, guardianGovernance = a.guardianGovernance, owner = alice guardianObserver, emitterId = 0 emitterAddress = a.emitterAddress, sequence = 0 -- (b) Operator + Mallory can create a same-id emitter, but its address differs. malloryEm <- submit (actAs operator <> actAs mallory) do createCmd Emitter with - operator, owner = mallory + operator, guardianGovernance = a.guardianGovernance, owner = mallory guardianObserver, emitterId = 0 emitterAddress = emitterAddressFor operator mallory 0, sequence = 0 Some m <- queryContractId mallory malloryEm diff --git a/canton/test/daml/Test/TestFees.daml b/canton/test/daml/Test/TestFees.daml index fd692b3f5b..3e64fd62a0 100644 --- a/canton/test/daml/Test/TestFees.daml +++ b/canton/test/daml/Test/TestFees.daml @@ -8,6 +8,7 @@ module Test.TestFees where import Daml.Script import DA.Assert ((===)) import DA.Optional (fromSome) +import DA.Text (isInfixOf) import Splice.Api.Token.HoldingV1 (InstrumentId(..)) import Splice.Api.Token.AllocationV1 import Wormhole.Core.Fees (feeToDecimal, emptyExtraArgs) @@ -185,7 +186,7 @@ testRegistrationFee = do Some cs <- queryContractId operator csId bob <- allocateParty "RegFeeBob" regCid <- submit operator do - createCmd (mkInitialEmitterRegistry operator cs.guardianObserver 500) + createCmd (mkInitialEmitterRegistry operator cs.guardianGovernance cs.guardianObserver 500) regDisc <- fromSome <$> queryDisclosure operator regCid csDisc <- fromSome <$> queryDisclosure operator csId -- Fee due: no CoreState / no allocation both rejected. @@ -212,7 +213,7 @@ testClaimFee = do Some cs <- queryContractId operator csId carol <- allocateParty "ClaimFeeCarol" regCid <- submit operator do - createCmd (mkInitialReplayRootRegistry operator 300) + createCmd (mkInitialReplayRootRegistry operator cs.guardianGovernance 300) regDisc <- fromSome <$> queryDisclosure operator regCid csDisc <- fromSome <$> queryDisclosure operator csId submitMustFail (actAs carol <> disclose regDisc <> disclose csDisc) do @@ -244,3 +245,112 @@ testNoSpuriousWithdrawalAuthorization = do pubKeys = [(0, devnetGuardianPubKey)] auths <- query @FeeWithdrawalAuthorization operator auths === [] + +---------------------------------------------------------------------- +-- guardianGovernance pin: the shadow-CoreState fee-redirect regression +---------------------------------------------------------------------- +-- +-- The call sites once pinned only the @operator@ returned by 'ChargeFee' / +-- 'ChargeMessageFee', but @operator@ does NOT determine @feeRecipient@ (that is +-- @guardianGovernance@). A compromised operator can co-sign a second, +-- ledger-valid 'CoreState' under an attacker-controlled gg (= @feeRecipient@) — +-- the operator field still matches, so an operator-only pin lets real fees flow +-- to the attacker. Each call site now pins 'GetGuardianGovernance' against the +-- gg committed on the emitter/registry before charging. Each test builds that +-- forgery (real operator + rogue gg) and asserts the failure carries +-- "guardianGovernance mismatch" — the operator pin would pass, so the substring +-- match proves the fix rather than an incidental failure. + +-- | 'PublishMessage' pins the Emitter's guardianGovernance anchor. The forged +-- CoreState even has its (rogue) messageFee raised and a matching allocation +-- attached, so the charge WOULD succeed against @rogueGov@ — the pin rejects it +-- first. +testPublishPinsGuardianGovernance : Script () +testPublishPinsGuardianGovernance = do + (operator, mockAdmin, _realCsId, inst, _realRecipient) <- setupFees + alice <- allocateParty "PinPublishAlice" + rogueGov <- allocateParty "RoguePublishGov" + rogueObserver <- allocateParty "RoguePublishObserver" + emId <- registerEmitter operator alice + -- The shadow CoreState: co-signed by the REAL operator and a throwaway gg it + -- controls, with rogueGov as both guardianGovernance and feeRecipient. + fakeCsId <- submit (actAs operator <> actAs rogueGov) do + createCmd (mkInitialCoreState operator rogueGov solanaGovernanceChainId + defaultGovernanceContract [devnetGuardian] rogueObserver inst rogueGov) + fakeCsId' <- submit operator do + exerciseCmd fakeCsId SubmitGovernanceVAA with + encodedVAA = govSetFeeVAA + pubKeys = [(0, devnetGuardianPubKey)] + fakeDisc <- fromSome <$> queryDisclosure operator fakeCsId' + feePay <- mintAndAllocate mockAdmin alice rogueGov inst (feeToDecimal mockFee) + res <- trySubmit (actAs alice <> disclose fakeDisc) do + exerciseCmd emId PublishMessage with + nonce = 1, payload = "aa", consistencyLevel = 0 + coreStateCid = fakeCsId', payer = alice, feeAllocation = Some feePay + case res of + Left (FailureStatusError fs) -> + assertMsg ("expected a guardianGovernance pin failure, got: " <> fs.message) + ("guardianGovernance mismatch" `isInfixOf` fs.message) + Left err -> abort ("expected a guardianGovernance pin failure, got: " <> show err) + Right _ -> abort "PublishMessage accepted a shadow CoreState under a rogue guardianGovernance" + +-- | 'RegisterEmitter' pins the registry's guardianGovernance anchor. The +-- nonzero-fee registry is anchored to the real gg; the forged CoreState routes +-- its @registrationFee@ to @rogueGov@, and a fully-funded allocation is +-- attached — the pin rejects it before the charge. +testRegisterEmitterPinsGuardianGovernance : Script () +testRegisterEmitterPinsGuardianGovernance = do + (operator, mockAdmin, realCsId, inst, _realRecipient) <- setupFees + Some cs <- queryContractId operator realCsId + bob <- allocateParty "PinRegBob" + rogueGov <- allocateParty "RogueRegGov" + rogueObserver <- allocateParty "RogueRegObserver" + -- A real, nonzero-fee registry pinned to the REAL guardianGovernance. + regCid <- submit operator do + createCmd (mkInitialEmitterRegistry operator cs.guardianGovernance cs.guardianObserver 500) + regDisc <- fromSome <$> queryDisclosure operator regCid + fakeCsId <- submit (actAs operator <> actAs rogueGov) do + createCmd (mkInitialCoreState operator rogueGov solanaGovernanceChainId + defaultGovernanceContract [devnetGuardian] rogueObserver inst rogueGov) + fakeDisc <- fromSome <$> queryDisclosure operator fakeCsId + feePay <- mintAndAllocate mockAdmin bob rogueGov inst (feeToDecimal 500) + res <- trySubmit (actAs bob <> disclose regDisc <> disclose fakeDisc) do + exerciseCmd regCid RegisterEmitter with + requester = bob + coreStateCid = Some fakeCsId + feeAllocation = Some feePay + case res of + Left (FailureStatusError fs) -> + assertMsg ("expected a guardianGovernance pin failure, got: " <> fs.message) + ("guardianGovernance mismatch" `isInfixOf` fs.message) + Left err -> abort ("expected a guardianGovernance pin failure, got: " <> show err) + Right _ -> abort "RegisterEmitter accepted a shadow CoreState under a rogue guardianGovernance" + +-- | 'ClaimReplayRoot' pins the registry's guardianGovernance anchor. Same +-- forgery, routing the @claimFee@ to @rogueGov@; the pin rejects it before the +-- charge. +testClaimReplayRootPinsGuardianGovernance : Script () +testClaimReplayRootPinsGuardianGovernance = do + (operator, mockAdmin, realCsId, inst, _realRecipient) <- setupFees + Some cs <- queryContractId operator realCsId + carol <- allocateParty "PinClaimCarol" + rogueGov <- allocateParty "RogueClaimGov" + rogueObserver <- allocateParty "RogueClaimObserver" + regCid <- submit operator do + createCmd (mkInitialReplayRootRegistry operator cs.guardianGovernance 300) + regDisc <- fromSome <$> queryDisclosure operator regCid + fakeCsId <- submit (actAs operator <> actAs rogueGov) do + createCmd (mkInitialCoreState operator rogueGov solanaGovernanceChainId + defaultGovernanceContract [devnetGuardian] rogueObserver inst rogueGov) + fakeDisc <- fromSome <$> queryDisclosure operator fakeCsId + feePay <- mintAndAllocate mockAdmin carol rogueGov inst (feeToDecimal 300) + res <- trySubmit (actAs carol <> disclose regDisc <> disclose fakeDisc) do + exerciseCmd regCid ClaimReplayRoot with + consumer = carol, splitThreshold = 4 + coreStateCid = Some fakeCsId, feeAllocation = Some feePay + case res of + Left (FailureStatusError fs) -> + assertMsg ("expected a guardianGovernance pin failure, got: " <> fs.message) + ("guardianGovernance mismatch" `isInfixOf` fs.message) + Left err -> abort ("expected a guardianGovernance pin failure, got: " <> show err) + Right _ -> abort "ClaimReplayRoot accepted a shadow CoreState under a rogue guardianGovernance" diff --git a/canton/test/daml/Test/TestGuardianObserver.daml b/canton/test/daml/Test/TestGuardianObserver.daml index f69fd5f5c5..a5fa48c7dc 100644 --- a/canton/test/daml/Test/TestGuardianObserver.daml +++ b/canton/test/daml/Test/TestGuardianObserver.daml @@ -88,7 +88,7 @@ testObserverHasNoAuthority = do -- Cannot create contracts bearing the real signatory parties (no authority). submitMustFail guardianObserver do createCmd Emitter with - operator, owner = alice + operator, guardianGovernance = cs.guardianGovernance, owner = alice emitterId = 0 emitterAddress = emitterAddressFor operator alice 0, sequence = 0 guardianObserver