Skip to Content

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

RoleCapabilities
DEFAULT_ADMIN_ROLEManage role membership; registerShard; set redeemFeeBps, managementFeeBps, performanceFeeBps; replace blocklist oracle (setBlocklistOracle); changeNameAndSymbol
MANAGER_ROLEAdd / remove deposit assets (addDepositAsset, removeDepositAsset); setAssetConverter; setFeeTreasury; setManagementFeeTreasury; setDepositFeeBps; setPricePerShareRateLimit; deregister shards; setDefaultShard; unpauseDeposit; unpauseRedeem; unpause (ERC-20 transfers)
PRICE_SETTER_ROLEpostPricePerShare
STRATEGY_PAUSER_ROLEpauseDeposit; pauseRedeem
TRANSFERS_PAUSER_ROLEpause (ERC-20 transfer pause)
PAY_REDEMPTIONS_ROLEfulfillRedeemRequests
PERF_FEE_CLAIM_ROLEharvestPerformanceFee

Shard roles

RoleContractCapabilities
DEFAULT_ADMIN_ROLEShardBaseUpgradeablesetValidator; grant / revoke all shard roles
Any caller allowed by the validatorShardBaseUpgradeableexec(to, value, data, validatorArgs): each call is checked by the configured validator (e.g. the Merkle allowlist)
PRIVILEGED_EXECUTOR_ROLEShardBaseUpgradeableexec(to, value, data) (validator bypass for emergencies)
SHARD_TRANSFER_ROLEShardpullFromStrategy; pushToStrategy

Blocklist Oracle roles

RoleCapabilities
DEFAULT_ADMIN_ROLEAdd / remove third-party sanction lists (e.g. Chainalysis); allowAccount / disallowAccount (screening allowlist)
BLOCKLIST_SETTER_ROLEblockAccount (manual blocklist add)
BLOCKLIST_REMOVER_ROLEunblockAccount (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:

SwitchWho pausesWho unpausesEffect
Deposit pauseSTRATEGY_PAUSER_ROLEMANAGER_ROLEBlocks deposit and mint
Redeem pauseSTRATEGY_PAUSER_ROLEMANAGER_ROLEBlocks requestRedeem and fulfillRedeemRequests
ERC-20 transfer pauseTRANSFERS_PAUSER_ROLEMANAGER_ROLEBlocks 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 requestRedeem and fulfillRedeemRequests; queued requests are paid out once redemptions are unpaused.
  • Pausing transfers stops deposits and redemption requests too, because they mint and burn shares.
  • postPricePerShare is blocked while any of the three pauses is active.

Automatic pause on price rate-limit breach

postPricePerShare accepts a revertOnRateLimit flag:

  • revertOnRateLimit = true: reverts with PriceRateLimiterRateLimitExceeded if 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 by BLOCKLIST_SETTER_ROLE and BLOCKLIST_REMOVER_ROLE respectively.
  • An iterable registry of ISanctionList contracts (e.g. Chainalysis), each of which is consulted on every check call.

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.

Last updated on