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);
}