Pros and cons of keeping a reference to a contract as a parameter of another contract

In several cases I’ve noticed that it is convenient to keep a ContractId to a contract, and more simply the contract itself (determined via this) as a parameter of another contract. I’m fairly confident that these holders will be archived before the contract that they keep a reference to, so things “should work”.

  1. Are there other things to consider?
  2. When my parameter is a template, what am I actually storing? A copy of the data portion of the contract.
  3. What if I were to use this parameter as part of the contract key?

Thanks,

3 Likes

What am I actually storing?
When the type of the parameter is a template, then you are storing a copy of the data of some contract. (Templates can also be used as records.) You can use it to see what the contract data is, but you cannot check whether the contract is active or to exercise choices.

When the type of the parameter is a contract id, then you are storing a contract id (without the actual contract data). You can use it to check whether the contract is active (i.e. fetch) and to exercise choices.

What if I use this parameter as part of the contract key?
When the type of the parameter is a template, it behaves as if you add a field of record type to the contract key.

The runtime team is working very hard to allow you adding contract ids to contract keys. I am not sure whether it has already been released, but it is coming soon.

Other things to consider:
Unlike for foreign keys in a database, the referenced contract can get archived before the referencing contract. We cannot guarantee integrity of foreign keys, because that would give opportunities for DOS attacks. I.e., if you are the controller of a consuming choice, we want to allow you to exercise it any time. If we enforced foreign key integrity, you could not do that.

EDIT: Expanded the answer to explain how you can use the contract data / id.

2 Likes

One more thing. If you include contract ids into templates you will likely run into visibility issues. (A party gets a contract id, but it cannot exercise choices on it, because it does not have the contract data.) To deal with that you need to understand divulgence.

https://docs.daml.com/concepts/ledger-model/ledger-privacy.html#divulgence-when-non-stakeholders-see-contracts

1 Like

Thank you @MatthiasSchmalz that clarifies things.

One alternative to storing the ContractID could be to store the key data, via something like:

template Foo
  with
    fooParty : Party
    someString : Text
    forKeyInt : Int
  where
    signatory fooParty
    key (fooParty, forKeyInt) : (Party, Int)
    maintainer key._1

    controller fooParty can
      nonconsuming ChannelYourFoo : ()
        do
          let () = traceRaw (someString <> " " <> show forKeyInt) ()
          return ()

template Bar
  with
    barParty : Party
    foo : Foo
  where
    signatory barParty

    controller barParty can
      nonconsuming BarFoo : ()
        do
          (fooId, latestFoo) <- fetchByKey @Foo (key foo)
          -- latestFoo == foo, true only the first time.
          exercise fooId ChannelYourFoo
          return ()

fooBar = scenario do
  alice <- getParty "Alice"
  f <- submit alice do create Foo with fooParty = alice, someString = "Data", forKeyInt = 10
  b <- submit alice do
          foo <- fetch f
          create Bar with barParty = alice, foo
  submit alice do exercise b BarFoo
  submit alice do archive f
  submit alice do create Foo with fooParty = alice, someString = "New Data", forKeyInt = 10
  submit alice do exercise b BarFoo
  return ()

What do you think?

1 Like

Storing a contract key is similar to storing a contract ID. The following differences come to my mind:

  • A fetch needs to be authorized by at least one stakeholder. A fetchByKey needs to be authorized by at least one maintainer.
  • Visibility is harder to manage. Whenever you update the contract referenced by the key, you need to disclose it again. If you are not familiar with divulgence, I would first practice without using contract keys.

Here is an example:

template Foo
  with
    m : Party
    o : Party
  where
    signatory m
    observer o
    key m : Party
    maintainer key

-- Use this to dislose a Foo with signatory s to a party r
template DiscloseFoo 
  with
    s : Party
    r : Party
  where
    signatory s
    observer r

    controller s can
      Disclose : ContractId Foo
        do
          (fooId, _) <- fetchByKey @Foo s
          return fooId

-- Contract where s authorizes arbitrary parties to fetch Foo contracts
template Baz -- Contract 
  with 
    s : Party
    o : Party
  where
    signatory s
    observer o

    nonconsuming choice BazFoo : ()
      with
        c : Party
        m : Party
      controller c
        do
          (fooId, _) <- fetchByKey @Foo m
          return ()

demo = scenario do
  alice <- getParty "Alice"
  bob <- getParty "Bob"
  
  bobFooId <- submit bob do create Foo with m = bob, o = bob

  -- Now let's do some delegation
  aliceBazId <- submit alice do create Baz with s = alice, o = bob
  bobBazId <- submit bob do create Baz with s = bob, o = alice

  -- Succeeds as expected because all involved contracts belong to bob
  _ <- submit bob do exercise bobBazId BazFoo with c = bob, m = bob

  -- Fails because alice cannot see bobFooId.
  _ <- submitMustFail alice do exercise bobBazId BazFoo with c = alice, m = bob

  -- Disclose bobFooId to alice
  discloseId <- submit bob do create DiscloseFoo with s = bob, r = alice
  _ <- submit bob do exercise discloseId Disclose

  -- Now succeeds, as bobFooId has been disclosed to alice.
  _ <- submit alice do exercise bobBazId BazFoo with c = alice, m = bob

  -- Fails because alice is not a maintainer of bobFooId
  _ <- submitMustFail alice do exercise aliceBazId BazFoo with c = alice, m = bob

  -- Now archive the contract.
  _ <- submit bob do archive bobFooId

  -- Fails because bobFooId is no longer active.
  _ <- submitMustFail alice do exercise bobBazId BazFoo with c = alice, m = bob

  -- Create a new version of bob's Foo
  bobFooId2 <- submit bob do create Foo with m = bob, o = bob

  -- Fails again because alice cannot see bobFooId.
  _ <- submitMustFail alice do exercise bobBazId BazFoo with c = alice, m = bob

  -- Disclose bobFooId to alice, again
  discloseId <- submit bob do create DiscloseFoo with s = bob, r = alice
  _ <- submit bob do exercise discloseId Disclose

  -- Now succeeds, as bobFooId has been disclosed to alice, again.
  _ <- submit alice do exercise bobBazId BazFoo with c = alice, m = bob

  return ()
1 Like