VestingVault

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/vesting/VestingVault.sol": {
      "content": "// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2025 Fireblocks <support@fireblocks.com>
// Copyright (C) 2025 Sovryn <support@sovryn.com> - Modifications
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
pragma solidity 0.8.27;

import { IVestingVault } from "./interfaces/IVestingVault.sol";
import { IVestingVaultErrors } from "./interfaces/IVestingVaultErrors.sol";

import { LibErrors } from "./library/Errors/LibErrors.sol";
import { BoundedRoleMembershipUpgradeable } from "./library/Utils/BoundedRoleMembershipUpgradeable.sol";
import { SalvageUpgradeable } from "./library/Utils/SalvageUpgradeable.sol";
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol";

import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { ContextUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/**
 * @title VestingVault
 * @author Fireblocks; Sovryn
 * @notice This contract manages token vesting schedules with support for multiple periods per schedule
 *         and granular claiming at beneficiary, schedule, and period levels
 *
 * @dev The contract manages its own schedule IDs, simplifying data validation and analytics.
 *      Schedules are stored in a mapping by ID and beneficiaries have arrays of their schedule IDs.
 *      Each schedule can contain multiple vesting periods with different parameters.
 *
 *      The contract supports both global and individual vesting modes:
 *      - Global mode: All schedules start relative to a global start time
 *      - Individual mode: Each schedule has absolute start/end times
 *
 *      This contract is upgradeable using the UUPS (Universal Upgradeable Proxy Standard) pattern.
 *      Only accounts with DEFAULT_ADMIN_ROLE can upgrade the contract.
 *
 *      Access control roles and member count limits:
 *      - VESTING_ADMIN_ROLE: Can create schedules and start global vesting. Maximum 1 account.
 *      - FORFEITURE_ADMIN_ROLE: Can cancel vesting schedules. Granted to default admin at initialization. No account limit.
 *      - DEFAULT_ADMIN_ROLE: Can manage roles, perform salvage operations, and upgrade the contract. No account limit.
 *      - SALVAGE_ROLE: Can salvage tokens and gas from the contract. Granted to default admin at initialization. No account limit.
 *
 *      Limitations:
 *      - Rebasing tokens are NOT supported. This contract assumes token balances remain constant
 *        except through explicit transfers. Rebasing tokens automatically adjust all holder balances (up or down),
 *        which would break vesting accounting as the contract tracks fixed amounts at schedule creation. Using
 *        rebasing tokens will lead to potential loss of funds.
 *      - Fee-on-transfer tokens are NOT supported. This contract assumes that when tokens are transferred,
 *        the full amount is received. Fee-on-transfer tokens automatically deduct fees during transfers,
 *        which would break vesting accounting as the contract would receive less tokens than expected.
 *        Using fee-on-transfer tokens will lead to incorrect vesting calculations and potential loss of funds.
 *      - Maximum of (2^32 - 1) vesting schedules.
 *      - Each vesting schedule can have at most 256 periods.
 *
 * @custom:security-contact support@fireblocks.com, support@sovryn.com
 */
contract VestingVault is
    ContextUpgradeable,
    BoundedRoleMembershipUpgradeable,
    SalvageUpgradeable,
    IVestingVault,
    IVestingVaultErrors,
    UUPSUpgradeable
{
    using SafeERC20 for IERC20;

    /// Constants

    /**
     * @notice The Access Control identifier for the Vesting Admin Role.
     * @dev Accounts with this role can create schedules and start global vesting
     */
    bytes32 public constant VESTING_ADMIN_ROLE = keccak256("VESTING_ADMIN_ROLE");

    /**
     * @notice The Access Control identifier for the Forfeiture Admin Role.
     * @dev Accounts with this role can cancel vesting schedules
     */
    bytes32 public constant FORFEITURE_ADMIN_ROLE = keccak256("FORFEITURE_ADMIN_ROLE");

    /**
     * @notice The Access Control identifier for the Salvager Role.
     * @dev An account with "SALVAGE_ROLE" can salvage tokens and gas.
     */
    bytes32 public constant SALVAGE_ROLE = keccak256("SALVAGE_ROLE");

    /**
     * @notice Maximum relative time threshold for global vesting mode. This means that when the global vesting mode
     *         is enabled, schedule periods will be limited to starting within the next ≈ 31.7 years after
     *         `globalVestingStartTime`.
     *
     * @dev Prevents accidental use of absolute Unix timestamps instead of relative offsets.
     *      Value of 1e9 seconds ≈ 31.7 years is considered a sensible upper bound for relative times.
     */
    uint256 private constant MAX_RELATIVE_TIME_THRESHOLD = 1e9;

    /**
     * @notice Maximum duration allowed for any vesting period (start to end time difference).
     * @dev This constant limits the duration of individual vesting periods to prevent excessively long vesting
     *      schedules as a result of incorrect input. Applied to both global and individual vesting modes.
     *      Value of 1.6e9 seconds represents ≈ 50.7 years.
     */
    uint256 private constant MAX_DURATION = 1.6e9;

    /// State - Mutable (removed immutable for upgradeability)

    /**
     * @notice The ERC20 token being vested
     * @dev Set during initialization and can be changed through upgrades
     */
    IERC20 public vestingToken;

    /**
     * @notice Whether the contract operates in global vesting mode
     * @dev In global mode, schedule times are relative to globalVestingStartTime
     *      In individual mode, schedule times are absolute timestamps
     */
    bool public globalVestingMode;

    /// State - Mutable

    /**
     * @notice Timestamp when global vesting started
     * @dev Only relevant when globalVestingMode is true
     */
    uint64 public globalVestingStartTime;

    /**
     * @notice Whether global vesting has been started
     * @dev Used to track if the global vesting has begun
     */
    bool public globalVestingStarted;

    /**
     * @notice Counter for generating unique schedule IDs
     * @dev Incremented for each new schedule created
     */
    uint32 public scheduleCounter;

    /**
     * @notice Mapping from schedule ID to schedule data
     * @dev Primary storage for all schedules
     */
    mapping(uint256 scheduleId => Schedule schedule) internal _scheduleById;

    /**
     * @notice Mapping from beneficiary address to their schedule IDs
     * @dev Enables querying all schedules for a beneficiary
     */
    mapping(address beneficiary => uint32[] scheduleIds) internal _beneficiaryToScheduleIds;

    /**
     * @notice Mapping from schedule ID to the period bitmap
     * @dev Enables checking which periods are still active for a schedule (not fully claimed or forfeited)
     *      Limited to 256 periods per schedule for simplicity. Each bit represents a period index.
     */
    mapping(uint32 scheduleId => uint256 activePeriodsBitmap) internal _scheduleIdToActivePeriodsBitmap;

    /**
     * @notice Current amount of tokens committed to vesting schedules
     * @dev Represents the sum of all unclaimed tokens across all active schedules.
     *      Increases when schedules are created, decreases when tokens are released or forfeited.
     */
    uint256 public committedTokens;

    /// Functions

    /**
     * @notice Constructor to disable initializers on implementation contract
     * @dev This prevents the implementation contract from being initialized
     */
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    /**
     * @notice Initializes the VestingVault contract. It receives the token to manage, accounts for RBAC,
     *         and defines whether to use global vesting mode. The token must be a valid ERC20 contract
     *         with a non-zero total supply.
     *
     * @dev Initializer function that sets up the token to manage in the vault, RBAC, and Global Vesting Mode.
     *
     * The FORFEITURE_ADMIN_ROLE and SALVAGE_ROLE are granted to the default admin at initialization time to ensure
     * immediate functionality for vesting cancellation and emergency token recovery.
     *
     * Calling Conditions:
     *
     * - `vestingToken_` must be a contract address (non-zero bytecode)
     * - `vestingToken_` must implement the `totalSupply()` function and return a value > 0
     * - `defaultAdmin` must not be the zero address
     * - `vestingAdmin` must not be the zero address
     *
     * Warning:
     * - This contract does not support rebasing tokens. Using rebasing tokens will lead to potential loss of funds.
     *
     * @param vestingToken_ Address of the ERC20 token to be vested
     * @param globalVestingMode_ Whether to use global vesting mode
     * @param defaultAdmin_ Address to be granted DEFAULT_ADMIN_ROLE
     * @param vestingAdmin_ Address to be granted VESTING_ADMIN_ROLE
     */
    function initialize(address vestingToken_, bool globalVestingMode_, address defaultAdmin_, address vestingAdmin_)
        external
        initializer
    {
        require(defaultAdmin_ != address(0), LibErrors.InvalidAddress());
        require(vestingAdmin_ != address(0), LibErrors.InvalidAddress());
        require(vestingToken_ != address(0), LibErrors.InvalidAddress());

        // Validate that vestingToken_ passes common ERC20 implementation checks
        require(vestingToken_.code.length > 0, LibErrors.AddressEmptyCode(vestingToken_));
        // Check if totalSupply() function exists and returns > 0
        (bool success, bytes memory data) = vestingToken_.staticcall(abi.encodeWithSignature("totalSupply()"));
        require(success && data.length > 0, LibErrors.InvalidImplementation());
        uint256 totalSupply = abi.decode(data, (uint256));
        require(totalSupply > 0, LibErrors.InvalidImplementation());

        vestingToken = IERC20(vestingToken_);

        // Validate that global vesting mode can only be set when no schedules exist
        // This prevents timestamp interpretation conflicts between existing and new schedules
        if (globalVestingMode_) {
            require(scheduleCounter == 0, IVestingVaultErrors.GlobalVestingModeConflict());
        }

        globalVestingMode = globalVestingMode_;

        // Initialize inherited contracts
        __AccessControlEnumerable_init(defaultAdmin_, vestingAdmin_);

        // Initialize salvage functionality and grant roles
        __Salvage_init(defaultAdmin_);
    }

    /**
     * @notice Override of Salvage initialization to grant SALVAGE_ROLE
     * @dev This function is called during contract initialization to set up salvage functionality
     * @dev SALVAGE_ROLE is for withdrawing accidentally sent tokens (not vesting tokens)
     * @param defaultAdmin_ The address to grant salvage role to
     */
    function __Salvage_init(address defaultAdmin_) internal virtual onlyInitializing {
        super.__Salvage_init();

        // Grant salvage role to the default admin
        // This role allows withdrawing accidentally sent tokens (not vesting tokens)
        _grantRole(SALVAGE_ROLE, defaultAdmin_);
    }

    /**
     * @notice Initialize access control roles for vesting operations
     * @dev This function grants DEFAULT_ADMIN_ROLE, VESTING_ADMIN_ROLE, and FORFEITURE_ADMIN_ROLE
     * @dev These roles are separate from SALVAGE_ROLE and are used for vesting operations
     * @param defaultAdmin_ The address to grant DEFAULT_ADMIN_ROLE and FORFEITURE_ADMIN_ROLE to
     * @param vestingAdmin_ The address to grant VESTING_ADMIN_ROLE to
     */
    function __AccessControlEnumerable_init(address defaultAdmin_, address vestingAdmin_)
        internal
        virtual
        onlyInitializing
    {
        // Grant default admin role for overall contract administration
        _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin_);

        // Grant vesting admin role for creating vesting schedules
        _grantRole(VESTING_ADMIN_ROLE, vestingAdmin_);

        // Grant forfeiture admin role for cancelling vesting schedules
        // This role is separate from salvage operations and vesting admin operations
        _grantRole(FORFEITURE_ADMIN_ROLE, defaultAdmin_);
    }

    /**
     * @notice Authorizes contract upgrades
     * @dev Only accounts with DEFAULT_ADMIN_ROLE can upgrade the contract
     * @param newImplementation The address of the new implementation contract
     */
    function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {
        // Only DEFAULT_ADMIN_ROLE can upgrade the contract
    }

    /// External Functions - Vesting Management

    /**
     * @notice Creates a vesting schedule for a beneficiary
     * @dev Creates a new vesting schedule with the specified periods. Each schedule gets a unique ID.
     *      All vesting periods must have non-zero amounts as zero-amount periods provide no value
     *      and can create confusion in vesting calculations and gas inefficiencies.
     *
     * Calling Conditions:
     *
     * - The caller must have `VESTING_ADMIN_ROLE`
     * - `beneficiary` must not be the zero address
     * - `periods` array must not be empty and can have at most 256 elements
     *
     * Additional validations that may cause reverts:
     * - The contract must have sufficient token balance to cover the total schedule amount
     * - If global vesting mode is off, each period's `startPeriod` must be >= current timestamp
     * - For each period:
     *   - `amount` must be greater than zero
     *   - `endPeriod` must be greater than `startPeriod`
     *   - If the period has a cliff, `startPeriod + cliff` must not exceed `endPeriod`
     *
     * Note maximum 4,294,967,295 (2^32 - 1) vesting schedules can be created due to uint32 scheduleCounter, though
     * this limit is unlikely to be reached in practice.
     *
     * @param beneficiary Address of the beneficiary who will receive vested tokens
     * @param isCancellable Whether this schedule can be cancelled by admins
     * @param periods Array of vesting periods for the schedule
     * @return scheduleId The ID of the created schedule
     */
    function createSchedule(address beneficiary, bool isCancellable, VestingPeriodParam[] memory periods)
        external
        virtual
        override
        onlyRole(VESTING_ADMIN_ROLE)
        returns (uint32 scheduleId)
    {
        return _createSchedule(beneficiary, isCancellable, periods);
    }

    /**
     * @notice Creates multiple vesting schedules for multiple beneficiaries in a single transaction.
     * @dev Calls `createSchedule` for each beneficiary with the provided periods and cancellable flag. Each beneficiary receives their own schedule.
     * @dev WARNING It DOESN'T check for the total amount of tokens required for all schedules in advance. Make sure the contract has sufficient balance before calling.
     * @dev WARNING It DOESN'T check for duplicates - if the same beneficiary appears multiple times, they will receive multiple schedules.
     *
     * Calling Conditions:
     * - The caller must have `VESTING_ADMIN_ROLE`.
     * - `beneficiaries`, `isCancellableList`, and `periodsList` arrays must have the same length and not be empty.
     * - Each beneficiary must not be the zero address.
     * - Each periods array must not be empty and can have at most 256 elements.
     * - The contract must have sufficient token balance to cover all schedules.
     *
     * Additional validations are performed by `createSchedule` for each beneficiary.
     *
     * @param beneficiaries Array of beneficiary addresses to receive vesting schedules.
     * @param isCancellableList Array of booleans, each indicating whether the corresponding schedule can be cancelled by admins.
     * @param periodsList Array of arrays, where each inner array contains the vesting periods for the corresponding beneficiary.
     * @return scheduleIds Array of created schedule IDs, in the same order as beneficiaries.
     */
    function createMultipleSchedules(
        address[] calldata beneficiaries,
        bool[] calldata isCancellableList,
        VestingPeriodParam[][] memory periodsList
    ) external virtual override onlyRole(VESTING_ADMIN_ROLE) returns (uint32[] memory scheduleIds) {
        uint256 len = beneficiaries.length;
        require(len > 0, LibErrors.InvalidArrayLength(len));
        require(periodsList.length == len, LibErrors.InvalidArrayLength(periodsList.length));
        require(isCancellableList.length == len, LibErrors.InvalidArrayLength(isCancellableList.length));

        // Now create each schedule (each _createSchedule will update committedTokens)
        scheduleIds = new uint32[](len);
        for (uint256 i = 0; i < len; ++i) {
            VestingPeriodParam[] memory periodsMem = periodsList[i];
            scheduleIds[i] = _createSchedule(beneficiaries[i], isCancellableList[i], periodsMem);
        }
    }

    /**
     * @dev Internal function containing the logic for creating a vesting schedule.
     *      Used by both createSchedule (external) and createSchedules (batch).
     */
    function _createSchedule(address beneficiary, bool isCancellable, VestingPeriodParam[] memory periods)
        internal
        returns (uint32 scheduleId)
    {
        // Validate inputs
        uint256 periodsLength = periods.length;
        require(beneficiary != address(0), LibErrors.InvalidAddress());
        require(periodsLength > 0 && periodsLength <= 256, LibErrors.InvalidArrayLength(periodsLength));

        // Generate new schedule ID and initialize storage
        scheduleId = ++scheduleCounter;
        Schedule storage newSchedule = _scheduleById[scheduleId];
        newSchedule.id = scheduleId;
        newSchedule.beneficiary = beneficiary;
        newSchedule.isCancellable = isCancellable;
        newSchedule.isCancelled = false;

        // Validate periods and copy to storage in single loop
        uint256 totalAmount = 0;
        for (uint256 i = 0; i < periodsLength; ++i) {
            VestingPeriodParam memory period = periods[i];

            // Validate period amount
            require(period.amount > 0, LibErrors.ZeroAmount());

            // Validate period times
            require(period.endPeriod > period.startPeriod, IVestingVaultErrors.InvalidEndTime(i, period.endPeriod));
            // Validate maximum duration for vesting period
            uint256 vestingDuration = period.endPeriod - period.startPeriod;
            require(vestingDuration <= MAX_DURATION, IVestingVaultErrors.InvalidDuration(i, vestingDuration));
            // Validate cliff doesn't exceed vesting duration
            if (period.cliff > 0) {
                require(
                    period.startPeriod + period.cliff <= period.endPeriod,
                    IVestingVaultErrors.InvalidCliff(i, period.cliff)
                );
            }
            // In global mode, validate startPeriod isn't accidentally a Unix timestamp
            if (globalVestingMode) {
                require(
                    period.startPeriod <= MAX_RELATIVE_TIME_THRESHOLD,
                    IVestingVaultErrors.InvalidStartTime(i, period.startPeriod)
                );
            }
            // In non-global mode, validate start time is not in the past
            if (!globalVestingMode) {
                require(
                    period.startPeriod >= block.timestamp, IVestingVaultErrors.InvalidStartTime(i, period.startPeriod)
                );
            }

            VestingPeriod memory newPeriod = VestingPeriod({
                startPeriod: period.startPeriod,
                endPeriod: period.endPeriod,
                cliff: period.cliff,
                amount: period.amount,
                claimedAmount: 0
            });

            // Store period and accumulate total amount
            newSchedule.periods.push(newPeriod);
            totalAmount += period.amount;
            // Set period as active in bitmap for gas optimization
            _scheduleIdToActivePeriodsBitmap[scheduleId] |= (1 << i);
        }

        // Check contract has sufficient uncommitted balance
        uint256 contractBalance = vestingToken.balanceOf(address(this));
        uint256 availableBalance = contractBalance - committedTokens;
        require(
            availableBalance >= totalAmount, IVestingVaultErrors.InsufficientBalance(contractBalance, availableBalance)
        );

        // Add schedule ID to beneficiary's list and update committed tokens
        _beneficiaryToScheduleIds[beneficiary].push(scheduleId);
        committedTokens += totalAmount;

        // Emit event with the complete schedule
        emit VestingScheduleCreated(_msgSender(), beneficiary, scheduleId, newSchedule);
    }

    /**
     * @notice Starts global vesting for all schedules
     * @dev Marks the beginning of the global vesting period. All schedules' periods will be
     *      calculated relative to this timestamp.
     *
     * Calling Conditions:
     *
     * - The caller must have `VESTING_ADMIN_ROLE`
     * - `globalVestingMode` must be true
     * - Global vesting must not have already been started
     * - If startTimestamp is provided (non-zero), it must be a valid future timestamp
     *
     * Emits a {GlobalVestingStarted} event.
     *
     * @param startTimestamp The timestamp to start global vesting. Use 0 to use current block timestamp.
     */
    function startGlobalVesting(uint256 startTimestamp) external virtual override onlyRole(VESTING_ADMIN_ROLE) {
        // Ensure global vesting mode is enabled
        require(globalVestingMode, IVestingVaultErrors.GlobalVestingNotEnabled());
        // Check global vesting hasn't already started
        require(!globalVestingStarted, IVestingVaultErrors.GlobalVestingAlreadyStarted());

        // Determine the start timestamp
        uint256 vestingStartTime;
        if (startTimestamp == 0) {
            vestingStartTime = block.timestamp;
        } else {
            // Allow past timestamps
            vestingStartTime = startTimestamp;
        }

        // Set global vesting as started
        globalVestingStarted = true;
        globalVestingStartTime = uint64(vestingStartTime);
        // Emit event
        emit GlobalVestingStarted(vestingStartTime);
    }

    /// External Functions - Token Claiming (Beneficiary Level)

    /**
     * @notice Claims all available vested tokens for the caller. Only the beneficiary
     *         can claim their own tokens.
     * @dev Claims from all schedules and periods belonging to the caller.
     *
     * Calling Conditions:
     *
     * - If global vesting mode is enabled, global vesting must have been started
     * - The caller must have vesting schedules
     *
     * Reverts if:
     *
     * - There are no schedules for the caller
     * - There are no tokens available to be claimed for the any of the beneficiary's schedules
     *
     * Emits {TokenRelease} events for each schedule and period with claimable tokens.
     */
    function claim() external virtual override {
        _claimAllForBeneficiary(_msgSender());
    }

    /**
     * @notice Internal function to claim all available vested tokens for a beneficiary
     * @dev This function contains the core claiming logic that is reused by claim()
     *
     * @param beneficiary The beneficiary address to claim tokens for
     */
    function _claimAllForBeneficiary(address beneficiary) internal virtual {
        _validateGlobalVestingStatus();

        uint32[] memory scheduleIds = _beneficiaryToScheduleIds[beneficiary];
        uint256 scheduleCount = scheduleIds.length;

        require(scheduleCount > 0, IVestingVaultErrors.NoTokensToClaim());

        uint256 totalClaimable = 0;

        // Process each schedule
        for (uint256 i = 0; i < scheduleCount; ++i) {
            uint32 scheduleId = scheduleIds[i];
            Schedule storage schedule = _scheduleById[scheduleId];
            // Skip cancelled schedules
            if (schedule.isCancelled) {
                continue;
            }
            uint256 periodBitmap = _scheduleIdToActivePeriodsBitmap[scheduleId];
            // Skip if no active periods
            if (periodBitmap == 0) {
                continue;
            }
            uint256 numPeriods = schedule.periods.length;
            // Process schedule periods
            for (uint256 j = 0; j < numPeriods; ++j) {
                // Check if this specific period is active using bit position
                if ((periodBitmap >> j) & 1 == 1) {
                    uint256 claimableAmount = _claim(schedule, j);
                    totalClaimable += claimableAmount;
                }
            }
        }

        require(totalClaimable > 0, IVestingVaultErrors.NoTokensToClaim());
        // Transfer all claimable tokens
        _processTokenRelease(beneficiary, totalClaimable);
    }

    /**
     * @notice Claims available vested tokens for a specific schedule. Only the beneficiary
     *         of the schedule can claim their tokens.
     * @dev Claims from all periods within the specified schedule.
     *
     * Calling Conditions:
     *
     * - If global vesting mode is enabled, global vesting must have been started
     * - The schedule must exist
     * - The schedule must belong to the caller
     *
     * Reverts if:
     * - There are no tokens available to be claimed for the specified schedule
     * - The schedule is cancelled
     *
     * Emits {TokenRelease} events for each period with claimable tokens as part of {_claim}.
     *
     * @param scheduleId The ID of the schedule to claim from
     */
    function claimSchedule(uint256 scheduleId) external virtual override {
        _validateGlobalVestingStatus();

        Schedule storage schedule = _scheduleById[scheduleId];
        address scheduleBeneficiary = schedule.beneficiary;
        // Validate schedule exists
        require(schedule.id != 0, LibErrors.NotFound(scheduleId));
        // Validate schedule belongs to caller
        require(_msgSender() == scheduleBeneficiary, LibErrors.UnauthorizedCaller());
        // Validate schedule is not cancelled
        require(!schedule.isCancelled, IVestingVaultErrors.CancelledSchedule(scheduleId));

        uint256 totalClaimable = 0;
        uint256 periodBitmap = _scheduleIdToActivePeriodsBitmap[uint32(scheduleId)];
        // Validate there are active periods
        require(periodBitmap > 0, IVestingVaultErrors.NoTokensToClaim());

        uint256 numPeriods = schedule.periods.length;
        // Process schedule periods
        for (uint256 i = 0; i < numPeriods; ++i) {
            // Check if this specific period is active using bit position
            if ((periodBitmap >> i) & 1 == 1) {
                uint256 claimableAmount = _claim(schedule, i);
                totalClaimable += claimableAmount;
            }
        }

        require(totalClaimable > 0, IVestingVaultErrors.NoTokensToClaim());
        // Transfer all claimable tokens
        _processTokenRelease(scheduleBeneficiary, totalClaimable);
    }

    /**
     * @notice Claims available vested tokens for a specific period within a schedule. Only the beneficiary
     *         of the schedule can claim their tokens.
     * @dev Most granular claim function - claims from a single period.
     *
     * Calling Conditions:
     *
     * - If global vesting mode is enabled, global vesting must have been started
     * - The schedule must exist
     * - The schedule must belong to the caller
     * - The period index must be valid, i.e. within the periods array bounds
     *
     * Reverts if:
     * - There are no tokens available to be claimed for the specified period
     * - The schedule is cancelled
     *
     * Emits a {TokenRelease} event as part of {_claim}.
     *
     * @param scheduleId The ID of the schedule
     * @param periodIndex The index of the period within the schedule
     */
    function claimSchedulePeriod(uint256 scheduleId, uint256 periodIndex) external virtual override {
        _validateGlobalVestingStatus();

        Schedule storage schedule = _scheduleById[scheduleId];
        address scheduleBeneficiary = schedule.beneficiary;
        // Validate schedule exists
        require(schedule.id != 0, LibErrors.NotFound(scheduleId));
        // Validate schedule belongs to caller
        require(_msgSender() == scheduleBeneficiary, LibErrors.UnauthorizedCaller());
        // Validate schedule is not cancelled
        require(!schedule.isCancelled, IVestingVaultErrors.CancelledSchedule(scheduleId));
        // Check if periodIndex is within bounds
        require(
            periodIndex < schedule.periods.length,
            IVestingVaultErrors.InvalidVestingPeriodIndex(scheduleId, periodIndex)
        );
        // Process the specific period
        uint256 claimableAmount = _claim(schedule, periodIndex);
        require(claimableAmount > 0, IVestingVaultErrors.NoTokensToClaim());
        // Transfer tokens
        _processTokenRelease(scheduleBeneficiary, claimableAmount);
    }

    /// External Functions - Schedule Cancellation

    /**
     * @notice Cancels a vesting schedule by distributing vested tokens and withdrawing unvested tokens
     * @dev Calculates vested amounts up to the current time, transfers them to the beneficiary,
     *      and reclaims unvested tokens to the admin. The schedule is permanently marked as cancelled.
     *
     * Calling Conditions:
     *
     * - The caller must have `FORFEITURE_ADMIN_ROLE`
     * - The schedule must exist
     * - The schedule must be marked as `isCancellable`
     * - The schedule must not already be cancelled
     *
     * Emits a {VestingScheduleCancelled} event.
     *
     * @param scheduleId The ID of the schedule to cancel
     * @param beneficiary The beneficiary address to validate against
     */
    function cancelSchedule(uint256 scheduleId, address beneficiary)
        external
        virtual
        override
        onlyRole(FORFEITURE_ADMIN_ROLE)
    {
        _cancelScheduleInternal(scheduleId, beneficiary, beneficiary);
    }

    /**
     * @notice Emergency function to cancel a schedule and send all tokens to the caller (admin)
     * @dev This function is designed for cases where beneficiaries lose access to their wallets
     *      or addresses are compromised. It sends both vested and unvested tokens to the admin.
     *
     * Calling Conditions:
     *
     * - The caller must have `FORFEITURE_ADMIN_ROLE`
     * - The schedule must exist
     * - The schedule must be marked as `isCancellable`
     * - The schedule must not already be cancelled
     *
     * Emits a {VestingScheduleCancelled} event.
     *
     * @param scheduleId The ID of the schedule to cancel
     * @param beneficiary The beneficiary address to validate against
     */
    function cancelScheduleRescueVested(uint256 scheduleId, address beneficiary)
        external
        virtual
        override
        onlyRole(FORFEITURE_ADMIN_ROLE)
    {
        _cancelScheduleInternal(scheduleId, beneficiary, _msgSender());
    }

    /**
     * @notice Internal function to cancel a vesting schedule with configurable token recipient
     * @dev This function handles the common cancellation logic for both normal cancellation
     *      and rescue operations. It calculates vested amounts and distributes tokens accordingly.
     *
     * @param scheduleId The ID of the schedule to cancel
     * @param beneficiary The beneficiary address to validate against
     * @param vestedTokenRecipient The address to receive vested tokens (beneficiary or admin)
     */
    function _cancelScheduleInternal(uint256 scheduleId, address beneficiary, address vestedTokenRecipient)
        internal
        virtual
    {
        Schedule storage schedule = _scheduleById[scheduleId];

        // Validate schedule exists
        require(schedule.id != 0, LibErrors.NotFound(scheduleId));
        // Validate that the schedule belongs to the specified beneficiary
        require(schedule.beneficiary == beneficiary, IVestingVaultErrors.BeneficiaryMismatch(scheduleId, beneficiary));
        // Check if schedule is cancellable
        require(schedule.isCancellable, IVestingVaultErrors.IrrevocableSchedule(scheduleId));
        // Check if already cancelled
        require(!schedule.isCancelled, IVestingVaultErrors.CancelledSchedule(scheduleId));
        // Cache beneficiary address
        address scheduleBeneficiary = schedule.beneficiary;

        uint256 claimAmount = 0;
        uint256 forfeitAmount = 0;

        // Calculate vested and total amounts across all periods
        uint256 periodsLength = schedule.periods.length;
        for (uint256 i = 0; i < periodsLength; ++i) {
            VestingPeriod storage period = schedule.periods[i];

            // Skip fully claimed periods
            if (period.claimedAmount >= period.amount) {
                continue;
            }
            // Claim any vested amount
            uint256 claimableAmount = _claim(schedule, i);
            claimAmount += claimableAmount;

            // Calculate unvested amount (total - already claimed)
            uint256 unvestedAmount = period.amount - period.claimedAmount;
            forfeitAmount += unvestedAmount;

            // Mark the full period as claimed to prevent future claims
            period.claimedAmount = period.amount;
        }
        // Mark schedule as cancelled
        schedule.isCancelled = true;

        // Transfer any unclaimed vested tokens to the specified recipient
        if (claimAmount > 0) {
            _processTokenRelease(vestedTokenRecipient, claimAmount);
        }
        // Transfer unvested tokens back to admin
        if (forfeitAmount > 0) {
            _processTokenRelease(_msgSender(), forfeitAmount);
        }
        // Emit event
        emit VestingScheduleCancelled(_msgSender(), scheduleBeneficiary, scheduleId, claimAmount, forfeitAmount);
    }

    /// External Functions - View Functions

    /**
     * @notice Returns all schedules for a beneficiary
     * @dev Returns a copy of the schedules array to prevent external modification.
     *      Includes both active and cancelled schedules.
     *
     * Note: This operation will copy the entire storage to memory, which can be quite expensive. This is designed to
     * mostly be used by view accessors that are queried without any gas fees (e.g. off-chain via RPC). Do not call
     * this function from other contracts. Instead, the recommended pattern is use `getScheduleIds` if the goal is to
     * enumerate the schedules for a beneficiary
     *
     * @param beneficiary The beneficiary address
     * @return schedules Array of schedules for the beneficiary
     */
    function getSchedules(address beneficiary) external view virtual override returns (Schedule[] memory schedules) {
        uint32[] memory scheduleIds = _beneficiaryToScheduleIds[beneficiary];
        schedules = new Schedule[](scheduleIds.length);

        for (uint256 i = 0; i < scheduleIds.length; ++i) {
            schedules[i] = _scheduleById[scheduleIds[i]];
        }

        return schedules;
    }

    /**
     * @notice Returns only cancellable schedules for a beneficiary
     * @dev Returns a copy of the cancellable schedules array to prevent external modification.
     *      Only includes schedules that are cancellable and not already cancelled.
     *
     * Note: This operation will copy the entire storage to memory, which can be quite expensive. This is designed to
     * mostly be used by view accessors that are queried without any gas fees (e.g. off-chain via RPC). Do not call
     * this function from other contracts.
     *
     * @param beneficiary The beneficiary address
     * @return schedules Array of cancellable schedules for the beneficiary
     */
    function getCancellableSchedules(address beneficiary)
        external
        view
        virtual
        override
        returns (Schedule[] memory schedules)
    {
        uint32[] memory scheduleIds = _beneficiaryToScheduleIds[beneficiary];

        // First pass: count truly cancellable schedules
        uint256 cancellableCount = 0;
        for (uint256 i = 0; i < scheduleIds.length; ++i) {
            if (_isTrulyCancellable(scheduleIds[i])) {
                cancellableCount++;
            }
        }

        // Create array with correct size
        schedules = new Schedule[](cancellableCount);

        // Second pass: populate array with truly cancellable schedules
        uint256 scheduleIndex = 0;
        for (uint256 i = 0; i < scheduleIds.length; ++i) {
            if (_isTrulyCancellable(scheduleIds[i])) {
                schedules[scheduleIndex] = _scheduleById[scheduleIds[i]];
                scheduleIndex++;
            }
        }

        return schedules;
    }

    /**
     * @notice Returns true if the schedule is cancellable and has at least one active period with locked amount > 0
     * @dev Used to filter schedules for getCancellableSchedules
     * @param scheduleId The schedule ID
     */
    function _isTrulyCancellable(uint32 scheduleId) internal view returns (bool) {
        Schedule storage schedule = _scheduleById[scheduleId];
        if (!schedule.isCancellable || schedule.isCancelled) {
            return false;
        }
        uint256 periodBitmap = _scheduleIdToActivePeriodsBitmap[scheduleId];
        if (periodBitmap == 0) {
            return false;
        }
        uint256 numPeriods = schedule.periods.length;
        for (uint256 i = 0; i < numPeriods; ++i) {
            if ((periodBitmap >> i) & 1 == 1) {
                VestingPeriod storage period = schedule.periods[i];
                if (period.amount > period.claimedAmount) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * @notice Returns a specific schedule by ID
     * @dev Retrieves the complete schedule information including all periods.
     *
     * Note this view function reverts if:
     *
     * - The schedule with `scheduleId` does not exist
     *
     * @param scheduleId The ID of the schedule
     * @return schedule The schedule with the specified ID
     */
    function getSchedule(uint32 scheduleId) external view virtual override returns (Schedule memory schedule) {
        schedule = _scheduleById[scheduleId];
        require(schedule.id != 0, LibErrors.NotFound(scheduleId));
        return schedule;
    }

    /**
     * @notice Returns all schedule IDs for a beneficiary
     * @dev Returns a copy of the schedule IDs array. Useful for iterating through
     *      a beneficiary's schedules without loading all schedule data.
     *
     * @param beneficiary The beneficiary address
     * @return scheduleIds Array of schedule IDs for the beneficiary
     */
    function getScheduleIds(address beneficiary) external view virtual override returns (uint32[] memory scheduleIds) {
        return _beneficiaryToScheduleIds[beneficiary];
    }

    /**
     * @notice Returns the total claimable amount for a beneficiary across all schedules
     * @dev Calculates the sum of all vested but unclaimed tokens. Note that cancelled schedules
     *      are fully accounted for as claimed, so they do not contribute to the claimable amount.
     *
     * @param beneficiary The beneficiary address
     * @return claimableAmount Total amount that can be claimed
     */
    function getClaimableAmount(address beneficiary) external view virtual override returns (uint256 claimableAmount) {
        uint32[] memory scheduleIds = _beneficiaryToScheduleIds[beneficiary];

        // If global vesting mode is enabled but not started, return 0
        if (globalVestingMode && !globalVestingStarted) {
            return 0;
        }

        // Sum claimable amounts across all schedules
        for (uint256 i = 0; i < scheduleIds.length; ++i) {
            Schedule memory schedule = _scheduleById[scheduleIds[i]];
            claimableAmount += _getClaimableAmountForSchedule(schedule);
        }
    }

    /**
     * @notice Returns the claimable amount for a specific schedule
     * @dev Calculates the sum of vested but unclaimed tokens across all periods
     *      in the schedule.
     *
     * Returns zero when:
     * - schedule does not exist (e.g. `scheduleId` is 0),
     * - schedule is cancelled,
     * - global vesting mode is enabled but not started
     *
     * @param scheduleId The ID of the schedule
     * @return claimableAmount Amount that can be claimed from the schedule
     */
    function getClaimableAmount(uint256 scheduleId) external view virtual override returns (uint256 claimableAmount) {
        if (globalVestingMode && !globalVestingStarted) {
            return 0;
        }

        Schedule memory schedule = _scheduleById[scheduleId];
        if (schedule.id == 0) {
            return 0;
        }

        return _getClaimableAmountForSchedule(schedule);
    }

    /**
     * @notice Returns the claimable amount for a specific period
     * @dev Calculates the vested but unclaimed tokens for a single period.
     *
     * Returns zero when:
     * - schedule does not exist (e.g. `scheduleId` is 0),
     * - schedule is cancelled,
     * - global vesting mode is enabled but not started,
     * - `periodIndex` is out of bounds
     *
     * @param scheduleId The ID of the schedule
     * @param periodIndex The index of the period
     * @return claimableAmount Amount that can be claimed from the period
     */
    function getClaimableAmount(uint256 scheduleId, uint256 periodIndex)
        external
        view
        virtual
        override
        returns (uint256 claimableAmount)
    {
        if (globalVestingMode && !globalVestingStarted) {
            return 0;
        }

        Schedule memory schedule = _scheduleById[scheduleId];
        if (schedule.id == 0 || schedule.isCancelled || periodIndex >= schedule.periods.length) {
            return 0;
        }

        return _getClaimableAmountForPeriod(schedule.periods[periodIndex]);
    }

    /**
     * @notice Returns the available balance for creating new vesting schedules
     * @dev Calculates how many tokens can be committed to new schedules without
     *      exceeding the contract's token balance.
     *
     * @return availableBalance The amount of tokens available for new commitments
     */
    function getAvailableBalance() external view returns (uint256 availableBalance) {
        uint256 contractBalance = vestingToken.balanceOf(address(this));
        // Available balance is current balance minus already committed tokens
        availableBalance = contractBalance > committedTokens ? contractBalance - committedTokens : 0;
    }

    /// Internal Functions - Salvage Authorization
    /**
     * @notice Authorizes ERC20 salvage operations, preventing salvaging of the vesting token to protect locked funds
     * @dev Reverts when:
     * - the caller does not have the "SALVAGE_ROLE".
     * - `salvagedToken` is the same as `vestingToken`
     * @param salvagedToken The address of the token being salvaged
     */
    function _authorizeSalvageERC20(address salvagedToken, uint256 /* amount */ ) internal virtual override {
        require(salvagedToken != address(vestingToken), LibErrors.InvalidAddress());
        _checkRole(SALVAGE_ROLE);
    }

    /**
     * @notice Authorizes gas (ETH) salvage operations
     * @dev Only accounts with SALVAGE_ROLE can salvage ETH
     */
    function _authorizeSalvageGas() internal virtual override {
        _checkRole(SALVAGE_ROLE);
    }

    /// Internal Functions - Role Management Override

    /**
     * @notice Prevents renouncing the DEFAULT_ADMIN_ROLE
     * @dev Ensures there's always at least one admin who can manage the contract.
     *
     * Calling Conditions:
     *
     * - If `role` is `DEFAULT_ADMIN_ROLE`, the function will revert
     * - For other roles, follows standard AccessControl renunciation rules
     *
     * @param role The role being renounced
     * @param account The account renouncing the role
     */
    function renounceRole(bytes32 role, address account)
        public
        virtual
        override(AccessControlUpgradeable, IAccessControl)
    {
        require(role != DEFAULT_ADMIN_ROLE, LibErrors.DefaultAdminError());
        super.renounceRole(role, account);
    }

    /**
     * @notice Prevents default admins from revoking their own DEFAULT_ADMIN_ROLE
     * @dev Ensures there's always at least one admin who can manage the contract.
     *      This prevents the bypass of the renounceRole restriction through revokeRole.
     *
     * Calling Conditions:
     *
     * - The `role` parameter can't be `DEFAULT_ADMIN_ROLE`, when the account is the caller
     * - For other role revocations, follows standard AccessControl rules
     *
     * @param role The role being revoked
     * @param account The account from which the role is being revoked
     */
    function revokeRole(bytes32 role, address account)
        public
        virtual
        override(AccessControlUpgradeable, IAccessControl)
    {
        if (account == _msgSender()) {
            require(role != DEFAULT_ADMIN_ROLE, LibErrors.DefaultAdminError());
        }
        super.revokeRole(role, account);
    }

    /**
     * @notice Returns the maximum number of members allowed for each role
     * @dev Implements BoundedRoleMembership's abstract function to enforce role member limits.
     *
     * Role limits:
     * - VESTING_ADMIN_ROLE: 1 member (to prevent token deposit and schedule creation coordination issues)
     * - Other roles: 0 (unlimited by default)
     *
     * @param role The role to check the limit for
     * @return The maximum number of members allowed (0 for unlimited)
     */
    function _maxRoleMembers(bytes32 role) internal pure override returns (uint256) {
        if (role == VESTING_ADMIN_ROLE) {
            return 1;
        }
        return 0;
    }

    /// Internal Functions - Vesting Calculations

    /**
     * @notice Marks the amount of unclaimed vested tokens as claimed, for a specific period within a schedule
     * @dev Most granular claim function - claims from a single period.
     *
     * Internal function without access control checks. It performs checks and effects, but doesn't perform
     * any interactions such as external calls to transfer tokens. This falls under the responsibility of
     * the entry-point function, to minimize total calls.
     *
     * Preconditions (unchecked):
     *
     * - The schedule must exist
     * - If global vesting mode is enabled, global vesting must have been started
     * - The period index must be valid (within the vesting periods size)
     *
     * Emits a {TokenRelease} event.
     *
     * @param schedule The schedule storage reference
     * @param periodIndex The index of the period within the schedule
     */
    function _claim(Schedule storage schedule, uint256 periodIndex) private returns (uint256 claimableAmount) {
        // Process the specific period
        VestingPeriod storage period = schedule.periods[periodIndex];
        claimableAmount = _getClaimableAmountForPeriod(period);

        // Update claimed amount
        if (claimableAmount > 0) {
            period.claimedAmount += claimableAmount;
            // If period is now fully claimed, remove it from active bitmap for gas optimization
            if (period.claimedAmount >= period.amount) {
                _scheduleIdToActivePeriodsBitmap[schedule.id] &= ~(1 << periodIndex);
            }
            // Emit event
            emit TokenRelease(_msgSender(), schedule.beneficiary, schedule.id, periodIndex, claimableAmount);
        }
    }

    /**
     * @notice Calculates the vested amount for a specific period
     * @dev Implements linear vesting with optional cliff period
     *
     * @param period The vesting period to calculate for
     * @return The amount vested up to the current time
     */
    function _getVestedAmountForPeriod(VestingPeriod memory period) internal view virtual returns (uint256) {
        uint256 currentTime = block.timestamp;
        uint256 vestingStartTime;
        uint256 vestingEndTime;

        // Calculate actual start and end times based on vesting mode
        if (globalVestingMode) {
            // In global mode, check if global vesting has started
            if (!globalVestingStarted) {
                return 0;
            }
            vestingStartTime = globalVestingStartTime + period.startPeriod;
            vestingEndTime = globalVestingStartTime + period.endPeriod;
        } else {
            // In individual mode, use absolute timestamps
            vestingStartTime = period.startPeriod;
            vestingEndTime = period.endPeriod;
        }

        // If we haven't reached the start time, nothing is vested
        if (currentTime < vestingStartTime) {
            return 0;
        }

        // Calculate cliff end time if applicable
        uint256 cliffEndTime = vestingStartTime + period.cliff;

        // If we're before the cliff end, nothing is vested
        if (period.cliff > 0 && currentTime < cliffEndTime) {
            return 0;
        }
        // If we've passed the end time, everything is vested
        if (currentTime >= vestingEndTime) {
            return period.amount;
        }

        uint256 elapsedTime = currentTime - vestingStartTime;
        uint256 vestingDuration = vestingEndTime - vestingStartTime;
        // Can only reach here if currentTime >= vestingStartTime && currentTime < vestingEndTime
        // Therefore, vestingDuration > 0

        // Calculate vested amount using proportion of time elapsed (linear vesting)
        return (period.amount * elapsedTime) / vestingDuration;
    }

    /**
     * @notice Calculates the claimable amount for a specific period
     * @dev Returns vested amount minus already claimed amount
     *
     * Note that this function does not check if the schedule is cancelled.
     *
     * @param period The vesting period to calculate for
     * @return claimableAmount The amount that can be claimed
     */
    function _getClaimableAmountForPeriod(VestingPeriod memory period)
        internal
        view
        virtual
        returns (uint256 claimableAmount)
    {
        // Skip if already fully claimed
        if (period.claimedAmount >= period.amount) {
            return 0;
        }
        uint256 vestedAmount = _getVestedAmountForPeriod(period);

        // Claimable is vested minus already claimed
        if (vestedAmount > period.claimedAmount) {
            claimableAmount = vestedAmount - period.claimedAmount;
        } else {
            claimableAmount = 0;
        }
    }

    /**
     * @notice Calculates the total claimable amount for a schedule
     * @dev Sums claimable amounts across all periods if schedule is not cancelled
     *
     * @param schedule The schedule to calculate for
     * @return totalClaimable The total claimable amount
     */
    function _getClaimableAmountForSchedule(Schedule memory schedule)
        internal
        view
        virtual
        returns (uint256 totalClaimable)
    {
        // If schedule is cancelled, nothing is claimable
        if (schedule.isCancelled) {
            return 0;
        }
        // Sum claimable amounts across all periods
        for (uint256 i = 0; i < schedule.periods.length; ++i) {
            totalClaimable += _getClaimableAmountForPeriod(schedule.periods[i]);
        }
    }

    /**
     * @notice Internal function to process token release to a beneficiary or admin
     * @dev Transfers tokens and emits events
     *
     * This function performs an external call and contrary to the Checks-Effects-Interactions (CEI) pattern, it has a
     * side-effect following the transfer. Note that while it doesn't strictly follow the CEI pattern, it is safe to
     * perform this interaction before the state update, because the `claimedAmount` checks and state-changing
     * operation (which are a determinant for the transfer) are done before this call, in `_claim` or `cancelSchedule`.
     *
     * @param recipient The token recipient address
     * @param amount The amount to release
     */
    function _processTokenRelease(address recipient, uint256 amount) private {
        // Transfer tokens first (note: safe anti-pattern)
        vestingToken.safeTransfer(recipient, amount);

        // Reduce the committed tokens constraint by the released amount
        committedTokens -= amount;
    }

    /// Internal Functions - Global Vesting Status checks

    /**
     * @dev Validates that global vesting has started if global vesting mode is enabled
     * @notice Reverts if global vesting mode is enabled but not yet started
     */
    function _validateGlobalVestingStatus() internal view virtual {
        if (globalVestingMode) {
            require(globalVestingStarted, IVestingVaultErrors.GlobalVestingNotStarted());
        }
    }
}
"
    },
    "src/vesting/interfaces/IVestingVault.sol": {
      "content": "// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2025 Fireblocks <support@fireblocks.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
pragma solidity 0.8.27;

/**
 * @title IVestingVault
 * @author Fireblocks
 * @notice Interface for the VestingVault contract that manages token vesting schedules
 * @custom:security-contact support@fireblocks.com
 */
interface IVestingVault {
    /// Type declarations
    /**
     * @notice Represents a vesting period within a schedule
     * @param startPeriod The start time of this vesting period
     * @param endPeriod The end time of this vesting period
     * @param cliff The cliff period for this vesting period
     * @param amount The total amount of tokens in this vesting period (must be greater than zero)
     * @param claimedAmount The amount of tokens already claimed from this period
     */
    struct VestingPeriod {
        uint64 startPeriod;
        uint64 endPeriod;
        uint64 cliff;
        uint256 amount;
        uint256 claimedAmount;
    }

    /**
     * @notice Represents vesting period parameters for schedule creation
     * @dev This is the DTO for VestingPeriod, used for creating new schedules. It does not include the `claimedAmount`
     *      property, which is only relevant after the schedule is created.
     * @param startPeriod The start time of this vesting period
     * @param endPeriod The end time of this vesting period
     * @param cliff The cliff period for this vesting period
     * @param amount The total amount of tokens in this vesting period (must be greater than zero)
     */
    struct VestingPeriodParam {
        uint64 startPeriod;
        uint64 endPeriod;
        uint64 cliff;
        uint256 amount;
    }

    /**
     * @notice Represents a complete vesting schedule for a beneficiary
     * @param id Global unique identifier for this schedule
     * @param beneficiary The recipient of vested tokens from this schedule
     * @param isCancellable Whether this schedule can be cancelled by admins
     * @param isCancelled Whether this schedule has been cancelled
     * @param periods Array of vesting periods that comprise this schedule
     */
    struct Schedule {
        uint32 id;
        address beneficiary;
        bool isCancellable;
        bool isCancelled;
        VestingPeriod[] periods;
    }

    /// Events

    /**
     * @notice Emitted when a new vesting schedule is created
     * @param caller The address that created the schedule
     * @param beneficiary The beneficiary of the vesting schedule
     * @param scheduleId The ID of the created schedule
     * @param schedule The created schedule details
     */
    event VestingScheduleCreated(
        address indexed caller, address indexed beneficiary, uint256 indexed scheduleId, Schedule schedule
    );

    /**
     * @notice Emitted when global vesting is started
     * @param timestamp The timestamp when global vesting started
     */
    event GlobalVestingStarted(uint256 timestamp);

    /**
     * @notice Emitted when tokens are released (claimed or admin-released)
     * @param caller The address that invoked the release (beneficiary for claim, admin for release)
     * @param beneficiary The beneficiary who received the tokens
     * @param scheduleId The schedule ID from which tokens were released
     * @param periodIndex The period index from which tokens were released
     * @param amount The amount of tokens released
     */
    event TokenRelease(
        address indexed caller,
        address indexed beneficiary,
        uint256 indexed scheduleId,
        uint256 periodIndex,
        uint256 amount
    );

    /**
     * @notice Emitted when a vesting schedule is cancelled
     * @param admin The admin who cancelled the schedule
     * @param beneficiary The beneficiary whose schedule was cancelled
     * @param scheduleId The ID of the cancelled schedule
     * @param claimedAmount The amount transferred to beneficiary during cancellation
     * @param reclaimedAmount The amount of unvested tokens transferred to the admin
     */
    event VestingScheduleCancelled(
        address indexed admin,
        address indexed beneficiary,
        uint256 indexed scheduleId,
        uint256 claimedAmount,
        uint256 reclaimedAmount
    );

    /// Functions

    /**
     * @notice Creates a vesting schedule for a beneficiary
     * @param beneficiary Address of the beneficiary
     * @param isCancellable Whether this schedule can be cancelled by admins
     * @param periods Array of vesting periods for the schedule
     * @return scheduleId The ID of the created schedule
     */
    function createSchedule(address beneficiary, bool isCancellable, VestingPeriodParam[] calldata periods)
        external
        returns (uint32 scheduleId);

    /**
     * @notice Creates multiple vesting schedules for multiple beneficiaries in a single transaction.
     * @dev Calls `createSchedule` for each beneficiary with the provided periods. Each beneficiary receives their own schedule.
     *
     * Calling Conditions:
     * - The caller must have `VESTING_ADMIN_ROLE`.
     * - `beneficiaries` and `periodsList` arrays must have the same length and not be empty.
     * - Each beneficiary must not be the zero address.
     * - Each periods array must not be empty and can have at most 256 elements.
     * - The contract must have sufficient token balance to cover all schedules.
     *
     * Additional validations are performed by `createSchedule` for each beneficiary.
     *
     * @param beneficiaries Array of beneficiary addresses to receive vesting schedules.
     * @param isCancellable Array of booleans indicating whether each schedule can be cancelled by admins.
     * @param periodsList Array of arrays, where each inner array contains the vesting periods for the corresponding beneficiary.
     * @return scheduleIds Array of created schedule IDs, in the same order as beneficiaries.
     */
    function createMultipleSchedules(
        address[] calldata beneficiaries,
        bool[] calldata isCancellable,
        VestingPeriodParam[][] memory periodsList
    ) external returns (uint32[] memory scheduleIds);

    /**
     * @notice Starts global vesting for all schedules
     * @param startTimestamp The timestamp to start global vesting. Use 0 to use current block timestamp.
     */
    function startGlobalVesting(uint256 startTimestamp) external;

    /**
     * @notice Claims all available vested tokens for the caller across all schedules
     */
    function claim() external;

    /**
     * @notice Claims available vested tokens for a specific schedule
     * @param scheduleId The ID of the schedule to claim from
     */
    function claimSchedule(uint256 scheduleId) external;

    /**
     * @notice Claims available vested tokens for a specific period within a schedule
     * @param scheduleId The ID of the schedule
     * @param periodIndex The index of the period within the schedule
     */
    function claimSchedulePeriod(uint256 scheduleId, uint256 periodIndex) external;

    /**
     * @notice Cancels a vesting schedule by distributing vested tokens and withdrawing unvested tokens
     * @dev Calculates vested amounts up to the current time, transfers them to the beneficiary,
     *      and reclaims unvested tokens to the admin. The schedule is permanently marked as cancelled.
     *
     * Calling Conditions:
     *
     * - The caller must have `FORFEITURE_ADMIN_ROLE`
     * - The schedule must exist
     * - The schedule must be marked as `isCancellable`
     * - The schedule must not already be cancelled
     *
     * Emits a {VestingScheduleCancelled} event.
     *
     * @param scheduleId The ID of the schedule to cancel
     * @param beneficiary The beneficiary address to validate against
     */
    function cancelSchedule(uint256 scheduleId, address beneficiary) external;

    /**
     * @notice Emergency function to cancel a schedule and send all tokens to the caller (admin)
     * @dev This function is designed for cases where beneficiaries lose access to their wallets
     *      or addresses are compromised. It sends both vested and unvested tokens to the admin.
     *
     * Calling Conditions:
     *
     * - The caller must have `FORFEITURE_ADMIN_ROLE`
     * - The schedule must exist
     * - The schedule must be marked as `isCancellable`
     * - The schedule must not already be cancelled
     *
     * Emits a {VestingScheduleCancelled} event.
     *
     * @param scheduleId The ID of the schedule to cancel
     * @param beneficiary The beneficiary address to validate against
     */
    function cancelScheduleRescueVested(uint256 scheduleId, address beneficiary) external;

    /**
     * @notice Returns all schedules for a beneficiary
     * @param beneficiary The beneficiary address
     * @return schedules Array of schedules for the beneficiary
     */
    

Tags:
ERC20, ERC165, Multisig, Swap, Upgradeable, Multi-Signature, Factory|addr:0xff9519f39e0e07c57fa81cd4f19502b60b0df974|verified:true|block:23639921|tx:0xd2c9bb5f1530dc1310e23edbbcf3c12b0f72e101873ef94793992eac983aa852|first_check:1761307385

Submitted on: 2025-10-24 14:03:08

Comments

Log in to comment.

No comments yet.