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/RewardsCycleManager.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {StakedVision} from "./StakedVision.sol";
import {VisionToken} from "./VisionToken.sol";
/**
* @title Rewards Cycle Manager
*
* @notice The rewards cycle manager contract combines the required steps into
* a single contract to configure a new rewards cycle in the Vision staking contract.
* Those steps consist of distributing the rewards, taking out the surplus,
* minting the required Vision tokens, transferring them to the staking contract,
* and finally creating the new rewards cycle. Additionally, the contract requires
* the MINTER_ROLE on the Vision token contract to mint the required tokens, and
* the CRITICAL_OPS_ROLE on the Staked Vision contract to create a new rewards cycle.
*/
contract RewardsCycleManager is AccessControl {
/*//////////////////////////////////////////////////////////////
USED LIBRARIES
//////////////////////////////////////////////////////////////*/
using SafeERC20 for IERC20;
using SafeERC20 for VisionToken;
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event TokensRescued(address indexed token, uint256 amount, address indexed to);
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error InvalidZeroAddress();
error PreviousCycleNotFinished();
error TokenOrStakingPaused();
error TransferFailed();
error InvalidRewardsAmount();
error RewardsCycleDurationTooLong();
error CycleEndTimestampInThePast();
/*//////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////*/
bytes32 public constant CRITICAL_OPS_ROLE = keccak256("CRITICAL_OPS_ROLE");
/*//////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////*/
// solhint-disable-next-line immutable-vars-naming
VisionToken public immutable visionToken;
// solhint-disable-next-line immutable-vars-naming
StakedVision public immutable stakedVision;
/**
* @notice Construct a Staked Vision instance
*
* @param visionToken_ The Vision token contract address
* @param stakedVision_ The Staked Vision contract address
* @param criticalOps The address of the critical operation role
* @param defaultAdmin The address of the default admin of the roles
*/
constructor(VisionToken visionToken_, StakedVision stakedVision_, address criticalOps, address defaultAdmin) {
if (
criticalOps == address(0) || defaultAdmin == address(0) || address(visionToken_) == address(0)
|| address(stakedVision_) == address(0)
) {
revert InvalidZeroAddress();
}
_grantRole(CRITICAL_OPS_ROLE, criticalOps);
_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
visionToken = visionToken_;
stakedVision = stakedVision_;
}
/**
* @notice Create a new rewards cycle in the Staked Vision contract.
*
* @param rewardsAmount Total amount of Vision tokens to be distributed as rewards in the new cycle
* @param rewardsCycleEndTimestamp The timestamp when the new rewards cycle ends
* @param bpsYieldCapPerSecond The yield cap per second in basis points scaled
*
* @dev The caller must have the CRITICAL_OPS_ROLE. The rewardsAmount parameter must be the total
* amount of rewards to be distributed in the new cycle, not the amount to be minted. The
* contract subtracts the surplus from the rewardsAmount to determine the mint amount.
*/
function createRewardsCycle(uint256 rewardsAmount, uint256 rewardsCycleEndTimestamp, uint256 bpsYieldCapPerSecond)
external
onlyRole(CRITICAL_OPS_ROLE)
{
if (rewardsAmount == 0) {
revert InvalidRewardsAmount();
}
if (rewardsCycleEndTimestamp <= block.timestamp) {
revert CycleEndTimestampInThePast();
}
if (rewardsCycleEndTimestamp - block.timestamp > stakedVision.maximumRewardsCycleDuration()) {
revert RewardsCycleDurationTooLong();
}
if (visionToken.paused() || stakedVision.paused()) {
revert TokenOrStakingPaused();
}
stakedVision.distributeRewards();
// slither-disable-next-line unused-return
(, uint256 currentEndTimestamp,, uint256 surplus,) = stakedVision.rewardsCycle();
// slither-disable-next-line timestamp
if (currentEndTimestamp > block.timestamp) {
revert PreviousCycleNotFinished();
}
// Withdraw surplus rewards to this contract; it reverts if there is no surplus
if (surplus > 0) {
stakedVision.withdrawSurplusRewards(address(this));
}
if (surplus < rewardsAmount) {
uint256 mintAmount = rewardsAmount - surplus;
visionToken.mint(address(this), mintAmount);
}
visionToken.safeTransfer(address(stakedVision), rewardsAmount);
stakedVision.createRewardsCycle(rewardsAmount, rewardsCycleEndTimestamp, bpsYieldCapPerSecond);
}
/**
* @notice Rescues tokens mistakenly sent to the contract and
* transfers them to a specified address
*
* @param token The address of the token to be rescued
* @param amount The amount of tokens to transfer
* @param to The recipient address of the rescued tokens
*/
function rescueTokens(address token, uint256 amount, address to) external onlyRole(CRITICAL_OPS_ROLE) {
if (to == address(0)) {
revert InvalidZeroAddress();
}
IERC20(token).safeTransfer(to, amount);
emit TokensRescued(token, amount, to);
}
}
"
},
"lib/openzeppelin-contracts/contracts/access/AccessControl.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/AccessControl.sol)
pragma solidity ^0.8.20;
import {IAccessControl} from "./IAccessControl.sol";
import {Context} from "../utils/Context.sol";
import {ERC165} from "../utils/introspection/ERC165.sol";
/**
* @dev Contract module that allows children to implement role-based access
* control mechanisms. This is a lightweight version that doesn't allow enumerating role
* members except through off-chain means by accessing the contract event logs. Some
* applications may benefit from on-chain enumerability, for those cases see
* {AccessControlEnumerable}.
*
* Roles are referred to by their `bytes32` identifier. These should be exposed
* in the external API and be unique. The best way to achieve this is by
* using `public constant` hash digests:
*
* ```solidity
* bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
* ```
*
* Roles can be used to represent a set of permissions. To restrict access to a
* function call, use {hasRole}:
*
* ```solidity
* function foo() public {
* require(hasRole(MY_ROLE, msg.sender));
* ...
* }
* ```
*
* Roles can be granted and revoked dynamically via the {grantRole} and
* {revokeRole} functions. Each role has an associated admin role, and only
* accounts that have a role's admin role can call {grantRole} and {revokeRole}.
*
* By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
* that only accounts with this role will be able to grant or revoke other
* roles. More complex role relationships can be created by using
* {_setRoleAdmin}.
*
* WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
* grant and revoke this role. Extra precautions should be taken to secure
* accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
* to enforce additional security measures for this role.
*/
abstract contract AccessControl is Context, IAccessControl, ERC165 {
struct RoleData {
mapping(address account => bool) hasRole;
bytes32 adminRole;
}
mapping(bytes32 role => RoleData) private _roles;
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
/**
* @dev Modifier that checks that an account has a specific role. Reverts
* with an {AccessControlUnauthorizedAccount} error including the required role.
*/
modifier onlyRole(bytes32 role) {
_checkRole(role);
_;
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) public view virtual returns (bool) {
return _roles[role].hasRole[account];
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
* is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
*/
function _checkRole(bytes32 role) internal view virtual {
_checkRole(role, _msgSender());
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
* is missing `role`.
*/
function _checkRole(bytes32 role, address account) internal view virtual {
if (!hasRole(role, account)) {
revert AccessControlUnauthorizedAccount(account, role);
}
}
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
return _roles[role].adminRole;
}
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleGranted} event.
*/
function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleRevoked} event.
*/
function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been revoked `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*
* May emit a {RoleRevoked} event.
*/
function renounceRole(bytes32 role, address callerConfirmation) public virtual {
if (callerConfirmation != _msgSender()) {
revert AccessControlBadConfirmation();
}
_revokeRole(role, callerConfirmation);
}
/**
* @dev Sets `adminRole` as ``role``'s admin role.
*
* Emits a {RoleAdminChanged} event.
*/
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
bytes32 previousAdminRole = getRoleAdmin(role);
_roles[role].adminRole = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
/**
* @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
*
* Internal function without access restriction.
*
* May emit a {RoleGranted} event.
*/
function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
if (!hasRole(role, account)) {
_roles[role].hasRole[account] = true;
emit RoleGranted(role, account, _msgSender());
return true;
} else {
return false;
}
}
/**
* @dev Attempts to revoke `role` to `account` and returns a boolean indicating if `role` was revoked.
*
* Internal function without access restriction.
*
* May emit a {RoleRevoked} event.
*/
function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
if (hasRole(role, account)) {
_roles[role].hasRole[account] = false;
emit RoleRevoked(role, account, _msgSender());
return true;
} else {
return false;
}
}
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
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);
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.0) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC-20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
/**
* @dev An operation with an ERC-20 token failed.
*/
error SafeERC20FailedOperation(address token);
/**
* @dev Indicates a failed `decreaseAllowance` request.
*/
error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
/**
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
}
/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
*/
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 oldAllowance = token.allowance(address(this), spender);
forceApprove(token, spender, oldAllowance + value);
}
/**
* @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
* value, non-reverting calls are assumed to be successful.
*
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
*/
function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
unchecked {
uint256 currentAllowance = token.allowance(address(this), spender);
if (currentAllowance < requestedDecrease) {
revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
}
forceApprove(token, spender, currentAllowance - requestedDecrease);
}
}
/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
* to be set to zero before setting it to a non-zero value, such as USDT.
*
* NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
* only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
* set here.
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));
if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
_callOptionalReturn(token, approvalCall);
}
}
/**
* @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
safeTransfer(token, to, value);
} else if (!token.transferAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
* has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
function transferFromAndCallRelaxed(
IERC1363 token,
address from,
address to,
uint256 value,
bytes memory data
) internal {
if (to.code.length == 0) {
safeTransferFrom(token, from, to, value);
} else if (!token.transferFromAndCall(from, to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
* Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
* once without retrying, and relies on the returned value to be true.
*
* Reverts if the returned value is other than `true`.
*/
function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
forceApprove(token, to, value);
} else if (!token.approveAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
// bubble errors
if iszero(success) {
let ptr := mload(0x40)
returndatacopy(ptr, 0, returndatasize())
revert(ptr, returndatasize())
}
returnSize := returndatasize()
returnValue := mload(0)
}
if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
bool success;
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
returnSize := returndatasize()
returnValue := mload(0)
}
return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
}
}
"
},
"src/StakedVision.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {ERC20} from "@solmate/contracts/tokens/ERC20.sol";
import {ERC4626} from "@solmate/contracts/tokens/ERC4626.sol";
import {SafeTransferLib} from "@solmate/contracts/utils/SafeTransferLib.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {IStakedVision, RewardsCycle, UserCooldown} from "./interfaces/IStakedVision.sol";
/**
* @title Staked Vision
*
* @notice The staking contract incentivizes users to lock tokens by distributing rewards.
* It receives Vision tokens from various sources and distributes them linearly over reward cycles.
* The contract enforces rules such as a cooldown period for withdrawals and a yield cap on the pool.
*
* @dev If the cooldown duration is set to zero, the contract follows the ERC4626 standard,
* disabling the `cooldownShares` and `cooldownAssets` functions.
* If the cooldown duration is greater than zero, the standard ERC4626 `withdraw` and `redeem`
* functions are disabled, enabling `cooldownShares` and `cooldownAssets` instead.
*/
contract StakedVision is IStakedVision, ERC4626, AccessControl, Pausable {
/*//////////////////////////////////////////////////////////////
USED LIBRARIES
//////////////////////////////////////////////////////////////*/
using SafeTransferLib for ERC20;
using SafeCast for uint256;
using Math for uint256;
/*//////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////*/
bytes32 internal constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 internal constant CRITICAL_OPS_ROLE = keccak256("CRITICAL_OPS_ROLE");
/// @dev 1BPS = 0.0000000001%
/// @dev 10000000000BPM = 1%
uint256 private constant BASIS_POINT_SCALE = 1e12;
/*//////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////*/
uint256 internal totalAssets_;
uint256 public cooldownDuration;
uint256 public maximumRewardsCycleDuration = 60 days;
RewardsCycle public rewardsCycle;
mapping(address => UserCooldown) public cooldowns;
/*//////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////*/
modifier ensureCooldownOff() {
if (cooldownDuration != 0) revert OperationNotAllowed();
_;
}
modifier ensureCooldownOn() {
if (cooldownDuration == 0) revert OperationNotAllowed();
_;
}
/**
* @notice Construct a Staked Vision instance
*
* @param asset_ The Vision token contract address
* @param cooldownDuration_ The cooldown duration when exiting
* @param pauser The address of the pauser role
* @param criticalOps The address of the critical operation role
* @param defaultAdmin The address of the default admin of the roles
*/
constructor(ERC20 asset_, uint256 cooldownDuration_, address pauser, address criticalOps, address defaultAdmin)
ERC4626(asset_, "Staked VSN", "sVSN")
{
if (pauser == address(0) || criticalOps == address(0) || address(defaultAdmin) == address(0)) {
revert InvalidZeroAddress();
}
_grantRole(PAUSER_ROLE, pauser);
_grantRole(CRITICAL_OPS_ROLE, criticalOps);
_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
_updateCooldownDuration(cooldownDuration_);
}
/*//////////////////////////////////////////////////////////////
DEPOSIT/WITHDRAWAL LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @notice Mints vault shares to receiver by depositing
* exactly amount of underlying tokens
*
* @param assets The amount of assets to deposit
* @param receiver The address of the vault shares receiver
*
* @return The amount of vault shares received
*/
function deposit(uint256 assets, address receiver) public override returns (uint256) {
distributeRewards();
return super.deposit(assets, receiver);
}
/**
* @notice Mints exactly shares vault shares to receiver by depositing
* amount of underlying tokens.
*
* @param shares The amount of vault shares to be received
* @param receiver The address of the vault shares receiver
*
* @return The amount of assets to deposit
*/
function mint(uint256 shares, address receiver) public override returns (uint256) {
distributeRewards();
return super.mint(shares, receiver);
}
/**
* @notice Burns vault shares from owner and sends exactly assets of
* underlying tokens to receiver
*
* @param assets The amount of assets to be received
* @param receiver The address of the assets receiver
* @param owner The owner of the vault shares
*
* @return The amount of vault shares to be burned
*/
function withdraw(uint256 assets, address receiver, address owner)
public
override
ensureCooldownOff
returns (uint256)
{
distributeRewards();
return super.withdraw(assets, receiver, owner);
}
/**
* @notice Burns exactly shares from owner and sends assets of
* underlying tokens to receiver
*
* @param shares The amount of shares to be burned
* @param receiver The address of the assets receiver
* @param owner The owner of the vault shares
*
* @return The The amount of assets to be received
*/
function redeem(uint256 shares, address receiver, address owner)
public
override
ensureCooldownOff
returns (uint256)
{
distributeRewards();
return super.redeem(shares, receiver, owner);
}
/**
* @dev See {IStakedVision-cooldownAssets}
*/
function cooldownAssets(uint256 assets) external ensureCooldownOn returns (uint256) {
distributeRewards();
uint256 shares = previewWithdraw(assets);
totalAssets_ -= assets;
cooldowns[msg.sender].cooldownEnd = block.timestamp.toUint104() + cooldownDuration.toUint104();
cooldowns[msg.sender].lockedAmount += assets.toUint152();
_burn(msg.sender, shares);
emit CooldownStarted(msg.sender, assets, shares, cooldowns[msg.sender].cooldownEnd);
return shares;
}
/**
* @dev See {IStakedVision-cooldownShares}
*/
function cooldownShares(uint256 shares) external ensureCooldownOn returns (uint256) {
distributeRewards();
uint256 assets = previewRedeem(shares);
totalAssets_ -= assets;
cooldowns[msg.sender].cooldownEnd = block.timestamp.toUint104() + cooldownDuration.toUint104();
cooldowns[msg.sender].lockedAmount += assets.toUint152();
_burn(msg.sender, shares);
emit CooldownStarted(msg.sender, assets, shares, cooldowns[msg.sender].cooldownEnd);
return assets;
}
/**
* @dev See {IStakedVision-claim}
*/
function claim(address receiver) external whenNotPaused {
UserCooldown storage userCooldown = cooldowns[msg.sender];
uint256 assets = userCooldown.lockedAmount;
// slither-disable-next-line timestamp
if (block.timestamp >= userCooldown.cooldownEnd || cooldownDuration == 0) {
userCooldown.cooldownEnd = 0;
userCooldown.lockedAmount = 0;
asset.safeTransfer(receiver, assets);
emit AssetsClaimed(msg.sender, receiver, assets);
} else {
revert CooldownNotElapsed();
}
}
function afterDeposit(uint256 amount, uint256) internal override {
totalAssets_ += amount;
}
function beforeWithdraw(uint256 amount, uint256) internal override {
totalAssets_ -= amount;
}
/*//////////////////////////////////////////////////////////////
REWARDS LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @dev See {IStakedVision-distributeRewards}
*/
function distributeRewards() public whenNotPaused {
(uint256 rewards, uint256 surplusRewards, uint256 uncappedRewards) = previewDistributeRewards();
// slither-disable-next-line timestamp
if (rewards != 0) {
rewardsCycle.unvestedAmount -= uncappedRewards;
rewardsCycle.surplus += surplusRewards;
rewardsCycle.lastDistributionTimestamp = block.timestamp;
totalAssets_ += rewards;
emit DistributeRewards(rewards);
}
}
/**
* @dev See {IStakedVision-previewDistributeRewards}
*/
function previewDistributeRewards() public view returns (uint256, uint256, uint256) {
uint256 elapsedTimeSinceLastRewards;
// slither-disable-next-line timestamp
if (block.timestamp < rewardsCycle.endTimestamp) {
elapsedTimeSinceLastRewards = block.timestamp - rewardsCycle.lastDistributionTimestamp;
} else {
if (rewardsCycle.lastDistributionTimestamp >= rewardsCycle.endTimestamp) return (0, 0, 0);
/// @dev Distribute the remaining rewards of the previous cycle
elapsedTimeSinceLastRewards = rewardsCycle.endTimestamp - rewardsCycle.lastDistributionTimestamp;
}
uint256 uncappedRewards = rewardsCycle.unvestedAmount.mulDiv(
elapsedTimeSinceLastRewards, rewardsCycle.endTimestamp - rewardsCycle.lastDistributionTimestamp
);
/// @dev No cap on the rewards
// slither-disable-next-line incorrect-equality
if (rewardsCycle.bpsYieldCapPerSecond == 0) {
return (uncappedRewards, 0, uncappedRewards);
}
uint256 cappedRewards =
totalAssets_.mulDiv(elapsedTimeSinceLastRewards * rewardsCycle.bpsYieldCapPerSecond, BASIS_POINT_SCALE);
uint256 surplusRewards = cappedRewards < uncappedRewards ? uncappedRewards - cappedRewards : 0;
uint256 rewards = uncappedRewards - surplusRewards;
return (rewards, surplusRewards, uncappedRewards);
}
/**
* @dev See {ERC4626-totalAssets}
*/
function totalAssets() public view override returns (uint256) {
(uint256 rewards,,) = previewDistributeRewards();
return totalAssets_ + rewards;
}
/*//////////////////////////////////////////////////////////////
RBAC OPERATIONS
//////////////////////////////////////////////////////////////*/
/**
* @dev See {IStakedVision-createRewardsCycle}
*/
function createRewardsCycle(uint256 rewardsAmount, uint256 rewardsCycleEndTimestamp, uint256 bpsYieldCapPerSecond)
external
onlyRole(CRITICAL_OPS_ROLE)
{
/// @dev Distribute remaining rewards of the previous cycle if necessary
distributeRewards();
// slither-disable-next-line timestamp
if (rewardsCycleEndTimestamp <= block.timestamp) {
revert CycleEndTimestampInThePast();
}
// slither-disable-next-line timestamp
if (rewardsCycle.endTimestamp > block.timestamp) {
revert PreviousCycleNotFinished();
}
// slither-disable-next-line timestamp
if (rewardsCycleEndTimestamp - block.timestamp > maximumRewardsCycleDuration) {
revert RewardsCycleDurationTooLong();
}
/// @dev Funds sent to the contract, but unaccounted for
uint256 availableRewards = asset.balanceOf(address(this)) - totalAssets_ - rewardsCycle.surplus;
if (rewardsAmount > availableRewards) revert NotEnoughRewardFunds();
rewardsCycle.unvestedAmount = rewardsAmount;
rewardsCycle.endTimestamp = rewardsCycleEndTimestamp;
rewardsCycle.lastDistributionTimestamp = block.timestamp;
rewardsCycle.bpsYieldCapPerSecond = bpsYieldCapPerSecond;
emit RewardsCycleCreated(
rewardsCycle.unvestedAmount, rewardsCycleEndTimestamp, rewardsCycle.bpsYieldCapPerSecond
);
}
/**
* @dev See {IStakedVision-withdrawSurplusRewards}
*/
function withdrawSurplusRewards(address receiver) external onlyRole(CRITICAL_OPS_ROLE) {
if (rewardsCycle.surplus == 0) revert NoSurplusToWithdraw();
uint256 surplusRewards = rewardsCycle.surplus;
rewardsCycle.surplus = 0;
asset.safeTransfer(receiver, surplusRewards);
emit WithdrawSurplus(receiver, surplusRewards);
}
/**
* @dev See {IStakedVision-rescueTokens}
*/
function rescueTokens(address token, uint256 amount, address to) external onlyRole(CRITICAL_OPS_ROLE) {
if (token == address(asset)) revert InvalidToken();
ERC20(token).safeTransfer(to, amount);
}
/**
* @dev See {IStakedVision-updateCooldownDuration}
*/
function updateCooldownDuration(uint256 cooldownDuration_) external onlyRole(CRITICAL_OPS_ROLE) {
if (cooldownDuration_ == cooldownDuration) {
revert SameCooldownDuration();
}
_updateCooldownDuration(cooldownDuration_);
}
/**
* @dev See {IStakedVision-updateBpsYieldCapPerSecond}
*/
function updateBpsYieldCapPerSecond(uint256 bpsYieldCapPerSecond) external onlyRole(CRITICAL_OPS_ROLE) {
uint256 previousBpsYieldCapPerSecond = rewardsCycle.bpsYieldCapPerSecond;
if (previousBpsYieldCapPerSecond == bpsYieldCapPerSecond) {
revert SameBpsYieldCapPerSecond();
}
rewardsCycle.bpsYieldCapPerSecond = bpsYieldCapPerSecond;
emit BpsYieldCapPerSecondUpdated(previousBpsYieldCapPerSecond, bpsYieldCapPerSecond);
}
/**
* @dev Updates the maximum rewards cycle duration.
* @param newMaximumRewardsCycleDuration The new maximum duration in seconds.
*/
function updateMaximumRewardsCycleDuration(uint256 newMaximumRewardsCycleDuration)
external
onlyRole(CRITICAL_OPS_ROLE)
{
if (newMaximumRewardsCycleDuration == maximumRewardsCycleDuration) {
revert SameMaximumRewardsCycleDuration();
}
uint256 previousDuration = maximumRewardsCycleDuration;
maximumRewardsCycleDuration = newMaximumRewardsCycleDuration;
emit MaximumRewardsCycleDurationUpdated(previousDuration, newMaximumRewardsCycleDuration);
}
/**
* @dev See {IStakedVision-pause}
*/
function pause() external override onlyRole(PAUSER_ROLE) {
super._pause();
}
/**
* @dev See {IStakedVision-unpause}
*/
function unpause() external override onlyRole(PAUSER_ROLE) {
super._unpause();
}
/*//////////////////////////////////////////////////////////////
INTERNAL
//////////////////////////////////////////////////////////////*/
function _updateCooldownDuration(uint256 cooldownDuration_) internal {
uint256 previousCooldownDuration = cooldownDuration;
cooldownDuration = cooldownDuration_;
emit CooldownDurationUpdated(previousCooldownDuration, cooldownDuration);
}
}
"
},
"src/VisionToken.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import {ERC20PausableUpgradeable} from
"@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol";
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import {ERC20PermitUpgradeable} from
"@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
/**
* @title VisionToken
* @notice An ERC20 token that includes pausing, role-based access control, off-chain approvals, and
* upgradeability through the UUPS proxy pattern.
*/
contract VisionToken is ERC20PausableUpgradeable, AccessControlUpgradeable, ERC20PermitUpgradeable, UUPSUpgradeable {
string private constant _NAME = "Vision";
string private constant _SYMBOL = "VSN";
/// @notice Role for pausing the contract.
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
/// @notice Role for minting and burning tokens.
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
/// @notice Role for upgrading the contract.
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
/**
* @dev Emitted when `amount` tokens are minted by `MINTER_ROLE` and assigned to `to`.
*/
event Mint(address indexed minter, address indexed to, uint256 amount);
/**
* @dev Emitted when `amount` tokens are burnt by `MINTER_ROLE` from its own balance.
*/
event Burn(address indexed burner, uint256 amount);
/**
* @dev Error thrown when mint/burn amount passed to {mint} / {burn} is zero.
*/
error ZeroAmount();
/**
* @dev Error thrown when address passed as param is zero.
*/
error ZeroAddress();
/// @custom:oz-upgrades-unsafe-allow constructor
// solhint-disable-next-line func-visibility
constructor() {
_disableInitializers();
}
function initialize(
uint256 initialSupply,
address recipient,
address roleAdmin,
address pauser,
address minter,
address upgrader
) public initializer {
__ERC20_init(_NAME, _SYMBOL);
__ERC20Pausable_init();
__AccessControl_init();
__ERC20Permit_init(_NAME);
__UUPSUpgradeable_init();
if (
recipient == address(0) || roleAdmin == address(0) || pauser == address(0) || minter == address(0)
|| upgrader == address(0)
) {
revert ZeroAddress();
}
_mint(recipient, initialSupply);
_grantRole(DEFAULT_ADMIN_ROLE, roleAdmin);
_grantRole(PAUSER_ROLE, pauser);
_grantRole(MINTER_ROLE, minter);
_grantRole(UPGRADER_ROLE, upgrader);
}
/**
* @notice Pauses the Token contract.
* @dev Only callable by accounts with the `PAUSER_ROLE`
* and only if the contract is not paused.
* Requirements the contract must not be paused.
*/
function pause() external onlyRole(PAUSER_ROLE) {
_pause();
}
/**
* @notice Unpauses the Token contract.
* @dev Only callable by accounts with the `PAUSER_ROLE`
* and only if the contract is paused.
* Requirement: the contract must be paused.
*/
function unpause() external onlyRole(PAUSER_ROLE) {
_unpause();
}
/**
* @notice Mints tokens to a specified address.
* @dev Only callable by accounts with the `MINTER_ROLE`.
* Emits a {Mint} event with `minter` set to the address that initiated the minting,
* `to` set to the recipient's address, and `amount` set to the amount of tokens minted.
* If the amount is zero, a {ZeroAmount} error will be triggered.
* Requirement: the contract must not be paused. {ERC20PausableUpgradeable-_update} enforces it.
* @param to The address to receive the minted tokens.
* @param amount The amount of tokens to mint.
*/
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
if (amount == 0) revert ZeroAmount();
_mint(to, amount);
emit Mint(_msgSender(), to, amount);
}
/**
* @notice Burn tokens from own balance.
* @dev Only callable by accounts with the `MINTER_ROLE`. The `value` must be greater than 0 and less than or
* equal to the caller's token balance.
* Emits a {Burn} event with `burner` set to the address that initiated the burn,
* and `amount` set to the number of tokens burned.
* If the amount is zero, a {ZeroAmount} error will be triggered.
* Requirement: the contract must not be paused. {ERC20PausableUpgradeable-_update} enforces it.
* @param amount The number of tokens to burn.
*/
function burn(uint256 amount) external onlyRole(MINTER_ROLE) {
if (amount == 0) revert ZeroAmount();
_burn(_msgSender(), amount);
emit Burn(_msgSender(), amount);
}
/**
* @dev See {IERC20-approve}.
*
* Requirement: the contract must not be paused.
*/
function approve(address spender, uint256 value) public virtual override whenNotPaused returns (bool) {
return super.approve(spender, value);
}
/**
* @dev See {IERC20Permit-permit}.
*
* Requirement: the contract must not be paused.
*/
function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
public
virtual
override
whenNotPaused
{
super.permit(owner, spender, value, deadline, v, r, s);
}
// solhint-disable-next-line no-empty-blocks
function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {}
/**
* @dev See {ERC20-_update}.
*
* Requirement: the contract must not be paused.
*/
function _update(address from, address to, uint256 value)
internal
virtual
override(ERC20Upgradeable, ERC20PausableUpgradeable)
{
super._update(from, to, value);
}
}
"
},
"lib/openzeppelin-contracts/contracts/access/IAccessControl.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/IAccessControl.sol)
pragma solidity ^0.8.20;
/**
* @dev External interface of AccessControl declared to support ERC-165 detection.
*/
interface IAccessControl {
/**
* @dev The `account` is missing a role.
*/
error AccessControlUnauthorizedAccount(address account, bytes32 neededRole);
/**
* @dev The caller of a function is not the expected one.
*
* NOTE: Don't confuse with {AccessControlUnauthorizedAccount}.
*/
error AccessControlBadConfirmation();
/**
* @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`
*
* `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite
* {RoleAdminChanged} not being emitted signaling this.
*/
event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);
/**
* @dev Emitted when `account` is granted `role`.
*
* `sender` is the account that originated the contract call. This account bears the admin role (for the granted role).
* Expected in cases where the role was granted using the internal {AccessControl-_grantRole}.
*/
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
/**
* @dev Emitted when `account` is revoked `role`.
*
* `sender` is the account that originated the contract call:
* - if using `revokeRole`, it is the admin role bearer
* - if using `renounceRole`, it is the role bearer (i.e. `account`)
*/
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) external view returns (bool);
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {AccessControl-_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) external view returns (bytes32);
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function grantRole(bytes32 role, address account) external;
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function revokeRole(bytes32 role, address account) external;
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been granted `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*/
function renounceRole(bytes32 role, address callerConfirmation) external;
}
"
},
"lib/openzeppelin-contracts/contracts/utils/Context.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}
"
},
"lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/ERC165.sol)
pragma solidity ^0.8.20;
import {IERC165} from "./IERC165.sol";
/**
* @dev Implementation of the {IERC165} interface.
*
* Contracts that want to implement ERC-165 should inherit from this contract and override {supportsInterface} to check
* for the additional interface id that will be supported. For example:
*
* ```solidity
* function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
* return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
* }
* ```
*/
abstract contract ERC165 is IERC165 {
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return interfaceId == type(IERC165).interfaceId;
}
}
"
},
"lib/openzeppelin-contracts/contracts/interfaces/IERC1363.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1363.sol)
pragma solidity ^0.8.20;
import {IERC20} from "./IERC20.sol";
import {IERC165} from "./IERC165.sol";
/**
* @title IERC1363
* @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].
*
* Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract
* after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.
*/
interface IERC1363 is IERC20, IERC165 {
/*
* Note: the ERC-165 identifier for this interface is 0xb0202a11.
* 0xb0202a11 ===
* bytes4(keccak256('transferAndCall(address,uint256)')) ^
* bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^
* bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^
* bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^
* bytes4(keccak256('approveAndCall(address,uint256)')) ^
* bytes4(keccak256('approveAndCall(address,uint256,bytes)'))
*/
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* @param to The address which you want to transfer to.
* @param value The amount of tokens to be transferred.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function transferAndCall(address to, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* @param to The address which you want to transfer to.
* @param value The amount of tokens to be transferred.
* @param data Additional data with no specified format, sent in call to `to`.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* @param from The address which you want to send tokens from.
* @param to The address which you want to transfer to.
* @param value The amount of tokens to be transferred.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function transferFromAndCall(address from, address to, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* @param from The address which you want to send tokens from.
* @param to The address which you want to transfer to.
* @param value The amount of tokens to be transferred.
* @param data Additional data with no specified format, sent in call to `to`.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
* @param spender The address which will spend the funds.
* @param value The amount of tokens to be spent.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function approveAndCall(address spender, uint256 value) external returns (bool);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
* @param spender The address which will spend the funds.
* @param value The amount of tokens to be spent.
* @param data Additional data with no specified format, sent in call to `spender`.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);
}
"
},
"lib/solmate/src/tokens/ERC20.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol)
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20 {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);
/*//////////////////////////////////////////////////////////////
METADATA STORAGE
//////////////////////////////////////////////////////////////*/
string public name;
string public symbol;
uint8 public immutable decimals;
/*//////////////////////////////////////////////////////////////
ERC20 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/*//////////////////////////////////////////////////////////////
EIP-2612 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 internal immutable INITIAL_CHAIN_ID;
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
}
/*//////////////////////////////////////////////////////////////
ERC20 LOGIC
//////////////////////////////////////////////////////////////*/
function approve(address spender, uint256 amount) public virtual returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount) public virtual returns (bool) {
balanceOf[msg.sender] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual returns (bool) {
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;
balanceOf[from] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
return true;
}
/*//////////////////////////////////////////////////////////////
EIP-2612 LOGIC
//////////////////////////////////////////////////////////////*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
// Unchecked because the only math done is incrementing
// the owner's nonce which cannot realistically overflow.
unchecked {
address recoveredAddress = ecrecover(
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
)
)
),
v,
r,
s
);
require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");
allowance[recoveredAddress][spender] = value;
}
emit Approval(owner, spender, value);
}
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
}
function computeDomainSeparator() internal view vir
Submitted on: 2025-09-24 16:55:51
Comments
Log in to comment.
No comments yet.