# Ripple policy

Source: https://docs.settlemint.com/docs/deployment/ripple-policy
The properties DALP sets on every Ripple intent, how to write Ripple policies that match them, and the operation names that changed between DALP 3.1 and 3.2.

Ripple Custody is a custody provider. It decides who must approve each request that DALP files with it, in the custody approval step before it signs. Ripple calls that request an intent and decides with its policies. This page lists the properties DALP sets on every intent, so that a policy can match them. A deployment that upgrades from 3.1 also changes the policies that name an old operation, as [Operation names that changed from 3.1 to 3.2](#operation-names-that-changed-from-31-to-32) lists. A new 3.2 installation can skip that section.

A policy that matches nothing stops the intent. Ripple runs only the intents that a policy covers, so a policy that names an old value leaves the matching operations unable to run. Set your policies before you onboard investors, and test them with one write of each kind you use.

## How Ripple picks a policy [#how-ripple-picks-a-policy]

Ripple runs one policy for each intent. Among the policies that apply to the intent and whose condition matches, Ripple runs the one with the highest rank, or the older one at equal rank. An intent that no policy matches cannot run.

[Figure] Ripple runs the highest ranked policy that matches the properties of an intent. An intent from DALP carries a description and the properties dalpKind token.grant-role, dalpRoles admin and dalpChainId 11155111. Ripple checks three policies. The rank 40 policy on dalpRolesUndescribed does not match, because DALP did not set that property. The rank 30 policy on dalpRoles containing admin matches. The rank 10 policy on dalpKind token.grant-role also matches. Ripple runs the rank 30 policy, the highest rank that matches. An intent that matches no policy cannot run. Labels: Intent from DALP; A description for approvers; dalpKind: token.grant-role; dalpRoles: admin; dalpChainId: 11155111; dalpLane: order; Rank 40, no match; dalpRolesUndescribed is true; Rank 30, match; dalpRoles contains admin; Rank 10, match; dalpKind is token.grant-role; Rank 30 runs; The highest rank that; matches decides who; must approve; An intent that matches no policy cannot run.

How Ripple picks one policy for an intent from its properties and the rank of each matching policy.

DALP signs each intent that it files, so Ripple treats it as a user-signed proposal. A policy that omits the intent origin matches it. A policy for system-signed proposals only does not. Under Ripple's `CoerceDescendants` governing strategy, a policy in a parent domain overrides a policy in a child domain, whatever its rank.

Ripple matches a value exactly with `is`, or matches text inside it with `contains`, and combines conditions with `AND` and `OR`, as its [policy reference](https://docs.ripple.com/products/custody/governance/policies/reference) describes.

## What DALP sets on every intent [#what-dalp-sets-on-every-intent]

Each intent carries a description for the approver and the custom properties below. The description starts with the name and symbol of the contract the request calls, such as `Acme Bond (ACME): mint(...)`, when the request calls one contract that has a name. It names the action, the asset or contract, the amount, and the counterparty, and DALP keeps it to 250 characters. A call that also sends the chain's coin ends with that amount, such as `depositTo(account=0x…) with 0.04 native`. A list of addresses, numbers or true/false values shows its first three entries and how many more follow. A list of roles shows the role names, and any other list shows its count, such as `[2 items]`. A list that holds a number of more than 32 digits, such as a list of claim topic ids, shows its count. A country code passed as a number in its own argument shows as its two letters and its number, such as `SA (682)`. A country code inside a structure does not read this way: a token created from an instrument template passes its settings as one structure, so its description shows the name, symbol, type and decimals of the token and not its country. A description that runs past 250 characters is cut from the end of the call, so a call that lists addresses can lose its last arguments, such as the country list of a batch registration. DALP limits each property value to 100 characters. DALP writes the description and every property itself, and Ripple does not check them against the request, so a policy that matches them trusts DALP's account of the request.

| Property | What DALP sends |
| --- | --- |
| `dalpKind` | The DALP operation, for example `token.mint` or `paymaster.sponsorship`. The list under this table says which operation each kind of request sends. |
| `dalpRoles` | The roles that the request changes, separated by commas, for example `supplyManagement,custodian`. Each role is on the contract in `dalpContract`. |
| `dalpRolesUndescribed` | `true` when DALP cannot list every role the request changes. DALP leaves it out otherwise. |
| `dalpContract` | The address of the contract the request is for, in lowercase: the contract a transaction calls, or the contract that checks a signature. On a user operation, it appears only with `dalpRoles`, so a policy on `dalpContract` matches a user operation only when the operation changes roles. |
| `dalpChainId` | The network, as a decimal chain ID, for example `11155111`. |
| `dalpLane` | `order` when Ripple signs and sends a transaction, `manifest` when Ripple signs a payload and returns the signature, `account` when Ripple creates or updates an account, `display` when Ripple files a ticker or a named endpoint. |
| `dalpSignature` | On a `manifest`, what Ripple signs: `typed-data`, `message`, `userop` for a user operation, or `raw` for a transaction that DALP sends. |
| `dalpEip712Domain` | On typed data and user operations, the name of the signing domain and its version, for example `DALPIdentityFactory v3`. |
| `dalpEip712PrimaryType` | On typed data and user operations, the type of the message that Ripple signs, for example `CreateIdentityWithManagementKeys`. |
| `dalpEip712Verifier` | On typed data and user operations, the contract that checks the signature, in lowercase. |
| `dalpEip712Message<Field>` | On typed data and user operations, one property for each field of the message that has a value, for example `dalpEip712MessageWallet`. An address is in lowercase and a number is in decimal. A list is separated by commas and cut like `dalpRoles`. A field that holds another structure is left out. |
| `dalpOrganizationId` | The DALP organisation that filed the intent. |
| `dalpExternalId` | DALP's ID for the request, to trace an intent back to DALP. |

The operation in `dalpKind` depends on the request:

- A data feed, settlement, token sale or trading venue transaction sends its own operation, for example `feeds.submit`, `xvp.execute`, `token-sale.buy` or `tradingvenue.halt-market`.
- A custody withdrawal payout sends `custody-ops.withdrawal-payout`.
- When a settlement or token sale write first sends an ERC20 allowance, that transaction carries the same kind as the write, for example `token-sale.buy`, and `dalpContract` names the token.
- A transaction that names no operation sends `txqueue.enqueue`, the generic name of DALP's transaction queue.
- A smart account runs its operations as user operations. When Ripple holds the owner key of a smart account, the user operation that Ripple signs sends the operation that it runs, for example `token.mint`, so a policy on `dalpKind` `is` `token.mint` matches that mint too. DALP sends `userOp` only for a user operation that carries no operation name.
- On a multisig wallet, a smart account that needs signatures from more than one member, the signature of the member who starts the operation also sends the operation, and each approval sends `user-operation.approve`.
- When Ripple holds the key that submits user operations to the chain, that transaction sends `aa.bundle`.
- With named endpoints on, the request that files a ticker sends `custody.display.ticker`, and the request that files an endpoint sends `custody.display.endpoint`.

DALP 3.1 sent every property in this table except `dalpRolesUndescribed` and the `dalpEip712` properties. The `dalpKind` values changed, as the last section lists.

## Name contract destinations [#name-contract-destinations]

`RIPPLE_NAMED_ENDPOINTS`, the config key `signer.ripple.namedEndpoints`, is off by default. While it is off, DALP makes no ticker, endpoint or ledger call, every order goes to the bare address it calls, and the intent description names the contract. The chart has no value for it. Set it in the `signer.ripple` config override that a Ripple signer needs.

With it on, DALP files a Ripple ticker for each token it creates and a named endpoint for each organisation smart wallet, system contract and token. An order to a contract goes to its endpoint once Ripple approves the endpoint and leaves it unlocked. Any other order keeps the bare address. The setting also sends text messages as `EIP-191` content, so an approver reads the message instead of prefixed bytes.

A policy written for orders to an address can stop matching an order to an endpoint, and the same holds for a message of the `EIP-191` type. Check both before you turn the setting on. Only tokens and contracts deployed after that get an endpoint, unless an administrator adds older ones by hand.

## Policies for role changes [#policies-for-role-changes]

A role change is the request most worth a stricter workflow. DALP marks each role change so that a policy can find it.

When the role list is longer than 100 characters, DALP sends the leading role names that fit, adds `+N` for the N roles it left out, and sets `dalpRolesUndescribed` to `true`. A grant of twelve roles on the platform's access manager contract, for example, can send `admin,auditor,systemManager,tokenManager,complianceManager,claimPolicyManager,claimIssuer,+5`.

DALP also sets `dalpRolesUndescribed` to `true`, with no `dalpRoles`, when it cannot name every role, or when the roles are not all on one contract:

- A role ID that DALP does not know.
- A batch that changes roles and also calls another contract.
- A call that passes a role change on to another contract to run.
- A call that carries other calls inside it and that DALP cannot unpack, even when it changes no role.

DALP reads roles only from calls to functions that it knows. A call to a function that DALP does not know adds no name to `dalpRoles` and does not set `dalpRolesUndescribed`, even when it changes a role, so that intent gets the policy that its other properties match.

Add a `dalpRolesUndescribed` policy and rank it above your role policies, because Ripple runs only one policy for each intent.

1. Add one policy that matches `dalpRolesUndescribed` `is` `true`. Apply it to every intent type, intent origin and domain that your role policies apply to, and give it your strictest workflow for role changes.
2. Give it a higher rank than every policy that a role change can also match.
3. Keep it in the domain of your role policies or in a parent domain, because of the `CoerceDescendants` rule above.

A request with `dalpRolesUndescribed` set then gets that workflow, never a looser one that matched only the names that fit. A role change through a function DALP does not know sets neither property, so this policy does not catch it.

Role changes reach Ripple as transaction orders and as manifests, so apply each role policy to both intent types. To match a single role, use `contains`, because `is` on `dalpRoles` matches only the complete list. Before you use `contains`, check that no longer role name includes the same text.

One request can change more than one role, and Ripple still runs only one policy. Rank your role policies by how strict their workflows are, with the strictest highest, and above any other policy that a role change can also match, such as a policy on `dalpKind`. A grant of `auditor` and `admin` together then gets the stricter `admin` workflow. Never give two role policies with different workflows the same rank, because at equal rank Ripple runs the older policy.

## Keep the paymaster and bundler wallets approval-free [#keep-the-paymaster-and-bundler-wallets-approval-free]

Two DALP wallets sign for every user, not for one investor. The paymaster wallet signs a sponsorship ticket, with `dalpKind` `paymaster.sponsorship`, for each operation that DALP pays gas for. The wallet that submits user operations to the chain sends `aa.bundle`. A policy that holds either intent for approval stops every sponsored operation on that network, not one.

Give these two intents a policy whose workflow needs no approver, and rank it ahead of every approval policy that they also match. When a policy holds a sponsorship ticket, DALP refuses the operation with "The paymaster wallet's Ripple policy holds sponsorship ticket signing", and names the Ripple intent. For each hold on either wallet, DALP logs one warning and adds one to the `dalp_execution_infrastructure_vault_holds_total` counter.

## Operation names that changed from 3.1 to 3.2 [#operation-names-that-changed-from-31-to-32]

A policy whose condition names a 3.1 value stops matching after the upgrade. Its intents then go to the next policy that matches, or cannot run when no policy matches. Until every deployment you run is on 3.2, match both values with an OR condition, so that an intent from either version finds its policy.

DALP 3.1 sent `contract.` and the contract function name when a workflow wrote to a contract directly, for example during organisation setup, invitations, token creation and system migration. Writes that came through the API sent the operation name of their route, and many of those names changed too. 3.2 sends an operation name for both. The tables pair each 3.1 value with its 3.2 name. The values `token.mint`, `token.burn`, `token.pause`, `token.unpause` and `xvp.execute` kept their 3.1 names. For any other value that the tables do not list, run the operation on a 3.2 test deployment and match the value that Ripple shows.

### Direct contract writes [#direct-contract-writes]

| 3.1 value | 3.2 value |
| --- | --- |
| `contract.createSystem` | `system.create-system` |
| `contract.bootstrap` | `system.bootstrap` |
| `contract.grantRole`, `contract.batchGrantRole`, `contract.grantMultipleRoles` on the system access manager | `system.grant-role` |
| `contract.revokeRole`, `contract.batchRevokeRole`, `contract.revokeMultipleRoles` on the system access manager | `system.revoke-role` |
| The same three grant values on a token's access manager | `token.grant-role` |
| `contract.registerTopicScheme`, `contract.updateTopicScheme`, `contract.removeTopicScheme` | `system.topic-create`, `system.topic-update`, `system.topic-delete` |
| `contract.addTrustedIssuer`, `contract.updateIssuerClaimTopics`, `contract.removeTrustedIssuer` | `system.trusted-issuer-create`, `system.trusted-issuer-update`, `system.trusted-issuer-delete` |
| `contract.installScopedModule`, `contract.uninstallModule` on the system compliance module | `system.compliance-module-configure` |
| `contract.registerComplianceModule` | `system.compliance-module-create` |
| `contract.registerSystemAddon` | `system.addon-factory-create` |
| `contract.setAddonImplementation`, `contract.updateImplementation` on an add-on | `system.addon-set-implementation`, `system.addon-factory-update-implementation` |
| `contract.registerIdentity`, `contract.deleteIdentity` | `system.identity-register`, `system.identity-delete` |
| `contract.registerPendingIdentity` | `system.identity-register-pending` |
| `contract.registerClaimAuthorizationContract` | `system.identity-register-claim-authorization` |
| `contract.createIdentityWithManagementKeyAuthorization` | `system.identity-create.create` |
| `contract.addKey` when an identity is created | `system.identity-create.mgmt-key-` and the wallet address in lowercase |
| `contract.addKey` when an organisation key is added | `system.identity-add-key` |
| `contract.addKey` for a claim issuer key | `system.identity-add-management-key` or `system.identity-add-action-key` |
| `contract.upgradeToAndCall`, `contract.migrate` | `system.upgrade-proxy`, `system.migrate` |
| `contract.setImplementation` on the system or its compliance module registry | `system.set-implementation`, `system.compliance-module-set-implementation` |
| `contract.registerTokenFactory` | `system.factory-create` |
| `contract.setTokenFactoryImplementation`, `contract.updateTokenImplementation` | `system.factory-set-implementation`, `system.factory-update-token-implementation` |
| `contract.remapKey` on the token factory registry | `system.factory-remap-key` |
| `contract.registerFeed` | `feeds.register` |
| `contract.batchCreate` for exchange rate feeds | `feeds.exchange-rate.create` |

### Token operations [#token-operations]

| 3.1 value | 3.2 value |
| --- | --- |
| `token.grantRole`, `token.revokeRole` | `token.grant-role`, `token.revoke-role` |
| `token.installComplianceModule`, `token.uninstallComplianceModule` | `token.install-compliance-module`, `token.uninstall-compliance-module` |
| `token.installScopedComplianceModule` | `token.install-scoped-compliance-module` |
| `token.configureComplianceModule`, `token.configureScopedComplianceModule` | `token.configure-compliance-module`, `token.configure-scoped-compliance-module` |
| `token.setModuleScope` | `token.set-module-scope` |
| `token.setMetadata`, `token.removeMetadata` | `token.set-metadata`, `token.remove-metadata` |
| `token.setYieldSchedule` | `token.set-yield-schedule` |
| `token.freezePartialTokens`, `token.unfreezePartialTokens` | `token.freeze-partial`, `token.unfreeze-partial` |
| `token.recoverTokens`, `token.recoverERC20`, `token.forcedRecoverTokens` | `token.recover-tokens`, `token.recover-erc20`, `token.forced-recover` |
| `token.forcedTransfer` from the API | `token.forced-transfer`. A batch of forced transfers keeps `token.forcedTransfer`. |
| `token.transfer-approval.approve`, `token.transfer-approval.revoke` | `token.transfer-approval-approve`, `token.transfer-approval-revoke` |
| `token.convertYield` | `token.convert-yield:` and the batch |
| `token.feature` and the feature name, then `Create`, for example `token.featureAumFeeCreate` | `token.feature-create` |
| `token.feature` and the feature name, then `Attach`, for example `token.featureAumFeeAttach` | `token.feature-attach`. When the feature contract already exists, `token.feature.attach`. |
| `token.featureDetach`, `token.featurePermitDetach` | `token.feature-detach` |
| `token.featureRotateCreate`, `token.featureRotateAttach` | `token.feature-rotate-create`, `token.feature-rotate-attach` |
| `addon.fixedYieldSchedule.create`, `.topUp`, `.withdraw`, `.claim` | `features.fixed-yield.create`, `features.fixed-yield.top-up`, `features.fixed-yield.withdraw`, `features.fixed-yield.claim` |
| `token.create.role`, the role grant that token creation files | `token.grant-role` |
| `claim.issue` and `claim.revoke` from the API | `system.claim-issue` or `token.claim-issue`, and `system.claim-revoke` or `token.claim-revoke` |

The pairs above are the values the API sends when you attach, replace or detach a feature on an existing token. A token that you create with its features, and a feature that a creation flow replaces, are filed under `token.feature.` and the feature ID, then `.create` or `.attach`. A replacement in a creation flow keeps `token.featureRotateCreate` for the create step and sends `token.feature-rotate-attach` for the attach step.

### System, identity and wallet operations [#system-identity-and-wallet-operations]

| 3.1 value | 3.2 value |
| --- | --- |
| `system.factory.create` | `system.factory-create` |
| `system.grant-role-by-participant`, `system.revoke-role-by-participant` | `system.grant-role`, `system.revoke-role` |
| `compliance.provider-webhook-reactivate` | `system.trusted-issuer-add-claim-topic` |
| `compliance.provider-webhook-revoke` | `system.trusted-issuer-remove-claim-topic` |
| `system.identity-create.mgmt-key` | `system.identity-create.mgmt-key-` and the wallet address in lowercase |
| `system.identity-update-country:` and the wallet | `system.identity-update-country` |
| `identity-recovery.identity-create` | `system.identity-create.create` |
| `identity-recovery.recover-identity` | `identity.recover` |
| `invitation.identity-attach.register-pending` | `system.identity-register` |
| `invitation.identity-attach.smart-wallet-mgmt-key` | `system.identity-add-management-key` |
| `invitation.identity-attach.register-claim-auth` | `system.identity-register-claim-authorization` |
| `claim.issue.add-claim` | `identity.claim-add` |
| `smart-wallet.create`, `smart-wallet.create.multisig`, when they named the account creation order | `custody.smart-wallet-create`, `custody.smart-wallet-create.multisig` |
| `smart-wallet.identity.add-key` | `custody.smart-wallet-create.add-key` |
| `smart-wallet.authorise`, `identity-recovery.authorise` | `smart-wallet.create`. 3.1 used this string for the account creation order, so a policy on `smart-wallet.create` that you wrote for 3.1 now matches a different intent. |
| `system.identity-create.register-pending`, `system.identity-create.register-controller` | `system.identity-register-pending.wallet:` and the wallet address in lowercase |
| `system.identity-create.batch-register-pending` | `system.identity-register-pending.batch:` and the wallet addresses in lowercase, joined with hyphens |
| `system.identity-create.claim-auth` | `system.identity-create.claim-auth` when an identity is created, and `system.identity-register-pending.claim-auth` when a pending identity is registered. A policy on the 3.1 value no longer matches the second. |
| `identity-recovery.register-replacements` | `identity.register` for a wallet with no identity, and `identity.recover` for the others |
| No value, for a user operation that a smart account runs | The operation that it runs, for example `token.mint`. A policy on `token.mint` now also matches that operation when a smart account runs it. |
| `smart-wallet.install-module`, `smart-wallet.uninstall-module`, `smart-wallet.set-threshold` | `custody.wallet-mutation` |
| `sponsorship.ticket` | `paymaster.sponsorship` |
| `paymaster.set-signer.` and the paymaster address | `paymaster.set-signer` |
| `refundSplitter.recoverNative` | `system.refund-splitter-recover-native` |
| `xvp.approve.erc20.` or `xvp.approve.settlement.` and the addresses | `xvp.approve` |
| `feeds.feed-submit`, `feeds.issuer-signed-create` | `feeds.submit`, `feeds.issuer-signed.create` |

### Operations that 3.2 no longer sends [#operations-that-32-no-longer-sends]

These 3.1 values have no 3.2 successor. Remove a policy condition that names only one of them.

- `paymaster.set-signer-rollback.` and the paymaster address
- `refundSplitter.recoverErc20`, `refundSplitter.setSponsorship`
- `token.delegateBySig`
- `token-sale.add-payment-currency`, `token-sale.remove-payment-currency`, `token-sale.configure-vesting`, `token-sale.set-purchase-limits`, `token-sale.set-soft-cap`
- `system.identity-delete.paired.` and the wallet, replaced by `system.identity-delete`

## Check your policies before you upgrade [#check-your-policies-before-you-upgrade]

1. Run the operations that your policies cover on a 3.2 test deployment.
2. Compare the `dalpKind` that Ripple shows for each intent with the condition of the policy you expect to run.
3. Check each policy against the property table above, and add the `dalpRolesUndescribed` policy.

## Troubleshooting [#troubleshooting]

| What you see | Cause | Fix |
| --- | --- | --- |
| An intent cannot run. | No policy matches the intent, often because a condition still names a 3.1 `dalpKind`. | Match the 3.2 value, or both values with OR. Then submit the operation again. |
| A role grant gets a looser workflow than you set. | A policy on `dalpKind` or `dalpRoles` has a higher rank than the `dalpRolesUndescribed` policy. | Rank the `dalpRolesUndescribed` policy ahead of every policy that a role change can match. |
| A sponsored operation fails with "The paymaster wallet's Ripple policy holds sponsorship ticket signing". | A policy with an approval workflow matches `paymaster.sponsorship` intents. | Rank a policy without approvers for that intent ahead of it, then submit the operation again. |
| A request stays in signing, and its error message says that the account has an amount in quarantine. | Ripple quarantines funds that it receives from an outside address, a minted token included, and an order waits until its funds are released. The message names each quarantined token or gas amount. | Release the quarantine in the Ripple console. DALP never releases a quarantine. |
| An intent stays open in Ripple after DALP stops waiting, and the Console shows the request as "May still go through". | Ripple has no call that withdraws a held intent, so DALP does not cancel one. DALP stops waiting after its poll limit and reports the intent as open. | Let the intent run or stop it in Ripple. Do not submit the operation again while the intent is open. |
| A request stays in signing, and its error message says that Ripple has not prepared or broadcast an order. | Ripple holds the order. After ten minutes without a broadcast, DALP names the order on the request, says how long ago it filed the order, and keeps waiting. | Find the order by its id in Ripple, and let it run or stop it there. |

## Ripple sources [#ripple-sources]

- [Policy reference](https://docs.ripple.com/products/custody/governance/policies/reference): the conditions a policy can use, and how Ripple ranks and picks a policy.
