pact-lang

Is it possible to use (compose-capability) with the coin.TRANSFER capability?


My contract contains a function that transfers funds with coin.transfer if some conditions are met. It is guarded by a capability (let's call it TRANSFER_WRAPPER). However, when a user calls this function, they must scope their signature to both the TRANSFER_WRAPPER and the coin.TRANSFER capabilities.

Instead, I would like to use compose-capability within TRANSFER_WRAPPER so they only need to scope their signature to the wrapper. For example:

(module test "sender-keyset"
  (defcap TRANSFER_WRAPPER (receiver:string amount:decimal)
    (compose-capability (coin.TRANSFER "sender" receiver amount))
  )

  (defun transfer-wrapper (receiver:string amount:decimal)
    (with-capability (TRANSFER_WRAPPER receiver amount)
      (coin.transfer "sender" receiver amount)
    )
  )
)

However, attempting to use this produces an unexpected error from the coin-v4 contract:

./root/coin-v4.pact:50:4: Enforce non-upgradeability
 at ./root/coin-v4.pact:50:4: (enforce false "Enforce non-upgradeability")
 at ./root/coin-v4.pact:49:2: (GOVERNANCE)
 at ./test.pact:3:4: (compose-capability (coin.TRANSFER "sender" "receiver" 20.0))
 at ./test.pact:7:21: (TRANSFER_WRAPPER "sender" "receiver" 20.0)
 at ./test.pact:7:4: (with-capability (test.TRANSFER_WRAPPER "sender" "receiver" 20.0) [(coin.transfer "sender" "receiver" 20.0)])

You can reproduce the error by placing the Pact snippet above in a file called test.pact, and the coin-v4, fungible-xchain-v1, and fungible-v2 contracts into a sibling root directory.

Then, paste the contents of this snippet in a test.repl file and run it with pact test.repl:

(env-data { "sender-keyset": { "keys": [ "sender-key" ], "pred": "keys-all" } })
(env-sigs [{"key": "sender-key", "caps": []}])
(begin-tx)
; The test depends on the 'coin-v4' contract, which in turn depends on these.
(load "./root/fungible-v2.pact")
(load "./root/fungible-xchain-v1.pact")
(load "./root/coin-v4.pact")
(define-keyset "sender-keyset" (read-keyset "sender-keyset"))
(load "./test.pact")
(commit-tx)

(env-sigs [{"key": "sender-key", "caps": [(test.TRANSFER_WRAPPER "sender" "receiver" 100.0)]}])
(test.transfer-wrapper "sender" "receiver" 20.0)

Solution

  • No, you cannot compose another module's capabilities, and your user must scope coin.TRANSFER explicitly, by design.

    This ensures that the user's authority over debiting their account can never be delegated away, that the user has explicit and visible control over funds being transferred.

    If this were not true, malfeasance could occur. For instance, your wrapper capability could misprepresent how much money was to be transferred, and/or your code could transfer twice, or to a different account, etc.

    This is also how Pact avoids the dangerous stateful authorizations seen in EVM contracts. With the TRANSFER capability the user is explicitly allowing any code to execute the transfer but only for that transaction, and only for up to the total amount indicated. So it is actually quite helpful for dapps as it avoids bugs from leftover authorizations.