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?
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
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.