Robinhood MainnetChain ID: 4663
Contract detail

VaultV2Supervisor

Live Robinhood Chain account facts for 0xca50d23f…c5c8db73 with at least 4 observed transactions. The history total is updating as indexing continues.

Address type
Smart contract
Transactions
4+ observed
First indexed block
#237207
Latest indexed block
#237330
History coverage
Recent activity available; total is catching up
ERC-20 transfers
1
Emitted events
2
Verification
Verified
Compiler
v0.8.28+commit.7893614a
Verification match
full
Creation transaction
0x0853e3d9…4cf47e89
Created at block
#237207
Language
solidity
EVM version
cancun
License
none
Compiler optimization
Enabled · 200 runs
Related activity

Transactions

Verified contract

Contract source code

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.28;

import {IVaultV2} from "vault-v2/src/interfaces/IVaultV2.sol";
import {EnumerableSet} from "openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol";

interface IRevokable {
    function revoke(bytes calldata data) external;
}

interface ISkimRecipient {
    function setSkimRecipient(address newSkimRecipient) external;
}

/// @title VaultV2Supervisor
/// @author Steakhouse Financial
/// @notice Act as improved owner for Vault V2 instances with timelocked owner actions and guardian vetoes.
/// @dev The supervisor is intended to own one or more Vault V2 contracts.
contract VaultV2Supervisor {
    using EnumerableSet for EnumerableSet.AddressSet;

    /// @dev Caller is not the supervisor owner.
    error NotOwner();
    /// @dev Provided address is zero.
    error ZeroAddress();
    /// @dev Caller is not a registered guardian for the vault.
    error NotGuardian();
    /// @dev Timelock has not expired yet.
    error TimelockNotExpired();
    /// @dev Caller is neither the supervisor owner nor the vault owner.
    error OnlyOwnerOrVaultOwner();
    /// @dev Timelock data is already scheduled.
    error DataAlreadyTimelocked();
    /// @dev Timelock data is not scheduled.
    error DataNotTimelocked();
    /// @dev Caller is neither the supervisor owner nor a guardian.
    error OnlyOwnerOrGuardian();
    /// @dev Calldata length is invalid.
    error InvalidAmount();
    /// @dev Selector is not supported for supervisor timelock submission.
    error UnsupportedSelector();
    /// @dev Provided timelock duration is invalid.
    error InvalidTimelock();
    /// @dev An ownership change is already scheduled.
    error OwnershipChangeAlreadyScheduled();
    /// @dev Operation would not change state.
    error NoOp();
    /// @dev Caller is not the pending supervisor owner.
    error NotPendingSupervisorOwner();
    /// @dev Sentinel removal attempted for the supervisor address.
    error CannotRemoveSupervisorSentinel();

    /// @notice Emitted when the supervisor owner changes.
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    /// @notice Emitted when a new supervisor owner is proposed.
    event OwnershipTransferStarted(address indexed previousOwner, address indexed pendingOwner);
    /// @notice Emitted when a vault owner is allowlisted or removed.
    event AllowedVaultOwnerSet(address indexed vaultOwner, bool allowed);
    /// @notice Emitted when a timelocked action is submitted.
    event TimelockSubmitted(
        address indexed sender, address indexed vault, bytes4 indexed selector, bytes data, uint256 executeAfter
    );
    /// @notice Emitted when a timelocked action is revoked.
    event TimelockRevoked(address indexed sender, address indexed vault, bytes4 indexed selector, bytes data);
    /// @notice Emitted when a guardian is added for a vault.
    event GuardianAdded(address indexed vault, address indexed guardian, address indexed sender);
    /// @notice Emitted when a guardian is removed for a vault.
    event GuardianRemoved(address indexed vault, address indexed guardian, address indexed sender);
    /// @notice Emitted when a vault-level revoke is forwarded.
    event VaultRevokeForwarded(address indexed sender, address indexed vault, bytes4 indexed selector, bytes data);

    /// @notice Address of the supervisor owner.
    address public owner;
    /// @notice Proposed supervisor owner waiting to accept ownership.
    address public pendingSupervisorOwner;
    /// @notice Timelock duration for sensitive actions, in seconds.
    uint256 public immutable TIMELOCK;
    /// @notice Execution timestamp for scheduled calldata.
    /// @dev Keyed by the exact calldata of the action.
    mapping(bytes data => uint256) public executableAt;
    /// @notice Map listing the vaults for which a new owner is submitted but not executed, if any
    mapping(address vault => address) public scheduledNewOwner;
    /// @notice Allowlist for vault owners that may add guardians for their vaults.
    mapping(address vaultOwner => bool) public allowedVaultOwners;
    /// @dev Set of vaults with at least one registered guardian.
    EnumerableSet.AddressSet private _vaults;
    /// @dev Per-vault guardian set.
    mapping(address vault => EnumerableSet.AddressSet) private _guardians;

    /// @dev Restricts execution to the supervisor owner.
    modifier onlyOwner() {
        require(msg.sender == owner, NotOwner());
        _;
    }

    /// @dev Restricts execution to a guardian registered for the vault.
    modifier onlyGuardian(address vault) {
        require(_guardians[vault].contains(msg.sender), NotGuardian());
        _;
    }

    /// @dev Restricts execution to the supervisor owner or a guardian registered for the vault.
    modifier onlyOwnerOrGuardian(address vault) {
        _checkOwnerOrGuardian(vault);
        _;
    }

    /// @notice Initializes the supervisor owner and timelock.
    /// @param timelock_ Timelock duration for sensitive actions, in seconds.
    constructor(uint256 timelock_) {
        require(timelock_ > 0, InvalidTimelock());
        owner = msg.sender;
        emit OwnershipTransferred(address(0), msg.sender);
        TIMELOCK = timelock_;
    }

    /// @notice Starts a two-step transfer of supervisor ownership.
    /// @param newOwner The proposed new owner address. Set to zero to cancel a pending transfer.
    function transferSupervisorOwnership(address newOwner) external onlyOwner {
        require(newOwner != owner, NoOp());
        require(newOwner != pendingSupervisorOwner, NoOp());

        pendingSupervisorOwner = newOwner;
        emit OwnershipTransferStarted(owner, newOwner);
    }

    /// @notice Accepts supervisor ownership as the pending owner.
    function acceptSupervisorOwnership() external {
        require(msg.sender == pendingSupervisorOwner, NotPendingSupervisorOwner());

        address previous = owner;
        owner = msg.sender;
        pendingSupervisorOwner = address(0);
        emit OwnershipTransferred(previous, msg.sender);
    }

    /// @notice Schedules a timelocked action.
    /// @param data Full calldata of the action to execute later.
    function submit(bytes calldata data) external onlyOwner {
        require(executableAt[data] == 0, DataAlreadyTimelocked());
        require(data.length >= 4, InvalidAmount());

        bytes4 selector = _selector(data);
        require(_selectorUsesVault(selector), UnsupportedSelector());
        address vault = _extractVaultAddress(selector, data);
        uint256 executeAfter = block.timestamp + TIMELOCK;

        if (selector == this.setOwner.selector) {
            (address vaultAddress, address newOwner) = _decodeCanonicalTwoAddressCalldata(selector, data);
            require(vaultAddress != address(0) && newOwner != address(0), ZeroAddress());
            require(newOwner != IVaultV2(vaultAddress).owner(), NoOp());
            require(scheduledNewOwner[vaultAddress] == address(0), OwnershipChangeAlreadyScheduled());
            scheduledNewOwner[vaultAddress] = newOwner;
        } else if (selector == this.removeGuardian.selector) {
            _decodeCanonicalTwoAddressCalldata(selector, data);
        }

        executableAt[data] = executeAfter;
        emit TimelockSubmitted(msg.sender, vault, selector, data, executeAfter);
    }

    /// @notice Cancels a pending timelocked action.
    /// @param data Full calldata that was previously submitted.
    function revoke(bytes calldata data) external {
        bytes4 selector = _selector(data);
        address vault = _extractVaultAddress(selector, data);
        _checkOwnerOrGuardian(vault);
        _revokeKnownVault(data, vault, selector);
    }

    /// @notice Cancels a pending guardian removal timelock.
    /// @param vault The vault whose guardian removal was submitted.
    /// @param guardian The guardian address being removed.
    function revokeGuardianRemoval(IVaultV2 vault, address guardian) external onlyOwnerOrGuardian(address(vault)) {
        address vaultAddress = address(vault);
        bytes memory data = abi.encodeWithSelector(VaultV2Supervisor.removeGuardian.selector, vault, guardian);
        _revokeKnownVault(data, vaultAddress, VaultV2Supervisor.removeGuardian.selector);
    }

    /// @notice Cancels a pending vault ownership transfer timelock.
    /// @param vault The vault whose ownership transfer was submitted.
    function revokeVaultOwnerChange(IVaultV2 vault) external onlyOwnerOrGuardian(address(vault)) {
        address vaultAddress = address(vault);
        bytes memory data =
            abi.encodeWithSelector(VaultV2Supervisor.setOwner.selector, vault, scheduledNewOwner[vaultAddress]);
        _revokeKnownVault(data, vaultAddress, VaultV2Supervisor.setOwner.selector);
    }

    /// @notice Sets the curator on a vault (no timelock).
    /// @param vault The vault to update.
    /// @param newCurator The new curator address.
    function setCurator(IVaultV2 vault, address newCurator) external onlyOwner {
        require(vault.curator() != newCurator, NoOp());
        vault.setCurator(newCurator);
    }

    /// @notice Sets the name on a vault (no timelock).
    /// @param vault The vault to update.
    /// @param newName The new name.
    function setName(IVaultV2 vault, string calldata newName) external onlyOwner {
        require(keccak256(bytes(vault.name())) != keccak256(bytes(newName)), NoOp());
        vault.setName(newName);
    }

    /// @notice Sets the symbol on a vault (no timelock).
    /// @param vault The vault to update.
    /// @param newSymbol The new symbol.
    function setSymbol(IVaultV2 vault, string calldata newSymbol) external onlyOwner {
        require(keccak256(bytes(vault.symbol())) != keccak256(bytes(newSymbol)), NoOp());
        vault.setSymbol(newSymbol);
    }

    /// @notice Sets the skim recipient on a vault adapter (no timelock).
    /// @param skimable The adapter to update.
    /// @param newSkimRecipient The new skim recipient address.
    function setSkimRecipient(address skimable, address newSkimRecipient) external onlyOwner {
        ISkimRecipient(skimable).setSkimRecipient(newSkimRecipient);
    }

    /// @notice Adds a sentinel on a vault (no timelock).
    /// @param vault The vault to update.
    /// @param account The sentinel address to add.
    function addSentinel(IVaultV2 vault, address account) external onlyOwner {
        vault.setIsSentinel(account, true);
    }

    /// @notice Allowlists or removes a vault owner for guardian self-management.
    /// @param vaultOwner The vault owner address.
    /// @param allowed Whether the owner is allowed to add guardians for its vaults.
    function setAllowedVaultOwner(address vaultOwner, bool allowed) external onlyOwner {
        require(vaultOwner != address(0), ZeroAddress());
        require(allowedVaultOwners[vaultOwner] != allowed, NoOp());
        allowedVaultOwners[vaultOwner] = allowed;
        emit AllowedVaultOwnerSet(vaultOwner, allowed);
    }

    /// @notice Adds a guardian for a vault.
    /// @param vault The vault to add the guardian for.
    /// @param guardian The guardian address to add.
    /// @dev Vault owners can call only if allowlisted; the supervisor owner can always call.
    function addGuardian(address vault, address guardian) external {
        require(vault != address(0) && guardian != address(0), ZeroAddress());

        address vaultOwner = IVaultV2(vault).owner();
        require(
            msg.sender == owner || (msg.sender == vaultOwner && allowedVaultOwners[vaultOwner]), OnlyOwnerOrVaultOwner()
        );

        require(_guardians[vault].add(guardian), NoOp());
        _vaults.add(vault);
        emit GuardianAdded(vault, guardian, msg.sender);
    }

    /// @notice Transfers vault ownership after the timelock.
    /// @param vault The vault to update.
    /// @param newOwner The new owner address.
    function setOwner(IVaultV2 vault, address newOwner) external onlyOwner {
        timelocked();
        require(vault.owner() != newOwner, NoOp());
        scheduledNewOwner[address(vault)] = address(0);
        vault.setOwner(newOwner);
    }

    /// @notice Removes a sentinel from a vault.
    /// @param vault The vault to update.
    /// @param sentinel The sentinel address to remove.
    function removeSentinel(IVaultV2 vault, address sentinel) external onlyOwner {
        require(sentinel != address(this), CannotRemoveSupervisorSentinel());
        vault.setIsSentinel(sentinel, false);
    }

    /// @notice Removes a guardian from a vault after the timelock.
    /// @param vault The vault to update.
    /// @param guardian The guardian address to remove.
    function removeGuardian(IVaultV2 vault, address guardian) external onlyOwner {
        timelocked();
        address vaultAddress = address(vault);

        require(_guardians[vaultAddress].remove(guardian), NoOp());

        if (_guardians[vaultAddress].length() == 0) {
            _vaults.remove(vaultAddress);
        }

        emit GuardianRemoved(vaultAddress, guardian, msg.sender);
    }

    /// @notice Forwards a revoke to the vault's timelock.
    /// @param vault The vault to target.
    /// @param data The vault calldata to revoke.
    function revoke(address vault, bytes calldata data) external onlyGuardian(vault) {
        IRevokable(vault).revoke(data);
        emit VaultRevokeForwarded(msg.sender, vault, _selector(data), data);
    }

    /// @notice Sets the supervisor contract as a sentinel on a vault (permissionless).
    /// @param vault The vault to update.
    /// @dev This makes sure the supervisor can revoke submitted operations on behalf of the guardians.
    /// @dev For a `Box` contract, the curator should add the supervisor as guardian manually.
    function setSupervisorAsSentinel(IVaultV2 vault) external {
        require(!vault.isSentinel(address(this)), NoOp());
        vault.setIsSentinel(address(this), true);
    }

    /// @notice Returns vaults with at least one registered guardian.
    function getVaults() external view returns (address[] memory) {
        return _vaults.values();
    }

    /// @notice Returns tracked vaults currently owned by this supervisor.
    function getOwnedVaults() external view returns (address[] memory ownedVaults) {
        ownedVaults = _filterVaultsByOwnership(true);
    }

    /// @notice Returns tracked vaults not currently owned by this supervisor.
    function getNonOwnedVaults() external view returns (address[] memory nonOwnedVaults) {
        nonOwnedVaults = _filterVaultsByOwnership(false);
    }

    /// @notice Returns the guardians registered for a vault.
    /// @param vault The vault to query.
    function getGuardians(address vault) external view returns (address[] memory) {
        return _guardians[vault].values();
    }

    /// @notice Returns whether a vault ownership transfer is currently scheduled.
    /// @param vault The vault to query.
    function isOwnershipChanging(address vault) external view returns (bool) {
        return scheduledNewOwner[vault] != address(0);
    }

    /// @notice Returns whether guardian removal is currently scheduled.
    /// @param vault The vault to query.
    /// @param guardian The guardian to query.
    function isGuardianBeingRemoved(address vault, address guardian) external view returns (bool) {
        bytes memory data = abi.encodeWithSelector(VaultV2Supervisor.removeGuardian.selector, vault, guardian);
        return executableAt[data] > 0;
    }

    /// @notice Returns whether a vault-like contract is currently supervised by this contract.
    /// @param vault The vault address to query.
    function isVaultSupervised(address vault) external view returns (bool) {
        return _ownerOf(vault) == address(this);
    }

    /// @dev Internal revoke routine when the vault and selector are known.
    function _revokeKnownVault(bytes memory data, address vault, bytes4 selector) internal {
        require(executableAt[data] != 0, DataNotTimelocked());

        if (selector == this.setOwner.selector) {
            scheduledNewOwner[vault] = address(0);
        }

        executableAt[data] = 0;
        emit TimelockRevoked(msg.sender, vault, selector, data);
    }

    /// @dev Validates and consumes the timelock for the current calldata.
    function timelocked() internal {
        uint256 eta = executableAt[msg.data];
        require(eta != 0, DataNotTimelocked());
        require(block.timestamp >= eta, TimelockNotExpired());
        executableAt[msg.data] = 0;
    }

    /// @dev Validates that caller is the supervisor owner or a guardian for the vault.
    function _checkOwnerOrGuardian(address vault) internal view {
        require(_guardians[vault].contains(msg.sender) || msg.sender == owner, OnlyOwnerOrGuardian());
    }

    /// @dev Returns tracked vaults filtered by ownership status.
    function _filterVaultsByOwnership(bool ownedBySupervisor) internal view returns (address[] memory vaults_) {
        uint256 length = _vaults.length();
        vaults_ = new address[](length);
        uint256 count;

        for (uint256 i; i < length; ++i) {
            address vault = _vaults.at(i);
            bool isOwned = _ownerOf(vault) == address(this);
            if (isOwned == ownedBySupervisor) {
                vaults_[count] = vault;
                ++count;
            }
        }

        assembly {
            mstore(vaults_, count)
        }
    }

    /// @dev Extracts the vault address from a supervisor calldata payload.
    /// @dev Returns address(0) for selectors that do not target a vault.
    /// @param selector Function selector parsed from data.
    /// @param data Full calldata (selector + ABI args) for a supervisor action.
    /// @return v The decoded vault address, or address(0) if data is too short.
    function _extractVaultAddress(bytes4 selector, bytes calldata data) internal pure returns (address v) {
        if (!_selectorUsesVault(selector)) return address(0);
        if (data.length < 36) return address(0);
        assembly {
            v := and(calldataload(add(data.offset, 4)), 0xffffffffffffffffffffffffffffffffffffffff)
        }
    }

    /// @dev Decodes two address arguments and enforces canonical ABI encoding (no trailing or dirty bytes).
    function _decodeCanonicalTwoAddressCalldata(bytes4 selector, bytes calldata data)
        internal
        pure
        returns (address first, address second)
    {
        require(data.length == 68, InvalidAmount());
        assembly {
            first := and(calldataload(add(data.offset, 4)), 0xffffffffffffffffffffffffffffffffffffffff)
            second := and(calldataload(add(data.offset, 36)), 0xffffffffffffffffffffffffffffffffffffffff)
        }
        bytes memory canonical = abi.encodeWithSelector(selector, first, second);
        require(keccak256(data) == keccak256(canonical), InvalidAmount());
    }

    function _selector(bytes calldata data) internal pure returns (bytes4 selector) {
        if (data.length < 4) return bytes4(0);
        assembly {
            selector := calldataload(data.offset)
        }
    }

    /// @dev Returns whether a selector encodes a vault as first argument.
    function _selectorUsesVault(bytes4 selector) internal pure returns (bool) {
        return selector == this.setOwner.selector || selector == this.removeGuardian.selector;
    }

    function _ownerOf(address vault) internal view returns (address owner_) {
        (bool success, bytes memory returnData) = vault.staticcall(abi.encodeWithSelector(IVaultV2.owner.selector));
        if (!success || returnData.length < 32) return address(0);
        owner_ = abi.decode(returnData, (address));
    }
}