Strategy: Roles and Security
Access control
Strategy contracts use OpenZeppelin AccessControlDefaultAdminRulesUpgradeable, which adds a mandatory timelock on DEFAULT_ADMIN_ROLE transfers. All other roles are granted and revoked by the default admin.
Roles are designed to protect the depositor, and balance security needs of the Strategy operator. Hence, whilst DEFAULT_ADMIN_ROLE is behind a timelock, other operational roles are not.
Strategy roles
| Role | Capabilities |
|---|---|
DEFAULT_ADMIN_ROLE | Manage role membership; registerShard; add / remove deposit assets; setAssetConverter; set redeemFeeBps, managementFeeBps, performanceFeeBps; replace blocklist oracle |
MANAGER_ROLE | 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 |
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 | unblockAccount; add / remove third-party oracle lists (e.g. Chainalysis) |
BLOCKLIST_SETTER_ROLE | blockAccount (manual blocklist add) |
Design: PRICE_SETTER_ROLE vs MANAGER_ROLE separation
The rate-limit configuration (MANAGER_ROLE via setPricePerShareRateLimit) and price posting itself (PRICE_SETTER_ROLE via postPricePerShare) are deliberately held by different parties. Compromising one key alone cannot both widen the allowed price band and post an out-of-range price; both keys would need to be compromised simultaneously.
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 |
| ERC-20 transfer pause | TRANSFERS_PAUSER_ROLE | MANAGER_ROLE | Blocks all share transfers (ERC20Pausable) |
These three switches are fully independent:
- Pausing deposits does not affect redemption requests or transfers.
- Pausing transfers does not affect deposit or redemption request entrypoints (but does block share transfers that are part of fee routing if combined with other conditions).
fulfillRedeemRequestsis not blocked by any of the three pauses; once shares are burned and a request is queued, the operator can always pay it out.
Automatic pause on price rate-limit breach
postPricePerShare accepts a revertOnRateLimit flag:
revertOnRateLimit = true: reverts withPriceRateLimiter_RateLimitExceededif the move exceeds the bucket capacity.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 bps = 10,000 micro-bps):
relativeChangeBps = |newPPS - oldPPS| × 10_000 / oldPPSThe 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. Current bucket state (with refill virtually applied) is readable via pricePerShareRateLimiterState().
Compliance: Blocklist Oracle
Every deposit / mint and requestRedeem call runs a blocklist check via IBlocklistOracle.check(account). If the account is blocked, the oracle reverts and the operation fails.
The BlocklistOracle contract combines:
- A manual blocklist (
blockAccount/unblockAccount), managed byBLOCKLIST_SETTER_ROLEandDEFAULT_ADMIN_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.
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: blocklist checks fire on deposit sender, deposit receiver, and requestRedeem owner only. Generic ERC-20 share transfers are not individually checked by the strategy.