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