# Access control and roles \[Role-based permissions and external access control in Railnet]

External Access Control (EAC) is Railnet's permission system, built on OpenZeppelin's `AccessControlDefaultAdminRules`. Every gated function in the protocol checks it.

## Global, scoped, public

A **global** role is a plain `bytes32` identifier that applies protocol-wide. A **scoped** role is restricted to one contract address, stored as `keccak256(abi.encodePacked(role, scope))`. A **public** role returns `true` for every account.

```solidity [Solidity]
// Protocol-wide
eac.grantRole(VEHICLE_STEAM_DEPOSIT, alice);

// One Vehicle only
eac.grantScopedRole(VEHICLE_STEAM_DEPOSIT, vehicle, alice);
eac.revokeScopedRole(VEHICLE_STEAM_DEPOSIT, vehicle, alice);

// Open deposits to everyone on one Vehicle; redeems stay gated
eac.setScopedRolePublic(VEHICLE_STEAM_DEPOSIT, vehicle, true);
```

Scope each role to the contract carrying the gated function, not the product it belongs to; a wrong scope is the usual reason a grant appears to do nothing.

:::warning
`DEFAULT_ADMIN_ROLE` cannot be made public. While a role is public, `grantRole`, `revokeRole`, `renounceRole` and their scoped variants all revert `PublicRoleAuthDenied` for it. Make it private again before managing individual holders.
:::

## Checking permissions

| Method | Checks |
|--------|--------|
| `hasRole(role, account)` | Global role, or public |
| `hasScopedRole(role, scope, account)` | Scoped role, or public for that scope |
| `hasRoleOrScopedRole(role, scope, account)` | Either — what contracts use |

Contracts reach these through `AccessControlLib`. `gatedCheckRole(role, scope, account)` is fail-closed: it reverts `ZeroAddress` when no access control is set, then `MissingRole` unless the account holds the role. Every operator-facing function uses it. `ungatedCheckRole(role, account)` is fail-open: an unset access control means access control is disabled. Vehicles reach it through `_onlyRoleWhenEnabled`, which also skips the check until the Vehicle is enabled, so the factory can seed the initial deposit before roles exist.

## Admin transfer

The current admin calls `beginDefaultAdminTransfer(newAdmin)`, the constructor's `initialDelay` must elapse, then the pending admin calls `acceptDefaultAdminTransfer()`. `changeDefaultAdminDelay(uint48)` is itself scheduled rather than immediate. The role can never be dropped: `beginDefaultAdminTransfer(address(0))` and `renounceRole(DEFAULT_ADMIN_ROLE, ...)` both revert `DefaultAdminCannotBeRenounced`.

## Role catalog

Every role, the contract it is checked on, and who normally holds it. Names match `Roles.sol`.

### Factories and assets

| Role | Scope | Typical holder |
|------|-------|----------------|
| `FACTORY_SPAWN` | The factory | Railnet |
| `CONDUIT_SPAWN` | `ConduitFactory` | Railnet |
| `FACTORY_DEPRECATE` | The factory | Railnet |
| `ASSET_REGISTRY_SET_ASSET` | [AssetRegistry](/developers/contracts/asset-registry) | Railnet |

### Beacons

| Role | Scope | Typical holder |
|------|-------|----------------|
| `BEACON_UPGRADE` | The beacon | Protocol admin |
| `BEACON_FREEZE` | The beacon | Protocol admin |
| `BEACON_PAUSE` / `BEACON_UNPAUSE` | The beacon | Protocol admin |

### Vehicles

| Role | Scope | Typical holder |
|------|-------|----------------|
| `VEHICLE_STEAM_DEPOSIT` | Vehicle or MultiVehicle | Users, usually public |
| `VEHICLE_STEAM_REDEEM` | Vehicle or MultiVehicle | Users, usually public |
| `VEHICLE_SET_INTERCEPTIONS` | Vehicle or MultiVehicle | Vehicle owner |
| `VEHICLE_ALLOW` | Vehicle | Vehicle owner |
| `VEHICLE_EXEC` | Vehicle | Asset manager, keeper |
| `VEHICLE_PROCESS_QUEUE` | Vehicle | Keeper |

STEAM authorization is split by direction so you can pause one leg without the other: keep deposits open while redemptions wind down, or gate deposits to KYC'd addresses while exits stay public. `VEHICLE_PROCESS_QUEUE` covers only the size-bounded async-queue request; the unbounded `processRequest()` is permissionless.

### Conduits

| Role | Scope | Typical holder |
|------|-------|----------------|
| `CONDUIT_SET_INTERCEPTIONS` | Conduit | Distributor |
| `CONDUIT_SET_TRANSFER_ENABLED` | Conduit | Distributor |
| `CONDUIT_PROCESS` | Conduit | Keeper |
| `CONDUIT_FORCE_REDEEM` | AccountList | Distributor |
| `ACCOUNT_LIST_MANAGER` | AccountList | Distributor |

A distributor running one Conduit over one Vehicle needs these plus the fee roles, nothing more.

### Fee manager

| Role | Scope | Typical holder |
|------|-------|----------------|
| `FEE_MANAGER_SET_FEES` | FeeManager | Platform |
| `FEE_MANAGER_SET_FEE_RECIPIENTS` | FeeManager | Platform |
| `FEE_MANAGER_DISPATCH_ERC20` | FeeManager | Operator, keeper |

### MultiVehicles

| Role | Scope | Typical holder |
|------|-------|----------------|
| `MULTI_VEHICLE_DEPOSIT` | SectorAccountingEngine | Asset manager |
| `MULTI_VEHICLE_MOVE` | SectorAccountingEngine | Asset manager |
| `MULTI_VEHICLE_DISPATCH` | SectorAccountingEngine | Asset manager |
| `MULTI_VEHICLE_SET_QUEUES` | QueueStrategyEngine | Asset manager |
| `MULTI_VEHICLE_PROGRESS_QUERY` | SubQueryEngine | Asset manager, keeper |
| `MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION` | VehicleManager | Platform |
| `MULTI_VEHICLE_SET_THRESHOLDS` | VehicleManager | Platform, asset manager |
| `MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE` | VehicleManager | Asset manager, keeper |
| `MULTI_VEHICLE_RETRIEVE_QUERY_REDEEM_QUEUE_ASSETS` | VehicleManager | Asset manager, keeper |

Keeping `MULTI_VEHICLE_SET_VEHICLE_AUTHORIZATION` with the platform is what makes the guardrails hold: the asset manager allocates, but cannot add a yield source. See [Guardrails](/strategies/allocation/guardrails).

### Modules and keepers

| Role | Scope | Typical holder |
|------|-------|----------------|
| `MODULE_MANAGER` | ModulesManager | Platform |
| `JOB_LISTING_REGISTER` / `JOB_LISTING_UNREGISTER` | Job listing | Platform |
| `JOB_LISTING_EXECUTE` | Job listing | `Keeper` contract |
| `KEEPER_ON_REPORT` | `Keeper` | Forwarder address |

The job listing itself must hold `MULTI_VEHICLE_FEED_QUERY_REDEEM_QUEUE` and `MULTI_VEHICLE_RETRIEVE_QUERY_REDEEM_QUEUE_ASSETS` on the VehicleManager: execution routes through `multiVehicle.manager()`, where the check runs.

## Beacon states

Vehicle and Conduit proxies read their implementation from a `FreezablePausableBeacon`. `freeze()` (`BEACON_FREEZE`) is irreversible: the implementation can never be upgraded again, and the beacon must not be paused. `pause()` and `pauseFor(duration)` (`BEACON_PAUSE`) are temporary — while paused, `implementation()` reverts `EnforcedPause` and every proxy on that beacon stops. `pauseFor` only extends an active pause; shortening reverts `CannotShortenPause`. `unpause()` needs `BEACON_UNPAUSE`.

## Security checklist

* Use a multisig for `DEFAULT_ADMIN_ROLE`. A compromised admin key can grant itself every role.
* Set a non-zero `initialDelay` so admin transfers are observable before they land.
* Prefer scoped roles over global ones, so one compromised operator key cannot reach every contract.
* Never grant `DEFAULT_ADMIN_ROLE` or vehicle authorization to an asset manager.
* Record which roles each operator holds, and check for in-flight queries before revoking them.
