Issue with The Delegation Pattern

I am trying to recreate the example shown in the delegation pattern explained here[The Delegation Pattern — Daml SDK 2.2.0 documentation].

I am facing the following issue, even though Bob is making the request, daml is suggesting that Bob’s Authorisation is missing.

Script execution failed on commit at Main:93:3:
  2: create of Main:TransferProposal at DA.Internal.Template.Functions:231:3
     failed due to a missing authorization from 'Bob'

Ledger time: 1970-01-01T00:00:00Z

Partial transaction:
  Failed exercise (unknown source):
    exercises Transfer on #1:1 (Main:Coin)
    with
      newOwner = 'Bob'
  Sub-transactions:
     0
     └─> 'Bob' exercises TransferCoin on #2:0 (Main:CoinPoA)
               with
                 coinId = #1:1; newOwner = 'Bob'
         children:
         1
         └─> 'Alice' exercises Transfer on #1:1 (Main:Coin)
                     with
                       newOwner = 'Bob'
             children:
             2
             └─> create Main:TransferProposal
                 with
                   newOwner = 'Bob'

Committed transactions: 
  TX 0 1970-01-01T00:00:00Z (Main:75:16)
  #0:0
  │   consumed by: #1:0
  │   referenced by #1:0
  │   disclosed to (since): 'Alice' (0)
  └─> create Main:Coin
      with
        owner = 'Alice'; issuer = 'Alice'; amount = 10.0000000000; delegates = ['Alice']
  
  TX 1 1970-01-01T00:00:00Z (Main:82:25)
  #1:0
  │   disclosed to (since): 'Alice' (1)
  └─> 'Alice' exercises Disclose on #0:0 (Main:Coin)
              with
                p = 'Bob'
      children:
      #1:1
      │   disclosed to (since): 'Alice' (1), 'Bob' (1)
      └─> create Main:Coin
          with
            owner = 'Alice';
            issuer = 'Alice';
            amount = 10.0000000000;
            delegates = ['Bob', 'Alice']
  
  TX 2 1970-01-01T00:00:00Z (Main:85:19)
  #2:0
  │   disclosed to (since): 'Alice' (2), 'Bob' (2)
  └─> create Main:CoinPoA
      with
        attorney = 'Bob'; principal = 'Alice'

Here is the code, most code is from the document, i have just created some dummy templates to fill the gaps

module Main where

import Daml.Script

template Coin
  with
    owner: Party
    issuer: Party
    amount: Decimal
    delegates : [Party]
  where
    signatory issuer, owner
    observer delegates


    choice Disclose : ContractId Coin
      with p : Party
      controller owner
      do create this with delegates = p :: delegates

    choice Transfer : ContractId TransferProposal
      with 
        newOwner :Party
      controller owner 
      do create TransferProposal with..

template TransferProposal
  with
    newOwner : Party
  where 
    signatory newOwner


template CoinPoA
  with
    attorney: Party
    principal: Party
  where
    signatory principal
    observer attorney

    choice WithdrawPoA
      : ()
      controller principal
      do return ()

    nonconsuming choice TransferCoin
      : ContractId TransferProposal
      with
        coinId: ContractId Coin
        newOwner: Party
      controller attorney
      do
        exercise coinId Transfer with newOwner



setup : Script()
setup = script do
-- user_setup_begin
  alice <- allocatePartyWithHint "Alice" (PartyIdHint "Alice")
  bob <- allocatePartyWithHint "Bob" (PartyIdHint "Bob")
  aliceId <- validateUserId "alice"
  bobId <- validateUserId "bob"
  createUser (User aliceId (Some alice)) [CanActAs alice]
  createUser (User bobId (Some bob)) [CanActAs bob]
-- user_setup_end

  aliceCoin <- submit alice do
    createCmd Coin with
      issuer = alice
      owner = alice
      amount = 10.0
      delegates =[alice]

  aliceCoinDisclosed <- submit alice do
    exerciseCmd aliceCoin Disclose with p = bob

  aliceCoinPoa <- submit alice do
    createCmd CoinPoA with
      attorney = bob
      principal = alice

  


  submit bob do
    exerciseCmd aliceCoinPoa TransferCoin with newOwner = bob, coinId = aliceCoinDisclosed

  return()

In the body of your Transfer choice you have authorization from the signatories & controllers so the issuer & owner. However, creating the TransferProposal requires authorization from the newOwner. There are different ways of fixing this. If you want to go for a proposal workflow the proposal should be signed by the old owner and the newOwner is only an observer. Alternatively, you can require that both the old owner & new owner authorize the transfer and skip the proposal.

Here’s a sketch of the latter:

template Coin
  with
    owner: Party
    issuer: Party
    amount: Decimal
    delegates : [Party]
  where
    signatory issuer, owner
    observer delegates


    choice Disclose : ContractId Coin
      with p : Party
      controller owner
      do create this with delegates = p :: delegates

    choice Transfer : ContractId Coin
      with
        newOwner :Party
      controller owner, newOwner
      do create this with owner = newOwner


template CoinPoA
  with
    attorney: Party
    principal: Party
  where
    signatory principal
    observer attorney

    choice WithdrawPoA
      : ()
      controller principal
      do return ()

    nonconsuming choice TransferCoin
      : ContractId Coin
      with
        coinId: ContractId Coin
        newOwner: Party
      controller attorney, newOwner
      do coin <- fetch coinId
         coin.owner === principal
         exercise coinId Transfer with newOwner

To check my understanding, would it be safe to remove the attorney from the controllers list of choice TransferCoin? It still seems to work if you do so I wonder if this is required given that attorney is a signatory? Thanks.

Controllers serve two functions:

  1. They provide additional authority that can be used in the choice body (where you have authorization from both signatories & controllers).
  2. They specify who is allowed to exercise that choice: all controllers need to agree to exercise a choice.

If the controller is also a signatory (as in this example) 1 clearly doesn’t do much. However it can still be very important for 2: Consider the example here, if you drop the attorney from the list of controllers you are saying any newOwner can transfer coins to themselves without the attorney or anyone else needing to agree. That’s almost certainly not what you want.

Makes sense, thanks! I was thinking that an arbitrary newOwner wouldn’t be able to see that contract? Of course reliance on that alone may not be best practice, which I guess is why you have gone for the two controllers approach?

Yeah I would recommend against relying on visibility to control authorization.

1 Like