Strategy: Roles and Security
Access control
Strategy contracts use OpenZeppelin AccessControlDefaultAdminRulesUpgradeable: DEFAULT_ADMIN_ROLE has a single holder and is transferred in two steps, after a configurable delay (defaultAdminDelay). All other roles are granted and revoked by the default admin, without a delay.
Strategy roles
| Role | Capabilities |
|---|---|
DEFAULT_ADMIN_ROLE | Manage role membership; registerShard; set redeemFeeBps, managementFeeBps, performanceFeeBps; replace blocklist oracle (setBlocklistOracle); changeNameAndSymbol |
MANAGER_ROLE | Add / remove deposit assets (addDepositAsset, removeDepositAsset); setAssetConverter; setFeeTreasury; setManagementFeeTreasury; setDepositFeeBps; setPricePerShareRateLimit; deregister shards; setDefaultShard; unpauseDeposit; unpauseRedeem; unpause (ERC-20 transfers) |
PRICE_SETTER_ROLE | postPricePerShare |
STRATEGY_PAUSER_ROLE | pauseDeposit; pauseRedeem |
TRANSFERS_PAUSER_ROLE | pause (ERC-20 transfer pause) |
PAY_REDEMPTIONS_ROLE | fulfillRedeemRequests |
PERF_FEE_CLAIM_ROLE | harvestPerformanceFee |
Shard roles
| Role | Contract | Capabilities |
|---|---|---|
DEFAULT_ADMIN_ROLE | ShardBaseUpgradeable | setValidator; grant / revoke all shard roles |
| Any caller allowed by the validator | ShardBaseUpgradeable | exec(to, value, data, validatorArgs): each call is checked by the configured validator (e.g. the Merkle allowlist) |
PRIVILEGED_EXECUTOR_ROLE | ShardBaseUpgradeable | exec(to, value, data) (validator bypass for emergencies) |
SHARD_TRANSFER_ROLE | Shard | pullFromStrategy; pushToStrategy |
Blocklist Oracle roles
| Role | Capabilities |
|---|---|
DEFAULT_ADMIN_ROLE | Add / remove third-party sanction lists (e.g. Chainalysis); allowAccount / disallowAccount (screening allowlist) |
BLOCKLIST_SETTER_ROLE | blockAccount (manual blocklist add) |
BLOCKLIST_REMOVER_ROLE | unblockAccount (manual blocklist removal) |
Design: PRICE_SETTER_ROLE vs MANAGER_ROLE separation
The rate-limit configuration (MANAGER_ROLE via setPricePerShareRateLimit) is held by a multisig, not by the operational key that posts prices day to day. Compromising the operational key therefore cannot widen the allowed price band: every post it makes stays within the limiter the multisig configured.
The multisig that holds MANAGER_ROLE also holds PRICE_SETTER_ROLE. Price posting is blocked while deposits or redemptions are paused, so during an incident the multisig posts a price as a single atomic transaction: unpause, post, pause.
Pause surfaces
The strategy has three independent pause switches, each with a distinct scope:
| Switch | Who pauses | Who unpauses | Effect |
|---|---|---|---|
| Deposit pause | STRATEGY_PAUSER_ROLE | MANAGER_ROLE | Blocks deposit and mint |
| Redeem pause | STRATEGY_PAUSER_ROLE | MANAGER_ROLE | Blocks requestRedeem and fulfillRedeemRequests |
| ERC-20 transfer pause | TRANSFERS_PAUSER_ROLE | MANAGER_ROLE | Blocks every share balance change (ERC20Pausable): transfers, and the mints and burns behind deposits, redemption requests and fee shares |
How the switches interact:
- Pausing deposits does not affect redemption requests, fulfilment or transfers.
- Pausing redemptions blocks both
requestRedeemandfulfillRedeemRequests; queued requests are paid out once redemptions are unpaused. - Pausing transfers stops deposits and redemption requests too, because they mint and burn shares.
postPricePerShareis blocked while any of the three pauses is active.
Automatic pause on price rate-limit breach
postPricePerShare accepts a revertOnRateLimit flag:
revertOnRateLimit = true: reverts withPriceRateLimiterRateLimitExceededif the move exceeds the bucket capacity. Scheduled production posts always use this mode.revertOnRateLimit = false: sets the price, drains the bucket, and automatically pauses both deposit and redeem. This is an auto-circuit-breaker for price feeds that push an unexpectedly large move.
Both pauses must be explicitly cleared by MANAGER_ROLE before normal operations resume.
Price rate limiter
pricePerShare moves are capped by a token-bucket rate limiter (PriceRateLimiter) that measures the relative price change in micro-bps (1,000,000 micro-bps = 100%, so 1 bps = 100 micro-bps):
relativeChangeBps = ceil(|newPPS - oldPPS| × 1_000_000 / oldPPS)The bucket has:
maxBps: maximum burst capacity (ceiling on how large a single move can be before the bucket is empty).refillRate: capacity refilled per second, allowing larger cumulative moves over time.
MANAGER_ROLE configures both parameters via setPricePerShareRateLimit. The bucket starts at zero capacity on deployment; setPricePerShareRateLimit must be called before the first postPricePerShare will succeed. Changing the configuration never raises the current capacity; the bucket is refilled only by setting the limit to zero and then to the new value. Current bucket state (with refill virtually applied) is readable via pricePerShareRateLimiterState().
Compliance: Blocklist Oracle
Every share balance change (transfer, mint and burn) runs a blocklist check on both parties via IBlocklistOracle.check(account), so deposits, redemption requests and plain share transfers are all covered. If the account is blocked, the oracle reverts and the operation fails.
The BlocklistOracle contract combines:
- A manual blocklist (
blockAccount/unblockAccount), managed byBLOCKLIST_SETTER_ROLEandBLOCKLIST_REMOVER_ROLErespectively. - An iterable registry of
ISanctionListcontracts (e.g. Chainalysis), each of which is consulted on everycheckcall.
If either the manual blocklist or any registered sanction list flags an account, check reverts. Accounts on the screening allowlist (DEFAULT_ADMIN_ROLE) skip the third-party sanction lists but are still subject to the manual blocklist.
The oracle address is set at initialisation and can be replaced by DEFAULT_ADMIN_ROLE via setBlocklistOracle. It can never be set to address(0); the compliance requirement cannot be disabled on-chain.
Scope: the check runs in the share token’s _update hook, on both the sender and the receiver of every balance change, including plain ERC-20 transfers.