Context
Surfacing a custody characteristic of vaulting restricted markers that have allow_forced_transfer = true. When a vault holds such an asset as backing, the asset's issuer (or governance) retains the ability to forcibly pull that asset back out of the vault at any time, without the vault's or depositors' consent. This is inherent to the marker's force-transfer design, not a code defect in the vault, but it is a trust assumption worth making explicit and deciding on deliberately. Filing for discussion.
The exposure
A vault holds depositor backing in its principal marker account (vault.PrincipalMarkerAddress()). The marker module explicitly permits forced transfers out of marker accounts (x/marker/keeper/marker.go canForceTransferFrom, lines 737-739), unlike module accounts and smart-contract accounts which it protects (sequence-0 accounts are blocked at line 746).
So for any vaulted asset whose marker has allow_forced_transfer = true, a holder of Access_ForceTransfer on that marker can submit MsgTransferRequest with from = the vault's principal marker and move the asset to themselves. TransferCoin (marker.go:647-698) requires the marker be active, allow_forced_transfer = true, and the signer hold Access_ForceTransfer. No depositor signature is involved. The vault's share accounting (TotalShares / NAV) is not updated, so the result is outstanding shares with reduced or missing backing.
Confirmed on-chain (payment denom uylds.fcc)
uylds.fcc is a restricted marker, status active, allow_forced_transfer = true on both pio-mainnet-1 and pio-testnet-1. It has live keys holding Access_ForceTransfer:
- mainnet (5):
pb10r29n9w7ja6qt7l3dzls32x7alvmc9j9l9q8x79xwusy0fjf372s348dzu, pb18r6ncean59nm9d2pql7ely74jxn7g7s4tqrldw, pb17eswnfkr0nl8p7jt52l2gpxpap7205zavkls07, pb1qt67v5kr6hdvwlwlenxvnjtzsm56j4msax696p, pb1lez9xkd8mw27xzzlyu0cjyqxxyapvcqq3fj3ld
- testnet (4):
tp10f4ga4mw9m7n8xnwvws4rchhygkm8088ew6275, tp1ma38wztmxlega6na60vwhqr8dkvzk63pte3r0f, tp17mptaudww2hnh3udlqeazpa7h3hvm5tygpysqn, tp17uu9hgey9epcjhr4dft6cfq4532mxr4sujpun0
allow_governance_control is also true on uylds.fcc, so a gov proposal could perform the transfer or grant the role to a new address.
Reproduce (REST marker route is disabled on public endpoints, use the RPC gRPC query):
curl -s 'https://rpc.provenance.io/abci_query?path=%22/provenance.marker.v1.Query/Marker%22&data=0x0a0975796c64732e666363'
# decode the MarkerAccount: field 10 = allow_forced_transfer, field 3 = access_control grants
How we see it
This looks like a known and accepted property of using a force-transfer-enabled marker: it hands a central entity (the issuer, or governance) the ability to claw funds back from any holder, including a vault, at any time. The question is not whether the code is wrong, but whether that trust assumption is acceptable for our vaults and depositors, and whether we want any guardrails.
To discuss / decide
- Is the issuer-clawback trust assumption acceptable for the assets we vault, and is it communicated to depositors?
- Inventory: which production-vaulted markers (underlying and payment) have
allow_forced_transfer = true, and who holds Access_ForceTransfer on each?
- Do we want any guardrail, e.g. refusing to onboard / flagging an asset whose marker allows forced transfer, monitoring for forced-transfer events against vault marker addresses, or accepting it as documented behavior?
- Record a decision (accept-as-designed / mitigate) with rationale.
Context
Surfacing a custody characteristic of vaulting restricted markers that have
allow_forced_transfer = true. When a vault holds such an asset as backing, the asset's issuer (or governance) retains the ability to forcibly pull that asset back out of the vault at any time, without the vault's or depositors' consent. This is inherent to the marker's force-transfer design, not a code defect in the vault, but it is a trust assumption worth making explicit and deciding on deliberately. Filing for discussion.The exposure
A vault holds depositor backing in its principal marker account (
vault.PrincipalMarkerAddress()). The marker module explicitly permits forced transfers out of marker accounts (x/marker/keeper/marker.gocanForceTransferFrom, lines 737-739), unlike module accounts and smart-contract accounts which it protects (sequence-0 accounts are blocked at line 746).So for any vaulted asset whose marker has
allow_forced_transfer = true, a holder ofAccess_ForceTransferon that marker can submitMsgTransferRequestwithfrom= the vault's principal marker and move the asset to themselves.TransferCoin(marker.go:647-698) requires the marker be active,allow_forced_transfer = true, and the signer holdAccess_ForceTransfer. No depositor signature is involved. The vault's share accounting (TotalShares/ NAV) is not updated, so the result is outstanding shares with reduced or missing backing.Confirmed on-chain (payment denom
uylds.fcc)uylds.fccis a restricted marker, status active,allow_forced_transfer = trueon both pio-mainnet-1 and pio-testnet-1. It has live keys holdingAccess_ForceTransfer:pb10r29n9w7ja6qt7l3dzls32x7alvmc9j9l9q8x79xwusy0fjf372s348dzu,pb18r6ncean59nm9d2pql7ely74jxn7g7s4tqrldw,pb17eswnfkr0nl8p7jt52l2gpxpap7205zavkls07,pb1qt67v5kr6hdvwlwlenxvnjtzsm56j4msax696p,pb1lez9xkd8mw27xzzlyu0cjyqxxyapvcqq3fj3ldtp10f4ga4mw9m7n8xnwvws4rchhygkm8088ew6275,tp1ma38wztmxlega6na60vwhqr8dkvzk63pte3r0f,tp17mptaudww2hnh3udlqeazpa7h3hvm5tygpysqn,tp17uu9hgey9epcjhr4dft6cfq4532mxr4sujpun0allow_governance_controlis also true onuylds.fcc, so a gov proposal could perform the transfer or grant the role to a new address.Reproduce (REST marker route is disabled on public endpoints, use the RPC gRPC query):
How we see it
This looks like a known and accepted property of using a force-transfer-enabled marker: it hands a central entity (the issuer, or governance) the ability to claw funds back from any holder, including a vault, at any time. The question is not whether the code is wrong, but whether that trust assumption is acceptable for our vaults and depositors, and whether we want any guardrails.
To discuss / decide
allow_forced_transfer = true, and who holdsAccess_ForceTransferon each?