> For the complete documentation index, see [llms.txt](https://docs.townsq.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.townsq.xyz/institution-backed-yield-vault-features/yield-vault/reference/roles-and-permissions.md).

# Roles and Permissions

## Roles & Permissions

Companion to `ARCHITECTURE.md`, written for auditors and counterparty diligence. This document enumerates every privileged role across the three proxies, every function each role can call, and what each role can and cannot cause to happen.

Every entry is sourced from the code. Where a capability is operational policy rather than an onchain constraint, it is marked as such.

### Summary

<table><thead><tr><th>Role</th><th>Contracts</th><th width="141">Intended holder</th><th>Can move assets</th><th>Can move share price</th></tr></thead><tbody><tr><td><code>DEFAULT_ADMIN_ROLE</code></td><td>all three</td><td>TimelockController (48h)</td><td><strong>Yes</strong> (via upgrade, <code>emergencyRecover</code>)</td><td><strong>Yes</strong> (via upgrade)</td></tr><tr><td><code>ADMIN_ROLE</code></td><td>all three</td><td>TimelockController (48h)</td><td><strong>Yes</strong> (<code>emergencyRecover</code>, whitelist)</td><td>Indirectly (parameters)</td></tr><tr><td><code>GUARDIAN_ROLE</code></td><td>TownSqToken, Manager</td><td>Safe</td><td>No</td><td>No</td></tr><tr><td><code>OPERATOR_ROLE</code></td><td>Manager</td><td>Executor (bot/Safe)</td><td><strong>Yes</strong> (<code>execute</code>)</td><td>No</td></tr><tr><td><code>REBALANCER_ROLE</code></td><td>Yield</td><td>Offchain yield engine</td><td>No</td><td><strong>Yes</strong> (entirely)</td></tr></tbody></table>

Role constants:

| Role                 | Value                          |
| -------------------- | ------------------------------ |
| `DEFAULT_ADMIN_ROLE` | `0x00`                         |
| `ADMIN_ROLE`         | `keccak256("ADMIN_ROLE")`      |
| `OPERATOR_ROLE`      | `keccak256("OPERATOR_ROLE")`   |
| `REBALANCER_ROLE`    | `keccak256("REBALANCER_ROLE")` |

All three contracts use OpenZeppelin `AccessControlUpgradeable`. `DEFAULT_ADMIN_ROLE` is the admin of every other role, so it can grant and revoke `ADMIN_ROLE`, `OPERATOR_ROLE` and `REBALANCER_ROLE` at will, on any of the three proxies, with no delay.

***

### `DEFAULT_ADMIN_ROLE`

Highest privilege. Holds two distinct powers.

#### Upgrade authority

`_authorizeUpgrade(address)` — `TownSqToken.sol:115`, `Manager.sol:68`, `Yield.sol:93`

All three contracts are UUPS. `DEFAULT_ADMIN_ROLE` can replace the implementation of any of them with arbitrary code, after the 48-hour timelock delay and with no upgrade-safety check enforced onchain. This subsumes every other capability in this document: a malicious upgrade can mint shares, rewrite accounting, or transfer custodied assets.

#### Role administration

`grantRole` / `revokeRole` on all five roles, across all three proxies. Effective after the 48-hour timelock delay.

**Consequence:** `DEFAULT_ADMIN_ROLE` is the root of trust for the entire protocol. It is held by the timelock, whose only proposer is the Safe — so the Safe's signing threshold, plus 48 hours of public notice, is what stands between the protocol and a hostile upgrade.

***

### `ADMIN_ROLE`

Governance parameters plus two custody-adjacent powers. Note the mix — this role is not separable into "risk only" as currently written.

#### TownSqToken

| Function            | Effect                                                                                                                     | Bound                             |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| `setFee`            | Exit fee charged on redemption                                                                                             | `<= ONE_PERCENT` (1%), hard-coded |
| `setCooldownPeriod` | Delay between request and withdrawal                                                                                       | `<= 7 days`, hard-coded           |
| `setVestingPeriod`  | Linear profit vesting window                                                                                               | `<= 30 days`, hard-coded          |
| `setManager`        | Repoints the Manager address                                                                                               | non-zero only                     |
| `pause` / `unpause` | Halts deposit, withdraw, redeem, redeemFor, requestRedeem. `pause` is shared with `GUARDIAN_ROLE`; `unpause` is admin-only | none                              |
| `emergencyRecover`  | Transfers any ERC-20 or native balance out of the vault                                                                    | balance only                      |

Two of these are not risk parameters:

* **`emergencyRecover`** can withdraw assets already earmarked for pending redemptions. `totalRequestedAmount` is not consulted. See `KNOWN_ISSUES.md`.
* **`setManager`** repoints the address authorised to call `updateTotalAssets`, `mintVaultToken` and `burnVaultToken`. A hostile Manager can forge vault accounting.
* **`pause`** blocks the exit path, not just entry (`KNOWN_ISSUES.md` K-01), so it is a fund-freezing power rather than a purely defensive one.

#### Manager

| Function                     | Line   | Effect                                 | Bound                                              |
| ---------------------------- | ------ | -------------------------------------- | -------------------------------------------------- |
| `setWhitelistedAddresses`    | `L100` | **The strategy/destination allowlist** | cannot whitelist `townSqToken` or `yield` (`L107`) |
| `setTreasury`                | `L75`  | Destination for all fee accrual        | non-zero only                                      |
| `setManagerAndYield`         | `L86`  | Repoints Yield and TownSqToken         | non-zero only                                      |
| `setMaxRedeemCap`            | `L168` | Max assets per redemption request      | none; `0` == uncapped                              |
| `setMinSharesInVaultToken`   | `L157` | Minimum shares per deposit             | must be non-zero                                   |
| `removeWhitelistedAddresses` | `L152` | Removes allowlist entries              | removal only; shared with `GUARDIAN_ROLE`          |

`setWhitelistedAddresses` is the most consequential `ADMIN_ROLE` function. The whitelist is the *only* constraint on where custodied assets can be sent by `OPERATOR_ROLE`. Additions go through the 48-hour timelock, so a new destination is public 48 hours before capital can reach it.

The guard at `L107` is load-bearing — it prevents whitelisting `townSqToken` or `yield`, which would otherwise let `OPERATOR_ROLE` reach `mintVaultToken` and `updateTotalAssets` through `execute` and forge share supply.

#### Yield

| Function                     | Line   | Effect                          | Bound                                                   |
| ---------------------------- | ------ | ------------------------------- | ------------------------------------------------------- |
| `setMaxYieldPercentage`      | `L122` | Cap on reported P\&L per epoch  | `1..10_000` bps of `totalAssets()`; default `50` = 0.5% |
| `setMaxFeePercentage`        | `L128` | Performance fee share of profit | `<= 10_000` bps; default `5000` = 50%                   |
| `setMinDistributionInterval` | `L116` | Minimum spacing between epochs  | none                                                    |
| `setManager`                 | `L138` | Repoints the Manager address    | —                                                       |

`setMaxFeePercentage` accepts up to `10_000` — **100% of reported profit assignable to the treasury**, after the 48-hour delay. Unlike the exit fee's hard-coded 1% ceiling, there is no structural bound here.

`setMinDistributionInterval` rejects `0` (`L117`) but accepts `1`, so the rate limit protecting against a compromised rebalancer can be reduced to a single second. There is also **no maximum** interval and no staleness check anywhere: nothing obliges the rebalancer to report at all, and if reporting stops the share price simply freezes at its last value.

***

### `OPERATOR_ROLE`

Manager only. Three functions. This role controls **where the assets physically are**.

#### `execute(address[] targets, bytes[] data)` — `Manager.sol:119-140`

```solidity
if (!whitelistedAddresses[targets[i]]) revert Errors.NotWhitelisted();
(bool success, bytes memory result) = targets[i].call(data[i]);
```

Arbitrary calldata to any whitelisted target, batched, executed from the address custodying 100% of deposited assets. Within the whitelist the operator can:

* transfer any amount of any asset to any whitelisted address
* grant unlimited ERC-20 approvals to whitelisted targets
* call any function with any arguments on any whitelisted contract
* batch all of the above atomically

**No amount cap, no per-target cap, no rate limit, no reentrancy guard.** The whitelist and the `L107` guard are the only constraints. This is the function through which capital leaves onchain infrastructure for the offchain trading book.

#### `transferToVault(address token, uint256 amount)` — `Manager.sol:194-196`

Pushes assets from Manager custody into the vault so pending redemptions can settle. The return leg of the flow.

This function is what makes withdrawal liveness an offchain dependency: per `INVARIANTS.md` I-22, *"users can always eventually withdraw"* **does not hold**. A user past cooldown cannot exit until an operator calls this, or until any address sends a plain ERC-20 transfer to the vault.

No zero-amount check, no event emitted, no NatSpec — the only function in `Manager` lacking all three.

#### `redeemFor(address _townSqUSD, address _user)` — `Manager.sol:147-150`

Executes a matured redemption on a user's behalf. Fully bounded: requires an existing request, cooldown elapsed, and pays the quoted amount to that user only (`TownSqToken.sol:492-500`). Cannot redirect funds or alter amounts. A UX convenience.

#### What `OPERATOR_ROLE` cannot do

Change the whitelist, grant roles, upgrade, pause, set parameters, set the treasury, report yield, or mint/burn shares.

***

### `REBALANCER_ROLE`

Yield only. One function. This role controls **what the books say the assets are worth**.

#### `distributeYield(uint256 amount, uint256 feeAmount, uint256 nonce, bool isProfit)` — `Yield.sol:171-281`

There is no oracle in this system. `totalAssets()` moves only through deposits, redemptions, and this call. The rebalancer asserts P\&L; the vault books it.

Effects:

* **On profit:** `amount` enters the linear vesting schedule for shareholders; `feeAmount` is minted to the treasury as shares, split into `recognisedFee` (credited immediately) and `vestingFee` (rides the same schedule) so the recognised share price does not step up.
* **On loss:** `amount` is debited instantly with no vesting; treasury shares are burned to absorb `feeAmount`, clamped to the treasury's actual balance, with any uncovered remainder booked to `treasuryDebt`.
* **Debt settlement:** outstanding `treasuryDebt` is repaid out of future `feeAmount` and routed to shareholders before the treasury earns anything new (`L201-207`).

Constraints, in full:

| Guard                                                 | Line       | Effect                                      |
| ----------------------------------------------------- | ---------- | ------------------------------------------- |
| `amount <= totalAssets * maxYieldPercentage / 10_000` | `L176`     | Default 0.5% of TVL per epoch               |
| `feeAmount <= amount * maxFeePercentage / 10_000`     | `L177`     | Default 50% of profit                       |
| `nonce == epoch + 1`                                  | `L180`     | Strictly sequential; primary replay defence |
| `trxns[proofHash]`                                    | `L182-184` | Duplicate rejection                         |
| `lastDistributionTime + minDistributionInterval`      | `L186-188` | Default 79200s (22h)                        |

Two notes on these guards:

* `proofHash` hashes `(vaultToken, amount, nonce, isProfit)` — **`feeAmount` is excluded** from the replay identity. The `nonce` check is what actually prevents replay.
* The two `require`s at `L176-177` carry no message and no custom error, unlike every other check in the file. A distribution rejected by a cap reverts with no reason string.

#### What `REBALANCER_ROLE` cannot do

Move any asset, change the whitelist, grant roles, upgrade, pause, or set parameters. Its entire surface is the accounting.

***

### Separation of duties

The intended split:

> **`OPERATOR_ROLE` controls where the money is. `REBALANCER_ROLE` controls what the books say it is worth. `ADMIN_ROLE` decides the rules. `DEFAULT_ADMIN_ROLE` decides the code.**

Compromise analysis:

| Compromised role     | Consequence                                              | Bounded by                          |
| -------------------- | -------------------------------------------------------- | ----------------------------------- |
| `REBALANCER_ROLE`    | Mispriced redemptions; value leaks gradually             | 0.5%/epoch, 22h interval            |
| `OPERATOR_ROLE`      | Assets sent to whitelisted destinations                  | The whitelist                       |
| `GUARDIAN_ROLE`      | Vault paused, whitelist emptied — denial of service      | `ADMIN_ROLE` can undo, after 48h    |
| `ADMIN_ROLE`         | Whitelist any address, then `emergencyRecover`; 100% fee | 48h public delay; Safe must propose |
| `DEFAULT_ADMIN_ROLE` | Total; can grant itself every other role or upgrade      | 48h public delay; Safe must propose |

Known weaknesses in the current separation:

1. **The timelock delays, it does not prevent.** One Safe is proposer, executor, canceller and guardian. It can pause instantly while unpausing takes 48 hours, so a pause can remove depositors' exit window during a pending change.
2. **`ADMIN_ROLE` mixes risk parameters with custody powers.** `setFee` and `emergencyRecover` sit behind the same role, so delegating parameter management is impossible without delegating fund extraction. This is the gap a `CURATOR_ROLE` would fill.
3. **`ADMIN_ROLE` and `OPERATOR_ROLE` may be the same key.** The whitelist setter and the fund mover must be distinct for the whitelist to function as a control.
4. **`setMaxFeePercentage` and `setMinDistributionInterval` are unbounded** in the directions that matter (100% fee; a 1-second interval, and no maximum interval or staleness check at all).

***

### Timelock model

**Live.** The handover batch has executed.

#### What changed

`DEFAULT_ADMIN_ROLE` and `ADMIN_ROLE` moved from the Safe to an OpenZeppelin `TimelockController` with a 48-hour `minDelay`, deployed with `admin == address(0)` so no standing bypass exists. There is **no code coupling** — no contract imports or stores the timelock address. The vault sees a call from an address holding `ADMIN_ROLE` and has no idea a delay occurred.

Upgrades inherit the delay at no cost: `_authorizeUpgrade` was already `onlyRole(DEFAULT_ADMIN_ROLE)`, so the role move is sufficient.

#### The fifth role

`GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE")`, declared on `TownSqToken` and `Manager`. It exists only because `TimelockController` applies one uniform delay and two actions cannot tolerate it.

| Can                                            | Cannot                                |
| ---------------------------------------------- | ------------------------------------- |
| `pause()` (`TownSqToken.sol`)                  | `unpause()`                           |
| `removeWhitelistedAddresses()` (`Manager.sol`) | `setWhitelistedAddresses()`           |
|                                                | Any parameter, upgrade, or role grant |

The asymmetry is the point: a compromised guardian can only cause a denial of service that `ADMIN_ROLE` can undo. It can never open a path for funds to leave, and it can never lift a halt it caused. Pause-fast/resume-slow is deliberate — pause blocks user withdrawals (K-01), so lifting it should carry the delay and the visibility.

#### Roles now

| Role                 | Holder             | Delay                                |
| -------------------- | ------------------ | ------------------------------------ |
| `DEFAULT_ADMIN_ROLE` | TimelockController | 48h                                  |
| `ADMIN_ROLE`         | TimelockController | 48h                                  |
| `GUARDIAN_ROLE`      | Safe               | none — pause, whitelist removal only |
| `OPERATOR_ROLE`      | Executor           | none — unchanged                     |
| `REBALANCER_ROLE`    | Offchain engine    | none — unchanged                     |

The operational roles are deliberately untouched. A 48-hour delay on `execute` or `distributeYield` would break normal operation, and neither can change the rules. `script/TimelockRoleChange.s.sol` asserts at runtime that its batch never touches either.

### Events

Every privileged action emits. Useful for monitoring and for diligence evidence.

| Event                                                                     | Contract    | Signals                                        |
| ------------------------------------------------------------------------- | ----------- | ---------------------------------------------- |
| `WhitelistUpdated`                                                        | Manager     | Destination allowlist change                   |
| `BatchExecuted`                                                           | Manager     | `execute` call, with full targets and calldata |
| `TreasurySet`                                                             | Manager     | Fee destination change                         |
| `MaxRedeemCapSet`, `MinSharesInVaultTokenSet`                             | Manager     | Capacity parameter change                      |
| `DistributeYield`                                                         | Yield       | Epoch P\&L report                              |
| `TreasuryDebtAccrued`, `TreasuryDebtRepaid`                               | Yield       | Unabsorbed loss / settlement                   |
| `MaxYieldPercentageSet`, `MaxFeePercentage`, `MinDistributionIntervalSet` | Yield       | Yield parameter change                         |
| `FeeSet`, `CooldownPeriodUpdated`, `VestingPeriodUpdated`                 | TownSqToken | Vault parameter change                         |
| `EmergencyRecovery`                                                       | TownSqToken | Asset extraction                               |
| `Paused`, `Unpaused`                                                      | TownSqToken | Pause state (OZ Pausable)                      |
| `RoleGranted`, `RoleRevoked`                                              | all three   | Role rotation (OZ AccessControl)               |
| `Upgraded`                                                                | all three   | Implementation change (OZ ERC1967)             |

Two monitoring gaps:

* **`Manager.transferToVault` emits nothing.** Redemption funding — the step withdrawal liveness depends on — is observable only via the underlying ERC-20 `Transfer`.
* **`AssetsWithdrawn` is declared in `IManager` but never emitted** anywhere in `src/`. Do not build monitoring against it.

Note also that `Yield`'s fee-cap event is named `MaxFeePercentage`, not `MaxFeePercentageSet` — it breaks the naming convention every other setter event follows.
