Contracts specs

ContractRegistry

constructor

addresses

names

versions

versionAddress

latestVersion

registerContract

  • ✅ registers IContractMeta compatible contract and updates respective view methods - #addresses - #versions - #names - #latestVersion - #versionAddress
Access control
  • ✅ allowed: operator (deployer)
  • ✅ denied with FRB: random address
  • ✅ allowed: protocol admin
Edge cases
  • ✅ when new contract major version differs more, than on one reverts with INVA
  • ✅ when new contract version lower or equal existing one reverts with INVA
  • ✅ when contract has invalid version reverts with INVA
  • ✅ when contract name is not alphanumeric reverts with INV
  • ✅ when address is already registered reverts with DUP

ProtocolGovernance

constructor

  • ✅ deploys a new contract

stagedParams

  • ✅ imestamp timestamp equals #stageParams's block.timestamp + governanceDelay
  • ✅ imestamp clears by #commitParams
  • ✅ imestamp edge cases when nothing is set returns zero
  • ✅ imestamp access control allowed: any address
Access control
  • ✅ allowed: any address
Properties
  • ✅ updates by #stageParams
  • ✅ clears by #commitParams

params

stagedValidatorsAddresses

validatorsAddresses

validatorsAddress

  • ✅ returns correct value
  • ✅ s properties @property: updates when committed validator grant for a new address
  • ✅ s properties @property: doesn't update when committed validator grant for an existing address
  • ✅ s access control allowed: any address

permissionAddresses

stagedPermissionGrantsAddresses

addressesByPermission

  • ✅ returns addresses that has the given permission set to true
Access control
  • ✅ allowed: any address
Properties
  • ✅ updates by #stagePermissionGrants + #commitPermissionGrants or #revokePermissions
  • ✅ is not affected by forceAllowMask
  • ✅ returns empty array on unknown permissionId

hasPermission

hasAllPermissions

  • ✅ checks if an address has all permissions set to true
Access control
  • ✅ allowed: any address
Properties
  • ✅ returns false on random address
  • ✅ is not affected by staged permissions
  • ✅ is affected by committed permissions
  • ✅ returns true for any address when forceAllowMask is set to true
Edge cases
  • ✅ on unknown permission id returns false

maxTokensPerVault

  • ✅ returns correct value

governanceDelay

  • ✅ returns correct value

protocolTreasury

  • ✅ returns correct value

forceAllowMask

  • ✅ returns correct value

withdrawLimit

  • ✅ returns correct value

supportsInterface

  • ✅ returns true for IProtocolGovernance interface (0xca11fe03)
  • ✅ access control: allowed: any address
  • ✅ edge cases: when contract does not support the given interface returns false

stageValidator

  • ✅ emits ValidatorStaged event
Access control
  • ✅ allowed: admin
  • ✅ denied: deployer
  • ✅ denied: random address
Edge cases
  • ✅ when attempting to stage grant to zero address reverts with AZ when target has zero address
  • ✅ when attempting to stage grant to zero address reverts with AZ when validator has zero address

rollbackStagedValidators

  • ✅ rolls back all staged validators
  • ✅ emits AllStagedValidatorsRolledBack event
Access control
  • ✅ allowed: admin
  • ✅ denied: deployer
  • ✅ denied: random address

commitValidator

  • ✅ commits staged validators
  • ✅ emits ValidatorCommitted event
Access control
  • ✅ allowed: admin
  • ✅ denied: deployer
  • ✅ denied: random address
Edge cases
  • ✅ when attempting to commit validator for zero address reverts with NULL
  • ✅ when nothing is staged for the given address reverts with NULL
  • ✅ when attempting to commit validator too early reverts with TS

commitAllValidatorsSurpassedDelay

  • ✅ emits ValidatorCommitted event
Access control
  • ✅ allowed: admin
  • ✅ denied: deployer
  • ✅ denied: random address
Properties
  • ✅ commits all staged validators
  • ✅ commits all staged validators after delay
Edge cases
  • ✅ when attempting to commit a single validator too early does not commit validator
  • ✅ when attempting to commit multiple validators too early does not commit these validators

revokeValidator

  • ✅ emits ValidatorRevoked event
Edge cases
  • ✅ when attempting to revoke from zero address reverts with NULL

rollbackStagedPermissionGrants

  • ✅ rolls back all staged permission grants
  • ✅ emits AllStagedPermissionGrantsRolledBack event
Access control
  • ✅ allowed: admin
  • ✅ denied: deployer
  • ✅ denied: random address

commitPermissionGrants

  • ✅ commits staged permission grants
  • ✅ emits PermissionGrantsCommitted event
Access control
  • ✅ allowed: admin
  • ✅ denied: deployer
  • ✅ denied: random address
Edge cases
  • ✅ when attempting to commit permissions for zero address reverts with NULL
  • ✅ when nothing is staged for the given address reverts with NULL
  • ✅ when attempting to commit permissions too early reverts with TS

commitAllPermissionGrantsSurpassedDelay

  • ✅ emits PermissionGrantsCommitted event
Access control
  • ✅ allowed: admin
  • ✅ denied: deployer
  • ✅ denied: random address
Properties
  • ✅ commits all staged permission grants
  • ✅ commits all staged permission grants after delay
Edge cases
  • ✅ when attempting to commit a single permission too early does not commit permission
  • ✅ when attempting to commit multiple permissions too early does not commit these permissions

revokePermissions

  • ✅ emits PermissionRevoked event
Edge cases
  • ✅ when attempting to revoke from zero address reverts with NULL

commitParams

  • ✅ emits ParamsCommitted event
Access control
  • ✅ allowed: protocol admin
  • ✅ denied: deployer
  • ✅ denied: random address
Edge cases
  • ✅ when attempting to commit params too early reverts with TS
  • ✅ when attempting to commit params without setting pending params reverts with NULL

stagePermissionGrants

  • ✅ emits PermissionGrantsStaged event
Access control
  • ✅ allowed: admin
  • ✅ denied: deployer
  • ✅ denied: random address
Edge cases
  • ✅ when attempting to stage grant to zero address reverts with NULL

stageParams

  • ✅ emits ParamsStaged event
Access control
  • ✅ allowed: admin
  • ✅ denied: random address
Edge cases
  • ✅ when given invalid params when maxTokensPerVault is zero reverts with NULL
  • ✅ when given invalid params when governanceDelay is zero reverts with NULL
  • ✅ when given invalid params when governanceDelay exceeds MAX_GOVERNANCE_DELAY reverts with LIMO
  • ✅ when given invalid params when withdrawLimit less than MIN_WITHDRAW_LIMIT reverts with LIMO

UnitPricesGovernance

constructor

supportsInterface

stageUnitPrice

rollbackUnitPrice

commitUnitPrice

VaultRegistry

constructor

  • ✅ creates VaultRegistry
  • ✅ initializes ProtocolGovernance address
  • ✅ initializes ERC721 token name
  • ✅ initializes ERC721 token symbol

vaults

  • ✅ returns all registered vaults
  • ✅ access control: allowed: any address
  • ✅ ount returns the number of registered vaults
  • ✅ ount access control: allowed: any address
  • ✅ ount properties @property: when N new vaults have been registered, vaults count will be increased by N

vaultForNft

  • ✅ resolves Vault address by VaultRegistry NFT
  • ✅ access control: allowed: any address
Edge cases
  • ✅ when Vault NFT is not registered in VaultRegistry returns zero address

nftForVault

  • ✅ resolves VaultRegistry NFT by Vault address
  • ✅ access control: allowed: any address
Edge cases
  • ✅ when Vault is not registered in VaultRegistry returns zero

isLocked

  • ✅ checks if token is locked (not transferable)
  • ✅ access control: allowed: any address
Edge cases
  • ✅ when VaultRegistry NFT is not registered in VaultRegistry returns false

protocolGovernance

  • ✅ returns ProtocolGovernance address
  • ✅ access control: allowed: any address

stagedProtocolGovernance

  • ✅ returns ProtocolGovernance address staged for commit
  • ✅ access control: allowed: any address
  • ✅ imestamp returns timestamp after which #commitStagedProtocolGovernance can be called
  • ✅ imestamp access control: allowed: any address
  • ✅ imestamp edge cases when nothing is staged returns 0
  • ✅ imestamp edge cases right after #commitStagedProtocolGovernance was called returns 0
Edge cases
  • ✅ when nothing is staged returns zero address
  • ✅ right after #commitStagedProtocolGovernance was called returns zero address

stagedProtocolGovernanceTimestamp

  • ✅ returns timestamp after which #commitStagedProtocolGovernance can be called
  • ✅ access control: allowed: any address
Edge cases
  • ✅ when nothing is staged returns 0
  • ✅ right after #commitStagedProtocolGovernance was called returns 0

vaultsCount

  • ✅ returns the number of registered vaults
  • ✅ access control: allowed: any address
Properties
  • ✅ when N new vaults have been registered, vaults count will be increased by N

supportsInterface

  • ✅ returns true if this contract supports a certain interface
  • ✅ access control: allowed: any address
  • ✅ edge cases: when contract does not support the given interface returns false

registerVault

  • ✅ binds minted ERC721 NFT to Vault address and transfers minted NFT to owner specified in args
  • ✅ emits VaultRegistered event
  • ✅ access control: allowed: any account with Register Vault permissions
  • ✅ access control: denied: any other address
  • ✅ access control: denied: protocol governance admin
Properties
  • ✅ minted NFT equals to vaultRegistry#vaultsCount
Edge cases
  • ✅ when address doesn't conform to IVault interface (IERC165) reverts with INVI
  • ✅ when vault has already been registered reverts with DUP
  • ✅ when owner address is zero reverts with AZ

stageProtocolGovernance

  • ✅ stages new ProtocolGovernance for commit
  • ✅ sets the stagedProtocolGovernanceTimestamp after which #commitStagedProtocolGovernance can be called
  • ✅ access control: allowed: ProtocolGovernance Admin
  • ✅ access control: denied: any other address
  • ✅ access control: denied: deployer
Edge cases
  • ✅ when new ProtocolGovernance is a zero address reverts with AZ

commitStagedProtocolGovernance

  • ✅ commits staged ProtocolGovernance
  • ✅ resets staged ProtocolGovernanceTimestamp
  • ✅ resets staged ProtocolGovernance
  • ✅ access control: allowed: ProtocolGovernance Admin
  • ✅ access control: denied: any other address
Edge cases
  • ✅ when nothing is staged reverts with INIT
  • ✅ when called before stagedProtocolGovernanceTimestamp reverts with TS
  • ✅ when called before stagedProtocolGovernanceTimestamp reverts with TS

lockNft

  • ✅ locks NFT (disables any transfer)
  • ✅ emits TokenLocked event
  • ✅ access control: allowed: NFT owner
  • ✅ access control: denied: any other address
  • ✅ access control: denied: protocol admin
Edge cases
  • ✅ when NFT has already been locked succeeds

CommonLibrary

ExceptionsLibrary

PermissionIdsLibrary

SemverLibrary

ChainlinkOracle

constructor

hasOracle

  • ✅ returns true if oracle is supported
  • ✅ edge cases: when oracle is not supported returns false

supportedTokens

  • ✅ returns list of supported tokens

priceX96

supportsInterface

  • ✅ returns true for ChainlinkOracle interface (0x8e3bd5d7)
  • ✅ edge cases: when contract does not support the given interface returns false

addChainlinkOracles

  • ✅ emits OraclesAdded event
  • ✅ when oracles have set by addChainLinkOracles function returns prices
  • ✅ edge cases: when arrays have different lengths reverts with INV
  • ✅ edge cases: when sender has no admin righs reverts with FRB

MellowOracle

constructor

priceX96

supportsInterface

  • ✅ returns true for IUniV3Oracle interface (0x6d80125b)
  • ✅ edge cases: when contract does not support the given interface returns false

UniV2Oracle

constructor

priceX96

supportsInterface

  • ✅ returns true for IUniV2Oracle interface (0x2748645e)
  • ✅ edge cases: when contract does not support the given interface returns false

UniV3Oracle

constructor

priceX96

supportsInterface

  • ✅ returns true for IUniV3Oracle interface (0x2a3602d6)
  • ✅ when contract does not support the given interface returns false

addUniV3Pools

  • ✅ when adding [weth, usdc] pools with fee = 500 adds pools
  • ✅ when adding [weth, usdc] pools with fee = 3000 adds pools
  • ✅ when adding [weth, usdc] pools with fee = 10000 does not return prices

LStrategy

constructor

getTargetPriceX96

targetUniV3LiquidityRatio

rebalanceERC20UniV3Vaults

rebalanceUniV3Vaults

postPreOrder

signOrder

resetCowswapAllowance

collectUniFees

manualPull

updateTradingParams

updateRatioParams

updateOtherParams

depositCallback

  • ✅ calls rebalance inside
  • ✅ access control: allowed: admin
  • ✅ access control: allowed: operator
  • ✅ access control: not allowed: any address

withdrawCallback

  • ✅ calls rebalance inside
  • ✅ access control: allowed: admin
  • ✅ access control: allowed: operator
  • ✅ access control: not allowed: any address

MStrategy

constructor

  • ✅ deploys a new contract
Edge cases
  • ✅ when positionManager_ address is zero reverts with AZ
  • ✅ when router_ address is zero passes

getAverageTick

  • ✅ returns average UniswapV3Pool price tick

initialize

createStrategy

  • ✅ creates a new strategy and initializes it
Access control
  • ✅ allowed: any address
Edge cases
  • ✅ when tokens.length is not equal 2 reverts with INVL
  • ✅ when erc20Vault vaultTokens do not match tokens_ reverts with INVA
  • ✅ when moneyVault vaultTokens do not match tokens_ reverts with INVA
  • ✅ when UniSwapV3 pool for tokens does not exist reverts with AZ

rebalance

  • ✅ performs a rebalance according to target ratios when token0/token1 ratio is greater than required
  • ✅ performs a rebalance according to target ratios when token0/token1 ratio is less than required
Access control
  • ✅ allowed: MStrategy admin
  • ✅ denied: any other address
Edge cases
  • ✅ when absolute tick deviation >= oracle.maxTickDeviation reverts with INVA
  • ✅ when tick is greater than tickMax - tickNeiborhood the upper bound of the interval is expanded by tickIncrease amount
  • ✅ when tick is less than tickMin + tickNeiborhood the lower bound of the interval is expanded by tickIncrease amount
  • ✅ when current tick has not deviated from the previous rebalance tick reverts with LIMU

manualPull

  • ✅ pulls token amounts from fromVault to toVault
Access control
  • ✅ allowed: MStrategy admin
  • ✅ denied: any other address
Edge cases
  • ✅ when token pull amounts are 0 passes

setOracleParams

  • ✅ sets new params for oracle
  • ✅ edge cases: when maxSlippageD is more than DENOMINATOR reverts with INVA
Access control
  • ✅ allowed: MStrategy admin
  • ✅ denied: any other address

setRatioParams

  • ✅ sets new ratio params
  • ✅ edge cases: tickMin is greater than tickMax reverts with INVA
  • ✅ edge cases: when erc20MoneyRatioD is more than DENOMINATOR reverts with INVA
  • ✅ edge cases: when minErc20MoneyRatioDeviationD is more than DENOMINATOR reverts with INVA
Access control
  • ✅ allowed: MStrategy admin
  • ✅ denied: any other address

BatchCall

batchcall

  • ✅ returns results
  • ✅ edge cases: when arrays have different lengths reverts with INVL
  • ✅ edge cases: when targets arrays not only of contracts reverts with "Address: delegate call to non-contract"

ContractMeta

contractName

contractNameBytes

contractVersion

contractVersionBytes

DefaultAccessControl

constructor

isAdmin

isOperator

DefaultAccessControlLateInit

isAdmin

  • ✅ returns true if sender is an admin, false otherwise

isOperator

  • ✅ returns true if sender is an operator, false otherwise

init

DefaultProxy

constructor

DefaultProxyAdmin

ERC20Token

approve

  • ✅ allows sender to transfer spender an amount and emits Approval

transfer

  • ✅ transfers amount from msg.sender to to
  • ✅ rom transfers amount from from to to if allowed

transferFrom

  • ✅ transfers amount from from to to if allowed

permit

  • ✅ emits Approval
  • ✅ edge cases: when deadline less than current timestamp reverts with TS
  • ✅ edge cases: when incorrect signature reverts with FRB

LStrategyHelper

constructor

checkOrder

tickFromPriceX96

UniV3Helper

liquidityToTokenAmounts

tokenAmountsToLiquidity

calculatePositionInfo

AllowAllValidator

constructor

  • ✅ deploys a new contract

validate

  • ✅ successful validate

BaseValidator

constructor

stagedValidatorParams

stagedValidatorParamsTimestamp

validatorParams

stageValidatorParams

commitValidatorParams

CowswapValidator

constructor

  • ✅ deploys a new contract

validate

  • ✅ successful validate
  • ✅ edge cases: when selector is not 0xec6cb13f reverts with INVS

CurveValidator

constructor

  • ✅ deploys a new contract

validate

  • ✅ successful validate
  • ✅ edge cases: when selector is not 0x3df02124 reverts with INVS
  • ✅ edge cases: when token ids are equal reverts with INV
  • ✅ edge cases: when not a vault token reverts with INVTO
  • ✅ edge cases: when pool has no approve permission reverts with FRB

ERC20Validator

constructor

  • ✅ deploys a new contract

validate

  • ✅ successful validate, spender can approve
  • ✅ successful validate, sender is trusted strategy
  • ✅ edge cases: when value is not zero reverts with INV
  • ✅ edge cases: when selector is not 0x095ea7b3 reverts with INVS
  • ✅ edge cases: when no transfer permission reverts with FRB
  • ✅ edge cases: when no approve permission reverts with FRB

UniV2Validator

constructor

  • ✅ deploys a new contract

validate

  • ✅ selector is 0x7ff36ab5 or 0xfb3bdb41 successful validate
  • ✅ selector is 0x7ff36ab5 or 0xfb3bdb41 edge cases: when addr is not swap reverts with INVTR
  • ✅ selector is 0x7ff36ab5 or 0xfb3bdb41 edge cases: when selector is wrong reverts with INVS
  • ✅ selector is 0x7ff36ab5 or 0xfb3bdb41 edge cases: when path is too small reverts with INVL
  • ✅ selector is 0x7ff36ab5 or 0xfb3bdb41 edge cases: when not a vault token reverts with INVTO
  • ✅ selector is 0x7ff36ab5 or 0xfb3bdb41 edge cases: when tokens are the same reverts with INVTO
  • ✅ selector is 0x7ff36ab5 or 0xfb3bdb41 edge cases: when pool has no approve permission reverts with FRB
  • ✅ selector is 0x7ff36ab5 or 0xfb3bdb41 edge cases: when sender is not a reciever reverts
  • ✅ selector is one of: 0x4a25d94a, 0x18cbafe5, 0x38ed1739, 0x8803dbee successful validate
  • ✅ selector is one of: 0x4a25d94a, 0x18cbafe5, 0x38ed1739, 0x8803dbee edge cases: when value is not zero reverts with INV
  • ✅ selector is one of: 0x4a25d94a, 0x18cbafe5, 0x38ed1739, 0x8803dbee edge cases: when sender is not reciever reverts
  • ✅ selector is one of: 0x4a25d94a, 0x18cbafe5, 0x38ed1739, 0x8803dbee edge cases: when path too small reverts with INVL
  • ✅ selector is one of: 0x4a25d94a, 0x18cbafe5, 0x38ed1739, 0x8803dbee edge cases: when not a vault token reverts with INVTO
  • ✅ selector is one of: 0x4a25d94a, 0x18cbafe5, 0x38ed1739, 0x8803dbee edge cases: when tokens are the same reverts with INVTO
  • ✅ selector is one of: 0x4a25d94a, 0x18cbafe5, 0x38ed1739, 0x8803dbee edge cases: when pool has no approve permission reverts with FRB

UniV3Validator

constructor

  • ✅ deploys a new contract

validate

  • ✅ edge cases: if addr is not swap reverts with INVTR
  • ✅ edge cases: if value is not zero reverts with INV
  • ✅ edge cases: if selector is wrong reverts with INVS
  • ✅ selector is 0x414bf389 successful validate
  • ✅ selector is 0x414bf389 edge cases: if recipient is not sender reverts with INVTR
  • ✅ selector is 0x414bf389 edge cases: if not a vault token reverts with INVTO
  • ✅ selector is 0x414bf389 edge cases: if tokens are the same reverts with INVTO
  • ✅ selector is 0x414bf389 edge cases: if pool has no permisson reverts with FRB
  • ✅ selector is 0xdb3e2198 successfull validate
  • ✅ selector is 0xdb3e2198 edge cases: if recipient is not sender reverts with INVTR
  • ✅ selector is 0xdb3e2198 edge cases: if not a vault token reverts with INVTO
  • ✅ selector is 0xdb3e2198 edge cases: if tokens are the same reverts with INVTO
  • ✅ selector is 0xdb3e2198 edge cases: if pool has no permisson reverts with FRB
  • ✅ selector is 0xc04b8d59 successfull validate
  • ✅ selector is 0xc04b8d59 edge cases: if recipient is not sender reverts with INVTR
  • ✅ selector is 0xc04b8d59 edge cases: if tokens are the same reverts with INVTO
  • ✅ selector is 0xc04b8d59 edge cases: if pool has no approve permission reverts with FRB
  • ✅ selector is 0xc04b8d59 edge cases: if not a vault token reverts with INVTO
  • ✅ selector is 0xf28c0498 successfull validate
  • ✅ selector is 0xf28c0498 edge cases: if recipient is not sender reverts with INVTR
  • ✅ selector is 0xf28c0498 edge cases: if tokens are the same reverts with INVTO
  • ✅ selector is 0xf28c0498 edge cases: if pool has no approve permission reverts with FRB
  • ✅ selector is 0xf28c0498 edge cases: if not a vault token reverts with INVTO

Validator

supportsInterface

AaveVault

tvl

  • ✅ returns total value locked
  • ✅ returns total value locked, no time passed from initialization
  • ✅ edge cases: when there are no initial funds returns zeroes

lendingPool

  • ✅ returns ILendingPool
  • ✅ access control: allowed: any address

supportsInterface

  • ✅ returns true if this contract supports 0x0cba2eff interface
  • ✅ access control: allowed: any address
  • ✅ returns true if this contract supports 0x7a63aa3a interface
  • ✅ access control: allowed: any address
  • ✅ edge cases: when contract does not support the given interface returns false

updateTvls

  • ✅ updates total value locked

initialize

  • ✅ emits Initialized event
  • ✅ initializes contract successfully
  • ✅ edge cases: when vault's nft is not 0 reverts with INIT
  • ✅ edge cases: when tokens are not sorted reverts with INVA
  • ✅ edge cases: when tokens are not unique reverts with INVA
  • ✅ edge cases: when setting zero nft reverts with VZ
  • ✅ edge cases: when setting token with address zero reverts with AZ
  • ✅ edge cases: when token has no permission to become a vault token reverts with FRB

AaveVaultGovernance

constructor

  • ✅ deploys a new contract
  • ✅ initializes internalParams
Edge cases
  • ✅ when lendingPool address is 0 reverts
  • ✅ when estimatedAaveAPY is 0 reverts
  • ✅ when estimatedAaaveAPY is larger than limit reverts
  • ✅ when protocolGovernance address is 0 reverts
  • ✅ when vaultRegistry address is 0 reverts

delayedProtocolParams

  • ✅ returns current DelayedProtocolParams
Access control
  • ✅ allowed: any address
Properties
  • ✅ staging DelayedProtocolParams doesn't change delayedProtocolParams
Edge cases
  • ✅ when no params were committed returns non-zero params initialized in constructor

supportsInterface

  • ✅ returns true if this contract supports 0x2f8c3ff3 interface
  • ✅ access control: allowed: any address

stagedDelayedProtocolParams

  • ✅ returns DelayedProtocolParams staged for commit
Access control
  • ✅ allowed: any address
Properties
  • ✅ always equals to params that were just staged
Edge cases
  • ✅ when no params are staged for commit returns zero struct
  • ✅ when params were just committed returns zero struct

stageDelayedProtocolParams

  • ✅ stages DelayedProtocolParams for commit
  • ✅ sets delay for commit
  • ✅ emits StageDelayedProtocolParams event
Access control
  • ✅ allowed: ProtocolGovernance admin
  • ✅ denied: Vault NFT Owner (aka liquidity provider)
  • ✅ denied: Vault NFT Approved (aka strategy)
  • ✅ denied: deployer
  • ✅ denied: random address
Properties
  • ✅ cannot be called by random address
Edge cases
  • ✅ when estimated Aave APY is larger than limit reverts
  • ✅ when called twice succeeds with the last value
  • ✅ when called with zero params reverts with zero params

commitDelayedProtocolParams

  • ✅ commits staged DelayedProtocolParams
  • ✅ resets delay for commit
  • ✅ emits CommitDelayedProtocolParams event
Access control
  • ✅ allowed: ProtocolGovernance admin
  • ✅ denied: Vault NFT Owner (aka liquidity provider)