> For the complete documentation index, see [llms.txt](https://docs.theo.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.theo.xyz/security-and-transparency/roles-and-access-control.md).

# Roles & access control

Every role that can change protocol state, who holds it, and the command that proves it.

Every privileged role on Theo contracts is listed below with its holder, its delay, and the command that verifies it. Verified onchain August 28, 2026, and re-verified against contract source September 1, 2026. The commands hit public RPCs through [Foundry's cast](https://book.getfoundry.sh/cast/) and need no accounts or API keys.

## Governance addresses

The last column is the reach of each address. If a key were compromised, this is what it could touch.

|                   | Address                                                                                  | What it is                                                                                                        | Holds a role on                                                             |
| ----------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Operator multisig | [`0x9487…1295`](https://etherscan.io/address/0x94877640dD9E6F1e3Cb56Bf7b5665b7152601295) | 4-of-6 Safe. Six hardware wallets, geographically distributed. Theo principals plus two external operators.       | Timelock (proposer, executor, canceller); TheoWhitelist (whitelist manager) |
| Timelock          | [`0x2bB4…CA02`](https://etherscan.io/address/0x2bB4b7E6E83FA6b77d0143dad631843cB73DCA02) | TimelockController, 2-day minimum delay. All admin actions flow through it.                                       | thUSD, sthUSD, Mint & Redeem, TheoWhitelist, thUSD OFT, sthUSD OFT, itself  |
| Guardian          | [`0xF936…DD8F`](https://etherscan.io/address/0xF936Df06D35a2f82f26083f32ff2Ab72F3EBDD8F) | EOA under Fordefi MPC. Holds the emergency pause.                                                                 | Mint & Redeem and sthUSD (pause only)                                       |
| Minter            | [`0x09ec…53b6`](https://etherscan.io/address/0x09ec7c2d4955525237b843f5338dd7982b5553b6) | EOA under Fordefi MPC. Authorizes mints. Cannot move user funds or change roles.                                  | Mint & Redeem only                                                          |
| Canceller         | [`0x7afb…d94a`](https://etherscan.io/address/0x7afb1d3308d22639f1ce698a2985cbf22f96d94a) | EOA under Fordefi MPC. Vetoes a queued operation before it executes.                                              | Timelock only                                                               |
| Yield distributor | [`0x5a69…5aac`](https://etherscan.io/address/0x5a69fb3e2fb7e78dfbdceafc5365d5803b745aac) | EOA under Fordefi MPC. Pushes thUSD into sthUSD as yield. Cannot withdraw from the vault or change any parameter. | sthUSD only                                                                 |

Full addresses are on Deployed addresses. The Minter, Canceller, and Yield distributor EOAs each reach exactly one contract. The Guardian reaches two, and its only power on both is pause. All four EOAs are held under Fordefi MPC. None of them can move user funds or change roles. Every path to a contract upgrade or a role change runs through the timelock, which means through the multisig and through the 2-day delay.

Each section below covers one contract: its roles, its holders, and the commands that verify them.

## thUSD

[`0xa3fE5c7596024E6811E14F029937D5bd8Ae485b3`](https://etherscan.io/token/0xa3fE5c7596024E6811E14F029937D5bd8Ae485b3)

| Role              | Holder   | Delay  | Scope                                   |
| ----------------- | -------- | ------ | --------------------------------------- |
| `Ownable.owner()` | Timelock | 2 days | All admin calls. Single-owner contract. |

```bash
cast call 0xa3fE5c7596024E6811E14F029937D5bd8Ae485b3 'owner()(address)' \
  --rpc-url https://ethereum-rpc.publicnode.com

# Expect: 0x2bb4b7e6e83fa6b77d0143dad631843cb73dca02
```

## sthUSD

[`0xA808Bc9775cb41c52C7842f8b50427fE7A770326`](https://etherscan.io/address/0xA808Bc9775cb41c52C7842f8b50427fE7A770326)

| Role               | Holder                | Delay   | Scope                                                                                    |
| ------------------ | --------------------- | ------- | ---------------------------------------------------------------------------------------- |
| `Ownable.owner()`  | Timelock              | 2 days  | Upgrades, lockup period, vesting duration, distributor and pauser addresses, unpause     |
| `emergencyPauser`  | Guardian EOA          | Instant | Pause deposits, redemption requests, claims, and yield. Transfers continue.              |
| `yieldDistributor` | Yield distributor EOA | Instant | Call `setYield`, which pulls thUSD from the distributor into the vault. Cannot withdraw. |

The vault has no function that moves thUSD out to an admin. Only a contract upgrade, through the timelock, could add one.

```bash
RPC=https://ethereum-rpc.publicnode.com
ADDR=0xA808Bc9775cb41c52C7842f8b50427fE7A770326

cast call $ADDR 'owner()(address)'            --rpc-url $RPC   # 0x2bb4b7e6e83fa6b77d0143dad631843cb73dca02
cast call $ADDR 'emergencyPauser()(address)'  --rpc-url $RPC   # 0xf936df06d35a2f82f26083f32ff2ab72f3ebdd8f
cast call $ADDR 'yieldDistributor()(address)' --rpc-url $RPC   # 0x5a69fb3e2fb7e78dfbdceafc5365d5803b745aac
```

## Mint & Redeem

[`0x2D99aC801DC0edadD53f5688FeF2317932E8696e`](https://etherscan.io/address/0x2D99aC801DC0edadD53f5688FeF2317932E8696e)

| Role                 | Holder       | Delay   | Scope                                                                                                                              |
| -------------------- | ------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `DEFAULT_ADMIN_ROLE` | Timelock     | 2 days  | Supported assets, destinations, per-block caps, redemption fee, unpause, role grants and revokes. The contract is not upgradeable. |
| `EMERGENCY_ROLE`     | Guardian EOA | Instant | Pause issuance and redemptions                                                                                                     |
| `MINTER_ROLE`        | Minter EOA   | Instant | Authorize mints. Cannot move user funds or change roles                                                                            |

```bash
RPC=https://ethereum-rpc.publicnode.com
ADDR=0x2D99aC801DC0edadD53f5688FeF2317932E8696e

# Each call returns true.
# DEFAULT_ADMIN_ROLE to Timelock
cast call $ADDR 'hasRole(bytes32,address)(bool)' \
  0x0000000000000000000000000000000000000000000000000000000000000000 \
  0x2bb4b7e6e83fa6b77d0143dad631843cb73dca02 \
  --rpc-url $RPC

# EMERGENCY_ROLE to Guardian
cast call $ADDR 'hasRole(bytes32,address)(bool)' \
  0xbf233dd2aafeb4d50879c4aa5c81e96d92f6e6945c906a58f9f2d1c1631b4b26 \
  0xf936df06d35a2f82f26083f32ff2ab72f3ebdd8f \
  --rpc-url $RPC

# MINTER_ROLE to Minter
cast call $ADDR 'hasRole(bytes32,address)(bool)' \
  0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6 \
  0x09ec7c2d4955525237b843f5338dd7982b5553b6 \
  --rpc-url $RPC
```

Those calls confirm the holders we list. To confirm there are no holders we have not listed, replay every role event from the deploy block. The holder set is grants minus revokes, applied in block and log-index order:

```bash
cast logs --address $ADDR --from-block 24837116 \
  'RoleGranted(bytes32,address,address)' --rpc-url $RPC
cast logs --address $ADDR --from-block 24837116 \
  'RoleRevoked(bytes32,address,address)' --rpc-url $RPC
```

## TheoWhitelist

[`0x14d38a3ed85ebddb3e22ff022e38e645a311f388`](https://etherscan.io/address/0x14d38a3ed85ebddb3e22ff022e38e645a311f388)

The whitelist decides who may mint and redeem. It has no effect on holding or transferring thUSD or sthUSD, or on staking.

| Role                     | Holder            | Delay   | Scope                                         |
| ------------------------ | ----------------- | ------- | --------------------------------------------- |
| `DEFAULT_ADMIN_ROLE`     | Timelock          | 2 days  | Grant or revoke `WHITELIST_MANAGER_ROLE`      |
| `WHITELIST_MANAGER_ROLE` | Operator multisig | Instant | Add or remove whitelist and blacklist entries |

```bash
RPC=https://ethereum-rpc.publicnode.com
ADDR=0x14d38a3ed85ebddb3e22ff022e38e645a311f388

# DEFAULT_ADMIN_ROLE to Timelock
cast call $ADDR 'hasRole(bytes32,address)(bool)' \
  0x0000000000000000000000000000000000000000000000000000000000000000 \
  0x2bb4b7e6e83fa6b77d0143dad631843cb73dca02 \
  --rpc-url $RPC

# WHITELIST_MANAGER_ROLE to operator multisig
cast call $ADDR 'hasRole(bytes32,address)(bool)' \
  0x2a3dab589bcc9747970dd85ac3f222668741ae51f2a1bbb8f8355be28dd8a868 \
  0x94877640dd9e6f1e3cb56bf7b5665b7152601295 \
  --rpc-url $RPC

# Is a given address allowed to mint or redeem?
cast call $ADDR 'isWhitelisted(address)(bool)' 0xYourAddress --rpc-url $RPC
cast call $ADDR 'isBlacklisted(address)(bool)' 0xYourAddress --rpc-url $RPC
```

## Timelock

[`0x2bB4b7E6E83FA6b77d0143dad631843cB73DCA02`](https://etherscan.io/address/0x2bB4b7E6E83FA6b77d0143dad631843cB73DCA02)

| Role                 | Holder                              | Delay  | Scope                                                                                                                  |
| -------------------- | ----------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------- |
| `DEFAULT_ADMIN_ROLE` | Timelock itself                     | 2 days | Grant or revoke any role on the timelock. Held only by the timelock, so every change flows through a delayed proposal. |
| `PROPOSER_ROLE`      | Operator multisig                   | 0      | Schedule operations. The delay is enforced downstream.                                                                 |
| `EXECUTOR_ROLE`      | Operator multisig                   | 0      | Execute an operation once its timer elapses.                                                                           |
| `CANCELLER_ROLE`     | Canceller EOA and operator multisig | 0      | Veto a queued operation before execution.                                                                              |

```bash
RPC=https://ethereum-rpc.publicnode.com
ADDR=0x2bB4b7E6E83FA6b77d0143dad631843cB73DCA02

# Each call returns true.
# DEFAULT_ADMIN_ROLE to the Timelock itself
cast call $ADDR 'hasRole(bytes32,address)(bool)' \
  0x0000000000000000000000000000000000000000000000000000000000000000 \
  0x2bb4b7e6e83fa6b77d0143dad631843cb73dca02 \
  --rpc-url $RPC

# CANCELLER_ROLE to Canceller EOA
cast call $ADDR 'hasRole(bytes32,address)(bool)' \
  0xfd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f783 \
  0x7afb1d3308d22639f1ce698a2985cbf22f96d94a \
  --rpc-url $RPC

# CANCELLER_ROLE to operator multisig
cast call $ADDR 'hasRole(bytes32,address)(bool)' \
  0xfd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f783 \
  0x94877640dd9e6f1e3cb56bf7b5665b7152601295 \
  --rpc-url $RPC

# EXECUTOR_ROLE to operator multisig
cast call $ADDR 'hasRole(bytes32,address)(bool)' \
  0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63 \
  0x94877640dd9e6f1e3cb56bf7b5665b7152601295 \
  --rpc-url $RPC

# PROPOSER_ROLE to operator multisig
cast call $ADDR 'hasRole(bytes32,address)(bool)' \
  0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1 \
  0x94877640dd9e6f1e3cb56bf7b5665b7152601295 \
  --rpc-url $RPC

# Complete holder set from the deploy block
cast logs --address $ADDR --from-block 25046194 \
  'RoleGranted(bytes32,address,address)' --rpc-url $RPC
cast logs --address $ADDR --from-block 25046194 \
  'RoleRevoked(bytes32,address,address)' --rpc-url $RPC
```

The minimum delay itself is readable, and the queue of pending operations is on Upgradeability & timelocks:

```bash
cast call $ADDR 'getMinDelay()(uint256)' --rpc-url $RPC

# Expect: 172800 (seconds, which is 2 days)
```

## thUSD OFT

[`0x9AA9Aa0530a6AF70EE7BC47cF1240100f514b065`](https://etherscan.io/address/0x9AA9Aa0530a6AF70EE7BC47cF1240100f514b065), deployed at the same address on every chain.

| Chain              | Role              | Holder               | Delay    | Scope                                                                    |
| ------------------ | ----------------- | -------------------- | -------- | ------------------------------------------------------------------------ |
| Ethereum (adapter) | `Ownable.owner()` | Timelock             | 2 days   | DVN stack and peer configuration. No onchain rate limit on this adapter. |
| Arbitrum           | `Ownable.owner()` | Chain-local timelock | 18 hours | Peers, send library, and rate limits                                     |
| Stable             | `Ownable.owner()` | Chain-local timelock | 18 hours | Peers, send library, and rate limits                                     |

```bash
ADDR=0x9AA9Aa0530a6AF70EE7BC47cF1240100f514b065

cast call $ADDR 'owner()(address)' --rpc-url https://ethereum-rpc.publicnode.com
cast call $ADDR 'owner()(address)' --rpc-url https://arbitrum-rpc.publicnode.com
cast call $ADDR 'owner()(address)' --rpc-url https://rpc.stable.xyz

# Each returns: 0x2bb4b7e6e83fa6b77d0143dad631843cb73dca02
```

## sthUSD OFT

[`0xd1db209087516883ec705cfeb99e80bb6032d540`](https://etherscan.io/address/0xd1db209087516883ec705cfeb99e80bb6032d540), deployed at the same address on every chain.

| Chain              | Role              | Holder               | Delay    | Scope                                                                    |
| ------------------ | ----------------- | -------------------- | -------- | ------------------------------------------------------------------------ |
| Ethereum (adapter) | `Ownable.owner()` | Timelock             | 2 days   | DVN stack, peers, and rate limits. 2M sthUSD per hour per outbound lane. |
| Arbitrum           | `Ownable.owner()` | Chain-local timelock | 18 hours | Peers, send library, and rate limits                                     |
| Stable             | `Ownable.owner()` | Chain-local timelock | 18 hours | Peers, send library, and rate limits                                     |

```bash
ADDR=0xd1db209087516883ec705cfeb99e80bb6032d540

cast call $ADDR 'owner()(address)' --rpc-url https://ethereum-rpc.publicnode.com
cast call $ADDR 'owner()(address)' --rpc-url https://arbitrum-rpc.publicnode.com
cast call $ADDR 'owner()(address)' --rpc-url https://rpc.stable.xyz

# Each returns: 0x2bb4b7e6e83fa6b77d0143dad631843cb73dca02
```

Raising a rate limit carries a 48-hour delay. See Bridge security for the DVN configuration and cap checks.

## Operator multisig

[`0x94877640dD9E6F1e3Cb56Bf7b5665b7152601295`](https://etherscan.io/address/0x94877640dD9E6F1e3Cb56Bf7b5665b7152601295)

A 4-of-6 Safe. The quorum is provable, because the signer set and threshold are readable from chain at any time.

```bash
SAFE=0x94877640dD9E6F1e3Cb56Bf7b5665b7152601295
RPC=https://ethereum-rpc.publicnode.com

cast call $SAFE 'getThreshold()(uint256)' --rpc-url $RPC   # Expect: 4
cast call $SAFE 'getOwners()(address[])' --rpc-url $RPC     # Expect: 6 addresses
```

## Cash Wallet

[`0xEc417Ccb6dD26868Cca993a92F37217b1D4b3c2f`](https://etherscan.io/address/0xEc417Ccb6dD26868Cca993a92F37217b1D4b3c2f)

A 4-of-5 Safe. It holds no role on any Theo contract. It receives mint collateral from the Mint & Redeem contract and has granted that contract an allowance so redemptions can be paid from it. Its signers control what happens to collateral after a mint.

```bash
SAFE=0xEc417Ccb6dD26868Cca993a92F37217b1D4b3c2f
RPC=https://ethereum-rpc.publicnode.com

cast call $SAFE 'getThreshold()(uint256)' --rpc-url $RPC   # Expect: 4
cast call $SAFE 'getOwners()(address[])' --rpc-url $RPC     # Expect: 5 addresses
```
