> For the complete documentation index, see [llms.txt](https://docs.t-rex.network/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.t-rex.network/guides/issuer/26.-authorize-spenders.md).

# 26. Authorize spenders

A spender is a third party that executes a transfer on a holder's behalf: a venue, a custodian, a settlement contract. By default, any spender a holder approves can call `transferFrom`. The Issuer restricts this with a spender policy, a compliance module that decides which spenders the asset accepts. This flow sets that policy and maintains the list of authorized operators. The Compliance Owner runs it when the asset first connects to a venue, and each time an operator is added or withdrawn.

## At a glance

|                   |                                                                                                                                                                                                                                                                                                                                |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Triggered by**  | Compliance Owner                                                                                                                                                                                                                                                                                                               |
| **Actors**        | Compliance Owner, Issuer, Venue or application, Investor, Platform                                                                                                                                                                                                                                                             |
| **Contracts**     | `ModularCompliance`, `SpenderWhitelistModule`, `SpenderVerificationModule`, `TREXRegistry` (read only, during verification)                                                                                                                                                                                                    |
| **Preconditions** | The token suite is deployed (see [01](/guides/issuer/01.-deploy-a-token-suite.md)); the caller holds `OWNER` in the asset's domain (see [25](/guides/issuer/25.-commission-a-suite-and-grant-roles.md)); the spender modules are deployed; the Issuer knows the address of each operator it authorizes, and the chain it is on |
| **Result**        | The asset has a spender policy, and `transferFrom` succeeds only for spenders the policy accepts                                                                                                                                                                                                                               |

## Actors and roles

| Actor                | Role in this flow                                                                                                      | What they hold                                                                                                                         |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Compliance Owner     | Binds the spender module and maintains the list.                                                                       | `OWNER` on the `ModularCompliance`                                                                                                     |
| Issuer               | Decides which policy the asset applies and which operators it accepts. Usually the same party as the Compliance Owner. | Its agreements with venues and operators                                                                                               |
| Venue or application | Becomes an authorized spender. Does not transact in this flow.                                                         | An address on the reference chain or on a satellite. For the verification policy, an ONCHAINID that satisfies the asset's requirements |
| Investor             | Still approves each spender itself. Does not transact in this flow.                                                    | A position                                                                                                                             |
| Platform             | Encodes addresses and calls, runs the reads and submits with the Compliance Owner's key.                               | The Compliance Owner's signing key, or a key authorized for it                                                                         |

## Sequence

```mermaid
sequenceDiagram
    participant Platform
    participant Owner as Compliance Owner
    participant MC as ModularCompliance
    participant WL as SpenderWhitelistModule

    Platform->>MC: getModulesByType(SPENDER) (read)
    Platform->>Owner: policy chosen, submit
    Owner->>MC: addAndSetModule(spenderListModule, [allowSpender(spender), ...])
    MC->>MC: caller holds OWNER, module can be bound
    MC->>WL: bindCompliance(MC)
    MC->>WL: allowSpender(spender) for each first entry
    WL-->>Platform: SpenderAllowed(MC, key, spender)
    opt an operator is added later
        Owner->>MC: callModuleFunction(allowSpender(spender), spenderListModule)
        MC->>WL: allowSpender(spender)
    end
    opt an operator is withdrawn
        Owner->>MC: callModuleFunction(disallowSpender(spender), spenderListModule)
        MC->>WL: disallowSpender(spender)
        WL-->>Platform: SpenderDisallowed(MC, key, spender)
    end
    Platform->>MC: canSpenderCall(spender, from, to, amount) (read)
```

## Step by step

### Step 1. Choose the policy

**Who:** Issuer, with the Platform

**Call:** Off-chain, using read calls

**Before the call:** The Issuer knows which operators move tokens for holders.

**What happens:** The Platform reads `ModularCompliance.getModulesByType(SPENDER)` to see the current policy. The Issuer chooses one.

| Policy                                 | Module to bind              | A spender is accepted when                                                               |
| -------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
| Any spender a holder approves          | None                        | Always                                                                                   |
| Eligible spenders only                 | `SpenderVerificationModule` | The spender's address resolves to an identity that satisfies the asset's required claims |
| Named operators only                   | `SpenderWhitelistModule`    | The spender is on the Issuer's list                                                      |
| Named operators that are also eligible | Both                        | Both conditions hold                                                                     |

For the list, the Platform encodes each operator as an ERC-7930 interoperable address in canonical form: the chain it is on and its address. One list covers operators on the reference chain and on satellites.

**What changes:** Nothing on-chain.

### Step 2. Bind the policy

**Who:** Compliance Owner (`OWNER`)

**Call:** For a list: `ModularCompliance.addAndSetModule(spenderListModule, interactions)`, where each interaction is an encoded `allowSpender(spender)`. For the verification policy: `ModularCompliance.addModule(verificationModule)`.

**Before the call:** Step 1 is done. For a list, the first operators are known.

**What the contract checks:**

1. The caller holds `OWNER` in the asset's domain. Otherwise the call reverts with `AccessManagedUnauthorized`.
2. The module can be bound: at most five interactions, fewer than 25 modules, not already bound. Otherwise `ArraySizeLimited`, `MaxModulesReached` or `ModuleAlreadyBound`. See [11](/guides/issuer/11.-add-a-compliance-rule.md) for every check of a binding.
3. For each `allowSpender`: the address is a canonical interoperable address and is not already listed. Otherwise the call reverts on the encoding, or with `SpenderAlreadyAllowed`.

**What changes:** The module is bound and recorded as a `SPENDER` module. The list holds its first entries. The module emits `SpenderAllowed(compliance, key, spender)` for each. From the next `transferFrom`, the spender is checked.

### Step 3. Add an operator

**Who:** Compliance Owner (`OWNER`)

**Call:** `ModularCompliance.callModuleFunction(callData, spenderListModule)`, where `callData` is the encoded `allowSpender(spender)`

**Before the call:** The Issuer has agreed to accept the operator.

**What the contract checks:**

1. The caller holds `OWNER`. Otherwise `AccessManagedUnauthorized`.
2. The module is bound. Otherwise `ModuleNotBound`.
3. The address is canonical and not already listed. Otherwise the call reverts on the encoding, or with `SpenderAlreadyAllowed`.

**What changes:** The operator is on the list. The module emits `SpenderAllowed`. The compliance emits `ModuleInteraction`.

### Step 4. Withdraw an operator

**Who:** Compliance Owner (`OWNER`)

**Call:** `ModularCompliance.callModuleFunction(callData, spenderListModule)`, where `callData` is the encoded `disallowSpender(spender)`

**Before the call:** The Issuer has decided to stop accepting the operator.

**What the contract checks:**

1. The caller holds `OWNER`. Otherwise `AccessManagedUnauthorized`.
2. The module is bound. Otherwise `ModuleNotBound`.
3. The operator is on the list. Otherwise `SpenderNotListed`.

**What changes:** The operator leaves the list. The module emits `SpenderDisallowed`. Its next `transferFrom` reverts. Allowances that holders gave it remain and are of no use to it.

### Step 5. Verify

**Who:** Platform

**Call:** Off-chain, using read calls

**Before the call:** The previous steps are confirmed.

**What happens:** The Platform reads `ModularCompliance.canSpenderCall(spender, from, to, amount)` for an authorized operator and for an address that is not authorized. For a list, it also reads `SpenderWhitelistModule.isSpenderAllowed(compliance, spender)`.

**What changes:** Nothing on-chain.

## State after the flow

* `ModularCompliance.getModulesByType(SPENDER)` returns the bound spender modules.
* `SpenderWhitelistModule.isSpenderAllowed(compliance, spender)` is true for each listed operator.
* `ModularCompliance.canSpenderCall(spender, from, to, amount)` is true only for a spender every bound spender module accepts.
* `Token.transferFrom` reverts with `SpenderNotAllowed` for any other caller, before the allowance is touched.
* `Token.transfer` and `Token.identityTransfer` are unaffected. They have no spender.
* No allowance has changed. Holders still approve their spenders themselves.

## Failure modes

| Symptom                                                             | Cause                                                                                                               | Fix                                                                                                                |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Reverts with `AccessManagedUnauthorized`                            | The caller does not hold `OWNER` in the asset's domain (check 1)                                                    | Grant the role: [25](/guides/issuer/25.-commission-a-suite-and-grant-roles.md)                                     |
| Reverts with `SpenderAlreadyAllowed`                                | The operator is already on the list (steps 2 and 3, check 3)                                                        | Nothing to do                                                                                                      |
| Reverts with `SpenderNotListed`                                     | The operator is not on the list (step 4, check 3)                                                                   | Read `isSpenderAllowed` first                                                                                      |
| `allowSpender` reverts on the address                               | The interoperable address is malformed or not in canonical form                                                     | Encode the chain and the address in the canonical ERC-7930 form                                                    |
| Reverts with `OnlyBoundComplianceCanCall`                           | `allowSpender` was called on the module directly                                                                    | Call it through `callModuleFunction`                                                                               |
| Every `transferFrom` reverts with `SpenderNotAllowed` after binding | The list module was bound with no entry                                                                             | List the operators. Bind with `addAndSetModule` next time                                                          |
| A listed venue is still refused                                     | Both spender modules are bound and the venue's address is not eligible, or the venue was listed under another chain | Read `canSpenderCall`. Give the venue an identity with the required claims, or list the address of the right chain |
| A venue is accepted and its transfers still revert                  | The spender is accepted, and the transfer itself fails a gate: recipient not eligible, a rule, a freeze, the pause  | Read `isVerified(to)` and `canTransfer(from, to, amount)`                                                          |
| A cross-chain validation is refused with `ValidationSpenderRefused` | The validation names a spender the policy does not accept                                                           | Authorize the operator, with the address it has on that satellite                                                  |

## Common misunderstandings

> **Wrong:** "Authorizing a spender lets it move holders' tokens."
>
> **Right:** It makes the spender acceptable to the asset. The spender still needs an allowance from each holder, given with `approve` or `permit`. The policy and the allowance are two separate conditions, and both must hold.

> **Wrong:** "A whitelisted spender bypasses compliance."
>
> **Right:** The spender check comes first, and the transfer then passes the same gates as any other: eligibility of the recipient, the rules, freezes, the pause. Accepting a spender exempts nothing.

> **Wrong:** "Without a spender module, nobody can use `transferFrom`."
>
> **Right:** Without a spender module, every spender is accepted, as with any ERC-20. A policy exists only once a `SPENDER` module is bound.

> **Wrong:** "The list is a list of eligible investors."
>
> **Right:** It is a list of operators. Eligibility of holders is configured in the `TREXRegistry`, with claim topics and trusted issuers, and has nothing to do with this list.

## Integration notes

* Bind the list module with its first entries in one call. Bound empty, it blocks every `transferFrom`.
* A venue that operates on a satellite is listed with the address it has on that chain. The same contract on two chains is two entries.
* A venue contract takes part under its own ONCHAINID, of type `SMART_CONTRACT`, with the claims the asset requires. A venue that only calls `transferFrom` needs to be an accepted spender. A venue that holds tokens in escrow also needs to be an eligible holder, which the spender policy does not decide: see [Identity-based control and spender compliance](/token-and-register/identity-based-control-and-spender-compliance.md).
* Read `canSpenderCall(spender, from, to, amount)` before submitting a `transferFrom`. It is the check the token runs.
* Index `SpenderAllowed` and `SpenderDisallowed` on the module, filtered by compliance, to keep the list off-chain. The list cannot be enumerated on-chain.
* The list belongs to the binding. If the module is removed and bound again, the list is empty.
* Withdrawing an operator does not cancel validations already issued that name it. They expire on their own, or the keeper discards them.

## Related flows

* [11 Add a compliance rule](/guides/issuer/11.-add-a-compliance-rule.md) : every check of a module binding
* [12 Remove a compliance rule](/guides/issuer/12.-remove-a-compliance-rule.md) : removing the policy
* [10 Transfer between investors](/guides/positions/10.-transfer-between-investors.md) : `transferFrom` and its checks
* [24 Transfer through the identity](/guides/identity/24.-transfer-through-the-identity.md) : moving a holder's tokens without a spender
* [31 Transfer on a satellite](/guides/cross-chain/31.-transfer-on-a-satellite.md) : a spender named in a validation


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.t-rex.network/guides/issuer/26.-authorize-spenders.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
