ETH Price: $1,974.52 (+0.69%)

Contract Diff Checker

Contract Name:
StrategyKeeperSablierValidator

Contract Source Code:

<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>

// SPDX-License-Identifier: BSD-3-Clause
pragma solidity ^0.8.24;

import {IValidator} from "lib/yieldnest-flex-strategy/lib/yieldnest-vault/src/interface/IValidator.sol";
import {ISablierLockupLinear} from "src/interfaces/sablier/ISablierLockupLinear.sol";
import {ISablierBatchLockup} from "src/interfaces/sablier/ISablierBatchLockup.sol";
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

/// @title StrategyKeeperSablierValidator
/// @notice Validates Sablier stream creation parameters for the strategy keeper
/// @dev Ensures that streams created via the processor meet security requirements:
///      - sender must be the configured safe
///      - recipient must be in the allowed recipients list
///      - token must be the configured token
///      - stream must be cancelable
///      - stream must be transferable
///      Supports both single stream (ISablierLockupLinear) and batch stream (ISablierBatchLockup) creation.
contract StrategyKeeperSablierValidator is IValidator {
    string public constant VERSION = "0.2.0";

    /*//////////////////////////////////////////////////////////////
                                 ERRORS
    //////////////////////////////////////////////////////////////*/

    /// @notice Thrown when the function selector doesn't match a supported function
    error InvalidFunctionSelector(bytes4 selector);

    /// @notice Thrown when the sender is not the configured safe
    error InvalidSender(address sender, address expectedSafe);

    /// @notice Thrown when the recipient is not in the allowed list
    error InvalidRecipient(address recipient);

    /// @notice Thrown when the token is not the configured token
    error InvalidToken(address token, address expectedToken);

    /// @notice Thrown when the stream is not cancelable
    error StreamMustBeCancelable();

    /// @notice Thrown when the stream is not transferable
    error StreamMustBeTransferable();

    /// @notice Thrown when an address is zero
    error ZeroAddress();

    /// @notice Thrown when the allowed recipients array is empty
    error EmptyAllowedRecipients();

    /// @notice Thrown when the lockup address doesn't match the configured lockup
    error InvalidLockupAddress(address lockup, address expectedLockup);

    /// @notice Thrown when the batch array is empty
    error EmptyBatch();

    /*//////////////////////////////////////////////////////////////
                                 EVENTS
    //////////////////////////////////////////////////////////////*/

    /// @notice Emitted when the validator is configured
    event ValidatorConfigured(address indexed safe, address indexed token, address[] allowedRecipients);

    /*//////////////////////////////////////////////////////////////
                                 STORAGE
    //////////////////////////////////////////////////////////////*/

    /// @notice The safe address that must be the sender of the stream
    address public immutable safe;

    /// @notice The Sablier LockupLinear contract address (validated in batch calls)
    address public immutable lockup;

    /// @notice The token that must be used for the stream
    address public immutable token;

    /// @notice Mapping of allowed recipients
    mapping(address => bool) public isAllowedRecipient;

    /// @notice Array of allowed recipients (for enumeration)
    address[] public allowedRecipients;

    /*//////////////////////////////////////////////////////////////
                              CONSTRUCTOR
    //////////////////////////////////////////////////////////////*/

    /// @notice Creates a new StrategyKeeperSablierValidator
    /// @param _safe The safe address that must be the sender of streams
    /// @param _lockup The Sablier LockupLinear contract address
    /// @param _token The token address that must be used for streams
    /// @param _allowedRecipients Array of addresses that can receive streams
    constructor(address _safe, address _lockup, address _token, address[] memory _allowedRecipients) {
        if (_safe == address(0)) revert ZeroAddress();
        if (_lockup == address(0)) revert ZeroAddress();
        if (_token == address(0)) revert ZeroAddress();
        if (_allowedRecipients.length == 0) revert EmptyAllowedRecipients();

        safe = _safe;
        lockup = _lockup;
        token = _token;

        for (uint256 i = 0; i < _allowedRecipients.length; i++) {
            if (_allowedRecipients[i] == address(0)) revert ZeroAddress();
            isAllowedRecipient[_allowedRecipients[i]] = true;
            allowedRecipients.push(_allowedRecipients[i]);
        }

        emit ValidatorConfigured(_safe, _token, _allowedRecipients);
    }

    /*//////////////////////////////////////////////////////////////
                            EXTERNAL FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Validates a Sablier stream creation call (single or batch)
    /// @param target The Sablier contract address (not validated here, should be validated by the rule)
    /// @param value The ETH value (should be 0 for this call)
    /// @param data The calldata containing the function selector and parameters
    /// @dev Reverts if any validation check fails
    function validate(address target, uint256 value, bytes calldata data) external view override {
        // Suppress unused variable warning
        target;
        value;

        // Check minimum data length (4 bytes selector + params)
        if (data.length < 4) revert InvalidFunctionSelector(bytes4(0));

        // Extract and verify function selector
        bytes4 selector = bytes4(data[:4]);

        if (selector == ISablierLockupLinear.createWithTimestampsLL.selector) {
            _validateSingle(data[4:]);
        } else if (selector == ISablierBatchLockup.createWithTimestampsLL.selector) {
            _validateBatch(data[4:]);
        } else {
            revert InvalidFunctionSelector(selector);
        }
    }

    /*//////////////////////////////////////////////////////////////
                            INTERNAL FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Validates a single createWithTimestampsLL call
    /// @param params The calldata after the selector
    function _validateSingle(bytes calldata params) internal view {
        (ISablierLockupLinear.CreateWithTimestamps memory createParams,,) =
            abi.decode(params, (ISablierLockupLinear.CreateWithTimestamps, ISablierLockupLinear.UnlockAmounts, uint40));

        _validateStreamParams(
            createParams.sender, createParams.recipient, createParams.cancelable, createParams.transferable
        );

        // Validate token is the configured token
        if (address(createParams.token) != token) {
            revert InvalidToken(address(createParams.token), token);
        }
    }

    /// @notice Validates a batch createWithTimestampsLL call
    /// @param params The calldata after the selector
    function _validateBatch(bytes calldata params) internal view {
        (address batchLockup, IERC20 batchToken, ISablierBatchLockup.CreateWithTimestampsLL[] memory batch) =
            abi.decode(params, (address, IERC20, ISablierBatchLockup.CreateWithTimestampsLL[]));

        // Validate lockup address
        if (batchLockup != lockup) {
            revert InvalidLockupAddress(batchLockup, lockup);
        }

        // Validate token
        if (address(batchToken) != token) {
            revert InvalidToken(address(batchToken), token);
        }

        // Validate batch is not empty
        if (batch.length == 0) revert EmptyBatch();

        // Validate each item in the batch
        for (uint256 i = 0; i < batch.length; i++) {
            _validateStreamParams(batch[i].sender, batch[i].recipient, batch[i].cancelable, batch[i].transferable);
        }
    }

    /// @notice Validates common stream parameters
    /// @param sender The stream sender (must be the safe)
    /// @param recipient The stream recipient (must be in allowed list)
    /// @param cancelable Whether the stream is cancelable (must be true)
    /// @param transferable Whether the stream is transferable (must be true)
    function _validateStreamParams(address sender, address recipient, bool cancelable, bool transferable)
        internal
        view
    {
        if (sender != safe) {
            revert InvalidSender(sender, safe);
        }

        if (!isAllowedRecipient[recipient]) {
            revert InvalidRecipient(recipient);
        }

        if (!cancelable) {
            revert StreamMustBeCancelable();
        }

        if (!transferable) {
            revert StreamMustBeTransferable();
        }
    }

    /*//////////////////////////////////////////////////////////////
                            VIEW FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Returns the number of allowed recipients
    /// @return The count of allowed recipients
    function getAllowedRecipientsCount() external view returns (uint256) {
        return allowedRecipients.length;
    }

    /// @notice Returns all allowed recipients
    /// @return Array of allowed recipient addresses
    function getAllowedRecipients() external view returns (address[] memory) {
        return allowedRecipients;
    }
}

<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>

// SPDX-License-Identifier: BSD-3-Clause
pragma solidity ^0.8.24;

interface IValidator {
    /// @notice Validates a transaction before execution
    /// @param target The address the transaction will be sent to
    /// @param value The amount of ETH (in wei) that will be sent with the transaction
    /// @param data The calldata that will be sent with the transaction
    /// @dev This function should revert if the transaction is invalid
    /// @dev This function is called before executing a transaction
    function validate(address target, uint256 value, bytes calldata data) external view;
}

<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

/// @title ISablierLockupLinear
/// @notice Creates Lockup streams with linear distribution model.
interface ISablierLockupLinear {
    /// @notice Struct encapsulating the Lockup timestamps.
    /// @param start The Unix timestamp for the stream's start.
    /// @param end The Unix timestamp for the stream's end.
    struct Timestamps {
        uint40 start;
        uint40 end;
    }

    /// @notice Struct encapsulating the parameters of the `createWithDurations` functions.
    /// @param sender The address distributing the tokens, with the ability to cancel the stream. It doesn't have to be
    /// the same as `msg.sender`.
    /// @param recipient The address receiving the tokens, as well as the NFT owner.
    /// @param depositAmount The deposit amount, denoted in units of the token's decimals.
    /// @param token The contract address of the ERC-20 token to be distributed.
    /// @param cancelable Indicates if the stream is cancelable.
    /// @param transferable Indicates if the stream NFT is transferable.
    /// @param shape An optional parameter to specify the shape of the distribution function. This helps differentiate
    /// streams in the UI.
    struct CreateWithDurations {
        address sender;
        address recipient;
        uint128 depositAmount;
        IERC20 token;
        bool cancelable;
        bool transferable;
        string shape;
    }

    /// @notice Struct encapsulating the parameters of the `createWithTimestamps` functions.
    /// @param sender The address distributing the tokens, with the ability to cancel the stream. It doesn't have to be
    /// the same as `msg.sender`.
    /// @param recipient The address receiving the tokens, as well as the NFT owner.
    /// @param depositAmount The deposit amount, denoted in units of the token's decimals.
    /// @param token The contract address of the ERC-20 token to be distributed.
    /// @param cancelable Indicates if the stream is cancelable.
    /// @param transferable Indicates if the stream NFT is transferable.
    /// @param timestamps Struct encapsulating (i) the stream's start time and (ii) end time, both as Unix timestamps.
    /// @param shape An optional parameter to specify the shape of the distribution function. This helps differentiate
    /// streams in the UI.
    struct CreateWithTimestamps {
        address sender;
        address recipient;
        uint128 depositAmount;
        IERC20 token;
        bool cancelable;
        bool transferable;
        Timestamps timestamps;
        string shape;
    }

    /// @notice Struct encapsulating the unlock amounts for the stream.
    /// @dev The sum of `start` and `cliff` must be less than or equal to deposit amount. Both amounts can be zero.
    /// @param start The amount to be unlocked at the start time.
    /// @param cliff The amount to be unlocked at the cliff time.
    struct UnlockAmounts {
        // slot 0
        uint128 start;
        uint128 cliff;
    }

    /// @notice Struct encapsulating the cliff duration and the total duration used at runtime in
    /// {SablierLockupLinear.createWithDurationsLL} function.
    /// @param cliff The cliff duration in seconds.
    /// @param total The total duration in seconds.
    struct Durations {
        uint40 cliff;
        uint40 total;
    }

    /*//////////////////////////////////////////////////////////////////////////
                        USER-FACING STATE-CHANGING FUNCTIONS
    //////////////////////////////////////////////////////////////////////////*/

    /// @notice Creates a stream by setting the start time to `block.timestamp`, and the end time to
    /// the sum of `block.timestamp` and `durations.total`. The stream is funded by `msg.sender` and is wrapped in an
    /// ERC-721 NFT.
    ///
    /// @dev Emits a {Transfer}, {CreateLockupLinearStream} and {MetadataUpdate} event.
    ///
    /// Requirements:
    /// - All requirements in {createWithTimestampsLL} must be met for the calculated parameters.
    ///
    /// @param params Struct encapsulating the function parameters, which are documented in {Lockup} type.
    /// @param durations Struct encapsulating (i) cliff period duration and (ii) total stream duration, both in seconds.
    /// @param unlockAmounts Struct encapsulating (i) the amount to unlock at the start time and (ii) the amount to
    /// unlock at the cliff time.
    /// @return streamId The ID of the newly created stream.
    function createWithDurationsLL(
        CreateWithDurations calldata params,
        UnlockAmounts calldata unlockAmounts,
        Durations calldata durations
    ) external payable returns (uint256 streamId);

    /// @notice Creates a stream with the provided start time and end time. The stream is funded by `msg.sender` and is
    /// wrapped in an ERC-721 NFT.
    ///
    /// @dev Emits a {Transfer}, {CreateLockupLinearStream} and {MetadataUpdate} event.
    ///
    /// Notes:
    /// - A cliff time of zero means there is no cliff.
    /// - As long as the times are ordered, it is not an error for the start or the cliff time to be in the past.
    ///
    /// Requirements:
    /// - Must not be delegate called.
    /// - `params.depositAmount` must be greater than zero.
    /// - `params.timestamps.start` must be greater than zero and less than `params.timestamps.end`.
    /// - If set, `cliffTime` must be greater than `params.timestamps.start` and less than
    /// `params.timestamps.end`.
    /// - `params.recipient` must not be the zero address.
    /// - `params.sender` must not be the zero address.
    /// - The sum of `params.unlockAmounts.start` and `params.unlockAmounts.cliff` must be less than or equal to
    /// deposit amount.
    /// - If `params.timestamps.cliff` not set, the `params.unlockAmounts.cliff` must be zero.
    /// - `msg.sender` must have allowed this contract to spend at least `params.depositAmount` tokens.
    /// - `params.token` must not be the native token.
    /// - `params.shape.length` must not be greater than 32 characters.
    ///
    /// @param params Struct encapsulating the function parameters, which are documented in {Lockup} type.
    /// @param cliffTime The Unix timestamp for the cliff period's end. A value of zero means there is no cliff.
    /// @param unlockAmounts Struct encapsulating (i) the amount to unlock at the start time and (ii) the amount to
    /// unlock at the cliff time.
    /// @return streamId The ID of the newly created stream.
    function createWithTimestampsLL(
        CreateWithTimestamps calldata params,
        UnlockAmounts calldata unlockAmounts,
        uint40 cliffTime
    ) external payable returns (uint256 streamId);
}

<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ISablierLockupLinear} from "./ISablierLockupLinear.sol";

/// @title ISablierBatchLockup
/// @notice Helper to batch create Lockup streams.
interface ISablierBatchLockup {
    /// @notice A struct encapsulating all parameters of {SablierLockupLinear.createWithTimestampsLL} except for the
    /// token.
    struct CreateWithTimestampsLL {
        address sender;
        address recipient;
        uint128 depositAmount;
        bool cancelable;
        bool transferable;
        ISablierLockupLinear.Timestamps timestamps;
        uint40 cliffTime;
        ISablierLockupLinear.UnlockAmounts unlockAmounts;
        string shape;
    }

    /// @notice Creates a batch of LL streams using `createWithTimestampsLL`.
    ///
    /// @dev Requirements:
    /// - There must be at least one element in `batch`.
    /// - All requirements from {ISablierLockupLinear.createWithTimestampsLL} must be met for each stream.
    ///
    /// @param lockup The address of the {SablierLockup} contract.
    /// @param token The contract address of the ERC-20 token to be distributed.
    /// @param batch An array of structs, each encapsulating a subset of the parameters of
    /// {ISablierLockupLinear.createWithTimestampsLL}.
    /// @return streamIds The ids of the newly created streams.
    function createWithTimestampsLL(address lockup, IERC20 token, CreateWithTimestampsLL[] calldata batch)
        external
        returns (uint256[] memory streamIds);
}

<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.20;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the value of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the value of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 value) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the
     * allowance mechanism. `value` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

Please enter a contract address above to load the contract details and source code.

Context size (optional):