Skip to main content

Transfer rules

Transfer rules are needed in order for Transfer to be permitted. A Transfer without a matching allow TransferRule will always be rejected.

Setup

We will assume a fresh treasury instance or demo for these examples.

treasury demo start --port 8777 --pull

We're also create some addresses and put them into accounts.

treasury script
sol1 = create internal address for SOL
sol2 = create internal address for SOL

btc1 = create internal address for BTC
btc2 = create internal address for BTC

SOL = asset SOL for SOL
BTC = asset BTC for BTC

account1 = create internal account { addresses = [$sol1, $btc1] }
account2 = create internal account { addresses = [$sol2, $btc2] }

Examples

No matching rule

This is the default behavior if there's no matching rule. Default deny.

create transfer {from = $sol1, to = $sol2, asset = $SOL, amount = "1"}

Basic allow rule

We can permit it using an allow rule.

create allow transfer-rule basic {from = $sol1, to = $sol2, asset = $SOL}

# now you can transfer.
create transfer {from = $sol1, to = $sol2, asset = $SOL, amount = "1"}

# but not bitcoin!
create transfer {from = $btc1, to = $btc2, asset = $BTC, amount = "1"}

# delete rule
_, err = delete transfer-rule basic

We can also make the rule based on matching the account(s) of the addresses.

# make rule based on accounts
create allow transfer-rule basic-account {from = $account1, to = $account2, asset = $SOL}

# transfer again
create transfer {from = $sol1, to = $sol2, asset = $SOL, amount = "1"}

# delete rule
_, err = delete transfer-rule basic-account

Wildcards

You can make patterns to match anything. This will allow sending any asset to $account2 from anywhere.

create allow transfer-rule wildcard-to-account {from = "any/address", to = $account2, asset = "any/asset"}
# now we can transfer both solana and btc
create transfer {from = $sol1, to = $sol2, asset = $SOL, amount = "1"}
create transfer {from = $btc1, to = $btc2, asset = $BTC, amount = "1"}

_, err = delete transfer-rule wildcard-to-account

Deny rule

If a deny rule matches, the outcome will always be rejected. We can build on our wildcard rule to deny sending BTC.

create allow transfer-rule wildcard-to-account {from = "any/address", to = $account2, asset = "any/asset"}
create deny transfer-rule deny-btc-to-account {from = "any/address", to = $account2, asset = $BTC}

# this will be denied
create transfer {from = $btc1, to = $btc2, asset = $BTC, amount = "1"}

_, err = delete transfer-rule wildcard-to-account
_, err = delete transfer-rule deny-btc-to-account

Approvals

Additional conditions about who is allowed to initiate a Transfer (and how many approvers) can be attached, much like an AccessRule.

To permit admin role to initiate or approve:

create allow transfer-rule initiate {from = $sol1, to = $sol2, asset = $SOL, initiate = "roles/admin", approvals = 1}
_, err = delete transfer-rule initiate

Users with the admin role can now make matching SOL transfers from $sol1 to $sol2 (but nothing else yet).

Global requirements

Attaching access conditions to all individual transfer rules can be tedious. You can define global requirements by using the require variant.

If any require variant rules match, they must all evaluate as true or the transfer will be rejected or remain pending.

To require all transfers out of $acc1 to require to be initiated and approved from roles/admin users:

# create an allow(s) rule permitting the movement of assets out of `$acc1`
create allow transfer-rule allowSol {from = $account1, to = $account2, asset = $SOL}
create allow transfer-rule allowBtc {from = $account1, to = $btc2, asset = $BTC}

# require all transfers out of `$account1` to need approval.
create require transfer-rule require-account1-approvals {from = $account1, to = "any/address", asset = "any/asset", initiate = "roles/admin", approvals = 1}

# now transfers are rejected by the root user.
create transfer {from = $btc1, to = $btc2, asset = $BTC, amount = "1"}

_, err = delete transfer-rule require-account1-approvals

We can add the admin role to our root user account to be able to initiate.

create role admin
update user root {roles = ["roles/admin"]}

# Now we can start a transfer operation. It will wait until another admin user approves.
create transfer {from = $sol1, to = $sol2, asset = $SOL, amount = "1"}

Require two different roles to approve

You may want two different roles to sign off on some matching set of transfers. Perhaps you want two different geographies signing off on something.

Here we'll create two require rules, that each require a trader-us and trader-eu role approving.

create require transfer-rule require-us {from = $account1, to = "any/address", asset = "any/asset", initiate = ["roles/trader-us", "roles/trader-eu"], approve = "roles/trader-us", approvals = 1}

create require transfer-rule require-eu {from = $account1, to = "any/address", asset = "any/asset", initiate = ["roles/trader-us", "roles/trader-eu"], approve = "roles/trader-eu", approvals = 1}

In order for any transfer to approve, it will need approvals from both the trader-us adn trader-eu roles.

Notional limits

Transfers can be rejected based on their notional (USD) amount, or the total amount transferred in a given time period.

Setup

info

Note in order for notional limits to work, you should connect your treasury instance or demo to our API to get pricing information on assets.

If there is no price for an asset, then any matching rule with a notional limit will reject (default deny).

If you're using a demo, you can restart it and pass your API key.

treasury demo stop
treasury demo start --port 8777 --pull --api-key ${TREASURY_API_KEY}

Alternatively, you can manually set prices for assets.

treasury assets price chains/BTC/assets/BTC --amount 70000 --valid-now
treasury assets price chains/SOL/assets/SOL --amount 150 --valid-now

Check prices are set on assets.

treasury assets ls

Limit by amount

Limit any matching transfer to not be more than $300 USD.

create allow transfer-rule limit-amount {from = $account1, to = $account2, amount = {at_most = "300", quote = "USD"}, asset = $SOL}

# transfering 1 SOL (~$150) works
create transfer {from = $sol1, to = $sol2, asset = $SOL, amount = "1"}

# transfering 3 SOL (~$450) will be rejected
create transfer {from = $sol1, to = $sol2, asset = $SOL, amount = "3"}

_, err = delete transfer-rule limit-amount

Rate limit by amount

Limit any matching transfer(s) to not exceed more than $1000 USD over a 1 day period.

create allow transfer-rule rate-limit {from = $account1, to = $account2, amount = {within = "1d", at_most = "1000", quote = "USD"}, asset = $SOL}

# transfering 5 SOL (~$750) works, unless we transferred >$1000 SOL before
create transfer {from = $sol1, to = $sol2, asset = $SOL, amount = "5"}

# transferring 5 SOL (~$750) does not work, since we exceed the daily limit
create transfer {from = $sol1, to = $sol2, asset = $SOL, amount = "1"}

_, err = delete transfer-rule rate-limit

Thresholding

We can make transfer rules apply after a certain dollar threshold is reached.

Permit transfers under $1k daily limit to $account2, but require approvals for when the limit is exceeded.

create allow transfer-rule permit-1k {from = $account1, to = $account2, amount = {within = "1d", at_most = "1000", quote = "USD"}, asset = "any/asset"}
create allow transfer-rule approval-1k {from = $account1, to = $account2, amount = {within = "1d", at_least = "1000", quote = "USD"}, asset = "any/asset", initiate = "roles/admin", approvals = 1}

Global thresholds

We can use notional limits in require rules as well.

This will require all transfers over $10k USD to require an approval from an roles/admin user.

create require transfer-rule global-approval-1k {from = "any/address", to = "any/address", amount = {within = "1d", at_least = "10000", quote = "USD"}, asset = "any/asset", initiate = "roles/admin", approvals = 1}