Empty Account controllers

In Daml Finance every instance of Account has sets of incoming and outgoing controllers. In some cases, the incoming controllers might be empty (maybe you just want to let anyone transfer to your account). Perhaps even the outgoing controllers could be empty too. The issue I’m facing is the implementation of Instruction provided in the library appears to always assume non-empty Account controllers. See here it is taking the head of the list of controllers. Would it be possible to remove this assumption?

1 Like

Hi @huw,

The incoming and outgoing controllers offers a flexible mechanism to model a variety of required permissions for transfers. For instance, if the incoming controllers of the receiving account are empty, and the outgoing controllers of the sending account solely consist of the owner, this setup empowers the owner to transfer to the newOwner without requiring the newOwner's involvement. This process is akin to a Bitcoin transfer.

I trust the subsequent test will offer a more vivid representation of this concept:

-- Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0

module TransferTest where

import DA.Map qualified as M (fromList)
import DA.Set qualified as S (empty, fromList, singleton, toList, union)
import Daml.Finance.Account.Account qualified as Account (Factory(..))
import Daml.Finance.Holding.Fungible qualified as Fungible (Factory(..))
import Daml.Finance.Interface.Account.Account qualified as Account (Credit(..), Controllers(..))
import Daml.Finance.Interface.Account.Factory qualified as Factory (Create(..), F)
import Daml.Finance.Interface.Holding.Transferable qualified as Transferable (I, Transfer(..))
import Daml.Finance.Interface.Types.Common.Types (AccountKey(..), Id(..), InstrumentKey(..), Quantity(..))
import Daml.Script

  -- | Options for transfer controllers.
data ControlledBy
  = Owner
    -- ^ Owner controls inbound and outbound transfers.
  | Custodian
    -- ^ Custodian controls inbound and outbound transfers.
  | OwnerAndCustodian
    -- ^ Owner and custodian jointly control inbound and outbound transfers.
  | OwnerWithAutoApproval
    -- ^ Owner controls outbound transfers, and inbound transfers are auto-approved.
  deriving (Eq, Show)

-- | Get account controllers depending on controlledBy.
toControllers : Party -> Party -> ControlledBy -> Account.Controllers
toControllers custodian owner controlledBy =
  case controlledBy of
    Owner -> Account.Controllers with
      outgoing = S.singleton owner; incoming = S.singleton owner
    Custodian -> Account.Controllers with
      outgoing = S.singleton custodian; incoming = S.singleton custodian
    OwnerAndCustodian -> Account.Controllers with
      outgoing = S.fromList [owner, custodian]; incoming = S.fromList [owner, custodian]
    OwnerWithAutoApproval -> Account.Controllers with
      outgoing = S.singleton owner; incoming = S.empty

test1 : Script ()
test1 = testTransferControlledBy Owner

test2 : Script ()
test2 = testTransferControlledBy Custodian

test3 : Script ()
test3 = testTransferControlledBy OwnerAndCustodian

test4 : Script ()
test4 = testTransferControlledBy OwnerWithAutoApproval

testTransferControlledBy : ControlledBy -> Script ()
testTransferControlledBy controlledBy = script do
  -- Allocate parties
  [depository, custodian, issuer, alice, bob, publicParty] <-
    forA
      ["Depository", "Custodian", "Issuer", "Alice", "Bob", "PublicParty"]
      \name -> allocatePartyWithHint name $ PartyIdHint name
  let observers = M.fromList [("PublicParty", S.singleton publicParty)]

  -- Account factory
  accountFactoryCid <- toInterfaceContractId @Factory.F <$> submitMulti [custodian] [] do
    createCmd Account.Factory with provider = custodian; observers

  -- Holding factory
  holdingFactoryCid <- toInterfaceContractId <$> submit custodian do
    createCmd Fungible.Factory with provider = custodian; observers

  -- Create Accounts for Alice and Bob
  let
    account owner =
      ( AccountKey with custodian; owner; id = Id $ show owner <> "@" <> show custodian
      , toControllers custodian owner controlledBy
      )
    (aliceAccount, aliceControllers) = account alice
    (bobAccount, bobControllers) = account bob
  aliceAccountCid <- submitMulti [custodian, alice] [] do
    exerciseCmd accountFactoryCid Factory.Create with
      account = aliceAccount
      holdingFactoryCid
      controllers = aliceControllers
      description = ""
      observers
  bobAccountCid <- submitMulti [custodian, bob] [] do
    exerciseCmd accountFactoryCid Factory.Create with
      account = bobAccount
      holdingFactoryCid
      controllers = bobControllers
      description = ""
      observers

  -- Credit holding to alice
  aliceHoldingCid <- submitMulti [custodian, alice] [] do
    exerciseCmd aliceAccountCid Account.Credit with
      quantity = Quantity with
        unit = InstrumentKey with
          id = Id "ABC.DE"
          version = "0"
          ..
        amount = 1_000.0

  -- Transfer Alice's holding to Bob
  let authorizers = S.toList $ S.union aliceControllers.outgoing bobControllers.incoming
  debug authorizers
  submitMulti authorizers [publicParty] do
    exerciseCmd (coerceInterfaceContractId @Transferable.I aliceHoldingCid)
      Transferable.Transfer with
        actors = S.fromList authorizers; newOwnerAccount = bobAccount

  pure ()

I can’t think of a use-case where the outgoing controllers would be empty.

Upon reviewing the settlement code, I found your observation to be accurate—the current implementation version does not support empty incoming controllers. However, this limitation has been rectified in the main branch. We’re yet to roll out this update in an official release.

Johan

2 Likes

Thanks for the detailed answer @Johan_Sjodin . Do you know when you are going to add this fix as part of an official release?

It will go into the next release which will be at the same time as SDK 2.7 is released.