SnuggleVaultAdminSatellite

0xf1f413ab4ef210f2aee5cdb998362b743a9e4fa2

Verification
Verified
v0.8.33+commit.64118f21
Type
Contract
8,696 bytes
ABI entries
56
14 read · 19 write
License
none

Contract information

Address
0xf1f413ab4ef210f2aee5cdb998362b743a9e4fa2
Chain
Robinhood Chain (4663)
Compiler
v0.8.33+commit.64118f21
Optimization
Enabled
Creation tx
0x0285d91f15…8c6d397b8f

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (14)

FEE_CHANGE_COOLDOWN()uint256
MAX_FEE_CHANGE_BPS()uint256
MAX_PROTOCOL_FEE_BPS()uint256
MAX_RANGE_WIDTH_BPS()uint24
MAX_REFERRAL_FEE_BPS()uint256
MAX_SLIPPAGE_BPS()uint256
MIN_TWAP_INTERVAL()uint32
TIMELOCK_DELAY()uint256
lastFeeChangeTime()uint256
pendingStakingManager()address
pendingStakingManagerTimestamp()uint256
pendingTreasury()address
pendingTreasuryTimestamp()uint256
vault()address

Events (5)

PoolAddedStakingManagerChangeCancelledStakingManagerChangeProposedTreasuryChangeCancelledTreasuryChangeProposed

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_vault",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "FeeChangeCooldown",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeChangeTooLarge",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidMaxTotalPositions",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidRangeWidth",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidSlippage",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidTWAPInterval",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoPendingChange",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotVaultOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolDoesNotExist",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PositionIsActive",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReferralFeeTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RewardAdapterValidationFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "StakingManagerAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TimelockNotReady",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TreasuryNotSet",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "token0",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "token1",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint24",
        "name": "fee",
        "type": "uint24"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "positionAdapter",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "rewardAdapter",
        "type": "address"
      }
    ],
    "name": "PoolAdded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "cancelledManager",
        "type": "address"
      }
    ],
    "name": "StakingManagerChangeCancelled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "newManager",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "timelockExpiry",
        "type": "uint256"
      }
    ],
    "name": "StakingManagerChangeProposed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "cancelledTreasury",
        "type": "address"
      }
    ],
    "name": "TreasuryChangeCancelled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "newTreasury",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "timelockExpiry",
        "type": "uint256"
      }
    ],
    "name": "TreasuryChangeProposed",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "FEE_CHANGE_COOLDOWN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_FEE_CHANGE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_PROTOCOL_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_RANGE_WIDTH_BPS",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_REFERRAL_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SLIPPAGE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_TWAP_INTERVAL",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TIMELOCK_DELAY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token0",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "token1",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "fee",
        "type": "uint24"
      },
      {
        "internalType": "address",
        "name": "positionAdapter",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "rewardAdapter",
        "type": "address"
      }
    ],
    "name": "addPool",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cancelStakingManager",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cancelTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "clearReferrer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "emergencyRevokeApproval",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "nftManager",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      }
    ],
    "name": "emergencyRevokeNFTApproval",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "executeStakingManager",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "executeTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lastFeeChangeTime",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingStakingManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingStakingManagerTimestamp",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingTreasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingTreasuryTimestamp",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newManager",
        "type": "address"
      }
    ],
    "name": "proposeStakingManager",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newTreasury",
        "type": "address"
      }
    ],
    "name": "proposeTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "removePool",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "nftContract",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "rescueERC721",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint24",
        "name": "rangeMin",
        "type": "uint24"
      },
      {
        "internalType": "uint24",
        "name": "rangeMax",
        "type": "uint24"
      },
      {
        "internalType": "uint256",
        "name": "newReferralFeeBps",
        "type": "uint256"
      },
      {
        "internalType": "uint32",
        "name": "newTwapInterval",
        "type": "uint32"
      },
      {
        "internalType": "uint256",
        "name": "newSlippageBps",
        "type": "uint256"
      }
    ],
    "name": "setConfig",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "exempt",
        "type": "bool"
      }
    ],
    "name": "setFeeExempt",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newManager",
        "type": "address"
      }
    ],
    "name": "setInitialStakingManager",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "keeper",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "authorized",
        "type": "bool"
      }
    ],
    "name": "setKeeper",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "newMax",
        "type": "uint256"
      }
    ],
    "name": "setMaxTotalPositions",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "newFeeBps",
        "type": "uint256"
      }
    ],
    "name": "setPerformanceFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "newRewardAdapter",
        "type": "address"
      }
    ],
    "name": "updatePoolRewardAdapter",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "vault",
    "outputs": [
      {
        "internalType": "contract ISnuggleVaultPoolManagement",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
import "./interfaces/IPositionAdapter.sol";
import "./interfaces/IRewardAdapter.sol";

/// @title ISnuggleVaultPoolManagement
/// @notice Interface for vault functions called by satellite
interface ISnuggleVaultPoolManagement {
    function owner() external view returns (address);
    function addPoolDirect(
        bytes32 poolId, address pool, address token0, address token1,
        uint24 fee, int24 tickSpacing, address positionAdapter,
        address rewardAdapter, address nftManager
    ) external;
    function updatePoolRewardAdapter(bytes32 poolId, address newRewardAdapter) external;
    function removePool(bytes32 poolId) external;
    function setPerformanceFee(uint256 newFeeBps) external;
    function setConfig(uint24 rangeMin, uint24 rangeMax, uint256 newReferralFeeBps, uint32 newTwapInterval, uint256 newSlippageBps) external;
    function setMaxTotalPositions(uint256 newMax) external;
    function setKeeper(address keeper, bool authorized) external;
    function setFeeExempt(address user, bool exempt) external;
    function clearReferrer(address user) external;
    function emergencyRevokeApproval(address token, address spender) external;
    function emergencyRevokeNFTApproval(address nftManager, address operator) external;
    function rescueERC721(address nftContract, uint256 tokenId, address to) external;
    function setTreasury(address newTreasury) external;
    function setStakingManager(address newManager) external;
    function performanceFeeBps() external view returns (uint256);
    function referralFeeBps() external view returns (uint256);
    function treasury() external view returns (address);
    function stakingManager() external view returns (address);
    function positions(uint256 tokenId) external view returns (uint256,bytes32,address,uint24,int24,int24,bool,bool,uint64,uint64,uint32,uint32,uint64,uint128,uint128,uint128,uint128);
    function allPositionIds(uint256 index) external view returns (uint256);
    function totalActivePositions() external view returns (uint256);
}

/// @title SnuggleVaultAdminSatellite
/// @notice Handles complex admin operations for SnuggleVaultUpgradeable
/// @dev Extracted to reduce main vault contract size below EVM bytecode limit
contract SnuggleVaultAdminSatellite {
    ISnuggleVaultPoolManagement public immutable vault;

    // ═══════════════════════════════════════════════════════════════════════════
    // TIMELOCK STATE
    // ═══════════════════════════════════════════════════════════════════════════

    uint256 public constant TIMELOCK_DELAY = 24 hours;

    address public pendingTreasury;
    uint256 public pendingTreasuryTimestamp;
    address public pendingStakingManager;
    uint256 public pendingStakingManagerTimestamp;

    // ═══════════════════════════════════════════════════════════════════════════
    // EVENTS
    // ═══════════════════════════════════════════════════════════════════════════

    event PoolAdded(
        bytes32 indexed poolId, address pool, address token0, address token1,
        uint24 fee, address positionAdapter, address rewardAdapter
    );
    event TreasuryChangeProposed(address indexed newTreasury, uint256 timelockExpiry);
    event TreasuryChangeCancelled(address indexed cancelledTreasury);
    event StakingManagerChangeProposed(address indexed newManager, uint256 timelockExpiry);
    event StakingManagerChangeCancelled(address indexed cancelledManager);

    // ═══════════════════════════════════════════════════════════════════════════
    // ERRORS
    // ═══════════════════════════════════════════════════════════════════════════

    error NotVaultOwner();
    error InvalidAddress();
    error PoolDoesNotExist();
    error FeeTooHigh();
    error FeeChangeTooLarge();
    error ReferralFeeTooHigh();
    error InvalidRangeWidth();
    error InvalidTWAPInterval();
    error InvalidMaxTotalPositions();
    error PositionIsActive();
    error NoPendingChange();
    error TimelockNotReady();
    error TreasuryNotSet();
    error InvalidSlippage();
    error StakingManagerAlreadySet();
    error FeeChangeCooldown();
    error RewardAdapterValidationFailed();

    // ═══════════════════════════════════════════════════════════════════════════
    // MODIFIERS
    // ═══════════════════════════════════════════════════════════════════════════

    modifier onlyVaultOwner() {
        if (msg.sender != vault.owner()) revert NotVaultOwner();
        _;
    }

    constructor(address _vault) {
        vault = ISnuggleVaultPoolManagement(_vault);
        // V25-L-03: Initialize cooldown so first fee change also respects cooldown period
        lastFeeChangeTime = block.timestamp;
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // POOL MANAGEMENT
    // ═══════════════════════════════════════════════════════════════════════════

    function addPool(
        address token0, address token1, uint24 fee,
        address positionAdapter, address rewardAdapter
    ) external onlyVaultOwner {
        if (positionAdapter == address(0)) revert InvalidAddress();
        IPositionAdapter adapter = IPositionAdapter(positionAdapter);
        if (token0 > token1) (token0, token1) = (token1, token0);
        address pool = adapter.getPool(token0, token1, fee);
        if (pool == address(0)) revert PoolDoesNotExist();

        // V28: Validate reward adapter can actually stake in this pool (gauge exists, MasterChef registered, etc.)
        // Catches configuration errors at registration time instead of silently failing at staking time
        if (rewardAdapter != address(0)) {
            try IRewardAdapter(rewardAdapter).validatePool(pool) {}
            catch { revert RewardAdapterValidationFailed(); }
        }

        bytes32 poolId = keccak256(abi.encodePacked(token0, token1, fee, positionAdapter));
        int24 tickSpacing = adapter.getTickSpacing(pool);
        address nftManager = adapter.positionManager();
        vault.addPoolDirect(poolId, pool, token0, token1, fee, tickSpacing, positionAdapter, rewardAdapter, nftManager);
        emit PoolAdded(poolId, pool, token0, token1, fee, positionAdapter, rewardAdapter);
    }

    function updatePoolRewardAdapter(bytes32 poolId, address newRewardAdapter) external onlyVaultOwner {
        vault.updatePoolRewardAdapter(poolId, newRewardAdapter);
    }

    /// @notice Remove a pool from the approved list
    /// @dev V20-L-01: Verifies no active positions exist for this pool before removal.
    ///      Use emergencyRevokeApproval/emergencyRevokeNFTApproval after removal if needed.
    /// @dev V24-M-01: WARNING — This function iterates ALL active positions O(n) to verify none
    ///      belong to the target pool. Gas cost scales linearly with totalActivePositions.
    ///      For pools with many positions, prefer deactivating via updatePoolRewardAdapter(poolId, address(0))
    ///      and letting positions withdraw naturally, rather than calling removePool().
    ///      A per-pool position counter would fix this but exceeds the vault's EIP-170 bytecode limit.
    function removePool(bytes32 poolId) external onlyVaultOwner {
        // M-04 (Valves): removed the O(n) active-position scan that previously guarded this.
        // It was defensive theater — vault.removePool only sets approvedPools[poolId].active =
        // false, and the withdraw path is NOT gated on `active`, so existing positions can always
        // be withdrawn after a pool is deactivated. The scan added no safety and let any user
        // permanently block pool removal by holding a single dust position in the pool.
        vault.removePool(poolId);
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // TIMELOCK OPERATIONS
    // ═══════════════════════════════════════════════════════════════════════════

    function proposeTreasury(address newTreasury) external onlyVaultOwner {
        if (newTreasury == address(0)) revert InvalidAddress();
        pendingTreasury = newTreasury;
        pendingTreasuryTimestamp = block.timestamp + TIMELOCK_DELAY;
        emit TreasuryChangeProposed(newTreasury, pendingTreasuryTimestamp);
    }

    function executeTreasury() external onlyVaultOwner {
        if (pendingTreasury == address(0)) revert NoPendingChange();
        if (block.timestamp < pendingTreasuryTimestamp) revert TimelockNotReady();
        vault.setTreasury(pendingTreasury);
        pendingTreasury = address(0);
        pendingTreasuryTimestamp = 0;
    }

    function cancelTreasury() external onlyVaultOwner {
        if (pendingTreasury == address(0)) revert NoPendingChange();
        emit TreasuryChangeCancelled(pendingTreasury);
        pendingTreasury = address(0);
        pendingTreasuryTimestamp = 0;
    }

    /// @notice Set staking manager — initial setup only (no timelock, reverts if already set)
    function setInitialStakingManager(address newManager) external onlyVaultOwner {
        if (newManager == address(0)) revert InvalidAddress();
        if (vault.treasury() == address(0)) revert TreasuryNotSet();
        if (vault.stakingManager() != address(0)) revert StakingManagerAlreadySet();
        vault.setStakingManager(newManager);
    }

    function proposeStakingManager(address newManager) external onlyVaultOwner {
        if (vault.treasury() == address(0)) revert TreasuryNotSet();
        if (newManager == address(0)) revert InvalidAddress();
        pendingStakingManager = newManager;
        pendingStakingManagerTimestamp = block.timestamp + TIMELOCK_DELAY;
        emit StakingManagerChangeProposed(newManager, pendingStakingManagerTimestamp);
    }

    function executeStakingManager() external onlyVaultOwner {
        if (pendingStakingManager == address(0) && pendingStakingManagerTimestamp == 0) revert NoPendingChange();
        if (block.timestamp < pendingStakingManagerTimestamp) revert TimelockNotReady();
        vault.setStakingManager(pendingStakingManager);
        pendingStakingManager = address(0);
        pendingStakingManagerTimestamp = 0;
    }

    function cancelStakingManager() external onlyVaultOwner {
        if (pendingStakingManager == address(0) && pendingStakingManagerTimestamp == 0) revert NoPendingChange();
        emit StakingManagerChangeCancelled(pendingStakingManager);
        pendingStakingManager = address(0);
        pendingStakingManagerTimestamp = 0;
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // ADMIN CONFIG (with validation — vault functions are thin setters)
    // ═══════════════════════════════════════════════════════════════════════════

    uint256 public constant MAX_PROTOCOL_FEE_BPS = 5000;
    uint256 public constant MAX_FEE_CHANGE_BPS = 500;
    uint256 public constant MAX_REFERRAL_FEE_BPS = 1000;
    uint256 public constant MAX_SLIPPAGE_BPS = 1000;
    uint32 public constant MIN_TWAP_INTERVAL = 5 minutes;
    /// @dev INT-11 (Valves internal V31): absolute ceiling on maxRangeWidthBps. rangeWidthBps is a tick
    ///      count; the full usable span is MAX_TICK - MIN_TICK = 1,774,544 (a full-range position).
    ///      Bounding rangeMax here prevents an owner from setting the cap into the int24 sign-wrap zone
    ///      (> ~8.39M), where TickMath._roundWidth would silently collapse deposits to a 1-tick range.
    uint24 public constant MAX_RANGE_WIDTH_BPS = 1_774_544;
    uint256 public constant FEE_CHANGE_COOLDOWN = 6 hours;

    /// @notice V17-M-03: Last time performance fee was changed (cooldown enforcement)
    uint256 public lastFeeChangeTime;

    function setPerformanceFee(uint256 newFeeBps) external onlyVaultOwner {
        if (newFeeBps > MAX_PROTOCOL_FEE_BPS) revert FeeTooHigh();
        if (lastFeeChangeTime > 0 && block.timestamp < lastFeeChangeTime + FEE_CHANGE_COOLDOWN) revert FeeChangeCooldown();
        uint256 current = vault.performanceFeeBps();
        uint256 delta = newFeeBps > current ? newFeeBps - current : current - newFeeBps;
        if (delta > MAX_FEE_CHANGE_BPS) revert FeeChangeTooLarge();
        lastFeeChangeTime = block.timestamp;
        vault.setPerformanceFee(newFeeBps);
        // V22-M-01: Cap referral fee if it would exceed new performance fee
        if (vault.referralFeeBps() > newFeeBps) {
            vault.setConfig(0, 0, newFeeBps, 0, type(uint256).max);
        }
    }

    function setConfig(
        uint24 rangeMin, uint24 rangeMax, uint256 newReferralFeeBps,
        uint32 newTwapInterval, uint256 newSlippageBps
    ) external onlyVaultOwner {
        if (rangeMin > 0 && rangeMax > 0 && rangeMin > rangeMax) revert InvalidRangeWidth();
        // INT-11: bound the upper cap so it can never enter the int24 sign-wrap zone (see _roundWidth).
        if (rangeMax > MAX_RANGE_WIDTH_BPS) revert InvalidRangeWidth();
        if (newReferralFeeBps != type(uint256).max) {
            if (newReferralFeeBps > MAX_REFERRAL_FEE_BPS || newReferralFeeBps > vault.performanceFeeBps()) revert ReferralFeeTooHigh();
        }
        if (newTwapInterval > 0 && (newTwapInterval < MIN_TWAP_INTERVAL || newTwapInterval > 1 hours)) revert InvalidTWAPInterval();
        // V19-I-03: Revert on invalid slippage instead of silently skipping
        if (newSlippageBps != type(uint256).max && newSlippageBps > 0 && newSlippageBps > MAX_SLIPPAGE_BPS) {
            revert InvalidSlippage();
        }
        vault.setConfig(rangeMin, rangeMax, newReferralFeeBps, newTwapInterval, newSlippageBps);
    }

    /// @notice M-02 (Valves): runtime-adjust the total-position cap. Guarded so it can't be
    ///         set below the live position count (which would freeze new deposits).
    function setMaxTotalPositions(uint256 newMax) external onlyVaultOwner {
        if (newMax < vault.totalActivePositions()) revert InvalidMaxTotalPositions();
        vault.setMaxTotalPositions(newMax);
    }

    function setKeeper(address keeper, bool authorized) external onlyVaultOwner {
        vault.setKeeper(keeper, authorized);
    }

    function setFeeExempt(address user, bool exempt) external onlyVaultOwner {
        vault.setFeeExempt(user, exempt);
    }

    function clearReferrer(address user) external onlyVaultOwner {
        vault.clearReferrer(user);
    }

    function emergencyRevokeApproval(address token, address spender) external onlyVaultOwner {
        vault.emergencyRevokeApproval(token, spender);
    }

    function emergencyRevokeNFTApproval(address nftManager, address operator) external onlyVaultOwner {
        vault.emergencyRevokeNFTApproval(nftManager, operator);
    }

    function rescueERC721(address nftContract, uint256 tokenId, address to) external onlyVaultOwner {
        if (to == address(0)) revert InvalidAddress();
        (,,address posOwner,,,,,,,,,,,,,,) = vault.positions(tokenId);
        if (posOwner != address(0)) revert PositionIsActive();
        vault.rescueERC721(nftContract, tokenId, to);
    }
}
Chain explorer3716msChain node84ms