Contract Name:
UpgradeableAutopoolFactory
Contract Source Code:
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { ISystemRegistry } from "src/interfaces/ISystemRegistry.sol";
import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import { IUpgradeableAutopoolFactory, IAutopoolFactory } from "src/interfaces/vault/IUpgradeableAutopoolFactory.sol";
import { Roles } from "src/libs/Roles.sol";
import { Errors } from "src/utils/Errors.sol";
import { LibAdapter } from "src/libs/LibAdapter.sol";
import { SystemComponent } from "src/SystemComponent.sol";
import { SecurityBase } from "src/security/SecurityBase.sol";
import { Ownable } from "openzeppelin-contracts/access/Ownable.sol";
import { AutopoolETH } from "src/vault/AutopoolETH.sol";
import { AutopoolMainRewarder } from "src/rewarders/AutopoolMainRewarder.sol";
import { SafeERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import { ProxyAdmin } from "openzeppelin-contracts/proxy/transparent/ProxyAdmin.sol";
import { TransparentUpgradeableProxy } from "openzeppelin-contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
/// @title Deploys upgradeable Autopools with a generalized base asset
/// @dev Factory can only create an autopool with a single base asset, meaning that a new implementation cannot
/// have a different base asset than the previous implementation
/// @dev Creates a ProxyAdmin.sol contract on construction. This contract is meant to be the admin of all of the
/// proxies created through this contract. See OpenZeppelin Transparent proxy pattern for more information
/// @dev Implements IAutopoolFactory as this interface is used by the SystemRegistry
contract UpgradeableAutopoolFactory is SystemComponent, SecurityBase, IUpgradeableAutopoolFactory {
using SafeERC20 for IERC20;
/// =====================================================
/// Errors
/// =====================================================
/// @notice Thrown when there is an invalid amount of base asset for autopool init
error InvalidBaseAmount(uint256 amount);
/// =====================================================
/// Events
/// =====================================================
/// @notice Emitted when a new ProxyAdmin.sol contract is set
event ProxyAdminSet(address oldAdmin, address newAdmin);
/// @notice Emitted when a new logic / implementation contract is set
event ImplementationSet(address oldImplementation, address newImplementation);
/// @notice Emitted when a new default reward ratio is set
event DefaultRewardRatioSet(uint256 oldRewardRatio, uint256 newRewardRatio);
/// @notice Emitted when a new reward block duration is set
event DefaultBlockDurationSet(uint256 oldBlockDuration, uint256 newBlockDuration);
/// =====================================================
/// State
/// =====================================================
/// @notice Base asset for autopool deployed from this factory
address public immutable baseAsset;
/// @notice Address of the proxy admin
address public proxyAdmin;
/// @notice Address of the implementation contract
address public implementation;
/// @notice Default reward ratio for rewarder set up
uint256 public defaultRewardRatio;
/// @notice Default block duration for rewarder set up
uint256 public defaultRewardBlockDuration;
/// =====================================================
/// Functions - Constructor
/// =====================================================
constructor(
ISystemRegistry _systemRegistry,
address _proxyAdminOwner,
address _implementation,
uint256 _defaultRewardRatio,
uint256 _defaultRewardBlockDuration
) SystemComponent(_systemRegistry) SecurityBase(address(_systemRegistry.accessController())) {
// Implementation first to get checks out of the way before base asset set up
Errors.verifyNotZero(_implementation, "_implementation");
_setImplementation(_implementation);
// Set base asset. Factory will only be able to support new implementations with the same base
address localBaseAsset = AutopoolETH(_implementation).asset();
Errors.verifyNotZero(localBaseAsset, "localBaseAsset");
// slither-disable-next-line missing-zero-check
baseAsset = localBaseAsset;
_setDefaultRewardRatio(_defaultRewardRatio);
_setDefaultRewardBlockDuration(_defaultRewardBlockDuration);
// Automatically sets msg.sender as owner. In this case, msg.sender will be this contract
address localProxyAdmin = address(new ProxyAdmin());
// Transfer ownership to desired proxy owner. Zero address check included in `transferOwnership` call
Ownable(localProxyAdmin).transferOwnership(_proxyAdminOwner);
_setProxyAdmin(localProxyAdmin);
}
/// =====================================================
/// Functions - External
/// =====================================================
/// @inheritdoc IUpgradeableAutopoolFactory
function setProxyAdmin(
address _newProxyAdmin
) external hasRole(Roles.AUTO_POOL_FACTORY_MANAGER) {
Errors.verifyNotZero(_newProxyAdmin, "_newProxyAdmin");
_setProxyAdmin(_newProxyAdmin);
}
/// @inheritdoc IUpgradeableAutopoolFactory
/// @dev Any implementations that need to be updated on already deployed proxies will have to be done separately,
/// this functionality only updates the implementation contract used for future proxies
function setImplementation(
address _newImplementation
) external hasRole(Roles.AUTO_POOL_FACTORY_MANAGER) {
Errors.verifyNotZero(_newImplementation, "_newImplementation");
if (AutopoolETH(_newImplementation).asset() != baseAsset) revert Errors.InvalidConfiguration();
_setImplementation(_newImplementation);
}
/// @inheritdoc IUpgradeableAutopoolFactory
function setDefaultRewardRatio(
uint256 _newRewardRatio
) external hasRole(Roles.AUTO_POOL_FACTORY_MANAGER) {
_setDefaultRewardRatio(_newRewardRatio);
}
/// @inheritdoc IUpgradeableAutopoolFactory
function setDefaultRewardBlockDuration(
uint256 _newBlockDuration
) external hasRole(Roles.AUTO_POOL_FACTORY_MANAGER) {
_setDefaultRewardBlockDuration(_newBlockDuration);
}
/// @inheritdoc IAutopoolFactory
/// @dev Compatibility with SystemRegistry. No strategy template in this factory
function addStrategyTemplate(
address
) external pure {
revert Errors.NotImplemented();
}
/// @inheritdoc IAutopoolFactory
/// @dev Compatibility with SystemRegistry. No strategy template in this factory
function removeStrategyTemplate(
address
) external pure {
revert Errors.NotImplemented();
}
/// @notice Returns the current implementation used to create Autopools
/// @dev Exists due to compatibility requirements with SystemRegistry
function template() external view returns (address) {
return implementation;
}
/// @inheritdoc IAutopoolFactory
/// @dev Only payable for compatibility with `IAutopoolFactory`, will not accept Eth
function createVault(
address,
string memory symbolSuffix,
string memory descPrefix,
bytes32,
bytes calldata extraData
) external payable hasRole(Roles.AUTO_POOL_FACTORY_VAULT_CREATOR) returns (address newVault) {
// Force weth for eth
if (msg.value > 0) revert Errors.MustBeZero();
// Create new proxy with implementation as logic contract
newVault = address(new TransparentUpgradeableProxy(implementation, proxyAdmin, ""));
// Create new rewarder
AutopoolMainRewarder rewarder = new AutopoolMainRewarder(
systemRegistry,
address(systemRegistry.toke()),
defaultRewardRatio,
defaultRewardBlockDuration,
true, // Allowing extra rewards
newVault
);
// Autopool init deposit
uint256 baseInitAmount = AutopoolETH(implementation).BASE_ASSET_INIT_DEPOSIT();
IERC20 baseAssetERC20 = IERC20(baseAsset);
baseAssetERC20.safeTransferFrom(msg.sender, address(this), baseInitAmount);
LibAdapter._approve(baseAssetERC20, newVault, baseInitAmount);
// Init and rewarder set
AutopoolETH(newVault).initialize(symbolSuffix, descPrefix, extraData);
AutopoolETH(newVault).setRewarder(address(rewarder));
// Add to autopool registry
systemRegistry.autoPoolRegistry().addVault(newVault);
}
/// =====================================================
/// Functions - Internal
/// =====================================================
function _setProxyAdmin(
address _newProxyAdmin
) internal {
if (_newProxyAdmin == proxyAdmin) revert Errors.ItemExists();
emit ProxyAdminSet(proxyAdmin, _newProxyAdmin);
proxyAdmin = _newProxyAdmin;
}
function _setImplementation(
address _newImplementation
) internal {
if (_newImplementation == implementation) revert Errors.ItemExists();
if (address(systemRegistry) != SystemComponent(_newImplementation).getSystemRegistry()) {
revert Errors.SystemMismatch(address(this), _newImplementation);
}
emit ImplementationSet(implementation, _newImplementation);
implementation = _newImplementation;
}
/// @dev Zero valid
function _setDefaultRewardRatio(
uint256 _newRewardRatio
) internal {
emit DefaultRewardRatioSet(defaultRewardRatio, _newRewardRatio);
defaultRewardRatio = _newRewardRatio;
}
/// @dev Zero valid
function _setDefaultRewardBlockDuration(
uint256 _newBlockDuration
) internal {
emit DefaultBlockDurationSet(defaultRewardBlockDuration, _newBlockDuration);
defaultRewardBlockDuration = _newBlockDuration;
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IWETH9 } from "src/interfaces/utils/IWETH9.sol";
import { IAccToke } from "src/interfaces/staking/IAccToke.sol";
import { IAutopoolRegistry } from "src/interfaces/vault/IAutopoolRegistry.sol";
import { IAccessController } from "src/interfaces/security/IAccessController.sol";
import { ISwapRouter } from "src/interfaces/swapper/ISwapRouter.sol";
import { ICurveResolver } from "src/interfaces/utils/ICurveResolver.sol";
import { IAutopilotRouter } from "src/interfaces/vault/IAutopilotRouter.sol";
import { IAutopoolFactory } from "src/interfaces/vault/IAutopoolFactory.sol";
import { ISystemSecurity } from "src/interfaces/security/ISystemSecurity.sol";
import { IDestinationRegistry } from "src/interfaces/destinations/IDestinationRegistry.sol";
import { IRootPriceOracle } from "src/interfaces/oracles/IRootPriceOracle.sol";
import { IDestinationVaultRegistry } from "src/interfaces/vault/IDestinationVaultRegistry.sol";
import { IAccessController } from "src/interfaces/security/IAccessController.sol";
import { IStatsCalculatorRegistry } from "src/interfaces/stats/IStatsCalculatorRegistry.sol";
import { IAsyncSwapperRegistry } from "src/interfaces/liquidation/IAsyncSwapperRegistry.sol";
import { IERC20Metadata } from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { IIncentivesPricingStats } from "src/interfaces/stats/IIncentivesPricingStats.sol";
import { IMessageProxy } from "src/interfaces/messageProxy/IMessageProxy.sol";
/// @notice Root most registry contract for the system
interface ISystemRegistry {
/// @notice Get the TOKE contract for the system
/// @return toke instance of TOKE used in the system
function toke() external view returns (IERC20Metadata);
/// @notice Get the referenced WETH contract for the system
/// @return weth contract pointer
function weth() external view returns (IWETH9);
/// @notice Get the AccToke staking contract
/// @return accToke instance of the accToke contract for the system
function accToke() external view returns (IAccToke);
/// @notice Get the AutopoolRegistry for this system
/// @return registry instance of the registry for this system
function autoPoolRegistry() external view returns (IAutopoolRegistry registry);
/// @notice Get the destination Vault registry for this system
/// @return registry instance of the registry for this system
function destinationVaultRegistry() external view returns (IDestinationVaultRegistry registry);
/// @notice Get the access Controller for this system
/// @return controller instance of the access controller for this system
function accessController() external view returns (IAccessController controller);
/// @notice Get the destination template registry for this system
/// @return registry instance of the registry for this system
function destinationTemplateRegistry() external view returns (IDestinationRegistry registry);
/// @notice Auto Pilot Router
/// @return router instance of the system
function autoPoolRouter() external view returns (IAutopilotRouter router);
/// @notice Vault factory lookup by type
/// @return vaultFactory instance of the vault factory for this vault type
function getAutopoolFactoryByType(
bytes32 vaultType
) external view returns (IAutopoolFactory vaultFactory);
/// @notice Get the stats calculator registry for this system
/// @return registry instance of the registry for this system
function statsCalculatorRegistry() external view returns (IStatsCalculatorRegistry registry);
/// @notice Get the root price oracle for this system
/// @return oracle instance of the root price oracle for this system
function rootPriceOracle() external view returns (IRootPriceOracle oracle);
/// @notice Get the async swapper registry for this system
/// @return registry instance of the registry for this system
function asyncSwapperRegistry() external view returns (IAsyncSwapperRegistry registry);
/// @notice Get the swap router for this system
/// @return router instance of the swap router for this system
function swapRouter() external view returns (ISwapRouter router);
/// @notice Get the curve resolver for this system
/// @return resolver instance of the curve resolver for this system
function curveResolver() external view returns (ICurveResolver resolver);
/// @notice Verify if given address is registered as Reward Token
/// @param rewardToken token address to verify
/// @return bool that indicates true if token is registered and false if not
function isRewardToken(
address rewardToken
) external view returns (bool);
/// @notice Get the system security instance for this system
/// @return security instance of system security for this system
function systemSecurity() external view returns (ISystemSecurity security);
/// @notice Get the Incentive Pricing Stats
/// @return incentivePricing the incentive pricing contract
function incentivePricing() external view returns (IIncentivesPricingStats);
/// @notice Get the Message Proxy
/// @return Message proxy contract
function messageProxy() external view returns (IMessageProxy);
/// @notice Get the receiving router contract.
/// @return Receiving router contract
function receivingRouter() external view returns (address);
/// @notice Check if an additional contract of type is valid in the system
/// @return True if the contract is a valid for the given type
function isValidContract(bytes32 contractType, address contractAddress) external view returns (bool);
/// @notice Returns the additional contract of the given type
/// @dev Revert if not set
function getUniqueContract(
bytes32 contractType
) external view returns (address);
/// @notice Returns all unique contracts configured
function listUniqueContracts() external view returns (bytes32[] memory contractTypes, address[] memory addresses);
/// @notice Returns all additional contract types configured
function listAdditionalContractTypes() external view returns (bytes32[] memory);
/// @notice Returns configured additional contracts by type
/// @param contractType Type of contract to list
function listAdditionalContracts(
bytes32 contractType
) external view returns (address[] memory);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.0;
/**
* @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 amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` 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 amount) 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 `amount` 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 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `from` to `to` using the
* allowance mechanism. `amount` 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 amount
) external returns (bool);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IAutopoolFactory } from "src/interfaces/vault/IAutopoolFactory.sol";
/// @dev Implements IAutopoolFactory for compatibility with system registry
interface IUpgradeableAutopoolFactory is IAutopoolFactory {
/// @notice Sets a new ProxyAdmin.sol contract. Admin contract controls admin proxy functionalities
/// @dev See OpenZeppelin transparent proxy pattern for more information
/// @dev Must implement role based access control
/// @param _newProxyAdmin Address of new ProxyAdmin.sol contract
function setProxyAdmin(
address _newProxyAdmin
) external;
/// @notice Sets a new implementation / logic contract
/// @dev Note that this does not update any already existing proxies to the new implementation, done separately
/// @dev Must implement role based access control
/// @param _newImplementation Address of the new autopool implementation / logic contract
function setImplementation(
address _newImplementation
) external;
/// @notice Updates default reward ratio
/// @dev Must implement role based access control
/// @param _newRewardRatio New reward ratio for rewarder set up
function setDefaultRewardRatio(
uint256 _newRewardRatio
) external;
/// @notice Updates default block duration for rewards
/// @dev Must implement role based access control
/// @param _newBlockDuration New reward block duration for rewarder set up
function setDefaultRewardBlockDuration(
uint256 _newBlockDuration
) external;
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
library Roles {
// Naming Conventions:
// - Use MANAGER, CREATOR, UPDATER, ..., for roles primarily managing on-chain activities.
// - Use EXECUTOR for roles that trigger off-chain initiated actions.
// - Group roles by functional area for clarity.
// Destination Vault Management
bytes32 public constant DESTINATION_VAULT_FACTORY_MANAGER = keccak256("CREATE_DESTINATION_VAULT_ROLE");
bytes32 public constant DESTINATION_VAULT_REGISTRY_MANAGER = keccak256("DESTINATION_VAULT_REGISTRY_MANAGER");
bytes32 public constant DESTINATION_VAULT_MANAGER = keccak256("DESTINATION_VAULT_MANAGER");
// Auto Pool Factory and Registry Management
bytes32 public constant AUTO_POOL_REGISTRY_UPDATER = keccak256("REGISTRY_UPDATER");
bytes32 public constant AUTO_POOL_FACTORY_MANAGER = keccak256("AUTO_POOL_FACTORY_MANAGER");
bytes32 public constant AUTO_POOL_FACTORY_VAULT_CREATOR = keccak256("CREATE_POOL_ROLE");
// Auto Pool Management
bytes32 public constant AUTO_POOL_DESTINATION_UPDATER = keccak256("DESTINATION_VAULTS_UPDATER");
bytes32 public constant AUTO_POOL_FEE_UPDATER = keccak256("AUTO_POOL_FEE_SETTER_ROLE");
bytes32 public constant AUTO_POOL_PERIODIC_FEE_UPDATER = keccak256("AUTO_POOL_PERIODIC_FEE_SETTER_ROLE");
bytes32 public constant AUTO_POOL_REWARD_MANAGER = keccak256("AUTO_POOL_REWARD_MANAGER_ROLE");
bytes32 public constant AUTO_POOL_MANAGER = keccak256("AUTO_POOL_ADMIN");
bytes32 public constant REBALANCER = keccak256("REBALANCER_ROLE");
bytes32 public constant STATS_HOOK_POINTS_ADMIN = keccak256("STATS_HOOK_POINTS_ADMIN");
// Reward Management
bytes32 public constant LIQUIDATOR_MANAGER = keccak256("LIQUIDATOR_ROLE");
bytes32 public constant DV_REWARD_MANAGER = keccak256("DV_REWARD_MANAGER_ROLE");
bytes32 public constant REWARD_LIQUIDATION_MANAGER = keccak256("REWARD_LIQUIDATION_MANAGER");
bytes32 public constant EXTRA_REWARD_MANAGER = keccak256("EXTRA_REWARD_MANAGER_ROLE");
bytes32 public constant REWARD_LIQUIDATION_EXECUTOR = keccak256("REWARD_LIQUIDATION_EXECUTOR");
bytes32 public constant BANK_SWAP_MANAGER = keccak256("BANK_SWAP_MANAGER");
// Statistics and Reporting
bytes32 public constant STATS_CALC_REGISTRY_MANAGER = keccak256("STATS_CALC_REGISTRY_MANAGER");
bytes32 public constant STATS_CALC_FACTORY_MANAGER = keccak256("CREATE_STATS_CALC_ROLE");
bytes32 public constant STATS_CALC_FACTORY_TEMPLATE_MANAGER = keccak256("STATS_CALC_TEMPLATE_MGMT_ROLE");
bytes32 public constant STATS_SNAPSHOT_EXECUTOR = keccak256("STATS_SNAPSHOT_ROLE");
bytes32 public constant STATS_INCENTIVE_TOKEN_UPDATER = keccak256("STATS_INCENTIVE_TOKEN_UPDATER");
bytes32 public constant STATS_GENERAL_MANAGER = keccak256("STATS_GENERAL_MANAGER");
bytes32 public constant STATS_LST_ETH_TOKEN_EXECUTOR = keccak256("STATS_LST_ETH_TOKEN_EXECUTOR");
bytes32 public constant STATS_CACHE_SET_TRANSIENT_EXECUTOR = keccak256("STATS_CACHE_SET_TRANSIENT_EXECUTOR");
// Emergency Management
bytes32 public constant EMERGENCY_PAUSER = keccak256("EMERGENCY_PAUSER");
bytes32 public constant SEQUENCER_OVERRIDE_MANAGER = keccak256("SEQUENCER_OVERRIDE_MANAGER");
// Miscellaneous Roles
bytes32 public constant SOLVER = keccak256("SOLVER_ROLE");
bytes32 public constant AUTO_POOL_REPORTING_EXECUTOR = keccak256("AUTO_POOL_UPDATE_DEBT_REPORTING_ROLE");
bytes32 public constant STRATEGY_HOOK_CONFIGURATION = keccak256("STRATEGY_HOOK_CONFIGURATION");
// Swapper Roles
bytes32 public constant SWAP_ROUTER_MANAGER = keccak256("SWAP_ROUTER_MANAGER");
// Price Oracles Roles
bytes32 public constant ORACLE_MANAGER = keccak256("ORACLE_MANAGER_ROLE");
bytes32 public constant CUSTOM_ORACLE_EXECUTOR = keccak256("CUSTOM_ORACLE_EXECUTOR");
bytes32 public constant MAVERICK_FEE_ORACLE_EXECUTOR = keccak256("MAVERICK_FEE_ORACLE_MANAGER");
// AccToke Roles
bytes32 public constant ACC_TOKE_MANAGER = keccak256("ACC_TOKE_MANAGER");
// Admin Roles
bytes32 public constant TOKEN_RECOVERY_MANAGER = keccak256("TOKEN_RECOVERY_ROLE");
bytes32 public constant INFRASTRUCTURE_MANAGER = keccak256("INFRASTRUCTURE_MANAGER");
// Cross chain communications roles
bytes32 public constant MESSAGE_PROXY_MANAGER = keccak256("MESSAGE_PROXY_MANAGER");
bytes32 public constant MESSAGE_PROXY_EXECUTOR = keccak256("MESSAGE_PROXY_EXECUTOR");
bytes32 public constant RECEIVING_ROUTER_MANAGER = keccak256("RECEIVING_ROUTER_MANAGER");
bytes32 public constant RECEIVING_ROUTER_EXECUTOR = keccak256("RECEIVING_ROUTER_EXECUTOR");
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { Address } from "openzeppelin-contracts/utils/Address.sol";
import { ISystemComponent } from "src/interfaces/ISystemComponent.sol";
// solhint-disable max-line-length
library Errors {
using Address for address;
///////////////////////////////////////////////////////////////////
// Set errors
///////////////////////////////////////////////////////////////////
error AccessDenied();
error ZeroAddress(string paramName);
error ZeroAmount();
error InsufficientBalance(address token);
error AssetNotAllowed(address token);
error NotImplemented();
error InvalidAddress(address addr);
error InvalidParam(string paramName);
error InvalidParams();
error UnsafePrice(address token, uint256 spotPrice, uint256 safePrice);
error AlreadySet(string param);
error AlreadyRegistered(address param);
error SlippageExceeded(uint256 expected, uint256 actual);
error ArrayLengthMismatch(uint256 length1, uint256 length2, string details);
error ItemNotFound();
error ItemExists();
error MissingRole(bytes32 role, address user);
error RegistryItemMissing(string item);
error NotRegistered();
// Used to check storage slot is empty before setting.
error MustBeZero();
// Used to check storage slot set before deleting.
error MustBeSet();
error ApprovalFailed(address token);
error FlashLoanFailed(address token, uint256 amount);
error SystemMismatch(address source1, address source2);
error InvalidToken(address token);
error UnreachableError();
error InvalidSigner(address signer);
error InvalidChainId(uint256 chainId);
error SenderMismatch(address recipient, address sender);
error UnsupportedMessage(bytes32 messageType, bytes message);
error NotSupported();
error InvalidConfiguration();
error InvalidDataReturned();
function verifyNotZero(address addr, string memory paramName) internal pure {
if (addr == address(0)) {
revert ZeroAddress(paramName);
}
}
function verifyNotZero(bytes32 key, string memory paramName) internal pure {
if (key == bytes32(0)) {
revert InvalidParam(paramName);
}
}
function verifyNotEmpty(string memory val, string memory paramName) internal pure {
if (bytes(val).length == 0) {
revert InvalidParam(paramName);
}
}
function verifyNotZero(uint256 num, string memory paramName) internal pure {
if (num == 0) {
revert InvalidParam(paramName);
}
}
function verifySystemsMatch(address component1, address component2) internal view {
address registry1 =
abi.decode(component1.functionStaticCall(abi.encodeCall(ISystemComponent.getSystemRegistry, ())), (address));
address registry2 =
abi.decode(component2.functionStaticCall(abi.encodeCall(ISystemComponent.getSystemRegistry, ())), (address));
if (registry1 != registry2) {
revert SystemMismatch(component1, component2);
}
}
function verifyArrayLengths(uint256 length1, uint256 length2, string memory details) internal pure {
if (length1 != length2) {
revert ArrayLengthMismatch(length1, length2, details);
}
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
library LibAdapter {
using SafeERC20 for IERC20;
address public constant CURVE_REGISTRY_ETH_ADDRESS_POINTER = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
error MinLpAmountNotReached();
error LpTokenAmountMismatch();
error NoNonZeroAmountProvided();
error InvalidBalanceChange();
// Utils
function _approve(IERC20 token, address spender, uint256 amount) internal {
uint256 currentAllowance = token.allowance(address(this), spender);
if (currentAllowance > 0) {
token.safeDecreaseAllowance(spender, currentAllowance);
}
token.safeIncreaseAllowance(spender, amount);
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { ISystemComponent } from "src/interfaces/ISystemComponent.sol";
import { ISystemRegistry } from "src/interfaces/ISystemRegistry.sol";
import { Errors } from "src/utils/Errors.sol";
contract SystemComponent is ISystemComponent {
ISystemRegistry internal immutable systemRegistry;
constructor(
ISystemRegistry _systemRegistry
) {
Errors.verifyNotZero(address(_systemRegistry), "_systemRegistry");
systemRegistry = _systemRegistry;
}
/// @inheritdoc ISystemComponent
function getSystemRegistry() external view returns (address) {
return address(systemRegistry);
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IAccessController } from "src/interfaces/security/IAccessController.sol";
import { Errors } from "src/utils/Errors.sol";
contract SecurityBase {
IAccessController public immutable accessController;
error UndefinedAddress();
constructor(
address _accessController
) {
if (_accessController == address(0)) revert UndefinedAddress();
accessController = IAccessController(_accessController);
}
modifier onlyOwner() {
accessController.verifyOwner(msg.sender);
_;
}
modifier hasRole(
bytes32 role
) {
if (!accessController.hasRole(role, msg.sender)) revert Errors.AccessDenied();
_;
}
///////////////////////////////////////////////////////////////////
//
// Forward all the regular methods to central security module
//
///////////////////////////////////////////////////////////////////
function _hasRole(bytes32 role, address account) internal view returns (bool) {
return accessController.hasRole(role, account);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)
pragma solidity ^0.8.0;
import "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* By default, the owner account will be the one that deploys the contract. This
* can later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor() {
_transferOwnership(_msgSender());
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
require(owner() == _msgSender(), "Ownable: caller is not the owner");
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions anymore. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby removing any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
// ██
// ██
// ██
// ██
// ██
// █████████████████████████████████████████
// ██
// ██
// ██
// ██
// ██
import { Roles } from "src/libs/Roles.sol";
import { Errors } from "src/utils/Errors.sol";
import { AutopoolDebt } from "src/vault/libs/AutopoolDebt.sol";
import { Pausable } from "src/security/Pausable.sol";
import { VaultTypes } from "src/vault/VaultTypes.sol";
import { NonReentrantUpgradeable } from "src/utils/NonReentrantUpgradeable.sol";
import { SecurityBase } from "src/security/SecurityBase.sol";
import { IAutopool } from "src/interfaces/vault/IAutopool.sol";
import { AutopoolFees } from "src/vault/libs/AutopoolFees.sol";
import { AutopoolToken } from "src/vault/libs/AutopoolToken.sol";
import { Autopool4626 } from "src/vault/libs/Autopool4626.sol";
import { IStrategy } from "src/interfaces/strategy/IStrategy.sol";
import { ISystemSecurity } from "src/interfaces/security/ISystemSecurity.sol";
import { Math } from "openzeppelin-contracts/utils/math/Math.sol";
import { WithdrawalQueue } from "src/strategy/WithdrawalQueue.sol";
import { AutopoolDestinations } from "src/vault/libs/AutopoolDestinations.sol";
import { ISystemRegistry } from "src/interfaces/ISystemRegistry.sol";
import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import { ISystemComponent } from "src/interfaces/ISystemComponent.sol";
import { IAutopoolStrategy } from "src/interfaces/strategy/IAutopoolStrategy.sol";
import { IMainRewarder } from "src/interfaces/rewarders/IMainRewarder.sol";
import { StructuredLinkedList } from "src/strategy/StructuredLinkedList.sol";
import { Initializable } from "openzeppelin-contracts/proxy/utils/Initializable.sol";
import { EnumerableSet } from "openzeppelin-contracts/utils/structs/EnumerableSet.sol";
import { IERC20Metadata } from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { IERC3156FlashBorrower } from "openzeppelin-contracts/interfaces/IERC3156FlashBorrower.sol";
import { AutopoolState, ProcessRebalanceParams, AutopoolStorage } from "src/vault/libs/AutopoolState.sol";
import { AutopoolStrategyHooks } from "src/vault/libs/AutopoolStrategyHooks.sol";
import { IStrategyHook, HookFunctionIndex } from "src/interfaces/strategy/IStrategyHook.sol";
// solhint-disable max-states-count,const-name-snakecase
contract AutopoolETH is
ISystemComponent,
Initializable,
IAutopool,
IStrategy,
SecurityBase,
Pausable,
NonReentrantUpgradeable
{
using EnumerableSet for EnumerableSet.AddressSet;
using Math for uint256;
using WithdrawalQueue for StructuredLinkedList.List;
using AutopoolToken for AutopoolToken.TokenData;
using AutopoolDestinations for AutopoolState;
using AutopoolFees for AutopoolState;
using Autopool4626 for AutopoolState;
using AutopoolDebt for AutopoolState;
using AutopoolStrategyHooks for AutopoolState;
/// Be careful around the use of totalSupply and balanceOf. If you go directly to the _token struct you may miss
/// out on the profit share unlock logic or the checking the balance of the pool itself
/// =====================================================
/// Constant Vars
/// =====================================================
/// @notice Amount of base asset to be sent to vault on initialization.
uint256 public constant BASE_ASSET_INIT_DEPOSIT = 100_000;
// @notice Decimals of the Autopool
uint8 internal constant AUTOPOOL_DECIMALS = 18;
/// @notice 100% == 10000
uint256 internal constant FEE_DIVISOR = 10_000;
/// @notice Dead address for init share burn.
address internal constant DEAD_ADDRESS = 0x000000000000000000000000000000000000dEaD;
// solhint-disable-next-line var-name-mixedcase
uint256 public constant ONE = 10 ** AUTOPOOL_DECIMALS;
/// @notice The strategy logic for the Autopool
/// @dev Intentionally set to dead
IAutopoolStrategy public constant autoPoolStrategy = IAutopoolStrategy(DEAD_ADDRESS);
/// =====================================================
/// Immutable Vars
/// =====================================================
/// @notice Overarching baseAsset type
bytes32 public immutable vaultType = VaultTypes.GENERAL_V1;
/// @notice Amount to pad scaling operations by
uint256 public immutable decimalPad;
/// @notice Instance of this system this vault is tied to
/// @dev Exposed via `getSystemRegistry()`
ISystemRegistry internal immutable _systemRegistry;
/// @notice The asset that is deposited into the vault
/// @dev Exposed via `asset()`
IERC20Metadata internal immutable _baseAsset;
/// @notice Decimals of the base asset
uint8 internal immutable _baseAssetDecimals;
/// =====================================================
/// Modifiers
/// =====================================================
/// @notice Reverts if nav/share decreases during a deposit/mint/withdraw/redeem
/// @dev Increases are allowed. Ignored when supply is 0
modifier noNavPerShareDecrease(
TotalAssetPurpose purpose
) {
(uint256 oldNav, uint256 startingTotalSupply) = _snapStartNav(purpose);
_;
_ensureNoNavPerShareDecrease(oldNav, startingTotalSupply, purpose);
}
/// @notice Reverts if any nav/share changing operations are in progress across the system
/// @dev Any rebalance or debtReporting on any pool
modifier ensureNoNavOps() {
_checkNoNavOps();
_;
}
/// @notice Globally track operations that change nav/share in a vault
/// @dev Doesn't revert, only meant to track so that `ensureNoNavOps()` can revert when appropriate
modifier trackNavOps() {
ISystemSecurity systemSecurity = _systemRegistry.systemSecurity();
systemSecurity.enterNavOperation();
_;
// slither-disable-next-line reentrancy-no-eth
systemSecurity.exitNavOperation();
}
/// =====================================================
/// Functions - Construction
/// =====================================================
constructor(
ISystemRegistry systemRegistry,
address _vaultAsset
) SecurityBase(address(systemRegistry.accessController())) Pausable(systemRegistry) {
Errors.verifyNotZero(address(systemRegistry), "systemRegistry");
AutopoolState storage $ = AutopoolStorage.load();
_systemRegistry = systemRegistry;
_baseAssetDecimals = IERC20Metadata(_vaultAsset).decimals();
_baseAsset = IERC20Metadata(_vaultAsset);
$.symbol = string(abi.encodePacked("autopool", IERC20Metadata(_vaultAsset).symbol(), "Template"));
$.name = string(abi.encodePacked($.symbol, " Token"));
_disableInitializers();
if (_baseAssetDecimals > AUTOPOOL_DECIMALS) {
revert InvalidDecimals();
}
decimalPad = 10 ** (AUTOPOOL_DECIMALS - _baseAssetDecimals);
}
function initialize(
string memory symbolSuffix,
string memory descPrefix,
bytes memory
) external virtual initializer {
NonReentrantUpgradeable.initialize();
Errors.verifyNotEmpty(symbolSuffix, "symbolSuffix");
Errors.verifyNotEmpty(descPrefix, "descPrefix");
AutopoolState storage $ = AutopoolStorage.load();
$.symbol = symbolSuffix;
$.name = descPrefix;
$.factory = msg.sender;
$.initializeFeeSettings();
// slither-disable-start reentrancy-no-eth
// Send 100_000 shares to dead address to prevent nav / share inflation attack that can happen
// with very small shares and totalAssets amount.
uint256 sharesMinted = deposit(BASE_ASSET_INIT_DEPOSIT, DEAD_ADDRESS);
if (sharesMinted != Autopool4626.changeDecimals(BASE_ASSET_INIT_DEPOSIT, _baseAssetDecimals, AUTOPOOL_DECIMALS))
{
revert ValueSharesMismatch(BASE_ASSET_INIT_DEPOSIT, sharesMinted);
}
// slither-disable-end reentrancy-no-eth
AutopoolFees.setProfitUnlockPeriod($, 86_400);
}
/// =====================================================
/// Functions - External
/// =====================================================
/// @notice Enable or disable the high water mark on the rebalance fee
/// @dev Will revert if set to the same value
function setRebalanceFeeHighWaterMarkEnabled(
bool enabled
) external {
_ensureCallerHasRole(Roles.AUTO_POOL_FEE_UPDATER);
AutopoolState storage $ = AutopoolStorage.load();
AutopoolFees.setRebalanceFeeHighWaterMarkEnabled($.feeSettings, enabled);
}
/// @notice Set the fee that will be taken when profit is realized
/// @dev Resets the high water to current value
/// @param fee Percent. 100% == 10000
function setStreamingFeeBps(
uint256 fee
) external nonReentrant {
_ensureCallerHasRole(Roles.AUTO_POOL_FEE_UPDATER);
AutopoolState storage $ = AutopoolStorage.load();
$.setStreamingFeeBps(fee, $.oldestDebtReporting());
}
/// @notice Set the periodic fee taken.
/// @dev Depending on time until next fee take, may update periodicFeeBps directly or queue fee.
/// @param fee Fee to update periodic fee to.
function setPeriodicFeeBps(
uint256 fee
) external {
_ensureCallerHasRole(Roles.AUTO_POOL_PERIODIC_FEE_UPDATER);
AutopoolState storage $ = AutopoolStorage.load();
$.setPeriodicFeeBps(fee, $.oldestDebtReporting());
}
/// @notice Set the address that will receive fees
/// @param newFeeSink Address that will receive fees
function setFeeSink(
address newFeeSink
) external {
_ensureCallerHasRole(Roles.AUTO_POOL_FEE_UPDATER);
AutopoolFees.setFeeSink(AutopoolStorage.load().feeSettings, newFeeSink);
}
/// @notice Sets the address that will receive periodic fees.
/// @dev Zero address allowable. Disables fees.
/// @param newPeriodicFeeSink New periodic fee address.
function setPeriodicFeeSink(
address newPeriodicFeeSink
) external {
_ensureCallerHasRole(Roles.AUTO_POOL_PERIODIC_FEE_UPDATER);
AutopoolFees.setPeriodicFeeSink(AutopoolStorage.load().feeSettings, newPeriodicFeeSink);
}
/// @notice Change the length of time it takes for profits to unlock
/// @dev If set to 0, existing shares will unlock immediately and increase nav/share.
function setProfitUnlockPeriod(
uint48 newUnlockPeriodSeconds
) external {
_ensureCallerHasRole(Roles.AUTO_POOL_MANAGER);
AutopoolFees.setProfitUnlockPeriod(AutopoolStorage.load(), newUnlockPeriodSeconds);
}
/// @notice Set the rewarder contract used by the Autopool.
/// @param newRewarder Address of new rewarder.
function setRewarder(
address newRewarder
) external {
AutopoolState storage $ = AutopoolStorage.load();
// Factory needs to be able to call for vault creation.
if (msg.sender != $.factory && !_hasRole(Roles.AUTO_POOL_REWARD_MANAGER, msg.sender)) {
revert Errors.AccessDenied();
}
$.setRewarder(newRewarder);
}
/// @notice Allow the updating of symbol/desc for the vault (only AFTER shutdown)
/// @param newSymbol Symbol the Autopool will use going forward
/// @param newName Name the Autopool will use going forward
function setSymbolAndDescAfterShutdown(string memory newSymbol, string memory newName) external {
_ensureCallerHasRole(Roles.AUTO_POOL_MANAGER);
AutopoolStorage.load().setSymbolAndDescAfterShutdown(newSymbol, newName);
}
/// @notice Transfer out eligible tokens
/// @param tokens List of tokens to transfer
/// @param amounts Amount of those tokens to transfer
/// @param destinations Recipient wallets of the transferred tokens
function recover(
address[] calldata tokens,
uint256[] calldata amounts,
address[] calldata destinations
) external virtual override {
_ensureCallerHasRole(Roles.TOKEN_RECOVERY_MANAGER);
Autopool4626.recover(tokens, amounts, destinations);
}
/// @inheritdoc IAutopool
function shutdown(
VaultShutdownStatus reason
) external {
_ensureCallerHasRole(Roles.AUTO_POOL_MANAGER);
Autopool4626.shutdownVault(AutopoolStorage.load(), reason);
}
/// @notice Add Destinations to the Autopool
function addDestinations(
address[] calldata destinations
) external {
_ensureCallerHasRole(Roles.AUTO_POOL_DESTINATION_UPDATER);
AutopoolStorage.load().addDestinations(destinations, _systemRegistry);
}
/// @notice Remove Destinations from the Autopool
/// @dev If Destination currently has deployments the Destination will be removed when its empty
function removeDestinations(
address[] calldata destinations
) external {
_ensureCallerHasRole(Roles.AUTO_POOL_DESTINATION_UPDATER);
AutopoolStorage.load().removeDestinations(destinations);
}
/// @notice Update our cached values of the deployed Destinations
/// @param numToProcess The number of Destinations in the list to process
function updateDebtReporting(
uint256 numToProcess
) external nonReentrant trackNavOps {
_ensureCallerHasRole(Roles.AUTO_POOL_REPORTING_EXECUTOR);
AutopoolState storage $ = AutopoolStorage.load();
bytes memory hooks = $.getHookBytes();
// slither-disable-next-line reentrancy-no-eth
AutopoolDebt.AssetChanges memory result = AutopoolDebt.updateDebtReporting($, numToProcess, hooks);
_feeAndProfitHandling(result, hooks, true);
}
/// @notice Add a set of hooks to the Autopool configuration
/// @param newHooks Set of hooks to add to the Autopool
/// @param data Set of onRegister data to pass to the onRegistered function of the hook
function addHooks(IStrategyHook[] memory newHooks, bytes[] memory data) external {
_ensureCallerHasRole(Roles.STRATEGY_HOOK_CONFIGURATION);
AutopoolStorage.load().addHooks(newHooks, data);
}
/// @notice Remove a hook from the Autopools configuration
/// @param hookToRemove Hook to remove from to the Autopool
/// @param cleanupData Data to pass to the onUnregistered function of the hook
function removeHook(IStrategyHook hookToRemove, bytes calldata cleanupData) external {
_ensureCallerHasRole(Roles.STRATEGY_HOOK_CONFIGURATION);
AutopoolStorage.load().removeHook(hookToRemove, cleanupData);
}
/// @notice Get hooks configured on the Autopool
/// @dev Do not use in any executing code
function getHooks() external view returns (AutopoolStrategyHooks.HookConfiguration memory) {
return AutopoolStorage.load().getHooks();
}
/// @notice Returns the main rewarder for this contract
function rewarder() external view returns (IMainRewarder) {
return AutopoolStorage.load().rewarder;
}
/// @inheritdoc IAutopool
function isPastRewarder(
address _pastRewarder
) external view returns (bool) {
return AutopoolStorage.load().pastRewarders.contains(_pastRewarder);
}
/// @inheritdoc IAutopool
function isShutdown() external view returns (bool) {
return AutopoolStorage.load().shutdown;
}
/// @inheritdoc IAutopool
function shutdownStatus() external view returns (VaultShutdownStatus) {
return AutopoolStorage.load().shutdownStatus;
}
/// @notice Returns state and settings related to gradual profit unlock
function getProfitUnlockSettings() external view returns (IAutopool.ProfitUnlockSettings memory) {
return AutopoolStorage.load().profitUnlockSettings;
}
/// @notice Returns state and settings related to periodic and streaming fees
function getFeeSettings() external view returns (IAutopool.AutopoolFeeSettings memory) {
return AutopoolStorage.load().feeSettings;
}
/// @notice Returns amount of assets for shares provided in an ideal scenario where all the conditions are met.
function convertToAssets(
uint256 shares
) external view virtual returns (uint256 assets) {
assets = convertToAssets(shares, totalAssets(TotalAssetPurpose.Global), totalSupply(), Math.Rounding.Down);
}
/// @notice Returns the system instance this contract is tied to
function getSystemRegistry() external view override returns (address) {
return address(_systemRegistry);
}
/// @notice Returns the full list of Destinations configured on the Autopool
function getDestinations() external view override(IAutopool, IStrategy) returns (address[] memory) {
return AutopoolStorage.load().destinations.values();
}
/// @notice Returns the ordered list of Destinations that users will withdraw from when required
function getWithdrawalQueue() external view returns (address[] memory) {
return AutopoolStorage.load().withdrawalQueue.getList();
}
/// @notice Returns the list of Destinations that should be debt reported on
function getDebtReportingQueue() external view returns (address[] memory) {
return AutopoolStorage.load().debtReportQueue.getList();
}
/// @inheritdoc IAutopool
function isDestinationRegistered(
address destination
) external view returns (bool) {
return AutopoolStorage.load().destinations.contains(destination);
}
/// @notice Returns all destination currently queued for removal
function getRemovalQueue() external view override returns (address[] memory) {
return AutopoolStorage.load().removalQueue.values();
}
/// @notice Factory contract that created this vault
function factory() external view returns (address) {
return AutopoolStorage.load().factory;
}
/// @inheritdoc IAutopool
function getDestinationInfo(
address destVault
) external view returns (AutopoolDebt.DestinationInfo memory) {
return AutopoolStorage.load().destinationInfo[destVault];
}
/// @notice Return the timestamp of the oldest debt reporting
function oldestDebtReporting() external view returns (uint256) {
return AutopoolStorage.load().oldestDebtReporting();
}
/// @inheritdoc IAutopool
function isDestinationQueuedForRemoval(
address dest
) external view returns (bool) {
return AutopoolStorage.load().removalQueue.contains(dest);
}
/// =====================================================
/// Functions - Public
/// =====================================================
/// @notice Mints Vault shares to receiver by depositing exactly amount of underlying tokens
/// @dev No nav/share changing operations, debt reportings or rebalances,
/// can be happening throughout the entire system
function deposit(
uint256 assets,
address receiver
)
public
virtual
override
nonReentrant
noNavPerShareDecrease(TotalAssetPurpose.Deposit)
ensureNoNavOps
returns (uint256 shares)
{
AutopoolState storage $ = AutopoolStorage.load();
shares = Autopool4626.deposit($, address(_baseAsset), assets, receiver, paused(), _baseAssetDecimals);
onDeposit(assets, shares, receiver);
}
/// @notice Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens.
function mint(
uint256 shares,
address receiver
)
public
virtual
override
nonReentrant
noNavPerShareDecrease(TotalAssetPurpose.Deposit)
ensureNoNavOps
returns (uint256 assets)
{
AutopoolState storage $ = AutopoolStorage.load();
// Handles the vault being paused, returns 0
if (shares > maxMint(receiver)) {
revert ERC4626MintExceedsMax(shares, maxMint(receiver));
}
uint256 ta = $.totalAssetsTimeChecked(TotalAssetPurpose.Deposit);
assets = convertToAssets(shares, ta, totalSupply(), Math.Rounding.Up);
$.transferAndMint(_baseAsset, assets, shares, receiver);
onDeposit(assets, shares, receiver);
}
/// @notice Burns shares from owner and sends exactly assets of underlying tokens to receiver.
function withdraw(
uint256 assets,
address receiver,
address owner
)
external
virtual
override
nonReentrant
whenNotPaused
noNavPerShareDecrease(TotalAssetPurpose.Withdraw)
ensureNoNavOps
returns (uint256 shares)
{
Errors.verifyNotZero(assets, "assets");
AutopoolState storage $ = AutopoolStorage.load();
//slither-disable-next-line unused-return
(uint256 actualAssets, uint256 actualShares,) =
AutopoolDebt.withdraw($, assets, $.totalAssetsTimeChecked(TotalAssetPurpose.Withdraw));
shares = actualShares;
_completeWithdrawal($, actualAssets, shares, owner, receiver);
}
/// @notice Burns exactly shares from owner and sends assets of underlying tokens to receiver.
function redeem(
uint256 shares,
address receiver,
address owner
)
external
virtual
override
nonReentrant
whenNotPaused
noNavPerShareDecrease(TotalAssetPurpose.Withdraw)
ensureNoNavOps
returns (uint256 assets)
{
AutopoolState storage $ = AutopoolStorage.load();
uint256 ta = $.totalAssetsTimeChecked(TotalAssetPurpose.Withdraw);
{
uint256 maxShares = _maxRedeem(owner, ta);
if (shares > maxShares) {
revert ERC4626ExceededMaxRedeem(owner, shares, maxShares);
}
}
uint256 possibleAssets = convertToAssets(shares, ta, totalSupply(), Math.Rounding.Down);
Errors.verifyNotZero(possibleAssets, "possibleAssets");
//slither-disable-next-line unused-return
(uint256 actualAssets, uint256 actualShares,) = AutopoolDebt.redeem($, possibleAssets, ta);
assets = actualAssets;
assert(actualShares <= shares);
_completeWithdrawal($, actualAssets, shares, owner, receiver);
}
/// @notice Sets a `value` amount of tokens as the allowance of `spender` over the caller's tokens.
function approve(address spender, uint256 value) public virtual returns (bool) {
AutopoolState storage $ = AutopoolStorage.load();
return $.token.approve(spender, value);
}
/// @notice Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism.
/// `value` is then deducted from the caller's allowance.
function transferFrom(address from, address to, uint256 value) public virtual whenNotPaused returns (bool) {
AutopoolState storage $ = AutopoolStorage.load();
return $.token.transferFrom(from, to, value);
}
/// @notice Moves a `value` amount of tokens from the caller's account to `to`
function transfer(address to, uint256 value) public virtual whenNotPaused returns (bool) {
AutopoolState storage $ = AutopoolStorage.load();
return $.token.transfer(to, value);
}
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
AutopoolState storage $ = AutopoolStorage.load();
$.token.permit(owner, spender, value, deadline, v, r, s);
}
/// @notice Returns the maximum amount of the underlying asset that can be
/// deposited into the Vault for the receiver, through a deposit call
function maxDeposit(
address wallet
) public virtual override returns (uint256 maxAssets) {
AutopoolState storage $ = AutopoolStorage.load();
maxAssets = Autopool4626.maxDeposit($, wallet, paused(), _baseAssetDecimals);
}
/// @notice Simulate the effects of the deposit at the current block, given current on-chain conditions.
function previewDeposit(
uint256 assets
) public virtual returns (uint256 shares) {
AutopoolState storage $ = AutopoolStorage.load();
shares = convertToShares(
assets, $.totalAssetsTimeChecked(TotalAssetPurpose.Deposit), totalSupply(), Math.Rounding.Down
);
}
/// @notice Returns the maximum amount of the Vault shares that
/// can be minted for the receiver, through a mint call.
function maxMint(
address wallet
) public virtual override returns (uint256 maxShares) {
AutopoolState storage $ = AutopoolStorage.load();
maxShares = Autopool4626.maxMint($, wallet, paused());
}
/// @notice Returns the maximum amount of the underlying asset that can
/// be withdrawn from the owner balance in the Vault, through a withdraw call
function maxWithdraw(
address owner
) public virtual returns (uint256 maxAssets) {
AutopoolState storage $ = AutopoolStorage.load();
uint256 ownerShareBalance = balanceOf(owner);
uint256 taChecked = $.totalAssetsTimeChecked(TotalAssetPurpose.Withdraw);
if (paused() || ownerShareBalance == 0 || taChecked == 0) {
return 0;
}
uint256 convertedAssets = convertToAssets(ownerShareBalance, taChecked, totalSupply(), Math.Rounding.Down);
// slither-disable-next-line unused-return
(maxAssets,) = AutopoolDebt.preview(
$, true, convertedAssets, taChecked, abi.encodeCall(this.previewWithdraw, (convertedAssets))
);
}
/// @notice Returns the maximum amount of Vault shares that can be redeemed
/// from the owner balance in the Vault, through a redeem call
function maxRedeem(
address owner
) public virtual returns (uint256 maxShares) {
AutopoolState storage $ = AutopoolStorage.load();
maxShares = _maxRedeem(owner, $.totalAssetsTimeChecked(TotalAssetPurpose.Withdraw));
}
/// @notice Simulate the effects of a mint at the current block, given current on-chain conditions
function previewMint(
uint256 shares
) public virtual returns (uint256 assets) {
AutopoolState storage $ = AutopoolStorage.load();
uint256 ta = $.totalAssetsTimeChecked(TotalAssetPurpose.Deposit);
assets = convertToAssets(shares, ta, totalSupply(), Math.Rounding.Up);
Errors.verifyNotZero(assets, "assets");
}
/// @notice Simulate the effects of their withdrawal at the current block, given current on-chain conditions.
function previewWithdraw(
uint256 assets
) public virtual returns (uint256 shares) {
AutopoolState storage $ = AutopoolStorage.load();
// slither-disable-next-line unused-return
(, shares) = AutopoolDebt.preview(
$,
true,
assets,
$.totalAssetsTimeChecked(TotalAssetPurpose.Withdraw),
abi.encodeCall(this.previewWithdraw, (assets))
);
}
/// @notice Simulate the effects of their redemption at the current block, given current on-chain conditions.
function previewRedeem(
uint256 shares
) public virtual override returns (uint256 assets) {
AutopoolState storage $ = AutopoolStorage.load();
// These values are not needed until the recursive call, gas savings.
uint256 applicableTotalAssets = 0;
uint256 convertedAssets = 0;
if (msg.sender == address(this)) {
applicableTotalAssets = $.totalAssetsTimeChecked(TotalAssetPurpose.Withdraw);
convertedAssets = convertToAssets(shares, applicableTotalAssets, totalSupply(), Math.Rounding.Down);
}
// slither-disable-next-line unused-return
(assets,) = AutopoolDebt.preview(
$, false, convertedAssets, applicableTotalAssets, abi.encodeCall(this.previewRedeem, (shares))
);
}
/// @inheritdoc IStrategy
function flashRebalance(
IERC3156FlashBorrower receiver,
RebalanceParams memory rebalanceParams,
bytes calldata data
) public nonReentrant whenNotPaused trackNavOps {
_ensureCallerHasRole(Roles.SOLVER);
AutopoolState storage $ = AutopoolStorage.load();
bytes memory hooks = $.getHookBytes();
ProcessRebalanceParams memory params =
ProcessRebalanceParams({ baseAsset: _baseAsset, receiver: receiver, rebalanceParams: rebalanceParams });
AutopoolDebt.AssetChanges memory updatedAssets = AutopoolDebt.processRebalance($, params, data, hooks);
_feeAndProfitHandling(updatedAssets, hooks, false);
AutopoolStrategyHooks.executeHooks(
hooks,
uint256(HookFunctionIndex.onRebalanceFeeProfitHandlingComplete),
abi.encodeCall(IStrategyHook.onRebalanceFeeProfitHandlingComplete, (params, msg.sender))
);
// Ensure the destinations are in the queues they should be
$.manageQueuesForDestination(rebalanceParams.destinationOut, false);
$.manageQueuesForDestination(rebalanceParams.destinationIn, true);
AutopoolStrategyHooks.executeHooks(
hooks,
uint256(HookFunctionIndex.onRebalanceComplete),
abi.encodeCall(IStrategyHook.onRebalanceComplete, (params, msg.sender))
);
}
/// @notice Returns the name of the token
function name() public view virtual override returns (string memory) {
return AutopoolStorage.load().name;
}
/// @notice Returns the symbol of the token
function symbol() public view virtual override returns (string memory) {
return AutopoolStorage.load().symbol;
}
/// @notice Returns the decimals of the autoPool token, always denominated in 18 decimals
function decimals() public view virtual override returns (uint8) {
return AUTOPOOL_DECIMALS;
}
/// @notice Returns the address of the underlying token used for the Vault for accounting, depositing, and
/// withdrawing.
function asset() public view virtual override returns (address) {
return address(_baseAsset);
}
/// @notice Returns the total amount of the underlying asset that is “managed” by Vault.
/// @dev Utilizes the "Global" purpose internally
function totalAssets() public view override returns (uint256) {
return Autopool4626.totalAssets(AutopoolStorage.load().assetBreakdown, TotalAssetPurpose.Global);
}
/// @notice Returns total amount of the asset() that is “managed” by the Autopool
/// @dev Value changes based on purpose. Global is an avg. Deposit is valued higher. Withdraw is valued lower.
/// @param purpose The calculation the total assets will be used in
function totalAssets(
TotalAssetPurpose purpose
) public view returns (uint256) {
return Autopool4626.totalAssets(AutopoolStorage.load().assetBreakdown, purpose);
}
/// @notice Returns the amount of shares that the Vault would exchange for the amount of assets provided,
/// in an ideal scenario where all the conditions are met
function convertToShares(
uint256 assets
) public view virtual returns (uint256 shares) {
shares = convertToShares(assets, totalAssets(TotalAssetPurpose.Global), totalSupply(), Math.Rounding.Down);
}
/// @notice Returns the amount of shares that the Vault would exchange for the amount of assets provided,
/// in an ideal scenario where all the conditions are met
function convertToShares(
uint256 assets,
uint256 totalAssetsForPurpose,
uint256 supply,
Math.Rounding rounding
) public view virtual returns (uint256 shares) {
shares = Autopool4626.convertToShares(assets, totalAssetsForPurpose, supply, rounding, _baseAssetDecimals);
}
/// @notice Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an
/// ideal
/// scenario where all the conditions are met.
function convertToAssets(
uint256 shares,
uint256 totalAssetsForPurpose,
uint256 supply,
Math.Rounding rounding
) public view virtual returns (uint256 assets) {
assets = Autopool4626.convertToAssets(shares, totalAssetsForPurpose, supply, rounding, _baseAssetDecimals);
}
/// @notice Returns the amount of tokens in existence.
/// @dev Subtracts any unlocked profit shares that will be burned
function totalSupply() public view virtual override(IERC20) returns (uint256 shares) {
shares = Autopool4626.totalSupply();
}
/// @notice Returns the amount of tokens owned by account.
/// @dev Subtracts any unlocked profit shares that will be burned when account is the Vault itself
function balanceOf(
address account
) public view override(IERC20) returns (uint256) {
return AutopoolStorage.load().balanceOf(account);
}
/// @notice Returns the amount of tokens owned by wallet.
/// @dev Does not subtract any unlocked profit shares that should be burned when wallet is the Vault itself
function balanceOfActual(
address account
) public view returns (uint256) {
return AutopoolStorage.load().token.balances[account];
}
/// @notice Returns the remaining number of tokens that `spender` will be allowed to spend on
/// behalf of `owner` through {transferFrom}. This is zero by default
/// @dev This value changes when `approve` or `transferFrom` are called
function allowance(address owner, address spender) public view virtual returns (uint256) {
return AutopoolStorage.load().token.allowances[owner][spender];
}
function getAssetBreakdown() public view override returns (IAutopool.AssetBreakdown memory) {
return AutopoolStorage.load().assetBreakdown;
}
/// @notice Returns the next unused nonce for an address.
function nonces(
address owner
) public view virtual returns (uint256) {
AutopoolState storage $ = AutopoolStorage.load();
return $.token.nonces[owner];
}
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return keccak256(
abi.encode(
AutopoolToken.TYPE_HASH,
keccak256(bytes("Tokemak")),
keccak256(bytes("1")),
block.chainid,
address(this)
)
);
}
/// =====================================================
/// Functions - Internal
/// =====================================================
function onDeposit(uint256 assets, uint256 shares, address receiver) internal virtual { }
function _completeWithdrawal(
AutopoolState storage $,
uint256 assets,
uint256 shares,
address owner,
address receiver
) internal virtual {
AutopoolDebt.completeWithdrawal($, assets, shares, owner, receiver, _baseAsset);
}
function _feeAndProfitHandling(
AutopoolDebt.AssetChanges memory assetChanges,
bytes memory hooks,
bool collectPeriodicFees
) internal {
AutopoolState storage $ = AutopoolStorage.load();
uint256 startingTotalAssets = assetChanges.startingIdle + assetChanges.startingDebt;
uint256 newTotalAssets = assetChanges.newIdle + assetChanges.newDebt;
// Collect any fees and lock any profit if appropriate
$.burnUnlockedShares();
assetChanges.startingTotalSupply = totalSupply();
assetChanges.endingTotalSupply =
_collectFees(newTotalAssets, assetChanges.startingTotalSupply, collectPeriodicFees);
assetChanges.endingTotalSupply = AutopoolFees.calculateProfitLocking(
$.profitUnlockSettings,
$.token,
assetChanges.endingTotalSupply - assetChanges.startingTotalSupply, // new feeShares
newTotalAssets,
startingTotalAssets,
assetChanges.endingTotalSupply,
balanceOfActual(address(this))
);
AutopoolStrategyHooks.executeHooks(
hooks, uint256(HookFunctionIndex.onNavUpdate), abi.encodeCall(IStrategyHook.onNavUpdate, (assetChanges))
);
emit Nav(assetChanges.newIdle, assetChanges.newDebt, assetChanges.endingTotalSupply);
}
/// @dev This has been broken it out for testing purposes
function _collectFees(
uint256 currentTotalAssets,
uint256 currentTotalSupply,
bool collectPeriodicFees
) internal virtual returns (uint256) {
AutopoolState storage $ = AutopoolStorage.load();
return AutopoolFees.collectFees($, currentTotalAssets, currentTotalSupply, collectPeriodicFees);
}
/// @dev Revert if a nav-changing operation is in progress in the system
function _checkNoNavOps() internal view {
if (_systemRegistry.systemSecurity().navOpsInProgress() > 0) {
revert NavOpsInProgress();
}
}
/// @dev Revert if nav/share decreases on withdraw/redeem. No-op when totalSupply is zero.
function _ensureNoNavPerShareDecrease(
uint256 oldNav,
uint256 startingTotalSupply,
TotalAssetPurpose purpose
) internal view virtual {
uint256 ts = totalSupply();
// slither-disable-next-line incorrect-equality
if (ts == 0 || startingTotalSupply == 0) {
return;
}
uint256 newNav = (totalAssets(purpose) * decimalPad * FEE_DIVISOR) / ts;
if (newNav < oldNav) {
revert NavDecreased(oldNav, newNav);
}
}
/// =====================================================
/// Functions - Private
/// =====================================================
function _ensureCallerHasRole(
bytes32 role
) private view {
if (!accessController.hasRole(role, msg.sender)) revert Errors.AccessDenied();
}
function _snapStartNav(
TotalAssetPurpose purpose
) private view returns (uint256 oldNav, uint256 startingTotalSupply) {
startingTotalSupply = totalSupply();
// slither-disable-next-line incorrect-equality
if (startingTotalSupply == 0) {
return (0, 0);
}
oldNav = (totalAssets(purpose) * decimalPad * FEE_DIVISOR) / startingTotalSupply;
}
/// @dev Local gas-saving function to pass pre-calculated total assets time checked value
function _maxRedeem(address owner, uint256 _totalAssets) private returns (uint256 maxShares) {
// If total assets are zero then we are considered uncollateralized and all redeem's will fail
if (_totalAssets > 0) {
maxShares = paused() ? 0 : balanceOf(owner);
}
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { SafeERC20, IERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import { MainRewarder, ISystemRegistry, Errors } from "src/rewarders/MainRewarder.sol";
import { Roles } from "src/libs/Roles.sol";
/**
* @title AutopoolMainRewarder
* @notice Main rewarder for Autopool contracts.
*/
contract AutopoolMainRewarder is MainRewarder {
using SafeERC20 for IERC20;
/// @notice IERC20 instance of token being staked in rewarder.
IERC20 public immutable stakingToken;
// slither-disable-start similar-names
constructor(
ISystemRegistry _systemRegistry,
address _rewardToken,
uint256 _newRewardRatio,
uint256 _durationInBlock,
bool _allowExtraReward,
address _stakingToken
)
MainRewarder(
_systemRegistry,
_rewardToken,
_newRewardRatio,
_durationInBlock,
Roles.AUTO_POOL_REWARD_MANAGER,
_allowExtraReward
)
{
Errors.verifyNotZero(_stakingToken, "_stakingToken");
stakingToken = IERC20(_stakingToken);
}
// slither-disable-end similar-names
/**
* @notice Withdraws autopilot vault token from rewarder.
* @dev Balance updates, reward calculations taken care of in inherited contract.
* @param account Account that is withdrawing assets.
* @param amount Amount of assets to be withdrawn.
* @param claim Whether or not to claim rewards.
*/
function withdraw(address account, uint256 amount, bool claim) public {
if (msg.sender != account && msg.sender != address(systemRegistry.autoPoolRouter())) {
revert Errors.AccessDenied();
}
_withdraw(account, amount, claim);
stakingToken.safeTransfer(account, amount);
}
/**
* @notice Stakes autopilot vault token to rewarder.
* @dev Balance updates, reward calculations taken care of in inherited contract.
* @param account Account staking.
* @param amount Amount of autopilot vault token to stake.
*/
function stake(address account, uint256 amount) public {
_stake(account, amount);
// slither-disable-next-line arbitrary-send-erc20
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
}
/**
* @notice Gets reward for msg.sender.
* @dev Used to enforce msg.sender check.
* @param account Account to claim rewards for
* @param recipient Address to send rewards to
*/
function getReward(address account, address recipient, bool claimExtras) public {
if (msg.sender != account && msg.sender != address(systemRegistry.autoPoolRouter())) {
revert Errors.AccessDenied();
}
_getReward(account, recipient, claimExtras);
}
/**
* @notice Checks if token can be recovered.
* @dev staked token cant be recovered
* @param token Token to check.
* @return bool True if token can be recovered.
*/
function canTokenBeRecovered(
address token
) public view override returns (bool) {
if (token == address(stakingToken)) {
return false;
}
return true;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.0;
import "../IERC20.sol";
import "../extensions/draft-IERC20Permit.sol";
import "../../../utils/Address.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC20 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 {
using Address for address;
function safeTransfer(
IERC20 token,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}
function safeTransferFrom(
IERC20 token,
address from,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
/**
* @dev Deprecated. This function has issues similar to the ones found in
* {IERC20-approve}, and its usage is discouraged.
*
* Whenever possible, use {safeIncreaseAllowance} and
* {safeDecreaseAllowance} instead.
*/
function safeApprove(
IERC20 token,
address spender,
uint256 value
) internal {
// safeApprove should only be called when setting an initial allowance,
// or when resetting it to zero. To increase and decrease it, use
// 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
require(
(value == 0) || (token.allowance(address(this), spender) == 0),
"SafeERC20: approve from non-zero to non-zero allowance"
);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
}
function safeIncreaseAllowance(
IERC20 token,
address spender,
uint256 value
) internal {
uint256 newAllowance = token.allowance(address(this), spender) + value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
}
function safeDecreaseAllowance(
IERC20 token,
address spender,
uint256 value
) internal {
unchecked {
uint256 oldAllowance = token.allowance(address(this), spender);
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
uint256 newAllowance = oldAllowance - value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
}
}
function safePermit(
IERC20Permit token,
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) internal {
uint256 nonceBefore = token.nonces(owner);
token.permit(owner, spender, value, deadline, v, r, s);
uint256 nonceAfter = token.nonces(owner);
require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
}
/**
* @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).
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
// the target address contains contract code and also asserts for success in the low-level call.
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
if (returndata.length > 0) {
// Return data is optional
require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (proxy/transparent/ProxyAdmin.sol)
pragma solidity ^0.8.0;
import "./TransparentUpgradeableProxy.sol";
import "../../access/Ownable.sol";
/**
* @dev This is an auxiliary contract meant to be assigned as the admin of a {TransparentUpgradeableProxy}. For an
* explanation of why you would want to use this see the documentation for {TransparentUpgradeableProxy}.
*/
contract ProxyAdmin is Ownable {
/**
* @dev Returns the current implementation of `proxy`.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function getProxyImplementation(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("implementation()")) == 0x5c60da1b
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b");
require(success);
return abi.decode(returndata, (address));
}
/**
* @dev Returns the current admin of `proxy`.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function getProxyAdmin(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("admin()")) == 0xf851a440
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440");
require(success);
return abi.decode(returndata, (address));
}
/**
* @dev Changes the admin of `proxy` to `newAdmin`.
*
* Requirements:
*
* - This contract must be the current admin of `proxy`.
*/
function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner {
proxy.changeAdmin(newAdmin);
}
/**
* @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function upgrade(TransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner {
proxy.upgradeTo(implementation);
}
/**
* @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation. See
* {TransparentUpgradeableProxy-upgradeToAndCall}.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function upgradeAndCall(
TransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
proxy.upgradeToAndCall{value: msg.value}(implementation, data);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (proxy/transparent/TransparentUpgradeableProxy.sol)
pragma solidity ^0.8.0;
import "../ERC1967/ERC1967Proxy.sol";
/**
* @dev This contract implements a proxy that is upgradeable by an admin.
*
* To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector
* clashing], which can potentially be used in an attack, this contract uses the
* https://blog.openzeppelin.com/the-transparent-proxy-pattern/[transparent proxy pattern]. This pattern implies two
* things that go hand in hand:
*
* 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if
* that call matches one of the admin functions exposed by the proxy itself.
* 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the
* implementation. If the admin tries to call a function on the implementation it will fail with an error that says
* "admin cannot fallback to proxy target".
*
* These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing
* the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due
* to sudden errors when trying to call a function from the proxy implementation.
*
* Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way,
* you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy.
*/
contract TransparentUpgradeableProxy is ERC1967Proxy {
/**
* @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and
* optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}.
*/
constructor(
address _logic,
address admin_,
bytes memory _data
) payable ERC1967Proxy(_logic, _data) {
_changeAdmin(admin_);
}
/**
* @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin.
*/
modifier ifAdmin() {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback();
}
}
/**
* @dev Returns the current admin.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyAdmin}.
*
* TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the
* https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
* `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103`
*/
function admin() external ifAdmin returns (address admin_) {
admin_ = _getAdmin();
}
/**
* @dev Returns the current implementation.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyImplementation}.
*
* TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the
* https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
* `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc`
*/
function implementation() external ifAdmin returns (address implementation_) {
implementation_ = _implementation();
}
/**
* @dev Changes the admin of the proxy.
*
* Emits an {AdminChanged} event.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}.
*/
function changeAdmin(address newAdmin) external virtual ifAdmin {
_changeAdmin(newAdmin);
}
/**
* @dev Upgrade the implementation of the proxy.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}.
*/
function upgradeTo(address newImplementation) external ifAdmin {
_upgradeToAndCall(newImplementation, bytes(""), false);
}
/**
* @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified
* by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the
* proxied contract.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}.
*/
function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin {
_upgradeToAndCall(newImplementation, data, true);
}
/**
* @dev Returns the current admin.
*/
function _admin() internal view virtual returns (address) {
return _getAdmin();
}
/**
* @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}.
*/
function _beforeFallback() internal virtual override {
require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target");
super._beforeFallback();
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol";
interface IWETH9 is IERC20 {
function symbol() external view returns (string memory);
function deposit() external payable;
function withdraw(
uint256 amount
) external;
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IERC20Metadata } from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol";
interface IAccToke {
///////////////////////////////////////////////////////////////////
// Variables
///////////////////////////////////////////////////////////////////
function startEpoch() external view returns (uint256);
function minStakeDuration() external view returns (uint256);
struct Lockup {
uint128 amount;
uint128 end;
uint256 points;
}
function getLockups(
address user
) external view returns (Lockup[] memory);
function toke() external view returns (IERC20Metadata);
///////////////////////////////////////////////////////////////////
// Errors
///////////////////////////////////////////////////////////////////
error ZeroAddress();
error StakingDurationTooShort();
error StakingDurationTooLong();
error StakingPointsExceeded();
error IncorrectStakingAmount();
error InsufficientFunds();
error LockupDoesNotExist();
error NotUnlockableYet();
error AlreadyUnlocked();
error ExtendDurationTooShort();
error TransfersDisabled();
error TransferFailed();
error NoRewardsToClaim();
error InsufficientAmount();
error InvalidLockupIds();
error InvalidDurationLength();
error InvalidMinStakeDuration();
error AdminUnlockActive();
///////////////////////////////////////////////////////////////////
// Events
///////////////////////////////////////////////////////////////////
event SetMaxStakeDuration(uint256 oldDuration, uint256 newDuration);
event Stake(address indexed user, uint256 lockupId, uint256 amount, uint256 end, uint256 points);
event Unstake(address indexed user, uint256 lockupId, uint256 amount, uint256 end, uint256 points);
event Extend(
address indexed user,
uint256 lockupId,
uint256 amount,
uint256 oldEnd,
uint256 newEnd,
uint256 oldPoints,
uint256 newPoints
);
event RewardsAdded(uint256 amount, uint256 accRewardPerShare);
event RewardsCollected(address indexed user, uint256 amount);
event RewardsClaimed(address indexed user, address indexed recipient, uint256 amount);
event AdminUnlockSet(bool newUnlockState);
///////////////////////////////////////////////////////////////////
//
// Staking Methods
//
///////////////////////////////////////////////////////////////////
/**
* @notice Stake TOKE to an address that may not be the same as the sender of the funds. This can be used to give
* staked funds to someone else.
*
* If staking before the start of staking (epoch), then the lockup start and end dates are shifted forward so that
* the lockup starts at the epoch.
*
* @param amount TOKE to lockup in the stake
* @param duration in seconds for the stake
* @param to address to receive ownership of the stake
*/
function stake(uint256 amount, uint256 duration, address to) external;
/**
* @notice Stake TOKE
*
* If staking before the start of staking (epoch), then the lockup start and end dates are shifted forward so that
* the lockup starts at the epoch.
*
* @notice Stake TOKE for myself.
* @param amount TOKE to lockup in the stake
* @param duration in seconds for the stake
*/
function stake(uint256 amount, uint256 duration) external;
/**
* @notice Collect staked TOKE for a lockup and any earned rewards.
* @param lockupIds the id of the lockup to unstake
*/
function unstake(
uint256[] memory lockupIds
) external;
/**
* @notice Collect staked TOKE for a lockup and any earned rewards.
* @param lockupIds the id of the lockup to unstake
* @param user address of the user to unstake for
* @param to address to receive the unstaked TOKE
*/
function unstake(uint256[] memory lockupIds, address user, address to) external;
/**
* @notice Extend a stake lockup for additional points.
*
* The stake end time is computed from the current time + duration, just like it is for new stakes. So a new stake
* for seven days duration and an old stake extended with a seven days duration would have the same end.
*
* If an extend is made before the start of staking, the start time for the new stake is shifted forwards to the
* start of staking, which also shifts forward the end date.
*
* @param lockupIds the id of the old lockup to extend
* @param durations number of seconds from now to stake for
*/
function extend(uint256[] memory lockupIds, uint256[] memory durations) external;
///////////////////////////////////////////////////////////////////
//
// Rewards
//
///////////////////////////////////////////////////////////////////
/// @notice The total amount of rewards earned for all stakes
function totalRewardsEarned() external returns (uint256);
/// @notice Total rewards claimed by all stakers
function totalRewardsClaimed() external returns (uint256);
/// @notice Rewards claimed by a specific wallet
/// @param user Address of the wallet to check
function rewardsClaimed(
address user
) external returns (uint256);
/**
* @notice Calculate points based on duration from the staking system's start epoch to the user's staking end date
*
* @param amount TOKE to be staked
* @param duration number of seconds to stake for
* @return points staking points that would be returned
* @return end staking period end date
*/
function previewPoints(uint256 amount, uint256 duration) external view returns (uint256, uint256);
/// @notice Preview the reward amount a caller can claim
function previewRewards() external view returns (uint256);
/// @notice Preview the reward amount a specified wallet can claim
function previewRewards(
address user
) external view returns (uint256);
/// @notice Claim rewards for the caller
function collectRewards() external returns (uint256);
/// @notice Claim rewards for the user and send to recipient
function collectRewards(address user, address recipient) external returns (uint256);
/// @notice Check if amount can be staked
function isStakeableAmount(
uint256 amount
) external pure returns (bool);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
/// @title Keep track of Vaults created through the Vault Factory
interface IAutopoolRegistry {
///////////////////////////////////////////////////////////////////
// Errors
///////////////////////////////////////////////////////////////////
error VaultNotFound(address vaultAddress);
error VaultAlreadyExists(address vaultAddress);
///////////////////////////////////////////////////////////////////
// Events
///////////////////////////////////////////////////////////////////
event VaultAdded(address indexed asset, address indexed vault);
event VaultRemoved(address indexed asset, address indexed vault);
///////////////////////////////////////////////////////////////////
// Functions
///////////////////////////////////////////////////////////////////
/// @notice Checks if an address is a valid vault
/// @param vaultAddress Vault address to be added
function isVault(
address vaultAddress
) external view returns (bool);
/// @notice Registers a vault
/// @param vaultAddress Vault address to be added
function addVault(
address vaultAddress
) external;
/// @notice Removes vault registration
/// @param vaultAddress Vault address to be removed
function removeVault(
address vaultAddress
) external;
/// @notice Returns a list of all registered vaults
function listVaults() external view returns (address[] memory);
/// @notice Returns a list of all registered vaults for a given asset
/// @param asset Asset address
function listVaultsForAsset(
address asset
) external view returns (address[] memory);
/// @notice Returns a list of all registered vaults for a given type
/// @param _vaultType Vault type
function listVaultsForType(
bytes32 _vaultType
) external view returns (address[] memory);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IAccessControlEnumerable } from "openzeppelin-contracts/access/IAccessControlEnumerable.sol";
interface IAccessController is IAccessControlEnumerable {
error AccessDenied();
/**
* @notice Setup a role for an account
* @param role The role to setup
* @param account The account to setup the role for
*/
function setupRole(bytes32 role, address account) external;
/**
* @notice Verify if an account is an owner. Reverts if not
* @param account The account to verify
*/
function verifyOwner(
address account
) external view;
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { ISyncSwapper } from "src/interfaces/swapper/ISyncSwapper.sol";
interface ISwapRouter {
struct SwapData {
address token;
address pool;
ISyncSwapper swapper;
bytes data;
}
error MaxSlippageExceeded();
error SwapRouteLookupFailed(address from, address to);
error SwapFailed();
event SwapRouteSet(address indexed token, SwapData[] routes);
event SwapForQuoteSuccessful(
address indexed assetToken,
uint256 sellAmount,
address indexed quoteToken,
uint256 minBuyAmount,
uint256 buyAmount
);
/**
* @notice Sets a new swap route for a given asset token.
* @param assetToken The asset token for which the swap route is being set.
* @param _swapRoute The new swap route as an array of SwapData. The last element represents the quoteToken.
* @dev Each 'hop' in the swap route is validated using the respective swapper's validate function. The validate
* function ensures that the encoded data contains the correct 'fromAddress' and 'toAddress' (swapData.token), and
* verifies that these tokens are in the pool.
*/
function setSwapRoute(address assetToken, SwapData[] calldata _swapRoute) external;
/**
* @notice Swaps the asset token for the quote token.
* @dev We're adopting an "exact in, variable out" model for all our swaps. This ensures that the entire sellAmount
* is used, eliminating the need for additional balance checks and refunds. This model is expected to be followed by
* all swapper implementations to maintain consistency and to optimize for gas efficiency.
* @param assetToken The address of the asset token to swap.
* @param sellAmount The exact amount of the asset token to swap.
* @param quoteToken The address of the quote token.
* @param minBuyAmount The minimum amount of the quote token expected to be received from the swap.
* @return The amount received from the swap.
*/
function swapForQuote(
address assetToken,
uint256 sellAmount,
address quoteToken,
uint256 minBuyAmount
) external returns (uint256);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
interface ICurveResolver {
/// @notice Resolve details of a Curve pool regardless of type or version
/// @dev This resolves tokens without unwrapping to underlying in the case of meta pools.
/// @param poolAddress pool address to lookup
/// @return tokens tokens that make up the pool
/// @return numTokens the number of tokens. tokens are not unwrapped.
/// @return isStableSwap is this a StableSwap pool. false = CryptoSwap
function resolve(
address poolAddress
) external view returns (address[8] memory tokens, uint256 numTokens, bool isStableSwap);
/// @notice Resolve details of a Curve pool regardless of type or version
/// @dev This resolves tokens without unwrapping to underlying in the case of meta pools.
/// @dev Use the isStableSwap value to differentiate between StableSwap (V1) and CryptoSwap (V2) pools.
/// @param poolAddress pool address to lookup
/// @return tokens tokens that make up the pool
/// @return numTokens the number of tokens. tokens are not unwrapped
/// @return lpToken lp token of the pool
/// @return isStableSwap is this a StableSwap pool. false = CryptoSwap
function resolveWithLpToken(
address poolAddress
) external view returns (address[8] memory tokens, uint256 numTokens, address lpToken, bool isStableSwap);
/// @notice Get the lp token of a Curve pool
/// @param poolAddress pool address to lookup
function getLpToken(
address poolAddress
) external view returns (address);
/// @notice Get the reserves of a Curve pools' tokens
/// @dev Actual balances length might differ from 8 and should be verified by the caller
/// @param poolAddress pool address to lookup
/// @return balances reserves of the pool tokens
function getReservesInfo(
address poolAddress
) external view returns (uint256[8] memory balances);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IAutopool } from "src/interfaces/vault/IAutopool.sol";
import { IAutopilotRouterBase } from "src/interfaces/vault/IAutopilotRouterBase.sol";
import { IRewards } from "src/interfaces/rewarders/IRewards.sol";
import { SwapParams } from "src/interfaces/liquidation/IAsyncSwapper.sol";
import { ISwapRouterV2 } from "src/interfaces/swapper/ISwapRouterV2.sol";
/**
* @title IAutopilotRouter Interface
* @notice Extends the IAutopilotRouterBase with specific flows to save gas
*/
interface IAutopilotRouter is IAutopilotRouterBase {
/**
* *************************** Deposit ********************************
*/
/**
* @notice deposit available asset balance to a AutopoolETH.
* @param vault The AutopoolETH to deposit assets to.
* @param to The destination of ownership shares.
* @param minSharesOut The min amount of `vault` shares received by `to`.
* @return sharesOut the amount of shares received by `to`.
* @dev throws MinSharesError
*/
function depositBalance(
IAutopool vault,
address to,
uint256 minSharesOut
) external payable returns (uint256 sharesOut);
/**
* @notice deposit max assets to a AutopoolETH.
* @param vault The AutopoolETH to deposit assets to.
* @param to The destination of ownership shares.
* @param minSharesOut The min amount of `vault` shares received by `to`.
* @return sharesOut the amount of shares received by `to`.
* @dev throws MinSharesError
*/
function depositMax(
IAutopool vault,
address to,
uint256 minSharesOut
) external payable returns (uint256 sharesOut);
/**
* ************************* Withdraw **********************************
*/
/**
* @notice withdraw `amount` to a AutopoolETH.
* @param fromVault The AutopoolETH to withdraw assets from.
* @param toVault The AutopoolETH to deposit assets to.
* @param to The destination of ownership shares.
* @param amount The amount of assets to withdraw from fromVault.
* @param maxSharesIn The max amount of fromVault shares withdrawn by caller.
* @param minSharesOut The min amount of toVault shares received by `to`.
* @return sharesOut the amount of shares received by `to`.
* @dev throws MaxSharesError, MinSharesError
*/
function withdrawToDeposit(
IAutopool fromVault,
IAutopool toVault,
address to,
uint256 amount,
uint256 maxSharesIn,
uint256 minSharesOut
) external payable returns (uint256 sharesOut);
/**
* ************************* Redeem ********************************
*/
/**
* @notice redeem `shares` to a AutopoolETH.
* @param fromVault The AutopoolETH to redeem shares from.
* @param toVault The AutopoolETH to deposit assets to.
* @param to The destination of ownership shares.
* @param shares The amount of shares to redeem from fromVault.
* @param minSharesOut The min amount of toVault shares received by `to`.
* @return sharesOut the amount of shares received by `to`.
* @dev throws MinAmountError, MinSharesError
*/
function redeemToDeposit(
IAutopool fromVault,
IAutopool toVault,
address to,
uint256 shares,
uint256 minSharesOut
) external payable returns (uint256 sharesOut);
/**
* @notice redeem max shares to a AutopoolETH.
* @param vault The AutopoolETH to redeem shares from.
* @param to The destination of assets.
* @param minAmountOut The min amount of assets received by `to`.
* @return amountOut the amount of assets received by `to`.
* @dev throws MinAmountError
*/
function redeemMax(
IAutopool vault,
address to,
uint256 minAmountOut
) external payable returns (uint256 amountOut);
/**
* @notice redeem `shares` shares from a AutopoolETH with a custom route
* @param vault The AutopoolETH to redeem shares from.
* @param to The destination of assets.
* @param shares The amount of shares to redeem from vault.
* @param minAmountOut The min amount of assets received by `to`.
* @param customRoute The custom route to use for the swap.
* @return amountOut the amount of assets received by `to`.
* @dev throws MinAmountError
*/
function redeemWithRoutes(
IAutopool vault,
address to,
uint256 shares,
uint256 minAmountOut,
ISwapRouterV2.UserSwapData[] calldata customRoute
) external payable returns (uint256 amountOut);
/**
* @notice swaps token
* @param swapper Address of the swapper to use
* @param swapParams Parameters for the swap
* @return amountReceived Swap output amount
*/
function swapToken(
address swapper,
SwapParams memory swapParams
) external payable returns (uint256 amountReceived);
/**
* @notice claims vault token rewards
* @param rewarder Address of the rewarder to claim from
* @param recipient Struct containing recipient details
* @return amountReceived Swap output amount
*/
function claimRewards(
IRewards rewarder,
IRewards.Recipient calldata recipient,
uint8 v,
bytes32 r,
bytes32 s
) external payable returns (uint256);
/**
* @notice swaps Exact token balance in the contract
* @param swapper Address of the swapper to use
* @param swapParams Parameters for the swap
* @return amountReceived Swap output amount
* @dev sets the sellAmount to the balance of the contract
*/
function swapTokenBalance(
address swapper,
SwapParams memory swapParams
) external payable returns (uint256 amountReceived);
/**
* @notice stake Acc token balance
* @param duration The duration of the stake
* @param accToke contract address of the AccToke
* @param to The destination of ownership shares.
*/
function stakeAccBalance(address accToke, uint256 duration, address to) external payable;
/**
* @notice stake Acc token for specified amount
* @param amount Amount of TOKE to stake
* @param accToke contract address of the AccToke
* @param duration The duration of the stake
* @param to The destination of ownership shares.
*/
function stakeAcc(address accToke, uint256 amount, uint256 duration, address to) external payable;
/**
* @notice unstake Acc token balance
* @param accToke contract address of the AccToke
* @param lockupIds The lockup ids to unstake
* @param to The destination of staked TOKE.
*/
function unstakeAcc(address accToke, uint256[] memory lockupIds, address to) external payable;
/**
* @notice Collect staking rewards
* @dev rewards can only be sent to user or router
* @param accToke contract address of the AccToke
* @param recipient The recipient of the rewards
* @return amountReceived Swap output amount
*/
function collectAccTokeRewards(address accToke, address recipient) external payable returns (uint256);
/**
* @notice AccTokeV1 function to lock TOKE for `numOfCycles` cycles
* @param amount Amount of TOKE to lock up
* @param duration Number of cycles to lock for
*/
function lockTokeFor(uint256 amount, uint256 duration) external payable;
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
interface IAutopoolFactory {
///////////////////////////////////////////////////////////////////
// Vault Creation
///////////////////////////////////////////////////////////////////
/**
* @notice Spin up a new AutopoolETH
* @param strategy Strategy template address
* @param symbolSuffix Symbol suffix of the new token
* @param descPrefix Description prefix of the new token
* @param salt Vault creation salt
* @param extraParams Any extra data needed for the vault
*/
function createVault(
address strategy,
string memory symbolSuffix,
string memory descPrefix,
bytes32 salt,
bytes calldata extraParams
) external payable returns (address newVaultAddress);
function addStrategyTemplate(
address strategyTemplate
) external;
function removeStrategyTemplate(
address strategyTemplate
) external;
/// @notice Returns the template used to create Autopools
function template() external returns (address);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
interface ISystemSecurity {
/// @notice Get the number of NAV/share operations currently in progress
/// @return Number of operations
function navOpsInProgress() external view returns (uint256);
/// @notice Called at the start of any NAV/share changing operation
function enterNavOperation() external;
/// @notice Called at the end of any NAV/share changing operation
function exitNavOperation() external;
/// @notice Whether or not the system as a whole is paused
function isSystemPaused() external returns (bool);
/// @notice Sets an autopool in transient storage , this is used to guard against malicious user payloads
/// that could be used to reenter the system
/// @param autopool The address of the autopool to set
/// @dev This is used in the AutopilotRouter to guard against reentrancy via malicious payload in the
/// swap routes when redeeming
function setAllowedAutopool(
address autopool
) external;
/// @notice Clears the autopool from transient storage
function clearAllowedAutopool() external;
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IDestinationAdapter } from "src/interfaces/destinations/IDestinationAdapter.sol";
interface IDestinationRegistry {
event Register(bytes32[] indexed destinationTypes, address[] indexed targets);
event Replace(bytes32[] indexed destinationTypes, address[] indexed targets);
event Unregister(bytes32[] indexed destinationTypes);
event Whitelist(bytes32[] indexed destinationTypes);
event RemoveFromWhitelist(bytes32[] indexed destinationTypes);
error InvalidAddress(address addr);
error NotAllowedDestination();
error DestinationAlreadySet();
/**
* @notice Adds a new addresses of the given destination types
* @dev Fails if trying to overwrite previous value of the same destination type
* @param destinationTypes Ones from the destination type whitelist
* @param targets addresses of the deployed DestinationAdapters, cannot be 0
*/
function register(bytes32[] calldata destinationTypes, address[] calldata targets) external;
/**
* @notice Replaces an addresses of the given destination types
* @dev Fails if given destination type was not set previously
* @param destinationTypes Ones from the destination type whitelist
* @param targets addresses of the deployed DestinationAdapters, cannot be 0
*/
function replace(bytes32[] calldata destinationTypes, address[] calldata targets) external;
/**
* @notice Removes an addresses of the given pre-registered destination types
* @param destinationTypes Ones from the destination types whitelist
*/
function unregister(
bytes32[] calldata destinationTypes
) external;
/**
* @notice Gives an address of the given destination type
* @dev Should revert on missing destination
* @param destination One from the destination type whitelist
*/
function getAdapter(
bytes32 destination
) external returns (IDestinationAdapter);
/**
* @notice Adds given destination types to the whitelist
* @param destinationTypes Types to whitelist
*/
function addToWhitelist(
bytes32[] calldata destinationTypes
) external;
/**
* @notice Removes given pre-whitelisted destination types
* @param destinationTypes Ones from the destination type whitelist
*/
function removeFromWhitelist(
bytes32[] calldata destinationTypes
) external;
/**
* @notice Checks if the given destination type is whitelisted
* @param destinationType Type to verify
*/
function isWhitelistedDestination(
bytes32 destinationType
) external view returns (bool);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
/// @notice Retrieve a price for any token used in the system
interface IRootPriceOracle {
/// @notice Returns a fair price for the provided token in ETH
/// @param token token to get the price of
/// @return price the price of the token in ETH
function getPriceInEth(
address token
) external returns (uint256 price);
/// @notice Returns a spot price for the provided token in ETH, utilizing specified liquidity pool
/// @param token token to get the spot price of
/// @param pool liquidity pool to be used for price determination
/// @return price the spot price of the token in ETH based on the provided pool
function getSpotPriceInEth(address token, address pool) external returns (uint256);
/// @notice Returns a price for base token in quote token.
/// @dev Requires both tokens to be registered.
/// @param base Address of base token.
/// @param quote Address of quote token.
/// @return price Price of the base token in quote token.
function getPriceInQuote(address base, address quote) external returns (uint256 price);
/// @notice Retrieve the price of LP token based on the reserves
/// @param lpToken LP token to get the price of
/// @param pool liquidity pool to be used for price determination
/// @param quoteToken token to quote the price in
function getRangePricesLP(
address lpToken,
address pool,
address quoteToken
) external returns (uint256 spotPriceInQuote, uint256 safePriceInQuote, bool isSpotSafe);
/// @notice Returns floor or ceiling price of the supplied lp token in terms of requested quote.
/// @dev Floor price: the minimum price among all the spot prices and safe prices of the tokens in the pool.
/// Ceiling price: the maximum price among all the spot prices and safe prices of the tokens in the pool.
/// @param pool Address of pool to get spot pricing from.
/// @param lpToken Address of the lp token to price.
/// @param inQuote Address of desired quote token.
/// @param ceiling Bool indicating whether to get floor or ceiling price.
/// @return floorOrCeilingPerLpToken Floor or ceiling price of the lp token.
function getFloorCeilingPrice(
address pool,
address lpToken,
address inQuote,
bool ceiling
) external returns (uint256 floorOrCeilingPerLpToken);
function getFloorPrice(address, address, address) external returns (uint256 price);
function getCeilingPrice(address, address, address) external returns (uint256 price);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IDestinationVaultFactory } from "src/interfaces/vault/IDestinationVaultFactory.sol";
/// @notice Tracks valid Destination Vaults for the system
interface IDestinationVaultRegistry {
/// @notice Determines if a given address is a valid Destination Vault in the system
/// @param destinationVault address to check
/// @return True if vault is registered
function isRegistered(
address destinationVault
) external view returns (bool);
/// @notice Registers a new Destination Vault
/// @dev Should be locked down to only a factory
/// @param newDestinationVault Address of the new vault
function register(
address newDestinationVault
) external;
/// @notice Checks if an address is a valid Destination Vault and reverts if not
/// @param destinationVault Destination Vault address to checked
function verifyIsRegistered(
address destinationVault
) external view;
/// @notice Returns a list of all registered vaults
function listVaults() external view returns (address[] memory);
/// @notice Factory that is allowed to create and registry Destination Vaults
function factory() external view returns (IDestinationVaultFactory);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IStatsCalculator } from "src/interfaces/stats/IStatsCalculator.sol";
/// @notice Track stat calculators for this instance of the system
interface IStatsCalculatorRegistry {
/// @notice Get a registered calculator
/// @dev Should revert if missing
/// @param aprId key of the calculator to get
/// @return calculator instance of the calculator
function getCalculator(
bytes32 aprId
) external view returns (IStatsCalculator calculator);
/// @notice List all calculator addresses registered
function listCalculators() external view returns (bytes32[] memory, address[] memory);
/// @notice Register a new stats calculator
/// @param calculator address of the calculator
function register(
address calculator
) external;
/// @notice Remove a stats calculator
/// @param aprId key of the calculator to remove
function removeCalculator(
bytes32 aprId
) external;
/// @notice Set the factory that can register calculators
/// @param factory address of the factory
function setCalculatorFactory(
address factory
) external;
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
interface IAsyncSwapperRegistry {
event SwapperAdded(address indexed item);
event SwapperRemoved(address indexed item);
/// @notice Registers an item
/// @param item Item address to be added
function register(
address item
) external;
/// @notice Removes item registration
/// @param item Item address to be removed
function unregister(
address item
) external;
/// @notice Returns a list of all registered items
function list() external view returns (address[] memory);
/// @notice Checks if an address is a valid item
/// @param item Item address to be checked
function isRegistered(
address item
) external view returns (bool);
/// @notice Checks if an address is a valid swapper and reverts if not
/// @param item Swapper address to be checked
function verifyIsRegistered(
address item
) external view;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol)
pragma solidity ^0.8.0;
import "../IERC20.sol";
/**
* @dev Interface for the optional metadata functions from the ERC20 standard.
*
* _Available since v4.1._
*/
interface IERC20Metadata is IERC20 {
/**
* @dev Returns the name of the token.
*/
function name() external view returns (string memory);
/**
* @dev Returns the symbol of the token.
*/
function symbol() external view returns (string memory);
/**
* @dev Returns the decimals places of the token.
*/
function decimals() external view returns (uint8);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
/// @title EWMA pricing for incentive tokens
interface IIncentivesPricingStats {
event TokenAdded(address indexed token);
event TokenRemoved(address indexed token);
event TokenSnapshot(
address indexed token,
uint40 lastSnapshot,
uint256 fastFilterPrice,
uint256 slowFilterPrice,
uint256 initCount,
bool initComplete
);
error TokenAlreadyRegistered(address token);
error TokenNotFound(address token);
error IncentiveTokenPriceStale(address token);
error TokenSnapshotNotReady(address token);
struct TokenSnapshotInfo {
uint40 lastSnapshot;
bool _initComplete;
uint8 _initCount;
uint256 _initAcc;
uint256 fastFilterPrice;
uint256 slowFilterPrice;
}
/// @notice add a token to snapshot
/// @dev the token must be configured in the RootPriceOracle before adding here
/// @param token the address of the token to add
function setRegisteredToken(
address token
) external;
/// @notice remove a token from being snapshot
/// @param token the address of the token to remove
function removeRegisteredToken(
address token
) external;
/// @notice get the addresses for all currently registered tokens
/// @return tokens all of the registered token addresses
function getRegisteredTokens() external view returns (address[] memory tokens);
/// @notice get all of the registered tokens with the latest snapshot info
/// @return tokenAddresses token addresses in the same order as info
/// @return info a list of snapshot info for the tokens
function getTokenPricingInfo()
external
view
returns (address[] memory tokenAddresses, TokenSnapshotInfo[] memory info);
/// @notice update the snapshot for the specified tokens
/// @dev if a token is not ready to be snapshot the entire call will fail
function snapshot(
address[] calldata tokensToSnapshot
) external;
/// @notice get the latest prices for an incentive token. Reverts if token is not registered
/// @return fastPrice the price based on the faster filter (weighted toward current prices)
/// @return slowPrice the price based on the slower filter (weighted toward older prices, relative to fast)
function getPrice(address token, uint40 staleCheck) external view returns (uint256 fastPrice, uint256 slowPrice);
/// @notice get the latest prices for an incentive token or zero if the token is not registered
/// @return fastPrice the price based on the faster filter (weighted toward current prices)
/// @return slowPrice the price based on the slower filter (weighted toward older prices, relative to fast)
function getPriceOrZero(
address token,
uint40 staleCheck
) external view returns (uint256 fastPrice, uint256 slowPrice);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
/// @title Send messages to our systems on other chains
interface IMessageProxy {
function sendMessage(bytes32 messageType, bytes memory message) external;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (utils/Address.sol)
pragma solidity ^0.8.1;
/**
* @dev Collection of functions related to the address type
*/
library Address {
/**
* @dev Returns true if `account` is a contract.
*
* [IMPORTANT]
* ====
* It is unsafe to assume that an address for which this function returns
* false is an externally-owned account (EOA) and not a contract.
*
* Among others, `isContract` will return false for the following
* types of addresses:
*
* - an externally-owned account
* - a contract in construction
* - an address where a contract will be created
* - an address where a contract lived, but was destroyed
* ====
*
* [IMPORTANT]
* ====
* You shouldn't rely on `isContract` to protect against flash loan attacks!
*
* Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
* like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
* constructor.
* ====
*/
function isContract(address account) internal view returns (bool) {
// This method relies on extcodesize/address.code.length, which returns 0
// for contracts in construction, since the code is only stored at the end
// of the constructor execution.
return account.code.length > 0;
}
/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendValue} removes this limitation.
*
* https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
(bool success, ) = recipient.call{value: amount}("");
require(success, "Address: unable to send value, recipient may have reverted");
}
/**
* @dev Performs a Solidity function call using a low level `call`. A
* plain `call` is an unsafe replacement for a function call: use this
* function instead.
*
* If `target` reverts with a revert reason, it is bubbled up by this
* function (like regular Solidity function calls).
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
*
* Requirements:
*
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*
* _Available since v3.1._
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, "Address: low-level call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
* `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but also transferring `value` wei to `target`.
*
* Requirements:
*
* - the calling contract must have an ETH balance of at least `value`.
* - the called Solidity function must be `payable`.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value
) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
/**
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
* with `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(
address target,
bytes memory data,
string memory errorMessage
) internal view returns (bytes memory) {
(bool success, bytes memory returndata) = target.staticcall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
(bool success, bytes memory returndata) = target.delegatecall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
* the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
*
* _Available since v4.8._
*/
function verifyCallResultFromTarget(
address target,
bool success,
bytes memory returndata,
string memory errorMessage
) internal view returns (bytes memory) {
if (success) {
if (returndata.length == 0) {
// only check isContract if the call was successful and the return data is empty
// otherwise we already know that it was a contract
require(isContract(target), "Address: call to non-contract");
}
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
/**
* @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
* revert reason or using the provided one.
*
* _Available since v4.3._
*/
function verifyCallResult(
bool success,
bytes memory returndata,
string memory errorMessage
) internal pure returns (bytes memory) {
if (success) {
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
function _revert(bytes memory returndata, string memory errorMessage) private pure {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
/// @solidity memory-safe-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
/// @notice Stores a reference to the registry for this system
interface ISystemComponent {
/// @notice The system instance this contract is tied to
function getSystemRegistry() external view returns (address registry);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
pragma solidity ^0.8.0;
/**
* @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;
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { Errors } from "src/utils/Errors.sol";
import { LibAdapter } from "src/libs/LibAdapter.sol";
import { IDestinationVault } from "src/interfaces/vault/IDestinationVault.sol";
import { Math } from "openzeppelin-contracts/utils/math/Math.sol";
import { EnumerableSet } from "openzeppelin-contracts/utils/structs/EnumerableSet.sol";
import { SafeERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20Metadata as IERC20 } from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { StructuredLinkedList } from "src/strategy/StructuredLinkedList.sol";
import { WithdrawalQueue } from "src/strategy/WithdrawalQueue.sol";
import { IAutopool } from "src/interfaces/vault/IAutopool.sol";
import { IMainRewarder } from "src/interfaces/rewarders/IMainRewarder.sol";
import { AutopoolToken } from "src/vault/libs/AutopoolToken.sol";
import { IRootPriceOracle } from "src/interfaces/oracles/IRootPriceOracle.sol";
import { ISystemRegistry } from "src/interfaces/ISystemRegistry.sol";
import { AutopoolState, ProcessRebalanceParams } from "src/vault/libs/AutopoolState.sol";
import { AutopoolStrategyHooks } from "src/vault/libs/AutopoolStrategyHooks.sol";
import { IStrategyHook, HookFunctionIndex } from "src/interfaces/strategy/IStrategyHook.sol";
library AutopoolDebt {
using Math for uint256;
using SafeERC20 for IERC20;
using WithdrawalQueue for StructuredLinkedList.List;
using EnumerableSet for EnumerableSet.AddressSet;
using AutopoolToken for AutopoolToken.TokenData;
/// @notice Max time a cached debt report can be used
uint256 public constant MAX_DEBT_REPORT_AGE_SECONDS = 1 days;
error VaultShutdown();
error WithdrawShareCalcInvalid(uint256 currentShares, uint256 cachedShares);
error RebalanceFailed(string message);
error InvalidPrices();
error InvalidTotalAssetPurpose();
error InvalidDestination(address destination);
error TooFewAssets(uint256 requested, uint256 actual);
error SharesAndAssetsReceived(uint256 assets, uint256 shares);
error AmountExceedsAllowance(uint256 shares, uint256 allowed);
error PositivePriceRecoupNotCovered(uint256 remaining);
error RebalanceDestinationsMatch();
error InsufficientAssets(address asset);
error RebalanceDestinationUnderlyerMismatch(address destination, address trueUnderlyer, address providedUnderlyer);
error OnlyRebalanceToIdleAvailable();
error UnregisteredDestination(address dest);
event DestinationDebtReporting(
address destination, AutopoolDebt.IdleDebtUpdates debtInfo, uint256 claimed, uint256 claimGasUsed
);
event NewNavShareFeeMark(uint256 navPerShare, uint256 timestamp);
event Nav(uint256 idle, uint256 debt, uint256 totalSupply);
event Withdraw(
address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares
);
struct DestinationInfo {
/// @notice Current underlying value at the destination vault
/// @dev Used for calculating totalDebt, mid point of min and max
uint256 cachedDebtValue;
/// @notice Current minimum underlying value at the destination vault
/// @dev Used for calculating totalDebt during withdrawal
uint256 cachedMinDebtValue;
/// @notice Current maximum underlying value at the destination vault
/// @dev Used for calculating totalDebt of the deposit
uint256 cachedMaxDebtValue;
/// @notice Last block timestamp this info was updated
uint256 lastReport;
/// @notice How many shares of the destination vault we owned at last report
uint256 ownedShares;
}
struct IdleDebtUpdates {
bool pricesWereSafe;
uint256 totalIdleDecrease;
uint256 totalIdleIncrease;
uint256 totalDebtIncrease;
uint256 totalDebtDecrease;
uint256 totalMinDebtIncrease;
uint256 totalMinDebtDecrease;
uint256 totalMaxDebtIncrease;
uint256 totalMaxDebtDecrease;
}
struct AssetChanges {
uint256 startingIdle;
uint256 startingDebt;
uint256 startingTotalSupply;
uint256 newIdle;
uint256 newDebt;
uint256 endingTotalSupply;
}
struct RebalanceOutParams {
/// Address that will received the withdrawn underlyer
address receiver;
/// The "out" destination vault
address destinationOut;
/// The amount of tokenOut that will be withdrawn
uint256 amountOut;
/// The underlyer for destinationOut
address tokenOut;
IERC20 _baseAsset;
bool _shutdown;
}
/// @dev In memory struct only for managing vars in _withdraw
struct WithdrawInfo {
uint256 currentIdle;
uint256 assetsFromIdle;
uint256 totalAssetsToPull;
uint256 assetsToPull;
uint256 assetsPulled;
uint256 idleIncrease;
uint256 debtDecrease;
uint256 debtMinDecrease;
uint256 debtMaxDecrease;
uint256 totalMinDebt;
uint256 destinationRound;
uint256 lastRoundSlippage;
uint256 expectedAssets;
uint256 remainingRecoup;
}
struct FlashRebalanceParams {
IERC20 baseAsset;
bool shutdown;
}
struct FlashResultInfo {
uint256 tokenInBalanceBefore;
uint256 tokenInBalanceAfter;
bytes32 flashResult;
}
function processRebalance(
AutopoolState storage $,
ProcessRebalanceParams memory args,
bytes calldata data,
bytes memory hooks
) external returns (AutopoolDebt.AssetChanges memory updates) {
validateRebalanceParams($, args);
updates.startingIdle = $.assetBreakdown.totalIdle;
updates.startingDebt = $.assetBreakdown.totalDebt;
AutopoolDebt.IdleDebtUpdates memory result = flashRebalance($, args, data, hooks);
updates.newIdle = updates.startingIdle + result.totalIdleIncrease - result.totalIdleDecrease;
updates.newDebt = updates.startingDebt + result.totalDebtIncrease - result.totalDebtDecrease;
$.assetBreakdown.totalIdle = updates.newIdle;
$.assetBreakdown.totalDebt = updates.newDebt;
$.assetBreakdown.totalDebtMin =
$.assetBreakdown.totalDebtMin + result.totalMinDebtIncrease - result.totalMinDebtDecrease;
$.assetBreakdown.totalDebtMax =
$.assetBreakdown.totalDebtMax + result.totalMaxDebtIncrease - result.totalMaxDebtDecrease;
}
function flashRebalance(
AutopoolState storage $,
ProcessRebalanceParams memory args,
bytes calldata data,
bytes memory hooks
) private returns (IdleDebtUpdates memory result) {
DestinationInfo storage destInfoOut = $.destinationInfo[args.rebalanceParams.destinationOut];
DestinationInfo storage destInfoIn = $.destinationInfo[args.rebalanceParams.destinationIn];
AutopoolStrategyHooks.executeHooks(
hooks,
uint256(HookFunctionIndex.onRebalanceStart),
abi.encodeCall(IStrategyHook.onRebalanceStart, (args, msg.sender))
);
// Handle decrease (shares going "Out", cashing in shares and sending underlying back to swapper)
// If the tokenOut is _asset we assume they are taking idle
// which is already in the contract
result = _handleRebalanceOut(
AutopoolDebt.RebalanceOutParams({
receiver: address(args.receiver),
destinationOut: args.rebalanceParams.destinationOut,
amountOut: args.rebalanceParams.amountOut,
tokenOut: args.rebalanceParams.tokenOut,
_baseAsset: args.baseAsset,
_shutdown: $.shutdown
}),
destInfoOut
);
if (!result.pricesWereSafe) {
revert InvalidPrices();
}
AutopoolStrategyHooks.executeHooks(
hooks,
uint256(HookFunctionIndex.onRebalanceOutAssetsReady),
abi.encodeCall(IStrategyHook.onRebalanceOutAssetsReady, (args, msg.sender))
);
// Handle increase (shares coming "In", getting underlying from the swapper and trading for new shares)
FlashResultInfo memory flashResultInfo;
// get "before" counts
flashResultInfo.tokenInBalanceBefore = IERC20(args.rebalanceParams.tokenIn).balanceOf(address(this));
// Give control back to the solver so they can make use of the "out" assets
// and get our "in" asset
flashResultInfo.flashResult =
args.receiver.onFlashLoan(msg.sender, args.rebalanceParams.tokenIn, args.rebalanceParams.amountIn, 0, data);
// We assume the solver will send us the assets
flashResultInfo.tokenInBalanceAfter = IERC20(args.rebalanceParams.tokenIn).balanceOf(address(this));
// Make sure the call was successful and verify we have at least the assets we think
// we were getting
if (
flashResultInfo.flashResult != keccak256("ERC3156FlashBorrower.onFlashLoan")
|| flashResultInfo.tokenInBalanceAfter
< flashResultInfo.tokenInBalanceBefore + args.rebalanceParams.amountIn
) {
revert Errors.FlashLoanFailed(args.rebalanceParams.tokenIn, args.rebalanceParams.amountIn);
}
AutopoolStrategyHooks.executeHooks(
hooks,
uint256(HookFunctionIndex.onRebalanceInAssetsReturned),
abi.encodeCall(IStrategyHook.onRebalanceInAssetsReturned, (args, msg.sender))
);
if (args.rebalanceParams.tokenIn != address(args.baseAsset)) {
IdleDebtUpdates memory inDebtResult = _handleRebalanceIn(
destInfoIn,
IDestinationVault(args.rebalanceParams.destinationIn),
args.rebalanceParams.tokenIn,
flashResultInfo.tokenInBalanceAfter
);
if (!inDebtResult.pricesWereSafe) {
revert InvalidPrices();
}
result.totalDebtDecrease += inDebtResult.totalDebtDecrease;
result.totalDebtIncrease += inDebtResult.totalDebtIncrease;
result.totalMinDebtDecrease += inDebtResult.totalMinDebtDecrease;
result.totalMinDebtIncrease += inDebtResult.totalMinDebtIncrease;
result.totalMaxDebtDecrease += inDebtResult.totalMaxDebtDecrease;
result.totalMaxDebtIncrease += inDebtResult.totalMaxDebtIncrease;
} else {
result.totalIdleIncrease += flashResultInfo.tokenInBalanceAfter - flashResultInfo.tokenInBalanceBefore;
}
AutopoolStrategyHooks.executeHooks(
hooks,
uint256(HookFunctionIndex.onRebalanceDestinationVaultUpdated),
abi.encodeCall(IStrategyHook.onRebalanceDestinationVaultUpdated, (args, msg.sender))
);
}
function validateRebalanceParams(AutopoolState storage $, ProcessRebalanceParams memory args) private view {
address autopool = address(this);
Errors.verifyNotZero(args.rebalanceParams.destinationIn, "destinationIn");
Errors.verifyNotZero(args.rebalanceParams.destinationOut, "destinationOut");
Errors.verifyNotZero(args.rebalanceParams.tokenIn, "tokenIn");
Errors.verifyNotZero(args.rebalanceParams.tokenOut, "tokenOut");
Errors.verifyNotZero(args.rebalanceParams.amountIn, "amountIn");
Errors.verifyNotZero(args.rebalanceParams.amountOut, "amountOut");
ensureDestinationRegistered(autopool, args.rebalanceParams.destinationIn);
ensureDestinationRegistered(autopool, args.rebalanceParams.destinationOut);
// when a vault is shutdown, rebalancing can only pull assets from destinations back to the vault
if ($.shutdown && args.rebalanceParams.destinationIn != autopool) {
revert OnlyRebalanceToIdleAvailable();
}
if (args.rebalanceParams.destinationIn == args.rebalanceParams.destinationOut) {
revert RebalanceDestinationsMatch();
}
address baseAsset = address(args.baseAsset);
// if the in/out destination is the AutopoolETH then the in/out token must be the baseAsset
// if the in/out is not the AutopoolETH then the in/out token must match the destinations underlying token
if (args.rebalanceParams.destinationIn == autopool) {
if (args.rebalanceParams.tokenIn != baseAsset) {
revert RebalanceDestinationUnderlyerMismatch(
args.rebalanceParams.destinationIn, args.rebalanceParams.tokenIn, baseAsset
);
}
} else {
IDestinationVault inDest = IDestinationVault(args.rebalanceParams.destinationIn);
if (args.rebalanceParams.tokenIn != inDest.underlying()) {
revert RebalanceDestinationUnderlyerMismatch(
args.rebalanceParams.destinationIn, inDest.underlying(), args.rebalanceParams.tokenIn
);
}
}
if (args.rebalanceParams.destinationOut == autopool) {
if (args.rebalanceParams.tokenOut != baseAsset) {
revert RebalanceDestinationUnderlyerMismatch(
args.rebalanceParams.destinationOut, args.rebalanceParams.tokenOut, baseAsset
);
}
if (args.rebalanceParams.amountOut > $.assetBreakdown.totalIdle) {
revert InsufficientAssets(args.rebalanceParams.tokenOut);
}
} else {
IDestinationVault outDest = IDestinationVault(args.rebalanceParams.destinationOut);
if (args.rebalanceParams.tokenOut != outDest.underlying()) {
revert RebalanceDestinationUnderlyerMismatch(
args.rebalanceParams.destinationOut, outDest.underlying(), args.rebalanceParams.tokenOut
);
}
if (args.rebalanceParams.amountOut > outDest.balanceOf(autopool)) {
revert InsufficientAssets(args.rebalanceParams.tokenOut);
}
}
}
function ensureDestinationRegistered(address autopool, address dest) private view {
if (dest == address(autopool)) return;
if (
!(
IAutopool(autopool).isDestinationRegistered(dest)
|| IAutopool(autopool).isDestinationQueuedForRemoval(dest)
)
) {
revert UnregisteredDestination(dest);
}
}
/// @notice Perform deposit and debt info update for the "in" destination during a rebalance
/// @dev This "in" function performs less validations than its "out" version
/// @param dvIn The "in" destination vault
/// @param tokenIn The underlyer for dvIn
/// @param depositAmount The amount of tokenIn that will be deposited
/// @return result Changes in debt values
function _handleRebalanceIn(
DestinationInfo storage destInfo,
IDestinationVault dvIn,
address tokenIn,
uint256 depositAmount
) private returns (IdleDebtUpdates memory result) {
LibAdapter._approve(IERC20(tokenIn), address(dvIn), depositAmount);
// Snapshot our current shares so we know how much to back out
uint256 originalShareBal = dvIn.balanceOf(address(this));
// deposit to dv
uint256 newShares = dvIn.depositUnderlying(depositAmount);
// Update the debt info snapshot
result = _recalculateDestInfo(destInfo, dvIn, originalShareBal, originalShareBal + newShares);
}
function oldestDebtReporting(
AutopoolState storage $
) public view returns (uint256) {
return $.destinationInfo[$.debtReportQueue.peekHead()].lastReport;
}
/**
* @notice Perform withdraw and debt info update for the "out" destination during a rebalance
* @dev This "out" function performs more validations and handles idle as opposed to "in" which does not
* debtDecrease The previous amount of debt destinationOut accounted for in totalDebt
* debtIncrease The current amount of debt destinationOut should account for in totalDebt
* idleDecrease Amount of baseAsset that was sent from the vault. > 0 only when tokenOut == baseAsset
* idleIncrease Amount of baseAsset that was claimed from Destination Vault
* @param params Rebalance out params
* @param destOutInfo The "out" destination vault info
* @return assetChange debt and idle change data
*/
function _handleRebalanceOut(
RebalanceOutParams memory params,
DestinationInfo storage destOutInfo
) private returns (IdleDebtUpdates memory assetChange) {
// Handle decrease (shares going "Out", cashing in shares and sending underlying back to swapper)
// If the tokenOut is _asset we assume they are taking idle
// which is already in the contract
if (params.tokenOut != address(params._baseAsset)) {
IDestinationVault dvOut = IDestinationVault(params.destinationOut);
// Snapshot our current shares so we know how much to back out
uint256 originalShareBal = dvOut.balanceOf(address(this));
// Burning our shares will claim any pending baseAsset
// rewards and send them to us.
// Get our starting balance
uint256 beforeBaseAssetBal = params._baseAsset.balanceOf(address(this));
// Withdraw underlying from the destination vault
// Shares are sent directly to the flashRebalance receiver
// slither-disable-next-line unused-return
dvOut.withdrawUnderlying(params.amountOut, params.receiver);
// Update the debt info snapshot
assetChange =
_recalculateDestInfo(destOutInfo, dvOut, originalShareBal, originalShareBal - params.amountOut);
// Capture any rewards we may have claimed as part of withdrawing
assetChange.totalIdleIncrease = params._baseAsset.balanceOf(address(this)) - beforeBaseAssetBal;
} else {
// Working with idle baseAsset which should be in the vault already
// Just send it out
IERC20(params.tokenOut).safeTransfer(params.receiver, params.amountOut);
assetChange.totalIdleDecrease = params.amountOut;
// We weren't dealing with any debt or pricing, just idle, so we can just mark
// it as safe
assetChange.pricesWereSafe = true;
}
}
function recalculateDestInfo(
DestinationInfo storage destInfo,
IDestinationVault destVault,
uint256 originalShares,
uint256 currentShares
) external returns (IdleDebtUpdates memory result) {
result = _recalculateDestInfo(destInfo, destVault, originalShares, currentShares);
}
/// @dev Will not revert on unsafe prices. Up to the caller.
function _recalculateDestInfo(
DestinationInfo storage destInfo,
IDestinationVault destVault,
uint256 originalShares,
uint256 currentShares
) private returns (IdleDebtUpdates memory result) {
// Figure out what to back out of our totalDebt number.
// We could have had withdraws since the last snapshot which means our
// cached currentDebt number should be decreased based on the remaining shares
// totalDebt is decreased using the same proportion of shares method during withdrawals
// so this should represent whatever is remaining.
// Prices are per LP token and whether or not the prices are safe to use
// If they aren't safe then just continue and we'll get it on the next go around
(uint256 spotPrice, uint256 safePrice, bool isSpotSafe) = destVault.getRangePricesLP();
// Calculate what we're backing out based on the original shares
uint256 minPrice = spotPrice > safePrice ? safePrice : spotPrice;
uint256 maxPrice = spotPrice > safePrice ? spotPrice : safePrice;
// If we previously had shares, calculate how much of our cached numbers
// still remain as this will be deducted from the overall debt numbers
// over time
uint256 prevOwnedShares = destInfo.ownedShares;
if (prevOwnedShares > 0) {
result.totalDebtDecrease = (destInfo.cachedDebtValue * originalShares) / prevOwnedShares;
result.totalMinDebtDecrease = (destInfo.cachedMinDebtValue * originalShares) / prevOwnedShares;
result.totalMaxDebtDecrease = (destInfo.cachedMaxDebtValue * originalShares) / prevOwnedShares;
}
// The overall debt value is the mid point of min and max
uint256 div = 10 ** destVault.decimals();
uint256 newDebtValue = (minPrice * currentShares + maxPrice * currentShares) / (div * 2);
result.pricesWereSafe = isSpotSafe;
result.totalDebtIncrease = newDebtValue;
result.totalMinDebtIncrease = minPrice * currentShares / div;
result.totalMaxDebtIncrease = maxPrice * currentShares / div;
// Save our current new values
destInfo.cachedDebtValue = newDebtValue;
destInfo.cachedMinDebtValue = result.totalMinDebtIncrease;
destInfo.cachedMaxDebtValue = result.totalMaxDebtIncrease;
destInfo.lastReport = block.timestamp;
destInfo.ownedShares = currentShares;
}
function totalAssetsTimeChecked(
AutopoolState storage $,
IAutopool.TotalAssetPurpose purpose
) external returns (uint256) {
IDestinationVault destVault = IDestinationVault($.debtReportQueue.peekHead());
uint256 recalculatedTotalAssets = IAutopool(address(this)).totalAssets(purpose);
while (address(destVault) != address(0)) {
uint256 lastReport = $.destinationInfo[address(destVault)].lastReport;
if (lastReport + MAX_DEBT_REPORT_AGE_SECONDS > block.timestamp) {
// Its not stale
// This report is OK, we don't need to recalculate anything
break;
} else {
// It is stale, recalculate
//slither-disable-next-line unused-return
uint256 currentShares = destVault.balanceOf(address(this));
uint256 staleDebt;
uint256 extremePrice;
// Figure out exactly which price to use based on its purpose
if (purpose == IAutopool.TotalAssetPurpose.Deposit) {
// We use max value so that anything deposited is worth less
extremePrice = destVault.getUnderlyerCeilingPrice();
// Round down. We are subtracting this value out of the total so some left
// behind just increases the value which is what we want
staleDebt = $.destinationInfo[address(destVault)].cachedMaxDebtValue.mulDiv(
currentShares, $.destinationInfo[address(destVault)].ownedShares, Math.Rounding.Down
);
} else if (purpose == IAutopool.TotalAssetPurpose.Withdraw) {
// We use min value so that we value the shares as worth less
extremePrice = destVault.getUnderlyerFloorPrice();
// Round up. We are subtracting this value out of the total so if we take a little
// extra it just decreases the value which is what we want
staleDebt = $.destinationInfo[address(destVault)].cachedMinDebtValue.mulDiv(
currentShares, $.destinationInfo[address(destVault)].ownedShares, Math.Rounding.Up
);
} else {
revert InvalidTotalAssetPurpose();
}
// Back out our stale debt, add in its new value
// Our goal is to find the most conservative value in each situation. If the current
// value we have represents that, then use it. Otherwise, use the new one.
uint256 newValue = (currentShares * extremePrice) / destVault.ONE();
if (purpose == IAutopool.TotalAssetPurpose.Deposit && staleDebt > newValue) {
newValue = staleDebt;
} else if (purpose == IAutopool.TotalAssetPurpose.Withdraw && staleDebt < newValue) {
newValue = staleDebt;
}
recalculatedTotalAssets = recalculatedTotalAssets + newValue - staleDebt;
}
destVault = IDestinationVault($.debtReportQueue.getAdjacent(address(destVault), true));
}
return recalculatedTotalAssets;
}
function updateDebtReporting(
AutopoolState storage $,
uint256 numToProcess,
bytes memory hooks
) external returns (AssetChanges memory changes) {
IdleDebtUpdates memory result;
// Persist our change in idle and debt
changes.startingIdle = $.assetBreakdown.totalIdle;
changes.startingDebt = $.assetBreakdown.totalDebt;
numToProcess = Math.min(numToProcess, $.debtReportQueue.sizeOf());
for (uint256 i = 0; i < numToProcess; ++i) {
IDestinationVault destVault = IDestinationVault($.debtReportQueue.popHead());
// Get the reward value we've earned. DV rewards are always in terms of base asset
// We track the gas used purely for off-chain stats purposes
// Main rewarder on DV's store the earned and liquidated rewards
// Extra rewarders are disabled at the DV level
uint256 claimGasUsed = gasleft();
uint256 beforeBaseAsset = IERC20(IAutopool(address(this)).asset()).balanceOf(address(this));
IMainRewarder(destVault.rewarder()).getReward(address(this), address(this), false);
uint256 claimedRewardValue =
IERC20(IAutopool(address(this)).asset()).balanceOf(address(this)) - beforeBaseAsset;
result.totalIdleIncrease += claimedRewardValue;
// Recalculate the debt info figuring out the change in
// total debt value we can roll up later
uint256 currentShareBalance = destVault.balanceOf(address(this));
AutopoolDebt.IdleDebtUpdates memory debtResult = _recalculateDestInfo(
$.destinationInfo[address(destVault)], destVault, currentShareBalance, currentShareBalance
);
result.totalDebtDecrease += debtResult.totalDebtDecrease;
result.totalDebtIncrease += debtResult.totalDebtIncrease;
result.totalMinDebtDecrease += debtResult.totalMinDebtDecrease;
result.totalMinDebtIncrease += debtResult.totalMinDebtIncrease;
result.totalMaxDebtDecrease += debtResult.totalMaxDebtDecrease;
result.totalMaxDebtIncrease += debtResult.totalMaxDebtIncrease;
// If we no longer have shares, then there's no reason to continue reporting on the destination.
// The strategy will only call for the info if its moving "out" of the destination
// and that will only happen if we have shares.
// A rebalance where we move "in" to the position will refresh the data at that time
if (currentShareBalance > 0) {
$.debtReportQueue.addToTail(address(destVault));
}
claimGasUsed -= gasleft();
emit DestinationDebtReporting(address(destVault), debtResult, claimedRewardValue, claimGasUsed);
AutopoolStrategyHooks.executeHooks(
hooks,
uint256(HookFunctionIndex.onDestinationDebtReport),
abi.encodeCall(IStrategyHook.onDestinationDebtReport, (address(destVault), debtResult))
);
}
changes.newIdle = changes.startingIdle + result.totalIdleIncrease;
changes.newDebt = changes.startingDebt + result.totalDebtIncrease - result.totalDebtDecrease;
$.assetBreakdown.totalIdle = changes.newIdle;
$.assetBreakdown.totalDebt = changes.newDebt;
$.assetBreakdown.totalDebtMin =
$.assetBreakdown.totalDebtMin + result.totalMinDebtIncrease - result.totalMinDebtDecrease;
$.assetBreakdown.totalDebtMax =
$.assetBreakdown.totalDebtMax + result.totalMaxDebtIncrease - result.totalMaxDebtDecrease;
}
function _initiateWithdrawInfo(
uint256 assets,
IAutopool.AssetBreakdown storage assetBreakdown
) private view returns (WithdrawInfo memory) {
uint256 idle = assetBreakdown.totalIdle;
WithdrawInfo memory info = WithdrawInfo({
currentIdle: idle,
// If idle can cover the full amount, then we want to pull all assets from there
// Otherwise, we want to pull from the market and only get idle if we exhaust the market
assetsFromIdle: assets > idle ? 0 : assets,
totalAssetsToPull: 0,
assetsToPull: 0,
assetsPulled: 0,
idleIncrease: 0,
debtDecrease: 0,
debtMinDecrease: 0,
debtMaxDecrease: 0,
totalMinDebt: assetBreakdown.totalDebtMin,
destinationRound: 0,
lastRoundSlippage: 0,
expectedAssets: 0,
remainingRecoup: 0
});
info.totalAssetsToPull = assets - info.assetsFromIdle;
// This var we use to track our progress later
info.assetsToPull = assets - info.assetsFromIdle;
// Idle + minDebt is the maximum amount of assets/debt we could burn during a withdraw.
// If the user is request more than that (like during a withdraw) we can just revert
// early without trying
if (info.totalAssetsToPull > info.currentIdle + info.totalMinDebt) {
revert TooFewAssets(assets, info.currentIdle + info.totalMinDebt);
}
return info;
}
function withdraw(
AutopoolState storage $,
uint256 assets,
uint256 applicableTotalAssets
) public returns (uint256 actualAssets, uint256 actualShares, uint256 debtBurned) {
WithdrawInfo memory info = _initiateWithdrawInfo(assets, $.assetBreakdown);
// Pull the market if there aren't enough funds in idle to cover the entire amount
// This flow is not bounded by a set number of shares. The user has requested X assets
// and a variable number of shares to burn so we don't have easy break out points like we do
// during redeem (like using debt burned). When we get slippage here and don't meet the requested assets
// we need to keep going if we can. This is tricky if we consider that (most of) our destinations are
// LP positions and we'll be swapping assets, so we can expect some slippage. Even
// if our minDebtValue numbers are up to date and perfectly accurate slippage could ensure we
// are always receiving less than we expect/calculate and we never hit the requested assets
// even though the owner would have shares to cover it. Under normal/expected conditions, our
// minDebtValue is lower than actual and we expect overall value to be going up, so we burn a tad
// more than we should and receive a tad more than we expect. This should cover us. However,
// in other conditions we have to be sure we aren't endlessly trying to approach 0 so we are tracking
// the slippage we received on the last pull, repricing, and applying an increasing multiplier until we either
// pull enough to cover or pull them all and/or move to the next destination.
uint256 dvSharesToBurn;
while (info.assetsToPull > 0) {
IDestinationVault destVault = IDestinationVault($.withdrawalQueue.peekHead());
// We've run out of destinations
if (address(destVault) == address(0)) {
break;
}
uint256 dvShares = destVault.balanceOf(address(this));
{
uint256 dvSharesValue;
if (info.destinationRound == 0) {
// First time pulling
// We use the min debt value here because its a withdrawal and we're trying to cover an amount
// of assets. Undervaluing the shares may mean we pull more but given that we expect slippage
// that is desirable.
dvSharesValue = $.destinationInfo[address(destVault)].cachedMinDebtValue * dvShares
/ $.destinationInfo[address(destVault)].ownedShares;
} else {
// When we've pulled from this destination before, i.e. destinationRound > 0, then we
// know a more accurate exchange rate and its worse than we were expecting.
// We even will pad it a bit as we want to account for any additional slippage we
// may receive by say being farther down an AMM curve.
// dvSharesToBurn is the last value we used when pulling from this destination
// info.expectedAssets is how much we expected to get on that last pull
// info.expectedAssets - info.lastRoundSlippage is how much we actually received
uint256 paddedSlippage = info.lastRoundSlippage * (info.destinationRound + 10_000) / 10_000;
if (paddedSlippage < info.expectedAssets) {
dvSharesValue = (info.expectedAssets - paddedSlippage) * dvShares / dvSharesToBurn;
} else {
// This will just mean we pull all shares
dvSharesValue = 0;
}
}
if (dvSharesValue > info.assetsToPull) {
dvSharesToBurn = (dvShares * info.assetsToPull) / dvSharesValue;
// On withdraw, we are trying to meet a specific number of assets without a limit
// on the debt we can burn. Burning 0 due to the valuations here would be an automatic failure
// as we still have assets to satisfy and debt to burn. We at least have to burn 1 even if it
// results in a larger over pull
if (dvSharesToBurn == 0) {
dvSharesToBurn = 1;
}
// Only need to set it here because the only time we'll use it is if
// we don't exhaust all shares and have to try the destination again
info.expectedAssets = info.assetsToPull;
} else {
dvSharesToBurn = dvShares;
}
}
uint256 pulledAssets;
uint256 debtValueBurned;
// Get the base asset back from the Destination. Also performs a check that we aren't receiving
// poor execution on our swaps based on safe prices
(info, pulledAssets, debtValueBurned) = _withdrawAssets(info, $.destinationInfo, destVault, dvSharesToBurn);
info.assetsPulled += pulledAssets;
if (info.remainingRecoup > 0) {
// If the destination is so severely undervalued that it can't cover its own recoup then we have no
// recourse but to burn the entire destination and the user would to have to cover the full overage
// from the next destinations can get nothing from this one. Should not be allowed.
revert PositivePriceRecoupNotCovered(info.remainingRecoup);
}
// If we've exhausted all shares we can remove the withdrawal from the queue
// We need to leave it in the debt report queue though so that our destination specific
// debt tracking values can be updated
if (dvShares == dvSharesToBurn) {
$.withdrawalQueue.popAddress(address(destVault));
info.destinationRound = 0;
info.lastRoundSlippage = 0;
} else {
// If we didn't burn all the shares and we received enough to cover our
// expected that means we'll break out below as we've hit our target
unchecked {
if (pulledAssets < info.expectedAssets) {
info.lastRoundSlippage = info.expectedAssets - pulledAssets;
if (info.destinationRound == 0) {
info.destinationRound = 100;
} else {
info.destinationRound *= 2;
}
}
}
}
// It's possible we'll get back more assets than we anticipate from a swap
// so if we do, throw it in idle and stop processing. You don't get more than we've calculated
if (info.assetsPulled >= info.totalAssetsToPull) {
info.idleIncrease += info.assetsPulled - info.totalAssetsToPull;
info.assetsPulled = info.totalAssetsToPull;
info.assetsToPull = 0;
break;
}
info.assetsToPull -= pulledAssets;
}
// We didn't get enough assets from the debt pull
// See if we can get the rest from idle
if (info.assetsPulled < assets && info.currentIdle > 0) {
uint256 remaining = assets - info.assetsPulled;
if (remaining <= info.currentIdle) {
info.assetsFromIdle = remaining;
}
// We don't worry about the else case because if currentIdle can't
// cover remaining then we'll fail the `actualAssets < assets`
// check below and revert
}
debtBurned = info.assetsFromIdle + info.debtMinDecrease;
actualAssets = info.assetsFromIdle + info.assetsPulled;
if (actualAssets < assets) {
revert TooFewAssets(assets, actualAssets);
}
actualShares = IAutopool(address(this)).convertToShares(
Math.max(actualAssets, debtBurned),
applicableTotalAssets,
IAutopool(address(this)).totalSupply(),
Math.Rounding.Up
);
// Subtract what's taken out of idle from totalIdle
// We may also have some increase to account for it we over pulled
// or received better execution than we were anticipating
// slither-disable-next-line events-maths
$.assetBreakdown.totalIdle = info.currentIdle + info.idleIncrease - info.assetsFromIdle;
// Save off our various debt numbers
if (info.debtDecrease > $.assetBreakdown.totalDebt) {
$.assetBreakdown.totalDebt = 0;
} else {
$.assetBreakdown.totalDebt -= info.debtDecrease;
}
if (info.debtMinDecrease > info.totalMinDebt) {
$.assetBreakdown.totalDebtMin = 0;
} else {
$.assetBreakdown.totalDebtMin -= info.debtMinDecrease;
}
if (info.debtMaxDecrease > $.assetBreakdown.totalDebtMax) {
$.assetBreakdown.totalDebtMax = 0;
} else {
$.assetBreakdown.totalDebtMax -= info.debtMaxDecrease;
}
}
function _withdrawAssets(
WithdrawInfo memory info,
mapping(address => AutopoolDebt.DestinationInfo) storage destinationInfo,
IDestinationVault destVault,
uint256 dvSharesToBurn
) internal returns (WithdrawInfo memory, uint256 pulledAssets, uint256 debtValueBurned) {
if (dvSharesToBurn > 0) {
address[] memory tokensBurned;
uint256[] memory amountsBurned;
// Destination Vaults always burn the exact amount we instruct them to
(pulledAssets, tokensBurned, amountsBurned) = destVault.withdrawBaseAsset(dvSharesToBurn, address(this));
// Calculate the totalDebt we'll need to remove based on the shares we're burning
// We're rounding up here so take care when actually applying to totalDebt
debtValueBurned = destinationInfo[address(destVault)].cachedMinDebtValue.mulDiv(
dvSharesToBurn, destinationInfo[address(destVault)].ownedShares, Math.Rounding.Up
);
info.debtMinDecrease += debtValueBurned;
info.debtDecrease += destinationInfo[address(destVault)].cachedDebtValue.mulDiv(
dvSharesToBurn, destinationInfo[address(destVault)].ownedShares, Math.Rounding.Up
);
uint256 maxDebtBurned = destinationInfo[address(destVault)].cachedMaxDebtValue.mulDiv(
dvSharesToBurn, destinationInfo[address(destVault)].ownedShares, Math.Rounding.Up
);
info.debtMaxDecrease += maxDebtBurned;
// See if we received a reasonable amount of the base asset back based on the value
// of the tokens that were burned.
uint256 totalValueBurned;
{
uint256 tokenLen = tokensBurned.length;
IRootPriceOracle rootPriceOracle = ISystemRegistry(destVault.getSystemRegistry()).rootPriceOracle();
for (uint256 i = 0; i < tokenLen;) {
totalValueBurned += amountsBurned[i]
* rootPriceOracle.getPriceInQuote(tokensBurned[i], destVault.baseAsset())
/ (10 ** IERC20(tokensBurned[i]).decimals());
unchecked {
++i;
}
}
}
// How much, if any, should be dropping into idle?
// Anything pulled over debtValueBurned goes to idle, user can't get more than we think its worth.
// However, if we pulled less than the current value of the tokens we burned, so long as
// that value is greater than debt min, we need to recoup that as well and put it into idle
uint256 amountToRecoup;
if (totalValueBurned > debtValueBurned) {
// The shares we burned are worth more than we'll be recouping from the debt burn
// the difference we still need to get
amountToRecoup = totalValueBurned - debtValueBurned;
uint256 maxCreditBps = destVault.recoupMaxCredit();
uint256 gapCredit = maxDebtBurned - debtValueBurned;
uint256 credit = Math.min(gapCredit, debtValueBurned * maxCreditBps / 10_000);
if (credit > amountToRecoup) {
amountToRecoup = 0;
} else {
amountToRecoup -= credit;
}
}
// This is done regardless of whether we were under valued. User can still only
// get what we've valued it at.
if (pulledAssets > debtValueBurned) {
uint256 overDebtValue = pulledAssets - debtValueBurned;
info.idleIncrease += overDebtValue;
pulledAssets -= overDebtValue;
// Since this is going to idle it goes to satisfy the recoup as well
if (amountToRecoup > 0) {
if (amountToRecoup > overDebtValue) {
amountToRecoup -= overDebtValue;
} else {
amountToRecoup = 0;
}
}
}
// If we still have a value we need to recoup it means that the debt range credit
// as well as what was pulled over the min debt value wasn't enough to cover
// the under valued burn. Now we have to try and take it from what is going back
// to the user
if (amountToRecoup > 0) {
if (amountToRecoup > pulledAssets) {
// Recoup is more than we pulled so we'll have some recoup left over
amountToRecoup -= pulledAssets;
// Everything that was pulled goes to idle
info.idleIncrease += pulledAssets;
pulledAssets = 0;
// We'll have to try and get the remaining amount from another destination
info.remainingRecoup += amountToRecoup;
} else {
// We pulled enough assets to cover the recoup
pulledAssets -= amountToRecoup;
// Ensure the recoup goes to idle
info.idleIncrease += amountToRecoup;
}
}
}
return (info, pulledAssets, debtValueBurned);
}
/// @notice Perform a removal of assets via the redeem path where the shares are the limiting factor.
/// This means we break out whenever we reach either `assets` retrieved or debt value equivalent to `assets` burned
function redeem(
AutopoolState storage $,
uint256 assets,
uint256 applicableTotalAssets
) public returns (uint256 actualAssets, uint256 actualShares, uint256 debtBurned) {
WithdrawInfo memory info = _initiateWithdrawInfo(assets, $.assetBreakdown);
// If not enough funds in idle, then pull what we need from destinations
bool exhaustedDestinations = false;
while (info.assetsToPull > 0) {
IDestinationVault destVault = IDestinationVault($.withdrawalQueue.peekHead());
if (address(destVault) == address(0)) {
exhaustedDestinations = true;
break;
}
uint256 dvShares = destVault.balanceOf(address(this));
uint256 dvSharesToBurn = dvShares;
{
// Valuing these shares higher, rounding up, will result in us burning less of them
// in the event we don't burn all of them. Good thing.
uint256 dvSharesValue = $.destinationInfo[address(destVault)].cachedMinDebtValue.mulDiv(
dvSharesToBurn, $.destinationInfo[address(destVault)].ownedShares, Math.Rounding.Up
);
// If the dv shares we own are worth more than we need, limit the shares to burn
// Any extra we get will be dropped into idle
if (dvSharesValue > info.assetsToPull) {
uint256 limitedShares = (dvSharesToBurn * info.assetsToPull) / dvSharesValue;
// Final set for the actual shares we'll burn later
dvSharesToBurn = limitedShares;
}
}
uint256 pulledAssets;
uint256 debtValueBurned;
// Get the base asset back from the Destination. Also performs a check that we aren't receiving
// poor execution on our swaps based on safe prices
// slither-disable-next-line unused-return
(info, pulledAssets, debtValueBurned) = _withdrawAssets(info, $.destinationInfo, destVault, dvSharesToBurn);
// If we've exhausted all shares we can remove the destination from the withdrawal queue
// We need to leave it in the debt report queue though so that our destination specific
// debt tracking values can be updated
if (dvShares == dvSharesToBurn) {
$.withdrawalQueue.popAddress(address(destVault));
}
info.assetsPulled += pulledAssets;
// Any deficiency in the amount we received is slippage.
// There is a round up on debtValueBurned so just making sure it never under flows here
// _withdrawAssets ensures that pulledAssets is always lte debtValueBurned and we always
// want to debit the max so we just use debtValueBurned
if (debtValueBurned > info.assetsToPull) {
info.assetsToPull = 0;
} else {
info.assetsToPull -= debtValueBurned;
}
// We either have enough assets, or we've burned the max debt we're allowed
if (info.assetsToPull == 0) {
break;
}
// If we didn't exhaust all of the shares from the destination it means we
// assume we will get everything we need from there and everything else is slippage
if (dvShares != dvSharesToBurn) {
info.assetsToPull = 0;
break;
}
}
// See if we can pull the remaining recoup from other destinations we may have pulled from
if (info.remainingRecoup > 0) {
if (info.remainingRecoup > info.assetsPulled) {
info.remainingRecoup -= info.assetsPulled;
info.idleIncrease += info.assetsPulled;
info.assetsPulled = 0;
} else {
info.assetsPulled -= info.remainingRecoup;
info.idleIncrease += info.remainingRecoup;
info.remainingRecoup = 0;
}
}
// We didn't get enough assets from the debt pull
// See if we can get the rest from idle
if (info.assetsToPull > 0 && info.currentIdle > 0 && exhaustedDestinations) {
if (info.assetsToPull < info.currentIdle) {
info.assetsFromIdle = info.assetsToPull;
} else {
info.assetsFromIdle = info.currentIdle;
}
}
debtBurned = info.assetsFromIdle + info.debtMinDecrease;
actualAssets = info.assetsFromIdle + info.assetsPulled;
// If we took from idle, and we have remaining assets to recoup
// we need to put some back in idle
if (info.remainingRecoup > 0 && info.assetsFromIdle > 0) {
// We only need to do this if the idle assets can cover the remaining recoup fully because
// we'll be reverting otherwise
if (info.assetsFromIdle >= info.remainingRecoup) {
// We still need to charge for the recoup so we're going to leave it in debtBurned
// but we'll take it back out of actualAssets so it stays in idle. We need to lower
// assetsFromIdle as well so that the final numbers get updated too
actualAssets -= info.remainingRecoup;
info.assetsFromIdle -= info.remainingRecoup;
info.remainingRecoup = 0;
} else {
// Just updating this number so we get an accurate value in the revert below
info.remainingRecoup -= info.assetsFromIdle;
}
}
// We took everything we could and still can't cover, time to revert
if (info.remainingRecoup > 0) {
revert PositivePriceRecoupNotCovered(info.remainingRecoup);
}
actualShares = IAutopool(address(this)).convertToShares(
debtBurned, applicableTotalAssets, IAutopool(address(this)).totalSupply(), Math.Rounding.Up
);
// Subtract what's taken out of idle from totalIdle
// We may also have some increase to account for it we over pulled
// or received better execution than we were anticipating
// slither-disable-next-line events-maths
$.assetBreakdown.totalIdle = info.currentIdle + info.idleIncrease - info.assetsFromIdle;
// Save off our various debt numbers
if (info.debtDecrease > $.assetBreakdown.totalDebt) {
$.assetBreakdown.totalDebt = 0;
} else {
$.assetBreakdown.totalDebt -= info.debtDecrease;
}
if (info.debtMinDecrease > info.totalMinDebt) {
$.assetBreakdown.totalDebtMin = 0;
} else {
$.assetBreakdown.totalDebtMin -= info.debtMinDecrease;
}
if (info.debtMaxDecrease > $.assetBreakdown.totalDebtMax) {
$.assetBreakdown.totalDebtMax = 0;
} else {
$.assetBreakdown.totalDebtMax -= info.debtMaxDecrease;
}
}
/**
* @notice Function to complete a withdrawal or redeem. This runs after shares to be burned and assets to be
* transferred are calculated.
* @param $ Storage related to the calling Autopool
* @param assets Amount of assets to be transferred to receiver.
* @param shares Amount of shares to be burned from owner.
* @param owner Owner of shares, user to burn shares from.
* @param receiver The receiver of the baseAsset.
* @param baseAsset Base asset of the Autopool.
*/
function completeWithdrawal(
AutopoolState storage $,
uint256 assets,
uint256 shares,
address owner,
address receiver,
IERC20 baseAsset
) external {
if (msg.sender != owner) {
uint256 allowed = IAutopool(address(this)).allowance(owner, msg.sender);
if (allowed != type(uint256).max) {
if (shares > allowed) revert AmountExceedsAllowance(shares, allowed);
unchecked {
$.token.approve(owner, msg.sender, allowed - shares);
}
}
}
$.token.burn(owner, shares);
uint256 ts = IAutopool(address(this)).totalSupply();
emit Withdraw(msg.sender, receiver, owner, assets, shares);
emit Nav($.assetBreakdown.totalIdle, $.assetBreakdown.totalDebt, ts);
baseAsset.safeTransfer(receiver, assets);
}
/**
* @notice A helper function to get estimates of what would happen on a withdraw or redeem.
* @dev Reverts all changing state.
* @param $ Storage related to the calling Autopool.
* @param previewWithdraw Bool denoting whether to preview a redeem or withdrawal.
* @param assets Assets to be withdrawn or redeemed.
* @param applicableTotalAssets Operation dependent assets in the Autopool.
* @param functionCallEncoded Abi encoded function signature for recursive call.
* @return assetsAmount Preview of amount of assets to send to receiver.
* @return sharesAmount Preview of amount of assets to burn from owner.
*/
function preview(
AutopoolState storage $,
bool previewWithdraw,
uint256 assets,
uint256 applicableTotalAssets,
bytes memory functionCallEncoded
) external returns (uint256 assetsAmount, uint256 sharesAmount) {
if (msg.sender != address(this)) {
// Perform a recursive call the function in `funcCallEncoded`. This will result in a call back to
// the Autopool, and then this function. The intention is to reach the "else" block in this function.
// solhint-disable avoid-low-level-calls
// slither-disable-next-line missing-zero-check,low-level-calls
(bool success, bytes memory returnData) = address(this).call(functionCallEncoded);
// solhint-enable avoid-low-level-calls
// If the recursive call is successful, it means an unintended code path was taken.
if (success) {
revert Errors.UnreachableError();
}
bytes4 sharesAmountSig = bytes4(keccak256("SharesAndAssetsReceived(uint256,uint256)"));
// Extract the error signature (first 4 bytes) from the revert reason.
bytes4 errorSignature;
// solhint-disable no-inline-assembly
assembly {
errorSignature := mload(add(returnData, 0x20))
}
// If the error matches the expected signature, extract the amount from the revert reason and return.
if (errorSignature == sharesAmountSig) {
// Extract subsequent bytes for uint256.
assembly {
assetsAmount := mload(add(returnData, 0x24))
sharesAmount := mload(add(returnData, 0x44))
}
} else {
// If the error is not the expected one, forward the original revert reason.
assembly {
revert(add(32, returnData), mload(returnData))
}
}
// solhint-enable no-inline-assembly
}
// This branch is taken during the recursive call.
else {
// Perform the actual withdrawal or redeem logic to compute the amount. This will be reverted to
// simulate the action.
uint256 previewAssets;
uint256 previewShares;
if (previewWithdraw) {
(previewAssets, previewShares,) = withdraw($, assets, applicableTotalAssets);
} else {
(previewAssets, previewShares,) = redeem($, assets, applicableTotalAssets);
}
// Revert with the computed amount as an error.
revert SharesAndAssetsReceived(previewAssets, previewShares);
}
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { Roles } from "src/libs/Roles.sol";
import { Errors } from "src/utils/Errors.sol";
import { ISystemRegistry } from "src/interfaces/ISystemRegistry.sol";
import { IAccessController } from "src/interfaces/security/IAccessController.sol";
import { ISystemSecurity } from "src/interfaces/security/ISystemSecurity.sol";
/**
* @notice Contract which allows children to implement an emergency stop mechanism that can be trigger
* by an account that has been granted the EMERGENCY_PAUSER role.
* Makes available the `whenNotPaused` and `whenPaused` modifiers.
* Respects a system level pause from the System Security.
*/
abstract contract Pausable {
IAccessController private immutable _accessController;
ISystemSecurity private immutable _systemSecurity;
/// @dev Emitted when the pause is triggered by `account`.
event Paused(address account);
/// @dev Emitted when the pause is lifted by `account`.
event Unpaused(address account);
error IsPaused();
error IsNotPaused();
bool private _paused;
modifier whenNotPaused() {
_requireNotPaused();
_;
}
modifier whenPaused() {
_requirePaused();
_;
}
modifier isPauser() {
if (!_accessController.hasRole(Roles.EMERGENCY_PAUSER, msg.sender)) {
revert Errors.AccessDenied();
}
_;
}
constructor(
ISystemRegistry systemRegistry
) {
Errors.verifyNotZero(address(systemRegistry), "systemRegistry");
// Validate the registry is in a state we can use it
IAccessController accessController = systemRegistry.accessController();
if (address(accessController) == address(0)) {
revert Errors.RegistryItemMissing("accessController");
}
ISystemSecurity systemSecurity = systemRegistry.systemSecurity();
if (address(systemSecurity) == address(0)) {
revert Errors.RegistryItemMissing("systemSecurity");
}
_accessController = accessController;
_systemSecurity = systemSecurity;
}
/// @notice Returns true if the contract or system is paused, and false otherwise.
function paused() public virtual returns (bool) {
return _paused || _systemSecurity.isSystemPaused();
}
/// @notice Pauses the contract
/// @dev Reverts if already paused or not EMERGENCY_PAUSER role
function pause() external virtual isPauser {
if (_paused) {
revert IsPaused();
}
_paused = true;
emit Paused(msg.sender);
}
/// @notice Unpauses the contract
/// @dev Reverts if not paused or not EMERGENCY_PAUSER role
function unpause() external virtual isPauser {
if (!_paused) {
revert IsNotPaused();
}
_paused = false;
emit Unpaused(msg.sender);
}
/// @dev Throws if the contract or system is paused.
function _requireNotPaused() internal virtual {
if (paused()) {
revert IsPaused();
}
}
/// @dev Throws if the contract or system is not paused.
function _requirePaused() internal virtual {
if (!paused()) {
revert IsNotPaused();
}
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
library VaultTypes {
bytes32 public constant LST = keccak256("LST");
bytes32 public constant GENERAL_V1 = keccak256("GENERAL_V1");
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { Initializable } from "openzeppelin-contracts/proxy/utils/Initializable.sol";
/// @title Copy of OZ's ReentrancyGuard with a read only variant added
abstract contract NonReentrantUpgradeable is Initializable {
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
uint256 private _status;
error ReentrancyGuardReentrantCall();
function initialize() internal onlyInitializing {
_status = NOT_ENTERED;
}
modifier nonReentrantReadOnly() {
if (_status == ENTERED) {
revert ReentrancyGuardReentrantCall();
}
_;
}
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be NOT_ENTERED
if (_status == ENTERED) {
revert ReentrancyGuardReentrantCall();
}
// Any calls to nonReentrant after this point will fail
_status = ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = NOT_ENTERED;
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { AutopoolDebt } from "src/vault/libs/AutopoolDebt.sol";
import { IERC4626 } from "src/interfaces/vault/IERC4626.sol";
import { Math } from "openzeppelin-contracts/utils/math/Math.sol";
import { IAutopoolStrategy } from "src/interfaces/strategy/IAutopoolStrategy.sol";
import { IMainRewarder } from "src/interfaces/rewarders/IMainRewarder.sol";
import { IERC20Permit } from "openzeppelin-contracts/token/ERC20/extensions/draft-IERC20Permit.sol";
interface IAutopool is IERC4626, IERC20Permit {
enum VaultShutdownStatus {
Active,
Deprecated,
Exploit
}
/// @param unlockPeriodInSeconds Time it takes for profit to unlock in seconds
/// @param fullProfitUnlockTime Time at which all profit will have been unlocked
/// @param lastProfitUnlockTime Last time profits were unlocked
/// @param profitUnlockRate Per second rate at which profit shares unlocks. Rate when calculated is denominated in
/// MAX_BPS_PROFIT. TODO: Get into uint112
struct ProfitUnlockSettings {
uint48 unlockPeriodInSeconds;
uint48 fullProfitUnlockTime;
uint48 lastProfitUnlockTime;
uint256 profitUnlockRate;
}
/// @param feeSink Where claimed fees are sent
/// @param totalAssetsHighMark The last totalAssets amount we took fees at
/// @param totalAssetsHighMarkTimestamp The last timestamp we updated the high water mark
/// @param lastPeriodicFeeTake Timestamp of when the last periodic fee was taken.
/// @param periodicFeeSink Address that receives periodic fee.
/// @param periodicFeeBps Current periodic fee. 100% == 10000.
/// @param streamingFeeBps Current streaming fee taken on profit. 100% == 10000
/// @param navPerShareLastFeeMark The last nav/share height we took fees at
/// @param navPerShareLastFeeMarkTimestamp The last timestamp we took fees at
/// @param rebalanceFeeHighWaterMarkEnabled Returns whether the nav/share high water mark is enabled for the
/// rebalance fee
struct AutopoolFeeSettings {
address feeSink;
uint256 totalAssetsHighMark;
uint256 totalAssetsHighMarkTimestamp;
uint256 lastPeriodicFeeTake;
address periodicFeeSink;
uint256 periodicFeeBps;
uint256 streamingFeeBps;
uint256 navPerShareLastFeeMark;
uint256 navPerShareLastFeeMarkTimestamp;
bool rebalanceFeeHighWaterMarkEnabled;
}
/// @param totalIdle The amount of baseAsset deposited into the contract pending deployment
/// @param totalDebt The current (though cached) value of assets we've deployed
/// @param totalDebtMin The current (though cached) value of assets we use for valuing during deposits
/// @param totalDebtMax The current (though cached) value of assets we use for valuing during withdrawals
struct AssetBreakdown {
uint256 totalIdle;
uint256 totalDebt;
uint256 totalDebtMin;
uint256 totalDebtMax;
}
enum TotalAssetPurpose {
Global,
Deposit,
Withdraw
}
/* ******************************** */
/* Events */
/* ******************************** */
// Autopool4626
// event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
event Nav(uint256 idle, uint256 debt, uint256 totalSupply);
event TokensRecovered(address[] tokens, uint256[] amounts, address[] destinations);
event Shutdown(IAutopool.VaultShutdownStatus reason);
event RewarderSet(address newRewarder, address oldRewarder);
event SymbolAndDescSet(string symbol, string desc);
// AutopoolDebt
event DestinationDebtReporting(
address destination, AutopoolDebt.IdleDebtUpdates debtInfo, uint256 claimed, uint256 claimGasUsed
);
event NewNavShareFeeMark(uint256 navPerShare, uint256 timestamp);
// event Nav(uint256 idle, uint256 debt, uint256 totalSupply);
// event Withdraw(
// address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares
// );
// AutopoolDestinations
event DestinationVaultAdded(address destination);
event DestinationVaultRemoved(address destination);
event WithdrawalQueueSet(address[] destinations);
event AddedToRemovalQueue(address destination);
event RemovedFromRemovalQueue(address destination);
// AutopoolFees
event FeeCollected(uint256 fees, address feeSink, uint256 mintedShares, uint256 profit, uint256 totalAssets);
event PeriodicFeeCollected(uint256 fees, address feeSink, uint256 mintedShares);
// event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
event PeriodicFeeSet(uint256 newFee);
event PeriodicFeeSinkSet(address newPeriodicFeeSink);
event LastPeriodicFeeTakeSet(uint256 lastPeriodicFeeTake);
event RebalanceFeeHighWaterMarkEnabledSet(bool enabled);
// event NewNavShareFeeMark(uint256 navPerShare, uint256 timestamp);
event NewTotalAssetsHighWatermark(uint256 assets, uint256 timestamp);
event StreamingFeeSet(uint256 newFee);
event FeeSinkSet(address newFeeSink);
event NewProfitUnlockTime(uint48 timeSeconds);
// AutopoolToken
/// @dev Emitted when `value` tokens are moved from one account `from` to another `to`.
//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);
/* ******************************** */
/* Errors */
/* ******************************** */
// Autopool
error InvalidDecimals();
error NavOpsInProgress();
error NavDecreased(uint256 oldNav, uint256 newNav);
error ValueSharesMismatch(uint256 value, uint256 shares);
error ERC4626MintExceedsMax(uint256 shares, uint256 maxMint);
error ERC4626DepositExceedsMax(uint256 assets, uint256 maxDeposit);
error ERC4626ExceededMaxWithdraw(address owner, uint256 assets, uint256 max);
error ERC4626ExceededMaxRedeem(address owner, uint256 shares, uint256 max);
// Autopool4626
error InvalidTotalAssetPurpose();
error InvalidShutdownStatus(IAutopool.VaultShutdownStatus status);
error RecoveryFailed();
// AutopoolDestination
error BaseAssetMismatch(address destinationVault);
// AutopoolDebt
error VaultShutdown();
error WithdrawShareCalcInvalid(uint256 currentShares, uint256 cachedShares);
//error RebalanceFailed(string message);
error InvalidPrices();
//error InvalidTotalAssetPurpose();
error InvalidDestination(address destination);
error TooFewAssets(uint256 requested, uint256 actual);
error SharesAndAssetsReceived(uint256 assets, uint256 shares);
error AmountExceedsAllowance(uint256 shares, uint256 allowed);
error PositivePriceRecoupNotCovered(uint256 remaining);
error InsufficientAssets(address asset);
error RebalanceDestinationUnderlyerMismatch(address destination, address trueUnderlyer, address providedUnderlyer);
error OnlyRebalanceToIdleAvailable();
error UnregisteredDestination(address dest);
error RebalanceDestinationsMatch();
// AutopoolFees
error InvalidFee(uint256 newFee);
error AlreadySet();
error DebtReportingStale();
// AutopoolStrategyHooks
/// @notice Fires when are at the maximum number of configured hooks for a function
error MaxHooksSet();
/// @notice Fires when a hook is already registered for a function
error HookAlreadySet(address hook, uint256 fn);
/// @notice Fires on removal when a hook doesn't exist
error HookNotSet(address hook);
/// @notice Fires on removal when a function is supposed to be registered but isn't
error FunctionNotSet(address hook, uint256 fn);
/// @notice Fires when a hook execution fails
error HookExecutionFailed(address hook, bytes underlyingError);
// AutopoolToken
/// @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.
/// @param sender Address whose tokens are being transferred.
/// @param balance Current balance for the interacting account.
/// @param needed Minimum amount required to perform a transfer.
error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
/// @dev Indicates a failure with the token `sender`. Used in transfers.
/// @param sender Address whose tokens are being transferred.
error ERC20InvalidSender(address sender);
/// @dev Indicates a failure with the token `receiver`. Used in transfers.
/// @param receiver Address to which tokens are being transferred.
error ERC20InvalidReceiver(address receiver);
/// @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers.
///@param spender Address that may be allowed to operate on tokens without being their owner.
/// @param allowance Amount of tokens a `spender` is allowed to operate with.
///@param needed Minimum amount required to perform a transfer.
error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);
/// @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.
/// @param approver Address initiating an approval operation.
error ERC20InvalidApprover(address approver);
/// @dev Indicates a failure with the `spender` to be approved. Used in approvals.
/// @param spender Address that may be allowed to operate on tokens without being their owner.
error ERC20InvalidSpender(address spender);
/// @dev Permit deadline has expired.
error ERC2612ExpiredSignature(uint256 deadline);
/// @dev Mismatched signature.
error ERC2612InvalidSigner(address signer, address owner);
/// @dev The nonce used for an `account` is not the expected current nonce.
error InvalidAccountNonce(address account, uint256 currentNonce);
/// @notice A full unit of this pool
// solhint-disable-next-line func-name-mixedcase
function ONE() external view returns (uint256);
/// @notice Amount to pad scaling operations by
function decimalPad() external view returns (uint256);
/// @notice Query the type of vault
function vaultType() external view returns (bytes32);
/// @notice Strategy governing the pools rebalances
function autoPoolStrategy() external view returns (IAutopoolStrategy);
/// @notice Allow token recoverer to collect dust / unintended transfers (non-tracked assets only)
function recover(address[] calldata tokens, uint256[] calldata amounts, address[] calldata destinations) external;
/// @notice Set the order of destination vaults used for withdrawals
// NOTE: will be done going directly to strategy (IStrategy) vault points to.
// How it'll delegate is still being decided
// function setWithdrawalQueue(address[] calldata destinations) external;
/// @notice Get a list of destination vaults with pending assets to clear out
function getRemovalQueue() external view returns (address[] memory);
function getFeeSettings() external view returns (AutopoolFeeSettings memory);
/// @notice Initiate the shutdown procedures for this vault
function shutdown(
VaultShutdownStatus reason
) external;
/// @notice True if the vault has been shutdown
function isShutdown() external view returns (bool);
/// @notice Returns the reason for shutdown (or `Active` if not shutdown)
function shutdownStatus() external view returns (VaultShutdownStatus);
/// @notice gets the list of supported destination vaults for the Autopool/Strategy
/// @return _destinations List of supported destination vaults
function getDestinations() external view returns (address[] memory _destinations);
function convertToShares(
uint256 assets,
uint256 totalAssetsForPurpose,
uint256 supply,
Math.Rounding rounding
) external view returns (uint256 shares);
function convertToAssets(
uint256 shares,
uint256 totalAssetsForPurpose,
uint256 supply,
Math.Rounding rounding
) external view returns (uint256 assets);
function totalAssets(
TotalAssetPurpose purpose
) external view returns (uint256);
function getAssetBreakdown() external view returns (AssetBreakdown memory);
/// @notice get a destinations last reported debt value
/// @param destVault the address of the target destination
/// @return destinations last reported debt value
function getDestinationInfo(
address destVault
) external view returns (AutopoolDebt.DestinationInfo memory);
/// @notice check if a destination is registered with the vault
function isDestinationRegistered(
address destination
) external view returns (bool);
/// @notice get if a destinationVault is queued for removal by the AutopoolETH
function isDestinationQueuedForRemoval(
address destination
) external view returns (bool);
/// @notice Returns instance of vault rewarder.
function rewarder() external view returns (IMainRewarder);
/// @notice Returns boolean telling whether address passed in is past rewarder.
function isPastRewarder(
address _pastRewarder
) external view returns (bool);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IAutopool } from "src/interfaces/vault/IAutopool.sol";
import { Math } from "openzeppelin-contracts/utils/math/Math.sol";
import { AutopoolToken } from "src/vault/libs/AutopoolToken.sol";
import { AutopoolState } from "src/vault/libs/AutopoolState.sol";
library AutopoolFees {
using Math for uint256;
using AutopoolToken for AutopoolToken.TokenData;
/// @notice Profit denomination
uint256 public constant MAX_BPS_PROFIT = 1_000_000_000;
/// @notice 100% == 10000
uint256 public constant FEE_DIVISOR = 10_000;
/// @notice Max periodic fee, 10%. 100% = 10_000.
uint256 public constant MAX_PERIODIC_FEE_BPS = 1000;
uint256 public constant SECONDS_IN_YEAR = 365 days;
event FeeCollected(uint256 fees, address feeSink, uint256 mintedShares, uint256 profit, uint256 totalAssets);
event PeriodicFeeCollected(uint256 fees, address feeSink, uint256 mintedShares);
event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
event PeriodicFeeSet(uint256 newFee);
event PeriodicFeeSinkSet(address newPeriodicFeeSink);
event LastPeriodicFeeTakeSet(uint256 lastPeriodicFeeTake);
event RebalanceFeeHighWaterMarkEnabledSet(bool enabled);
event NewNavShareFeeMark(uint256 navPerShare, uint256 timestamp);
event NewTotalAssetsHighWatermark(uint256 assets, uint256 timestamp);
event StreamingFeeSet(uint256 newFee);
event FeeSinkSet(address newFeeSink);
event NewProfitUnlockTime(uint48 timeSeconds);
error InvalidFee(uint256 newFee);
error AlreadySet();
error DebtReportingStale();
/// @notice Returns the amount of unlocked profit shares that will be burned
function unlockedShares(
AutopoolState storage $
) public view returns (uint256 shares) {
uint256 fullTime = $.profitUnlockSettings.fullProfitUnlockTime;
if (fullTime > block.timestamp) {
shares = $.profitUnlockSettings.profitUnlockRate
* (block.timestamp - $.profitUnlockSettings.lastProfitUnlockTime) / MAX_BPS_PROFIT;
} else if (fullTime != 0) {
shares = $.token.balances[address(this)];
}
}
function initializeFeeSettings(
AutopoolState storage $
) external {
uint256 timestamp = block.timestamp;
// Stops fees from being able to be claimed before init timestamp.
$.feeSettings.lastPeriodicFeeTake = timestamp;
$.feeSettings.navPerShareLastFeeMark = FEE_DIVISOR;
$.feeSettings.navPerShareLastFeeMarkTimestamp = timestamp;
emit LastPeriodicFeeTakeSet(timestamp);
}
function burnUnlockedShares(
AutopoolState storage $
) external {
uint256 shares = unlockedShares($);
if (shares == 0) {
return;
}
if ($.profitUnlockSettings.fullProfitUnlockTime > block.timestamp) {
$.profitUnlockSettings.lastProfitUnlockTime = uint48(block.timestamp);
}
$.token.burn(address(this), shares);
}
function _calculateEffectiveNavPerShareLastFeeMark(
IAutopool.AutopoolFeeSettings storage settings,
uint256 currentBlock,
uint256 currentNavPerShare,
uint256 aumCurrent
) private view returns (uint256) {
uint256 workingHigh = settings.navPerShareLastFeeMark;
if (workingHigh == 0) {
// If we got 0, we shouldn't increase it
return 0;
}
if (!settings.rebalanceFeeHighWaterMarkEnabled) {
// No calculations or checks to do in this case
return workingHigh;
}
uint256 daysSinceLastFeeEarned = (currentBlock - settings.navPerShareLastFeeMarkTimestamp) / 60 / 60 / 24;
if (daysSinceLastFeeEarned > 600) {
return currentNavPerShare;
}
if (daysSinceLastFeeEarned > 60 && daysSinceLastFeeEarned <= 600) {
uint8 decimals = IAutopool(address(this)).decimals();
uint256 one = 10 ** decimals;
uint256 aumHighMark = settings.totalAssetsHighMark;
// AUM_min = min(AUM_high, AUM_current)
uint256 minAssets = aumCurrent < aumHighMark ? aumCurrent : aumHighMark;
// AUM_max = max(AUM_high, AUM_current);
uint256 maxAssets = aumCurrent > aumHighMark ? aumCurrent : aumHighMark;
/// 0.999 * (AUM_min / AUM_max)
// dividing by `one` because we need end up with a number in the 100's wei range
uint256 g1 = ((999 * minAssets * one) / (maxAssets * one));
/// 0.99 * (1 - AUM_min / AUM_max)
// dividing by `10 ** (decimals() - 1)` because we need to divide 100 out for our % and then
// we want to end up with a number in the 10's wei range
uint256 g2 = (99 * (one - (minAssets * one / maxAssets))) / 10 ** (decimals - 1);
uint256 gamma = g1 + g2;
uint256 daysDiff = daysSinceLastFeeEarned - 60;
for (uint256 i = 0; i < daysDiff / 25; ++i) {
// slither-disable-next-line divide-before-multiply
workingHigh = workingHigh * (gamma ** 25 / 1e72) / 1000;
}
// slither-disable-next-line weak-prng
for (uint256 i = 0; i < daysDiff % 25; ++i) {
// slither-disable-next-line divide-before-multiply
workingHigh = workingHigh * gamma / 1000;
}
}
return workingHigh;
}
function collectFees(
AutopoolState storage $,
uint256 totalAssets,
uint256 currentTotalSupply,
bool collectPeriodicFees
) external returns (uint256) {
// If there's no supply then there should be no assets and so nothing
// to actually take fees on
// slither-disable-next-line incorrect-equality
if (currentTotalSupply == 0) {
return 0;
}
uint256 decimalPad = IAutopool(address(this)).decimalPad();
// slither-disable-start timestamp
if (collectPeriodicFees) {
address periodicFeeSink = $.feeSettings.periodicFeeSink;
uint256 periodicFeeBps = $.feeSettings.periodicFeeBps;
// If there is a periodic fee and fee sink set, take the fee.
if (periodicFeeBps > 0 && periodicFeeSink != address(0)) {
uint256 durationSinceLastPeriodicFeeTake = block.timestamp - $.feeSettings.lastPeriodicFeeTake;
uint256 timeAdjustedBps = durationSinceLastPeriodicFeeTake.mulDiv(
periodicFeeBps * FEE_DIVISOR, SECONDS_IN_YEAR, Math.Rounding.Up
);
uint256 periodicShares =
_collectPeriodicFees(periodicFeeSink, timeAdjustedBps, currentTotalSupply, totalAssets);
currentTotalSupply += periodicShares;
$.token.mint(periodicFeeSink, periodicShares);
}
// Needs to be kept up to date so if a fee is suddenly turned on a large part of assets do not get
// claimed as fees.
$.feeSettings.lastPeriodicFeeTake = block.timestamp;
emit LastPeriodicFeeTakeSet(block.timestamp);
}
// slither-disable-end timestamp
uint256 currentNavPerShare = (totalAssets * decimalPad * FEE_DIVISOR) / currentTotalSupply;
// If the high mark is disabled then this just returns the `navPerShareLastFeeMark`
// Otherwise, it'll check if it needs to decay
uint256 effectiveNavPerShareLastFeeMark =
_calculateEffectiveNavPerShareLastFeeMark($.feeSettings, block.timestamp, currentNavPerShare, totalAssets);
if (currentNavPerShare > effectiveNavPerShareLastFeeMark) {
// Even if we aren't going to take the fee (haven't set a sink)
// We still want to calculate so we can emit for off-chain analysis
uint256 profit = (currentNavPerShare - effectiveNavPerShareLastFeeMark).mulDiv(
currentTotalSupply, decimalPad, Math.Rounding.Up
);
uint256 fees = profit.mulDiv($.feeSettings.streamingFeeBps, (FEE_DIVISOR ** 2), Math.Rounding.Up);
if (fees > 0) {
currentTotalSupply = _mintStreamingFee($, fees, profit, currentTotalSupply, totalAssets);
currentNavPerShare = (totalAssets * decimalPad * FEE_DIVISOR) / currentTotalSupply;
}
}
// Two situations we're covering here
// 1. If the high mark is disabled then we just always need to know the last
// time we evaluated fees so we can catch any run up. i.e. the `navPerShareLastFeeMark`
// can go down
// 2. When the high mark is enabled, then we only want to set `navPerShareLastFeeMark`
// when it is greater than the last time we captured fees (or would have)
if (currentNavPerShare >= effectiveNavPerShareLastFeeMark || !$.feeSettings.rebalanceFeeHighWaterMarkEnabled) {
$.feeSettings.navPerShareLastFeeMark = currentNavPerShare;
$.feeSettings.navPerShareLastFeeMarkTimestamp = block.timestamp;
emit NewNavShareFeeMark(currentNavPerShare, block.timestamp);
}
// Set our new high water mark for totalAssets, regardless if we took fees
if ($.feeSettings.totalAssetsHighMark < totalAssets) {
$.feeSettings.totalAssetsHighMark = totalAssets;
$.feeSettings.totalAssetsHighMarkTimestamp = block.timestamp;
emit NewTotalAssetsHighWatermark(
$.feeSettings.totalAssetsHighMark, $.feeSettings.totalAssetsHighMarkTimestamp
);
}
return currentTotalSupply;
}
function _mintStreamingFee(
AutopoolState storage $,
uint256 fees,
uint256 profit,
uint256 currentTotalSupply,
uint256 totalAssets
) private returns (uint256) {
address sink = $.feeSettings.feeSink;
if (sink == address(0)) {
return currentTotalSupply;
}
uint256 streamingFeeShares =
_calculateSharesToMintFeeCollection($.feeSettings.streamingFeeBps, profit, totalAssets, currentTotalSupply);
$.token.mint(sink, streamingFeeShares);
currentTotalSupply += streamingFeeShares;
emit Deposit(address(this), sink, 0, streamingFeeShares);
emit FeeCollected(fees, sink, streamingFeeShares, profit, totalAssets);
return currentTotalSupply;
}
/// @dev Collects periodic fees.
function _collectPeriodicFees(
address periodicSink,
uint256 timeAdjustedFeeBps,
uint256 currentTotalSupply,
uint256 totalAssets
) private returns (uint256 newShares) {
newShares =
_calculateSharesToMintFeeCollection(timeAdjustedFeeBps, totalAssets, totalAssets, currentTotalSupply);
// Fee in assets that we are taking.
uint256 fees = (timeAdjustedFeeBps * totalAssets / FEE_DIVISOR).ceilDiv(FEE_DIVISOR);
emit Deposit(address(this), periodicSink, 0, newShares);
emit PeriodicFeeCollected(fees, periodicSink, newShares);
return newShares;
}
function _calculateSharesToMintFeeCollection(
uint256 feeBps,
uint256 amountForFee,
uint256 totalAssets,
uint256 totalSupply
) private pure returns (uint256 toMint) {
// Gas savings, this is used twice.
uint256 feeTotalAssets = feeBps * amountForFee / FEE_DIVISOR;
// Separate from other mints as normal share mint is round down
// Mints shares taking into account the dilution so we end up with the expected amount
// `feeBps` is padded by FEE_DIVISOR when taking periodic fee
// `amountForFee` is padded by FEE_DIVISOR when taking streaming fee
toMint =
Math.mulDiv(feeTotalAssets, totalSupply, (totalAssets * FEE_DIVISOR) - (feeTotalAssets), Math.Rounding.Up);
}
/// @dev If set to 0, existing shares will unlock immediately and increase nav/share. This is intentional
function setProfitUnlockPeriod(AutopoolState storage $, uint48 newUnlockPeriodInSeconds) external {
$.profitUnlockSettings.unlockPeriodInSeconds = newUnlockPeriodInSeconds;
// If we are turning off the unlock, setting it to 0, then
// unlock all existing shares
if (newUnlockPeriodInSeconds == 0) {
uint256 currentShares = $.token.balances[address(this)];
if (currentShares > 0) {
$.profitUnlockSettings.lastProfitUnlockTime = uint48(block.timestamp);
$.token.burn(address(this), currentShares);
}
// Reset vars so old values aren't used during a subsequent lockup
$.profitUnlockSettings.fullProfitUnlockTime = 0;
$.profitUnlockSettings.profitUnlockRate = 0;
}
emit NewProfitUnlockTime(newUnlockPeriodInSeconds);
}
function calculateProfitLocking(
IAutopool.ProfitUnlockSettings storage settings,
AutopoolToken.TokenData storage token,
uint256 feeShares,
uint256 newTotalAssets,
uint256 startTotalAssets,
uint256 startTotalSupply,
uint256 previousLockShares
) external returns (uint256) {
uint256 unlockPeriod = settings.unlockPeriodInSeconds;
// If there were existing shares and we set the unlock period to 0 they are immediately unlocked
// so we don't have to worry about existing shares here. And if the period is 0 then we
// won't be locking any new shares
if (unlockPeriod == 0 || startTotalAssets == 0) {
return startTotalSupply;
}
uint256 newLockShares = 0;
uint256 previousLockToBurn = 0;
uint256 effectiveTs = startTotalSupply;
// The total supply we would need to not see a change in nav/share
uint256 targetTotalSupply = newTotalAssets * (effectiveTs - feeShares) / startTotalAssets;
if (effectiveTs > targetTotalSupply) {
// Our actual total supply is greater than our target.
// This means we would see a decrease in nav/share
// See if we can burn any profit shares to offset that
if (previousLockShares > 0) {
uint256 diff = effectiveTs - targetTotalSupply;
if (previousLockShares >= diff) {
previousLockToBurn = diff;
effectiveTs -= diff;
} else {
previousLockToBurn = previousLockShares;
effectiveTs -= previousLockShares;
}
}
}
if (targetTotalSupply > effectiveTs) {
// Our actual total supply is less than our target.
// This means we would see an increase in nav/share (due to gains) which we can't allow
// We need to mint shares to the vault to offset
newLockShares = targetTotalSupply - effectiveTs;
effectiveTs += newLockShares;
}
// We know how many shares should be locked at this point
// Mint or burn what we need to match if necessary
uint256 totalLockShares = previousLockShares - previousLockToBurn + newLockShares;
if (totalLockShares > previousLockShares) {
uint256 mintAmount = totalLockShares - previousLockShares;
token.mint(address(this), mintAmount);
startTotalSupply += mintAmount;
} else if (totalLockShares < previousLockShares) {
uint256 burnAmount = previousLockShares - totalLockShares;
token.burn(address(this), burnAmount);
startTotalSupply -= burnAmount;
}
// If we're going to end up with no profit shares, zero the rate
// We don't need to 0 the other timing vars if we just zero the rate
if (totalLockShares == 0) {
settings.profitUnlockRate = 0;
}
// We have shares and they are going to unlocked later
if (totalLockShares > 0 && unlockPeriod > 0) {
_updateProfitUnlockTimings(
settings, unlockPeriod, previousLockToBurn, previousLockShares, newLockShares, totalLockShares
);
}
return startTotalSupply;
}
function _updateProfitUnlockTimings(
IAutopool.ProfitUnlockSettings storage settings,
uint256 unlockPeriod,
uint256 previousLockToBurn,
uint256 previousLockShares,
uint256 newLockShares,
uint256 totalLockShares
) private {
uint256 previousLockTime;
uint256 fullUnlockTime = settings.fullProfitUnlockTime;
// Determine how much time is left for the remaining previous profit shares
if (fullUnlockTime > block.timestamp) {
previousLockTime = (previousLockShares - previousLockToBurn) * (fullUnlockTime - block.timestamp);
}
// Amount of time it will take to unlock all shares, weighted avg over current and new shares
uint256 newUnlockPeriod = (previousLockTime + newLockShares * unlockPeriod) / totalLockShares;
if (newUnlockPeriod == 0) {
settings.profitUnlockRate = 0;
} else {
// Rate at which totalLockShares will unlock
settings.profitUnlockRate = totalLockShares * MAX_BPS_PROFIT / newUnlockPeriod;
}
// Time the full of amount of totalLockShares will be unlocked
settings.fullProfitUnlockTime = uint48(block.timestamp + newUnlockPeriod);
settings.lastProfitUnlockTime = uint48(block.timestamp);
}
/// @notice Enable or disable the high water mark on the rebalance fee
/// @dev Will revert if set to the same value
function setRebalanceFeeHighWaterMarkEnabled(
IAutopool.AutopoolFeeSettings storage feeSettings,
bool enabled
) external {
if (feeSettings.rebalanceFeeHighWaterMarkEnabled == enabled) {
revert AlreadySet();
}
feeSettings.rebalanceFeeHighWaterMarkEnabled = enabled;
emit RebalanceFeeHighWaterMarkEnabledSet(enabled);
}
/// @notice Set the fee that will be taken when profit is realized
/// @dev Resets the high water to current value
/// @param $ Storage related to the calling Autopool.
/// @param fee Percent. 100% == 10000
/// @param oldestDebtReporting Debt reporting timestamp to be checked
function setStreamingFeeBps(AutopoolState storage $, uint256 fee, uint256 oldestDebtReporting) external {
if (fee >= FEE_DIVISOR) {
revert InvalidFee(fee);
}
_checkLastDebtReportingTime(oldestDebtReporting, $.debtReportQueue.size);
IAutopool vault = IAutopool(address(this));
$.feeSettings.streamingFeeBps = fee;
// Set the high mark when we change the fee so we aren't able to go farther back in
// time than one debt reporting and claim fee's against past profits
uint256 ts = vault.totalSupply();
if (ts > 0) {
uint256 ta = vault.totalAssets();
if (ta > 0) {
$.feeSettings.navPerShareLastFeeMark = (ta * vault.decimalPad() * FEE_DIVISOR) / ts;
} else {
$.feeSettings.navPerShareLastFeeMark = FEE_DIVISOR;
}
}
emit StreamingFeeSet(fee);
}
/// @notice Set the periodic fee taken.
/// @dev Zero is allowed, no fee taken.
/// @param $ Storage related to the calling Autopool.
/// @param fee Fee to update periodic fee to.
/// @param oldestDebtReporting Debt reporting timestamp to be checked
function setPeriodicFeeBps(AutopoolState storage $, uint256 fee, uint256 oldestDebtReporting) external {
if (fee > MAX_PERIODIC_FEE_BPS) {
revert InvalidFee(fee);
}
_checkLastDebtReportingTime(oldestDebtReporting, $.debtReportQueue.size);
// Fee checked to fit into uint16 above, able to be wrapped without safe cast here.
emit PeriodicFeeSet(fee);
$.feeSettings.periodicFeeBps = uint16(fee);
}
/// @notice Set the address that will receive fees
/// @param newFeeSink Address that will receive fees
function setFeeSink(IAutopool.AutopoolFeeSettings storage feeSettings, address newFeeSink) external {
emit FeeSinkSet(newFeeSink);
// Zero is valid. One way to disable taking fees
// slither-disable-next-line missing-zero-check
feeSettings.feeSink = newFeeSink;
}
/// @notice Sets the address that will receive periodic fees.
/// @dev Zero address allowable. Disables fees.
/// @param newPeriodicFeeSink New periodic fee address.
function setPeriodicFeeSink(
IAutopool.AutopoolFeeSettings storage feeSettings,
address newPeriodicFeeSink
) external {
emit PeriodicFeeSinkSet(newPeriodicFeeSink);
// slither-disable-next-line missing-zero-check
feeSettings.periodicFeeSink = newPeriodicFeeSink;
}
function _checkLastDebtReportingTime(uint256 oldestDebtReporting, uint256 debtReportQueueLength) private view {
if (debtReportQueueLength > 0 && oldestDebtReporting < block.timestamp - 10 minutes) {
revert DebtReportingStale();
}
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { ECDSA } from "openzeppelin-contracts/utils/cryptography/ECDSA.sol";
import { IERC20Permit } from "openzeppelin-contracts/token/ERC20/extensions/draft-IERC20Permit.sol";
/// @notice ERC20 token functionality converted into a library. Based on OZ's v5
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.1/contracts/token/ERC20/ERC20.sol
library AutopoolToken {
struct TokenData {
/// @notice Token balances
/// @dev account => balance
mapping(address => uint256) balances;
/// @notice Account spender allowances
/// @dev account => spender => allowance
mapping(address => mapping(address => uint256)) allowances;
/// @notice Total supply of the pool. Be careful when using this directly from the struct. The pool itself
/// modifies this number based on unlocked profited shares
uint256 totalSupply;
/// @notice ERC20 Permit nonces
/// @dev account -> nonce. Exposed via `nonces(owner)`
mapping(address => uint256) nonces;
}
/// @notice EIP2612 permit type hash
bytes32 public constant PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
/// @notice EIP712 domain type hash
bytes32 public constant TYPE_HASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
/// @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.
/// @param sender Address whose tokens are being transferred.
/// @param balance Current balance for the interacting account.
/// @param needed Minimum amount required to perform a transfer.
error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
/// @dev Indicates a failure with the token `sender`. Used in transfers.
/// @param sender Address whose tokens are being transferred.
error ERC20InvalidSender(address sender);
/// @dev Indicates a failure with the token `receiver`. Used in transfers.
/// @param receiver Address to which tokens are being transferred.
error ERC20InvalidReceiver(address receiver);
/// @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers.
///@param spender Address that may be allowed to operate on tokens without being their owner.
/// @param allowance Amount of tokens a `spender` is allowed to operate with.
///@param needed Minimum amount required to perform a transfer.
error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);
/// @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.
/// @param approver Address initiating an approval operation.
error ERC20InvalidApprover(address approver);
/// @dev Indicates a failure with the `spender` to be approved. Used in approvals.
/// @param spender Address that may be allowed to operate on tokens without being their owner.
error ERC20InvalidSpender(address spender);
/// @dev Permit deadline has expired.
error ERC2612ExpiredSignature(uint256 deadline);
/// @dev Mismatched signature.
error ERC2612InvalidSigner(address signer, address owner);
/// @dev The nonce used for an `account` is not the expected current nonce.
error InvalidAccountNonce(address account, uint256 currentNonce);
/// @dev Emitted when `value` tokens are moved from one account `from` to another `to`.
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 Sets a `value` amount of tokens as the allowance of `spender` over the caller's tokens.
function approve(TokenData storage data, address spender, uint256 value) external returns (bool) {
address owner = msg.sender;
approve(data, owner, spender, value);
return true;
}
/// @dev Sets `value` as the allowance of `spender` over the `owner` s tokens.
function approve(TokenData storage data, address owner, address spender, uint256 value) public {
_approve(data, owner, spender, value, true);
}
function transfer(TokenData storage data, address to, uint256 value) external returns (bool) {
address owner = msg.sender;
_transfer(data, owner, to, value);
return true;
}
/// @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism.
/// value` is then deducted from the caller's allowance.
function transferFrom(TokenData storage data, address from, address to, uint256 value) external returns (bool) {
address spender = msg.sender;
_spendAllowance(data, from, spender, value);
_transfer(data, from, to, value);
return true;
}
/// @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).
function mint(TokenData storage data, address account, uint256 value) external {
if (account == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(data, address(0), account, value);
}
/// @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.
function burn(TokenData storage data, address account, uint256 value) external {
if (account == address(0)) {
revert ERC20InvalidSender(address(0));
}
_update(data, account, address(0), value);
}
function permit(
TokenData storage data,
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
if (block.timestamp > deadline) {
revert ERC2612ExpiredSignature(deadline);
}
uint256 nonce;
// For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be
// decremented or reset. This guarantees that the nonce never overflows.
unchecked {
// It is important to do x++ and not ++x here. Nonces starts at 0
nonce = data.nonces[owner]++;
}
bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline));
bytes32 hash = ECDSA.toTypedDataHash(IERC20Permit(address(this)).DOMAIN_SEPARATOR(), structHash);
address signer = ECDSA.recover(hash, v, r, s);
if (signer != owner) {
revert ERC2612InvalidSigner(signer, owner);
}
approve(data, owner, spender, value);
}
/// @dev Moves a `value` amount of tokens from `from` to `to`.
function _transfer(TokenData storage data, address from, address to, uint256 value) private {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(data, from, to, value);
}
/// @dev Updates `owner` s allowance for `spender` based on spent `value`.
function _spendAllowance(TokenData storage data, address owner, address spender, uint256 value) private {
uint256 currentAllowance = data.allowances[owner][spender];
if (currentAllowance != type(uint256).max) {
if (currentAllowance < value) {
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
}
unchecked {
_approve(data, owner, spender, currentAllowance - value, false);
}
}
}
/// @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
/// (or `to`) is the zero address.
function _update(TokenData storage data, address from, address to, uint256 value) private {
if (from == address(0)) {
// Overflow check required: The rest of the code assumes that totalSupply never overflows
data.totalSupply += value;
} else {
uint256 fromBalance = data.balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
// Overflow not possible: value <= fromBalance <= totalSupply.
data.balances[from] = fromBalance - value;
}
}
if (to == address(0)) {
unchecked {
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
data.totalSupply -= value;
}
} else {
unchecked {
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
data.balances[to] += value;
}
}
emit Transfer(from, to, value);
}
/// @dev Variant of `_approve` with an optional flag to enable or disable the Approval event.
function _approve(TokenData storage data, address owner, address spender, uint256 value, bool emitEvent) private {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
data.allowances[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { Errors } from "src/utils/Errors.sol";
import { AutopoolFees } from "src/vault/libs/AutopoolFees.sol";
import { AutopoolToken } from "src/vault/libs/AutopoolToken.sol";
import { AutopoolDebt } from "src/vault/libs/AutopoolDebt.sol";
import { IAutopool } from "src/interfaces/vault/IAutopool.sol";
import { IERC20Metadata } from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import { StructuredLinkedList } from "src/strategy/StructuredLinkedList.sol";
import { WithdrawalQueue } from "src/strategy/WithdrawalQueue.sol";
import { AutopoolState, AutopoolStorage } from "src/vault/libs/AutopoolState.sol";
import { IMainRewarder } from "src/interfaces/rewarders/IMainRewarder.sol";
import { EnumerableSet } from "openzeppelin-contracts/utils/structs/EnumerableSet.sol";
import { Math } from "openzeppelin-contracts/utils/math/Math.sol";
library Autopool4626 {
using Math for uint256;
using SafeERC20 for IERC20Metadata;
using WithdrawalQueue for StructuredLinkedList.List;
using AutopoolToken for AutopoolToken.TokenData;
using AutopoolFees for AutopoolState;
using AutopoolDebt for AutopoolState;
using EnumerableSet for EnumerableSet.AddressSet;
/// =====================================================
/// Errors
/// =====================================================
error InvalidTotalAssetPurpose();
error InvalidShutdownStatus(IAutopool.VaultShutdownStatus status);
error RecoveryFailed();
/// =====================================================
/// Events
/// =====================================================
event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
event Nav(uint256 idle, uint256 debt, uint256 totalSupply);
event TokensRecovered(address[] tokens, uint256[] amounts, address[] destinations);
event Shutdown(IAutopool.VaultShutdownStatus reason);
event RewarderSet(address newRewarder, address oldRewarder);
event SymbolAndDescSet(string symbol, string desc);
/// @notice Mints Vault shares to receiver by depositing exactly amount of underlying tokens
/// @dev No nav/share changing operations, debt reportings or rebalances,
/// can be happening throughout the entire system
function deposit(
AutopoolState storage $,
address baseAsset,
uint256 assets,
address receiver,
bool paused,
uint256 _baseAssetDecimals
) external returns (uint256 shares) {
Errors.verifyNotZero(assets, "assets");
uint256 ta = $.totalAssetsTimeChecked(IAutopool.TotalAssetPurpose.Deposit);
// Handles the vault being paused, returns 0
uint256 maxDepositAmount = maxDeposit($, receiver, paused, _baseAssetDecimals);
if (assets > maxDepositAmount) {
revert IAutopool.ERC4626DepositExceedsMax(assets, maxDepositAmount);
}
// Factor in base Asset decimals
shares = convertToShares(assets, ta, totalSupply(), Math.Rounding.Down, _baseAssetDecimals);
Errors.verifyNotZero(shares, "shares");
transferAndMint($, IERC20Metadata(baseAsset), assets, shares, receiver);
}
/// @notice Returns the amount of tokens owned by account.
/// @dev Subtracts any unlocked profit shares that will be burned when account is the Vault itself
function balanceOf(AutopoolState storage $, address account) public view returns (uint256) {
if (account == address(this)) {
return $.token.balances[account] - $.unlockedShares();
}
return $.token.balances[account];
}
/// @notice Returns the total amount of the underlying asset that is “managed” by Vault.
/// @dev Utilizes the "Global" purpose internally
function totalAssets(
IAutopool.AssetBreakdown storage assetBreakdown
) public view returns (uint256) {
return totalAssets(assetBreakdown, IAutopool.TotalAssetPurpose.Global);
}
/// @notice Returns the total amount of the underlying asset that is “managed” by the Vault with respect to its
/// usage
/// @dev Value changes based on purpose. Global is an avg. Deposit is valued higher. Withdraw is valued lower.
/// @param purpose The calculation the total assets will be used in
function totalAssets(
IAutopool.AssetBreakdown storage assetBreakdown,
IAutopool.TotalAssetPurpose purpose
) public view returns (uint256) {
if (purpose == IAutopool.TotalAssetPurpose.Global) {
return assetBreakdown.totalIdle + assetBreakdown.totalDebt;
} else if (purpose == IAutopool.TotalAssetPurpose.Deposit) {
return assetBreakdown.totalIdle + assetBreakdown.totalDebtMax;
} else if (purpose == IAutopool.TotalAssetPurpose.Withdraw) {
return assetBreakdown.totalIdle + assetBreakdown.totalDebtMin;
} else {
revert InvalidTotalAssetPurpose();
}
}
// @notice Scale the amount from existing decimals to newDecimals
function changeDecimals(
uint256 amount,
uint256 existingDecimals,
uint256 newDecimals
) internal pure returns (uint256) {
if (existingDecimals == newDecimals) {
return amount;
}
if (existingDecimals > newDecimals) {
return amount / (10 ** (existingDecimals - newDecimals));
} else {
return amount * (10 ** (newDecimals - existingDecimals));
}
}
/// @notice Returns the amount of shares that the Vault would exchange for the amount of assets provided,
/// in an ideal scenario where all the conditions are met
function convertToShares(
uint256 assets,
uint256 totalAssetsForPurpose,
uint256 supply,
Math.Rounding rounding,
uint256 baseAssetDecimals
) internal pure returns (uint256 shares) {
// slither-disable-next-line incorrect-equality
shares = (assets == 0 || supply == 0)
? changeDecimals(assets, baseAssetDecimals, 18)
: assets.mulDiv(supply, totalAssetsForPurpose, rounding);
}
/// @notice Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an
/// ideal
/// scenario where all the conditions are met.
function convertToAssets(
uint256 shares,
uint256 totalAssetsForPurpose,
uint256 supply,
Math.Rounding rounding,
uint256 baseAssetDecimals
) internal pure returns (uint256 assets) {
// slither-disable-next-line incorrect-equality
assets = (supply == 0)
? changeDecimals(shares, 18, baseAssetDecimals)
: shares.mulDiv(totalAssetsForPurpose, supply, rounding);
}
/// @notice Returns the maximum amount of the underlying asset that can be
/// deposited into the Vault for the receiver, through a deposit call
function maxDeposit(
AutopoolState storage $,
address wallet,
bool paused,
uint256 baseAssetDecimals
) public returns (uint256 maxAssets) {
uint256 aptTotalAssets = $.totalAssetsTimeChecked(IAutopool.TotalAssetPurpose.Deposit);
uint256 maxMintShares = maxMint($, wallet, paused);
maxAssets = convertToAssets(maxMintShares, aptTotalAssets, totalSupply(), Math.Rounding.Up, baseAssetDecimals);
}
/// @notice Returns the maximum amount of the Vault shares that
/// can be minted for the receiver, through a mint call.
function maxMint(AutopoolState storage $, address, bool paused) public returns (uint256) {
// If we are temporarily paused, or in full shutdown mode,
// no new shares are able to be minted
if (paused || $.shutdown) {
return 0;
}
// First deposit
uint256 ts = totalSupply();
if (ts == 0) {
return type(uint112).max;
}
// We know totalSupply greater than zero now so if totalAssets is zero
// the vault is in an invalid state and users would be able to mint shares for free
uint256 ta = AutopoolDebt.totalAssetsTimeChecked($, IAutopool.TotalAssetPurpose.Deposit);
if (ta == 0) {
return 0;
}
if (ts > type(uint112).max) {
return 0;
}
return type(uint112).max - ts;
}
/// @notice Set the rewarder contract used by the vault.
/// @param _rewarder Address of new rewarder.
function setRewarder(AutopoolState storage $, address _rewarder) external {
Errors.verifyNotZero(_rewarder, "rewarder");
address toBeReplaced = address($.rewarder);
// Check that the new rewarder has not been a rewarder before, and that the current rewarder and
// new rewarder addresses are not the same.
if ($.pastRewarders.contains(_rewarder) || toBeReplaced == _rewarder) {
revert Errors.ItemExists();
}
if (toBeReplaced != address(0)) {
// slither-disable-next-line unused-return
$.pastRewarders.add(toBeReplaced);
}
$.rewarder = IMainRewarder(_rewarder);
emit RewarderSet(_rewarder, toBeReplaced);
}
/// @notice Returns the amount of tokens in existence.
/// @dev Subtracts any unlocked profit shares that will be burned
function totalSupply() public view returns (uint256 shares) {
AutopoolState storage $ = AutopoolStorage.load();
shares = $.token.totalSupply - $.unlockedShares();
}
function transferAndMint(
AutopoolState storage $,
IERC20Metadata baseAsset,
uint256 assets,
uint256 shares,
address receiver
) public {
// From OZ documentation:
// ----------------------
// If _asset is ERC777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
// `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
// assets are transferred and before the shares are minted, which is a valid state.
// slither-disable-next-line reentrancy-no-eth
Errors.verifyNotZero(assets, "assets");
baseAsset.safeTransferFrom(msg.sender, address(this), assets);
uint256 newIdle = $.assetBreakdown.totalIdle + assets;
$.assetBreakdown.totalIdle = newIdle;
$.token.mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
emit Nav(newIdle, $.assetBreakdown.totalDebt, totalSupply());
}
/// @notice Initiate the shutdown procedures for this vault
function shutdownVault(AutopoolState storage $, IAutopool.VaultShutdownStatus reason) external {
if (reason == IAutopool.VaultShutdownStatus.Active) {
revert InvalidShutdownStatus(reason);
}
$.shutdown = true;
$.shutdownStatus = reason;
emit Shutdown(reason);
}
/// @notice Allow the updating of symbol/desc for the vault (only AFTER shutdown)
/// @param newSymbol Symbol the Autopool will use going forward
/// @param newName Name the Autopool will use going forward
function setSymbolAndDescAfterShutdown(
AutopoolState storage $,
string memory newSymbol,
string memory newName
) external {
Errors.verifyNotEmpty(newSymbol, "newSymbol");
Errors.verifyNotEmpty(newName, "newName");
// make sure the vault is no longer active
if ($.shutdownStatus == IAutopool.VaultShutdownStatus.Active) {
revert InvalidShutdownStatus($.shutdownStatus);
}
emit SymbolAndDescSet(newSymbol, newName);
$.symbol = newSymbol;
$.name = newName;
}
/// @notice Transfer out non-tracked tokens
function recover(address[] calldata tokens, uint256[] calldata amounts, address[] calldata destinations) external {
// Makes sure our params are valid
uint256 len = tokens.length;
Errors.verifyNotZero(len, "len");
Errors.verifyArrayLengths(len, amounts.length, "tokens+amounts");
Errors.verifyArrayLengths(len, destinations.length, "tokens+destinations");
emit TokensRecovered(tokens, amounts, destinations);
//IAutopool autoPool = IAutopool(address(this));
for (uint256 i = 0; i < len; ++i) {
(address tokenAddress, uint256 amount, address destination) = (tokens[i], amounts[i], destinations[i]);
// Ensure this isn't an asset we care about
// if (
// tokenAddress == address(this) || tokenAddress == autoPool.asset()
// || autoPool.isDestinationRegistered(tokenAddress)
// || autoPool.isDestinationQueuedForRemoval(tokenAddress)
// ) {
// revert Errors.AssetNotAllowed(tokenAddress);
// }
if (tokenAddress != 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) {
IERC20Metadata(tokenAddress).safeTransfer(destination, amount);
} else {
// solhint-disable-next-line avoid-low-level-calls
(bool result,) = payable(destination).call{ value: amount }("");
if (!result) {
revert RecoveryFailed();
}
}
}
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IERC3156FlashBorrower } from "openzeppelin-contracts/interfaces/IERC3156FlashBorrower.sol";
interface IStrategy {
/* ******************************** */
/* Events */
/* ******************************** */
// event DestinationVaultAdded(address destination);
// event DestinationVaultRemoved(address destination);
// event WithdrawalQueueSet(address[] destinations);
// event AddedToRemovalQueue(address destination);
// event RemovedFromRemovalQueue(address destination);
// error InvalidDestinationVault();
// error RebalanceFailed(string message);
/// @notice gets the list of supported destination vaults for the Autopool/Strategy
/// @return _destinations List of supported destination vaults
function getDestinations() external view returns (address[] memory _destinations);
/// @notice add supported destination vaults for the Autopool/Strategy
/// @param _destinations The list of destination vaults to add
function addDestinations(
address[] calldata _destinations
) external;
/// @notice remove supported destination vaults for the Autopool/Strategy
/// @param _destinations The list of destination vaults to remove
function removeDestinations(
address[] calldata _destinations
) external;
/// @param destinationIn The address / lp token of the destination vault that will increase
/// @param tokenIn The address of the underlyer token that will be provided by the swapper
/// @param amountIn The amount of the underlying LP tokens that will be received
/// @param destinationOut The address of the destination vault that will decrease
/// @param tokenOut The address of the underlyer token that will be received by the swapper
/// @param amountOut The amount of the tokenOut that will be received by the swapper
struct RebalanceParams {
address destinationIn;
address tokenIn;
uint256 amountIn;
address destinationOut;
address tokenOut;
uint256 amountOut;
}
/// @param destination The address / lp token of the destination vault
/// @param baseApr Base Apr is the yield generated by staking rewards
/// @param feeApr Yield for pool trading fees
/// @param incentiveApr Incentives for LP
/// @param safeTotalSupply Safe supply for LP tokens
/// @param priceReturn Return from price movement to & away from peg
/// @param maxDiscount Max discount to peg
/// @param maxPremium Max premium to peg
/// @param ownedShares Shares owned for this destination
/// @param compositeReturn Total return combined from the individual yield components
/// @param pricePerShare Price per share
struct SummaryStats {
address destination;
uint256 baseApr;
uint256 feeApr;
uint256 incentiveApr;
uint256 safeTotalSupply;
int256 priceReturn;
int256 maxDiscount;
int256 maxPremium;
uint256 ownedShares;
int256 compositeReturn;
uint256 pricePerShare;
}
/// @notice rebalance the Autopool from the tokenOut (decrease) to the tokenIn (increase)
/// This uses a flash loan to receive the tokenOut to reduce the working capital requirements of the swapper
/// @param receiver The contract receiving the tokens, needs to implement the
/// `onFlashLoan(address user, address token, uint256 amount, uint256 fee, bytes calldata)` interface
/// @param params Parameters by which to perform the rebalance
/// @param data A data parameter to be passed on to the `receiver` for any custom use
function flashRebalance(
IERC3156FlashBorrower receiver,
RebalanceParams calldata params,
bytes calldata data
) external;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (utils/math/Math.sol)
pragma solidity ^0.8.0;
/**
* @dev Standard math utilities missing in the Solidity language.
*/
library Math {
enum Rounding {
Down, // Toward negative infinity
Up, // Toward infinity
Zero // Toward zero
}
/**
* @dev Returns the largest of two numbers.
*/
function max(uint256 a, uint256 b) internal pure returns (uint256) {
return a > b ? a : b;
}
/**
* @dev Returns the smallest of two numbers.
*/
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
/**
* @dev Returns the average of two numbers. The result is rounded towards
* zero.
*/
function average(uint256 a, uint256 b) internal pure returns (uint256) {
// (a + b) / 2 can overflow.
return (a & b) + (a ^ b) / 2;
}
/**
* @dev Returns the ceiling of the division of two numbers.
*
* This differs from standard division with `/` in that it rounds up instead
* of rounding down.
*/
function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
// (a + b - 1) / b can overflow on addition, so we distribute.
return a == 0 ? 0 : (a - 1) / b + 1;
}
/**
* @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
* @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv)
* with further edits by Uniswap Labs also under MIT license.
*/
function mulDiv(
uint256 x,
uint256 y,
uint256 denominator
) internal pure returns (uint256 result) {
unchecked {
// 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use
// use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
// variables such that product = prod1 * 2^256 + prod0.
uint256 prod0; // Least significant 256 bits of the product
uint256 prod1; // Most significant 256 bits of the product
assembly {
let mm := mulmod(x, y, not(0))
prod0 := mul(x, y)
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}
// Handle non-overflow cases, 256 by 256 division.
if (prod1 == 0) {
return prod0 / denominator;
}
// Make sure the result is less than 2^256. Also prevents denominator == 0.
require(denominator > prod1);
///////////////////////////////////////////////
// 512 by 256 division.
///////////////////////////////////////////////
// Make division exact by subtracting the remainder from [prod1 prod0].
uint256 remainder;
assembly {
// Compute remainder using mulmod.
remainder := mulmod(x, y, denominator)
// Subtract 256 bit number from 512 bit number.
prod1 := sub(prod1, gt(remainder, prod0))
prod0 := sub(prod0, remainder)
}
// Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1.
// See https://cs.stackexchange.com/q/138556/92363.
// Does not overflow because the denominator cannot be zero at this stage in the function.
uint256 twos = denominator & (~denominator + 1);
assembly {
// Divide denominator by twos.
denominator := div(denominator, twos)
// Divide [prod1 prod0] by twos.
prod0 := div(prod0, twos)
// Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one.
twos := add(div(sub(0, twos), twos), 1)
}
// Shift in bits from prod1 into prod0.
prod0 |= prod1 * twos;
// Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such
// that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for
// four bits. That is, denominator * inv = 1 mod 2^4.
uint256 inverse = (3 * denominator) ^ 2;
// Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works
// in modular arithmetic, doubling the correct bits in each step.
inverse *= 2 - denominator * inverse; // inverse mod 2^8
inverse *= 2 - denominator * inverse; // inverse mod 2^16
inverse *= 2 - denominator * inverse; // inverse mod 2^32
inverse *= 2 - denominator * inverse; // inverse mod 2^64
inverse *= 2 - denominator * inverse; // inverse mod 2^128
inverse *= 2 - denominator * inverse; // inverse mod 2^256
// Because the division is now exact we can divide by multiplying with the modular inverse of denominator.
// This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is
// less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1
// is no longer required.
result = prod0 * inverse;
return result;
}
}
/**
* @notice Calculates x * y / denominator with full precision, following the selected rounding direction.
*/
function mulDiv(
uint256 x,
uint256 y,
uint256 denominator,
Rounding rounding
) internal pure returns (uint256) {
uint256 result = mulDiv(x, y, denominator);
if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) {
result += 1;
}
return result;
}
/**
* @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded down.
*
* Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11).
*/
function sqrt(uint256 a) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
// For our first guess, we get the biggest power of 2 which is smaller than the square root of the target.
//
// We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have
// `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`.
//
// This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)`
// → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))`
// → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)`
//
// Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit.
uint256 result = 1 << (log2(a) >> 1);
// At this point `result` is an estimation with one bit of precision. We know the true value is a uint128,
// since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at
// every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision
// into the expected uint128 result.
unchecked {
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
return min(result, a / result);
}
}
/**
* @notice Calculates sqrt(a), following the selected rounding direction.
*/
function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = sqrt(a);
return result + (rounding == Rounding.Up && result * result < a ? 1 : 0);
}
}
/**
* @dev Return the log in base 2, rounded down, of a positive value.
* Returns 0 if given 0.
*/
function log2(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >> 128 > 0) {
value >>= 128;
result += 128;
}
if (value >> 64 > 0) {
value >>= 64;
result += 64;
}
if (value >> 32 > 0) {
value >>= 32;
result += 32;
}
if (value >> 16 > 0) {
value >>= 16;
result += 16;
}
if (value >> 8 > 0) {
value >>= 8;
result += 8;
}
if (value >> 4 > 0) {
value >>= 4;
result += 4;
}
if (value >> 2 > 0) {
value >>= 2;
result += 2;
}
if (value >> 1 > 0) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 2, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log2(value);
return result + (rounding == Rounding.Up && 1 << result < value ? 1 : 0);
}
}
/**
* @dev Return the log in base 10, rounded down, of a positive value.
* Returns 0 if given 0.
*/
function log10(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >= 10**64) {
value /= 10**64;
result += 64;
}
if (value >= 10**32) {
value /= 10**32;
result += 32;
}
if (value >= 10**16) {
value /= 10**16;
result += 16;
}
if (value >= 10**8) {
value /= 10**8;
result += 8;
}
if (value >= 10**4) {
value /= 10**4;
result += 4;
}
if (value >= 10**2) {
value /= 10**2;
result += 2;
}
if (value >= 10**1) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 10, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log10(value);
return result + (rounding == Rounding.Up && 10**result < value ? 1 : 0);
}
}
/**
* @dev Return the log in base 256, rounded down, of a positive value.
* Returns 0 if given 0.
*
* Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.
*/
function log256(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >> 128 > 0) {
value >>= 128;
result += 16;
}
if (value >> 64 > 0) {
value >>= 64;
result += 8;
}
if (value >> 32 > 0) {
value >>= 32;
result += 4;
}
if (value >> 16 > 0) {
value >>= 16;
result += 2;
}
if (value >> 8 > 0) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 10, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log256(value);
return result + (rounding == Rounding.Up && 1 << (result * 8) < value ? 1 : 0);
}
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24; // their version was using 8.12?
import { StructuredLinkedList } from "src/strategy/StructuredLinkedList.sol";
// https://github.com/Layr-Labs/eigenlayer-contracts/blob/master/src/contracts/libraries/StructuredLinkedList.sol
library WithdrawalQueue {
using StructuredLinkedList for StructuredLinkedList.List;
error CannotInsertZeroAddress();
error UnexpectedNodeRemoved();
error AddToHeadFailed();
error AddToTailFailed();
error NodeDoesNotExist();
/// @notice Returns true if the address is in the queue.
function addressExists(StructuredLinkedList.List storage queue, address addr) public view returns (bool) {
return StructuredLinkedList.nodeExists(queue, _addressToUint(addr));
}
/// @notice Returns the current head.
function peekHead(
StructuredLinkedList.List storage queue
) public view returns (address) {
return _uintToAddress(StructuredLinkedList.getHead(queue));
}
/// @notice Returns the current tail.
function peekTail(
StructuredLinkedList.List storage queue
) public view returns (address) {
return _uintToAddress(StructuredLinkedList.getTail(queue));
}
/// @notice Returns the number of items in the queue
function sizeOf(
StructuredLinkedList.List storage queue
) public view returns (uint256) {
return StructuredLinkedList.sizeOf(queue);
}
/// @notice Return all items in the queue
/// @dev Enumerates from head to tail
function getList(
StructuredLinkedList.List storage self
) public view returns (address[] memory list) {
uint256 size = self.sizeOf();
list = new address[](size);
if (size > 0) {
uint256 lastNode = self.getHead();
list[0] = _uintToAddress(lastNode);
for (uint256 i = 1; i < size; ++i) {
(bool exists, uint256 node) = self.getAdjacent(lastNode, true);
if (!exists) {
revert NodeDoesNotExist();
}
list[i] = _uintToAddress(node);
lastNode = node;
}
}
}
/// @notice Returns the current tail.
function popHead(
StructuredLinkedList.List storage queue
) public returns (address) {
return _uintToAddress(StructuredLinkedList.popFront(queue));
}
/// @notice remove address toRemove from queue if it exists.
function popAddress(StructuredLinkedList.List storage queue, address toRemove) public {
uint256 addrAsUint = _addressToUint(toRemove);
uint256 _removedNode = StructuredLinkedList.remove(queue, addrAsUint);
if (!((_removedNode == addrAsUint) || (_removedNode == 0))) {
revert UnexpectedNodeRemoved();
}
}
/// @notice returns true if there are no addresses in queue.
function isEmpty(
StructuredLinkedList.List storage queue
) public view returns (bool) {
return !StructuredLinkedList.listExists(queue);
}
/// @notice if addr in queue, move it to the top
// if addr not in queue, add it to the top of the queue.
// if queue is empty, make a new queue with addr as the only node
function addToHead(StructuredLinkedList.List storage queue, address addr) public {
if (addr == address(0)) {
revert CannotInsertZeroAddress();
}
popAddress(queue, addr);
bool success = StructuredLinkedList.pushFront(queue, _addressToUint(addr));
if (!success) {
revert AddToHeadFailed();
}
}
function getAdjacent(
StructuredLinkedList.List storage queue,
address addr,
bool direction
) public view returns (address) {
(bool exists, uint256 addrNum) = queue.getAdjacent(_addressToUint(addr), direction);
if (!exists) {
return address(0);
}
return _uintToAddress(addrNum);
}
/// @notice if addr in queue, move it to the end
// if addr not in queue, add it to the end of the queue.
// if queue is empty, make a new queue with addr as the only node
function addToTail(StructuredLinkedList.List storage queue, address addr) public {
if (addr == address(0)) {
revert CannotInsertZeroAddress();
}
popAddress(queue, addr);
bool success = StructuredLinkedList.pushBack(queue, _addressToUint(addr));
if (!success) {
revert AddToTailFailed();
}
}
function _addressToUint(
address addr
) private pure returns (uint256) {
return uint256(uint160(addr));
}
function _uintToAddress(
uint256 x
) private pure returns (address) {
return address(uint160(x));
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { Errors } from "src/utils/Errors.sol";
import { WithdrawalQueue } from "src/strategy/WithdrawalQueue.sol";
import { StructuredLinkedList } from "src/strategy/StructuredLinkedList.sol";
import { IDestinationVault } from "src/interfaces/vault/IDestinationVault.sol";
import { EnumerableSet } from "openzeppelin-contracts/utils/structs/EnumerableSet.sol";
import { ISystemRegistry } from "src/interfaces/ISystemRegistry.sol";
import { IDestinationVaultRegistry } from "src/interfaces/vault/IDestinationVaultRegistry.sol";
import { AutopoolState } from "src/vault/libs/AutopoolState.sol";
import { IAutopool } from "src/interfaces/vault/IAutopool.sol";
library AutopoolDestinations {
using EnumerableSet for EnumerableSet.AddressSet;
using WithdrawalQueue for StructuredLinkedList.List;
event DestinationVaultAdded(address destination);
event DestinationVaultRemoved(address destination);
event WithdrawalQueueSet(address[] destinations);
event AddedToRemovalQueue(address destination);
event RemovedFromRemovalQueue(address destination);
error TooManyDeployedDestinations();
error BaseAssetMismatch(address destinationVault);
/// @notice Maximum amount of destinations we can be deployed to a given time
uint256 public constant MAX_DEPLOYED_DESTINATIONS = 50;
/// @notice Remove, or queue to remove if necessary, destinations from the vault
/// @dev No need to handle withdrawal queue as it will be popped once it hits balance 0 in withdraw or rebalance.
/// Debt report queue is handled the same way
/// @param $ Storage data of the calling Autopool
/// @param _destinations Destinations to remove
function removeDestinations(AutopoolState storage $, address[] calldata _destinations) external {
for (uint256 i = 0; i < _destinations.length; ++i) {
address dAddress = _destinations[i];
IDestinationVault destination = IDestinationVault(dAddress);
// remove from main list (NOTE: done here so balance check below doesn't explode if address is invalid)
if (!$.destinations.remove(dAddress)) {
revert Errors.ItemNotFound();
}
if (destination.balanceOf(address(this)) > 0 && !$.removalQueue.contains(dAddress)) {
// we still have funds in it! move it to removalQueue for rebalancer to handle it later
// slither-disable-next-line unused-return
$.removalQueue.add(dAddress);
emit AddedToRemovalQueue(dAddress);
}
emit DestinationVaultRemoved(dAddress);
}
}
/// @notice Add a destination to the vault
/// @dev No need to add to debtReport and withdrawal queue from the vault as the rebalance will take care of that
/// @param $ Storage data of the calling Autopool
/// @param destinations New destinations to add
/// @param systemRegistry System registry reference for the vault
function addDestinations(
AutopoolState storage $,
address[] calldata destinations,
ISystemRegistry systemRegistry
) external {
IDestinationVaultRegistry destinationRegistry = systemRegistry.destinationVaultRegistry();
uint256 numDestinations = destinations.length;
if (numDestinations == 0) {
revert Errors.InvalidParams();
}
address autopoolAsset = IAutopool(address(this)).asset();
address dAddress;
for (uint256 i = 0; i < numDestinations; ++i) {
dAddress = destinations[i];
// Address must be setup in our registry
if (dAddress == address(0) || !destinationRegistry.isRegistered(dAddress)) {
revert Errors.InvalidAddress(dAddress);
}
// Don't allow duplicates
if (!$.destinations.add(dAddress)) {
revert Errors.ItemExists();
}
address dvBaseAsset = IDestinationVault(dAddress).baseAsset();
if (dvBaseAsset != autopoolAsset) {
revert BaseAssetMismatch(dAddress);
}
// A destination could be queued for removal but we decided
// to keep it
// slither-disable-next-line unused-return
$.removalQueue.remove(dAddress);
emit DestinationVaultAdded(dAddress);
}
}
/// @notice Ensure a destination is in the queues it should be after a rebalance or debt report
/// @param destination The destination to manage
/// @param destinationIn Whether the destination was moved into, true, or out of, false.
function manageQueuesForDestination(AutopoolState storage $, address destination, bool destinationIn) external {
// The vault itself, when we are moving idle around, should never be
// in the queues.
if (destination != address(this)) {
// If we have a balance, we need to continue to report on it
if (IDestinationVault(destination).balanceOf(address(this)) > 0) {
// For debt reporting, we just updated the values so we can put
// it at the end of the queue.
$.debtReportQueue.addToTail(destination);
// Debt reporting queue is a proxy for destinations we are deployed to
// Easiest to check after doing the add as "addToTail" can move
// the destination when it already exists. Also, we run this fn for the "out"
// destination first so we're sure to free the spots we can
if ($.debtReportQueue.sizeOf() > MAX_DEPLOYED_DESTINATIONS) {
revert TooManyDeployedDestinations();
}
// For withdraws, if we moved into the position then we want to put it
// at the end of the queue so we don't exit from our higher projected
// apr positions first. If we exited, that means its a lower apr
// and we want to continue to exit via user withdrawals so put it at the top
if (destinationIn) {
$.withdrawalQueue.addToTail(destination);
} else {
$.withdrawalQueue.addToHead(destination);
}
} else {
// If we no longer have a balance we don't need to continue to report
// on it and we also have nothing to withdraw from it
// We don't remove the destination from the debt queue here as it
// still might have unclaimed rewards.
$.withdrawalQueue.popAddress(destination);
if ($.removalQueue.remove(destination)) {
emit RemovedFromRemovalQueue(destination);
}
}
}
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IStrategy } from "src/interfaces/strategy/IStrategy.sol";
interface IAutopoolStrategy {
enum RebalanceDirection {
In,
Out
}
/// @notice verify that a rebalance (swap between destinations) meets all the strategy constraints
/// @dev Signature identical to IStrategy.verifyRebalance
function verifyRebalance(
IStrategy.RebalanceParams memory,
IStrategy.SummaryStats memory
) external returns (bool, string memory message);
/// @notice called by the Autopool when NAV is updated
/// @dev can only be called by the strategy's registered Autopool
/// @param navPerShare The navPerShare to record
function navUpdate(
uint256 navPerShare
) external;
/// @notice called by the Autopool when a rebalance is completed
/// @dev can only be called by the strategy's registered Autopool
/// @param rebalanceParams The parameters for the rebalance that was executed
function rebalanceSuccessfullyExecuted(
IStrategy.RebalanceParams memory rebalanceParams
) external;
/// @notice called by the Autopool during rebalance process
/// @param rebalanceParams The parameters for the rebalance that was executed
function getRebalanceOutSummaryStats(
IStrategy.RebalanceParams memory rebalanceParams
) external returns (IStrategy.SummaryStats memory outSummary);
/// @notice Returns stats for a given destination
/// @dev Used to evaluate the current state of the destinations and decide best action
/// @param destAddress Destination address. Can be a DestinationVault or the AutoPool
/// @param direction Direction to evaluate the stats at
/// @param amount Amount to evaluate the stats at
function getDestinationSummaryStats(
address destAddress,
IAutopoolStrategy.RebalanceDirection direction,
uint256 amount
) external returns (IStrategy.SummaryStats memory);
/// @notice Returns all hooks registered on strategy
/// @dev Will return zero addresses for unregistered hooks
/// @return hooks Array of hook addresses
function getHooks() external view returns (address[] memory hooks);
/// @notice the number of days to pause rebalancing due to NAV decay
function pauseRebalancePeriodInDays() external view returns (uint16);
/// @notice the number of seconds gap between consecutive rebalances
function rebalanceTimeGapInSeconds() external view returns (uint256);
/// @notice destinations trading a premium above maxPremium will be blocked from new capital deployments
function maxPremium() external view returns (int256); // 100% = 1e18
/// @notice destinations trading a discount above maxDiscount will be blocked from new capital deployments
function maxDiscount() external view returns (int256); // 100% = 1e18
/// @notice the allowed staleness of stats data before a revert occurs
function staleDataToleranceInSeconds() external view returns (uint40);
/// @notice the swap cost offset period to initialize the strategy with
function swapCostOffsetInitInDays() external view returns (uint16);
/// @notice the number of violations required to trigger a tightening of the swap cost offset period (1 to 10)
function swapCostOffsetTightenThresholdInViolations() external view returns (uint16);
/// @notice the number of days to decrease the swap offset period for each tightening step
function swapCostOffsetTightenStepInDays() external view returns (uint16);
/// @notice the number of days since a rebalance required to trigger a relaxing of the swap cost offset period
function swapCostOffsetRelaxThresholdInDays() external view returns (uint16);
/// @notice the number of days to increase the swap offset period for each relaxing step
function swapCostOffsetRelaxStepInDays() external view returns (uint16);
// slither-disable-start similar-names
/// @notice the maximum the swap cost offset period can reach. This is the loosest the strategy will be
function swapCostOffsetMaxInDays() external view returns (uint16);
/// @notice the minimum the swap cost offset period can reach. This is the most conservative the strategy will be
function swapCostOffsetMinInDays() external view returns (uint16);
/// @notice the number of days for the first NAV decay comparison (e.g., 30 days)
function navLookback1InDays() external view returns (uint8);
/// @notice the number of days for the second NAV decay comparison (e.g., 60 days)
function navLookback2InDays() external view returns (uint8);
/// @notice the number of days for the third NAV decay comparison (e.g., 90 days)
function navLookback3InDays() external view returns (uint8);
// slither-disable-end similar-names
/// @notice the maximum slippage that is allowed for a normal rebalance
function maxNormalOperationSlippage() external view returns (uint256); // 100% = 1e18
/// @notice the maximum amount of slippage to allow when a destination is trimmed due to constraint violations
/// recommend setting this higher than maxNormalOperationSlippage
function maxTrimOperationSlippage() external view returns (uint256); // 100% = 1e18
/// @notice the maximum amount of slippage to allow when a destinationVault has been shutdown
/// shutdown for a vault is abnormal and means there is an issue at that destination
/// recommend setting this higher than maxNormalOperationSlippage
function maxEmergencyOperationSlippage() external view returns (uint256); // 100% = 1e18
/// @notice the maximum amount of slippage to allow when the Autopool has been shutdown
function maxShutdownOperationSlippage() external view returns (uint256); // 100% = 1e18
/// @notice the maximum discount used for price return
function maxAllowedDiscount() external view returns (int256); // 18 precision
/// @notice model weight used for LSTs base yield, 1e6 is the highest
function weightBase() external view returns (uint256);
/// @notice model weight used for DEX fee yield, 1e6 is the highest
function weightFee() external view returns (uint256);
/// @notice model weight used for incentive yield
function weightIncentive() external view returns (uint256);
/// @notice model weight applied to an LST discount when exiting the position
function weightPriceDiscountExit() external view returns (int256);
/// @notice model weight applied to an LST discount when entering the position
function weightPriceDiscountEnter() external view returns (int256);
/// @notice model weight applied to an LST premium when entering or exiting the position
function weightPricePremium() external view returns (int256);
/// @notice initial value of the swap cost offset to use
function swapCostOffsetInit() external view returns (uint16);
/// @notice initial lst price gap tolerance
function defaultLstPriceGapTolerance() external view returns (uint256);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IBaseRewarder } from "src/interfaces/rewarders/IBaseRewarder.sol";
import { IExtraRewarder } from "src/interfaces/rewarders/IExtraRewarder.sol";
interface IMainRewarder is IBaseRewarder {
error ExtraRewardsNotAllowed();
error MaxExtraRewardsReached();
/// @notice Extra rewards can be added, but not removed, ref: https://github.com/Tokemak/v2-core/issues/659
event ExtraRewardAdded(address reward);
/**
* @notice Adds an ExtraRewarder contract address to the extraRewards array.
* @param reward The address of the ExtraRewarder contract.
*/
function addExtraReward(
address reward
) external;
/**
* @notice Withdraws the specified amount of tokens from the vault for the specified account, and transfers all
* rewards for the account from this contract and any linked extra reward contracts.
* @param account The address of the account to withdraw tokens and claim rewards for.
* @param amount The amount of tokens to withdraw.
* @param claim If true, claims all rewards for the account from this contract and any linked extra reward
* contracts.
*/
function withdraw(address account, uint256 amount, bool claim) external;
/**
* @notice Claims and transfers all rewards for the specified account from this contract and any linked extra reward
* contracts.
* @dev If claimExtras is true, also claims all rewards from linked extra reward contracts.
* @param account The address of the account to claim rewards for.
* @param recipient The address to send the rewards to.
* @param claimExtras If true, claims rewards from linked extra reward contracts.
*/
function getReward(address account, address recipient, bool claimExtras) external;
/**
* @notice Number of extra rewards currently registered
*/
function extraRewardsLength() external view returns (uint256);
/**
* @notice Get the extra rewards array values
*/
function extraRewards() external view returns (address[] memory);
/**
* @notice Get the rewarder at the specified index
*/
function getExtraRewarder(
uint256 index
) external view returns (IExtraRewarder);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title StructuredLinkedList
* @author Vittorio Minacori (https://github.com/vittominacori)
* @dev An utility library for using sorted linked list data structures in your Solidity project.
* @notice Adapted from
* https://github.com/Layr-Labs/eigenlayer-contracts/blob/master/src/contracts/libraries/StructuredLinkedList.sol
*/
library StructuredLinkedList {
uint256 private constant _NULL = 0;
uint256 private constant _HEAD = 0;
bool private constant _PREV = false;
bool private constant _NEXT = true;
struct List {
uint256 size;
mapping(uint256 => mapping(bool => uint256)) list;
}
/**
* @dev Checks if the list exists
* @param self stored linked list from contract
* @return bool true if list exists, false otherwise
*/
function listExists(
List storage self
) public view returns (bool) {
// if the head nodes previous or next pointers both point to itself, then there are no items in the list
if (self.list[_HEAD][_PREV] != _HEAD || self.list[_HEAD][_NEXT] != _HEAD) {
return true;
} else {
return false;
}
}
/**
* @dev Checks if the node exists
* @param self stored linked list from contract
* @param _node a node to search for
* @return bool true if node exists, false otherwise
*/
function nodeExists(List storage self, uint256 _node) public view returns (bool) {
if (self.list[_node][_PREV] == _HEAD && self.list[_node][_NEXT] == _HEAD) {
if (self.list[_HEAD][_NEXT] == _node) {
return true;
} else {
return false;
}
} else {
return true;
}
}
/**
* @dev Returns the number of elements in the list
* @param self stored linked list from contract
* @return uint256
*/
// slither-disable-next-line dead-code
function sizeOf(
List storage self
) public view returns (uint256) {
return self.size;
}
/**
* @dev Gets the head of the list
* @param self stored linked list from contract
* @return uint256 the head of the list
*/
function getHead(
List storage self
) public view returns (uint256) {
return self.list[_HEAD][_NEXT];
}
/**
* @dev Gets the head of the list
* @param self stored linked list from contract
* @return uint256 the head of the list
*/
function getTail(
List storage self
) public view returns (uint256) {
return self.list[_HEAD][_PREV];
}
/**
* @dev Returns the links of a node as a tuple
* @param self stored linked list from contract
* @param _node id of the node to get
* @return bool, uint256, uint256 true if node exists or false otherwise, previous node, next node
*/
// slither-disable-next-line dead-code
function getNode(List storage self, uint256 _node) public view returns (bool, uint256, uint256) {
if (!nodeExists(self, _node)) {
return (false, 0, 0);
} else {
return (true, self.list[_node][_PREV], self.list[_node][_NEXT]);
}
}
/**
* @dev Returns the link of a node `_node` in direction `_direction`.
* @param self stored linked list from contract
* @param _node id of the node to step from
* @param _direction direction to step in
* @return bool, uint256 true if node exists or false otherwise, node in _direction
*/
// slither-disable-next-line dead-code
function getAdjacent(List storage self, uint256 _node, bool _direction) public view returns (bool, uint256) {
if (!nodeExists(self, _node)) {
return (false, 0);
} else {
uint256 adjacent = self.list[_node][_direction];
return (adjacent != _HEAD, adjacent);
}
}
/**
* @dev Returns the link of a node `_node` in direction `_NEXT`.
* @param self stored linked list from contract
* @param _node id of the node to step from
* @return bool, uint256 true if node exists or false otherwise, next node
*/
// slither-disable-next-line dead-code
function getNextNode(List storage self, uint256 _node) public view returns (bool, uint256) {
return getAdjacent(self, _node, _NEXT);
}
/**
* @dev Returns the link of a node `_node` in direction `_PREV`.
* @param self stored linked list from contract
* @param _node id of the node to step from
* @return bool, uint256 true if node exists or false otherwise, previous node
*/
// slither-disable-next-line dead-code
function getPreviousNode(List storage self, uint256 _node) public view returns (bool, uint256) {
return getAdjacent(self, _node, _PREV);
}
/**
* @dev Insert node `_new` beside existing node `_node` in direction `_NEXT`.
* @param self stored linked list from contract
* @param _node existing node
* @param _new new node to insert
* @return bool true if success, false otherwise
*/
// slither-disable-next-line dead-code
function insertAfter(List storage self, uint256 _node, uint256 _new) public returns (bool) {
return _insert(self, _node, _new, _NEXT);
}
/**
* @dev Insert node `_new` beside existing node `_node` in direction `_PREV`.
* @param self stored linked list from contract
* @param _node existing node
* @param _new new node to insert
* @return bool true if success, false otherwise
*/
// slither-disable-next-line dead-code
function insertBefore(List storage self, uint256 _node, uint256 _new) public returns (bool) {
return _insert(self, _node, _new, _PREV);
}
/**
* @dev Removes an entry from the linked list
* @param self stored linked list from contract
* @param _node node to remove from the list
* @return uint256 the removed node
*/
function remove(List storage self, uint256 _node) public returns (uint256) {
if ((_node == _NULL) || (!nodeExists(self, _node))) {
return 0;
}
_createLink(self, self.list[_node][_PREV], self.list[_node][_NEXT], _NEXT);
delete self.list[_node][_PREV];
delete self.list[_node][_NEXT];
self.size -= 1;
return _node;
}
/**
* @dev Pushes an entry to the head of the linked list
* @param self stored linked list from contract
* @param _node new entry to push to the head
* @return bool true if success, false otherwise
*/
function pushFront(List storage self, uint256 _node) public returns (bool) {
return _push(self, _node, _NEXT);
}
/**
* @dev Pushes an entry to the tail of the linked list
* @param self stored linked list from contract
* @param _node new entry to push to the tail
* @return bool true if success, false otherwise
*/
function pushBack(List storage self, uint256 _node) public returns (bool) {
return _push(self, _node, _PREV);
}
/**
* @dev Pops the first entry from the head of the linked list
* @param self stored linked list from contract
* @return uint256 the removed node
*/
// slither-disable-next-line dead-code
function popFront(
List storage self
) public returns (uint256) {
return _pop(self, _NEXT);
}
/**
* @dev Pops the first entry from the tail of the linked list
* @param self stored linked list from contract
* @return uint256 the removed node
*/
// slither-disable-next-line dead-code
function popBack(
List storage self
) public returns (uint256) {
return _pop(self, _PREV);
}
/**
* @dev Pushes an entry to the head of the linked list
* @param self stored linked list from contract
* @param _node new entry to push to the head
* @param _direction push to the head (_NEXT) or tail (_PREV)
* @return bool true if success, false otherwise
*/
function _push(List storage self, uint256 _node, bool _direction) private returns (bool) {
return _insert(self, _HEAD, _node, _direction);
}
/**
* @dev Pops the first entry from the linked list
* @param self stored linked list from contract
* @param _direction pop from the head (_NEXT) or the tail (_PREV)
* @return uint256 the removed node
*/
// slither-disable-next-line dead-code
function _pop(List storage self, bool _direction) private returns (uint256) {
uint256 adj;
(, adj) = getAdjacent(self, _HEAD, _direction);
return remove(self, adj);
}
/**
* @dev Insert node `_new` beside existing node `_node` in direction `_direction`.
* @param self stored linked list from contract
* @param _node existing node
* @param _new new node to insert
* @param _direction direction to insert node in
* @return bool true if success, false otherwise
*/
function _insert(List storage self, uint256 _node, uint256 _new, bool _direction) private returns (bool) {
if (!nodeExists(self, _new) && nodeExists(self, _node)) {
uint256 c = self.list[_node][_direction];
_createLink(self, _node, _new, _direction);
_createLink(self, _new, c, _direction);
self.size += 1;
return true;
}
return false;
}
/**
* @dev Creates a bidirectional link between two nodes on direction `_direction`
* @param self stored linked list from contract
* @param _node existing node
* @param _link node to link to in the _direction
* @param _direction direction to insert node in
*/
function _createLink(List storage self, uint256 _node, uint256 _link, bool _direction) private {
self.list[_link][!_direction] = _node;
self.list[_node][_direction] = _link;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.1) (proxy/utils/Initializable.sol)
pragma solidity ^0.8.2;
import "../../utils/Address.sol";
/**
* @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
* behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an
* external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
* function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
*
* The initialization functions use a version number. Once a version number is used, it is consumed and cannot be
* reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in
* case an upgrade adds a module that needs to be initialized.
*
* For example:
*
* [.hljs-theme-light.nopadding]
* ```
* contract MyToken is ERC20Upgradeable {
* function initialize() initializer public {
* __ERC20_init("MyToken", "MTK");
* }
* }
* contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {
* function initializeV2() reinitializer(2) public {
* __ERC20Permit_init("MyToken");
* }
* }
* ```
*
* TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
* possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.
*
* CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
* that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
*
* [CAUTION]
* ====
* Avoid leaving a contract uninitialized.
*
* An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation
* contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke
* the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:
*
* [.hljs-theme-light.nopadding]
* ```
* /// @custom:oz-upgrades-unsafe-allow constructor
* constructor() {
* _disableInitializers();
* }
* ```
* ====
*/
abstract contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
* @custom:oz-retyped-from bool
*/
uint8 private _initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private _initializing;
/**
* @dev Triggered when the contract has been initialized or reinitialized.
*/
event Initialized(uint8 version);
/**
* @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
* `onlyInitializing` functions can be used to initialize parent contracts.
*
* Similar to `reinitializer(1)`, except that functions marked with `initializer` can be nested in the context of a
* constructor.
*
* Emits an {Initialized} event.
*/
modifier initializer() {
bool isTopLevelCall = !_initializing;
require(
(isTopLevelCall && _initialized < 1) || (!Address.isContract(address(this)) && _initialized == 1),
"Initializable: contract is already initialized"
);
_initialized = 1;
if (isTopLevelCall) {
_initializing = true;
}
_;
if (isTopLevelCall) {
_initializing = false;
emit Initialized(1);
}
}
/**
* @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the
* contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be
* used to initialize parent contracts.
*
* A reinitializer may be used after the original initialization step. This is essential to configure modules that
* are added through upgrades and that require initialization.
*
* When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer`
* cannot be nested. If one is invoked in the context of another, execution will revert.
*
* Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in
* a contract, executing them in the right order is up to the developer or operator.
*
* WARNING: setting the version to 255 will prevent any future reinitialization.
*
* Emits an {Initialized} event.
*/
modifier reinitializer(uint8 version) {
require(!_initializing && _initialized < version, "Initializable: contract is already initialized");
_initialized = version;
_initializing = true;
_;
_initializing = false;
emit Initialized(version);
}
/**
* @dev Modifier to protect an initialization function so that it can only be invoked by functions with the
* {initializer} and {reinitializer} modifiers, directly or indirectly.
*/
modifier onlyInitializing() {
require(_initializing, "Initializable: contract is not initializing");
_;
}
/**
* @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
* Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
* to any version. It is recommended to use this to lock implementation contracts that are designed to be called
* through proxies.
*
* Emits an {Initialized} event the first time it is successfully executed.
*/
function _disableInitializers() internal virtual {
require(!_initializing, "Initializable: contract is initializing");
if (_initialized < type(uint8).max) {
_initialized = type(uint8).max;
emit Initialized(type(uint8).max);
}
}
/**
* @dev Returns the highest version that has been initialized. See {reinitializer}.
*/
function _getInitializedVersion() internal view returns (uint8) {
return _initialized;
}
/**
* @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
*/
function _isInitializing() internal view returns (bool) {
return _initializing;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (utils/structs/EnumerableSet.sol)
// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.
pragma solidity ^0.8.0;
/**
* @dev Library for managing
* https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
* types.
*
* Sets have the following properties:
*
* - Elements are added, removed, and checked for existence in constant time
* (O(1)).
* - Elements are enumerated in O(n). No guarantees are made on the ordering.
*
* ```
* contract Example {
* // Add the library methods
* using EnumerableSet for EnumerableSet.AddressSet;
*
* // Declare a set state variable
* EnumerableSet.AddressSet private mySet;
* }
* ```
*
* As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)
* and `uint256` (`UintSet`) are supported.
*
* [WARNING]
* ====
* Trying to delete such a structure from storage will likely result in data corruption, rendering the structure
* unusable.
* See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.
*
* In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an
* array of EnumerableSet.
* ====
*/
library EnumerableSet {
// To implement this library for multiple types with as little code
// repetition as possible, we write it in terms of a generic Set type with
// bytes32 values.
// The Set implementation uses private functions, and user-facing
// implementations (such as AddressSet) are just wrappers around the
// underlying Set.
// This means that we can only create new EnumerableSets for types that fit
// in bytes32.
struct Set {
// Storage of set values
bytes32[] _values;
// Position of the value in the `values` array, plus 1 because index 0
// means a value is not in the set.
mapping(bytes32 => uint256) _indexes;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function _add(Set storage set, bytes32 value) private returns (bool) {
if (!_contains(set, value)) {
set._values.push(value);
// The value is stored at length-1, but we add 1 to all indexes
// and use 0 as a sentinel value
set._indexes[value] = set._values.length;
return true;
} else {
return false;
}
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function _remove(Set storage set, bytes32 value) private returns (bool) {
// We read and store the value's index to prevent multiple reads from the same storage slot
uint256 valueIndex = set._indexes[value];
if (valueIndex != 0) {
// Equivalent to contains(set, value)
// To delete an element from the _values array in O(1), we swap the element to delete with the last one in
// the array, and then remove the last element (sometimes called as 'swap and pop').
// This modifies the order of the array, as noted in {at}.
uint256 toDeleteIndex = valueIndex - 1;
uint256 lastIndex = set._values.length - 1;
if (lastIndex != toDeleteIndex) {
bytes32 lastValue = set._values[lastIndex];
// Move the last value to the index where the value to delete is
set._values[toDeleteIndex] = lastValue;
// Update the index for the moved value
set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex
}
// Delete the slot where the moved value was stored
set._values.pop();
// Delete the index for the deleted slot
delete set._indexes[value];
return true;
} else {
return false;
}
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function _contains(Set storage set, bytes32 value) private view returns (bool) {
return set._indexes[value] != 0;
}
/**
* @dev Returns the number of values on the set. O(1).
*/
function _length(Set storage set) private view returns (uint256) {
return set._values.length;
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function _at(Set storage set, uint256 index) private view returns (bytes32) {
return set._values[index];
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function _values(Set storage set) private view returns (bytes32[] memory) {
return set._values;
}
// Bytes32Set
struct Bytes32Set {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
return _add(set._inner, value);
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
return _remove(set._inner, value);
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
return _contains(set._inner, value);
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(Bytes32Set storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
return _at(set._inner, index);
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
bytes32[] memory store = _values(set._inner);
bytes32[] memory result;
/// @solidity memory-safe-assembly
assembly {
result := store
}
return result;
}
// AddressSet
struct AddressSet {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(AddressSet storage set, address value) internal returns (bool) {
return _add(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(AddressSet storage set, address value) internal returns (bool) {
return _remove(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(AddressSet storage set, address value) internal view returns (bool) {
return _contains(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(AddressSet storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(AddressSet storage set, uint256 index) internal view returns (address) {
return address(uint160(uint256(_at(set._inner, index))));
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(AddressSet storage set) internal view returns (address[] memory) {
bytes32[] memory store = _values(set._inner);
address[] memory result;
/// @solidity memory-safe-assembly
assembly {
result := store
}
return result;
}
// UintSet
struct UintSet {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(UintSet storage set, uint256 value) internal returns (bool) {
return _add(set._inner, bytes32(value));
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(UintSet storage set, uint256 value) internal returns (bool) {
return _remove(set._inner, bytes32(value));
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(UintSet storage set, uint256 value) internal view returns (bool) {
return _contains(set._inner, bytes32(value));
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(UintSet storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(UintSet storage set, uint256 index) internal view returns (uint256) {
return uint256(_at(set._inner, index));
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(UintSet storage set) internal view returns (uint256[] memory) {
bytes32[] memory store = _values(set._inner);
uint256[] memory result;
/// @solidity memory-safe-assembly
assembly {
result := store
}
return result;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (interfaces/IERC3156FlashBorrower.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC3156 FlashBorrower, as defined in
* https://eips.ethereum.org/EIPS/eip-3156[ERC-3156].
*
* _Available since v4.1._
*/
interface IERC3156FlashBorrower {
/**
* @dev Receive a flash loan.
* @param initiator The initiator of the loan.
* @param token The loan currency.
* @param amount The amount of tokens lent.
* @param fee The additional amount of tokens to repay.
* @param data Arbitrary data structure, intended to contain user-defined parameters.
* @return The keccak256 hash of "IERC3156FlashBorrower.onFlashLoan"
*/
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
// solhint-disable no-inline-assembly
import { IAutopool } from "src/interfaces/vault/IAutopool.sol";
import { EnumerableSet } from "openzeppelin-contracts/utils/structs/EnumerableSet.sol";
import { AutopoolDebt } from "src/vault/libs/AutopoolDebt.sol";
import { StructuredLinkedList } from "src/strategy/StructuredLinkedList.sol";
import { AutopoolToken } from "src/vault/libs/AutopoolToken.sol";
import { IMainRewarder } from "src/interfaces/rewarders/IMainRewarder.sol";
import { IERC20Metadata } from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { IERC3156FlashBorrower } from "openzeppelin-contracts/interfaces/IERC3156FlashBorrower.sol";
import { IStrategy } from "src/interfaces/strategy/IStrategy.sol";
struct AutopoolState {
/// @notice Balances, allowances, and supply for the pool
/// @dev Want to keep this var in this position
AutopoolToken.TokenData token;
/// @notice Full list of possible destinations that could be deployed to
/// @dev Exposed via `getDestinations()`
EnumerableSet.AddressSet destinations;
/// @notice Destinations that are queued for removal
/// @dev Exposed via `getRemovalQueue`
EnumerableSet.AddressSet removalQueue;
/// @notice Whether or not the vault has been shutdown
/// @dev Exposed via `isShutdown()`
bool shutdown;
/// @notice Reason for shutdown (or `Active` if not shutdown)
/// @dev Exposed via `shutdownStatus()`
IAutopool.VaultShutdownStatus shutdownStatus;
/// @notice Lookup of destinationVaultAddress -> Info .. Debt reporting snapshot info
/// @dev Exposed via `getDestinationInfo`
mapping(address => AutopoolDebt.DestinationInfo) destinationInfo;
/// @notice Ordered list of destinations to withdraw from
/// @dev Exposed via `getWithdrawalQueue()`
StructuredLinkedList.List withdrawalQueue;
/// @notice Ordered list of destinations to debt report on. Ordered from oldest to newest
/// @dev Exposed via `getDebtReportingQueue()`
StructuredLinkedList.List debtReportQueue;
/// @notice State and settings related to gradual profit unlock
/// @dev Exposed via `getProfitUnlockSettings()`
IAutopool.ProfitUnlockSettings profitUnlockSettings;
/// @notice State and settings related to periodic and streaming fees
/// @dev Exposed via `getFeeSettings()`
IAutopool.AutopoolFeeSettings feeSettings;
/// @notice Rewarders that have been replaced.
/// @dev Exposed via `isPastRewarder()`
EnumerableSet.AddressSet pastRewarders;
/// @notice Main rewarder for this contract
IMainRewarder rewarder;
/// @notice Pool/token name
string name;
/// @notice Pool/token symbol
string symbol;
/// @notice Storage address of hook configurations
address hookStore;
/// @notice Factory contract that created this vault
address factory;
/// @notice Asset tracking for idle and debt values
/// @dev Exposed via `getAssetBreakdown()`
IAutopool.AssetBreakdown assetBreakdown;
}
struct ProcessRebalanceParams {
IERC20Metadata baseAsset;
IERC3156FlashBorrower receiver;
IStrategy.RebalanceParams rebalanceParams;
}
library AutopoolStorage {
// keccak256(abi.encode(uint256(keccak256("autopilot.storage.AutopoolState")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant SLOT = 0x17264fbcd79a365fd3ccff89407ad487986f8b37b9035d6bc8b51cacd5832200;
function load() internal pure returns (AutopoolState storage $) {
assembly {
$.slot := SLOT
}
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
// solhint-disable no-inline-assembly,avoid-low-level-calls
import { Errors } from "src/utils/Errors.sol";
import { SSTORE2 } from "src/external/solady/SSTORE2.sol";
import { LibBytes } from "src/external/solady/LibBytes.sol";
import { AutopoolState } from "src/vault/libs/AutopoolState.sol";
import { IStrategyHook } from "src/interfaces/strategy/IStrategyHook.sol";
library AutopoolStrategyHooks {
/// =====================================================
/// Hook configurations are read and stored via SSTORE2.
/// The storage is formatted as fixed length packed byte20(address) arrays
/// that are themselves packed in the order of the function definitions
/// on the interface.
/// For example, lets say we have 3 hooks configured (with 8 functions and 10 hooks supported):
/// - Hook 1, address(1), supports onRebalanceStart (1) and onRebalanceFeeProfitHandlingComplete (16)
/// - Hook 2, address(2), supports onRebalanceStart (1) and onRebalanceInAssetsReturned (4)
/// - Hook 3, address(3), supports onRebalanceComplete (32)
/// Storage would look like:
/// 0000000000000000000000000000000000000001 - Start of hooks to call onRebalanceStart() (flag 1) for
/// 0000000000000000000000000000000000000002
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000 - Hooks to call onRebalanceOutAssetsReady() (flag 2) for
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000002 - Hooks to call onRebalanceInAssetsReturned() (flag 4) for
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000 - Hooks to call onRebalanceDestinationVaultUpdated() (flag 8) for
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000001 - Hooks to call onRebalanceFeeProfitHandlingComplete() (flag 16) for
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000003 - Start of hooks to call onRebalanceComplete() (flag 32) for
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000 - Start of hooks to call onDestinationDebtReport() (flag 64) for
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000 - Start of hooks to call onNavUpdate() (flag 128) for
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// 0000000000000000000000000000000000000000
/// Adding hooks should append to the lowest empty of the section
/// Removing hooks should shift remainder down to maintain order
/// =====================================================
/// Constants
/// =====================================================
/// @notice Returns maximum number of hooks supported
uint256 public constant NUM_HOOKS = 10;
/// @notice Returns the number of hook functions supported
uint256 public constant NUM_FUNCTIONS = 8;
/// @notice Max function flag given the number of functions
uint256 private constant MAX_FN_FLAG = 2 ** NUM_FUNCTIONS;
/// =====================================================
/// Errors
/// =====================================================
/// @notice Fires when are at the maximum number of configured hooks for a function
error MaxHooksSet();
/// @notice Fires when a hook is already registered for a function
error HookAlreadySet(address hook, uint256 fn);
/// @notice Fires on removal when a hook doesn't exist
error HookNotSet(address hook);
/// @notice Fires on removal when a function is supposed to be registered but isn't
error FunctionNotSet(address hook, uint256 fn);
/// @notice Fires when a hook execution fails
error HookExecutionFailed(address hook, bytes underlyingError);
/// =====================================================
/// Structs
/// =====================================================
/// @notice Used for view/display purposes
struct HookConfiguration {
address[NUM_HOOKS] onRebalanceStart;
address[NUM_HOOKS] onRebalanceOutAssetsReady;
address[NUM_HOOKS] onRebalanceInAssetsReturned;
address[NUM_HOOKS] onRebalanceDestinationVaultUpdated;
address[NUM_HOOKS] onRebalanceFeeProfitHandlingComplete;
address[NUM_HOOKS] onRebalanceComplete;
address[NUM_HOOKS] onDestinationDebtReport;
address[NUM_HOOKS] onNavUpdate;
}
/// =====================================================
/// Functions - External
/// =====================================================
/// @notice Execute all hooks, in order, based on the provided configuration
/// @dev Caller is responsible for ensuring fnIndex and call are correctly paired
/// @param hooks Packed hooks configuration data
/// @param fnIndex Index of the function to execute
/// @param call Call to make to the hook
function executeHooks(bytes memory hooks, uint256 fnIndex, bytes memory call) external {
uint256 hookIx = (fnIndex * NUM_HOOKS * 20) + 32; // 32 for the length of bytes
uint256 hookEndIx = hookIx + (20 * NUM_HOOKS);
while (hookIx < hookEndIx) {
// Pull the bytes out at the index and convert to address
address addrVal;
assembly {
addrVal := shr(96, mload(add(hooks, hookIx)))
}
// If its there an address, we execute, otherwise we're done
if (addrVal != address(0)) {
(bool result, bytes memory errorData) = addrVal.call(call);
if (!result) {
revert HookExecutionFailed(addrVal, errorData);
}
} else {
break;
}
unchecked {
hookIx += 20;
}
}
}
/// @notice Add a set of hooks to the Autopools configuration
/// @param $ Storage data of the calling Autopool
/// @param newHooks Set of hooks to add to the Autopool
/// @param configDatas Set of datas to pass to the onRegistered function of the hook
function addHooks(AutopoolState storage $, IStrategyHook[] memory newHooks, bytes[] memory configDatas) external {
bytes memory flags = getHookBytes($);
uint256 len = newHooks.length;
Errors.verifyNotZero(len, "len");
Errors.verifyArrayLengths(len, configDatas.length, "ars");
for (uint256 i = 0; i < len;) {
flags = _addHook(flags, newHooks[i], configDatas[i]);
unchecked {
++i;
}
}
$.hookStore = SSTORE2.write(flags);
}
/// @notice Remove a hook from the Autopools configuration
/// @param $ Storage data of the calling Autopool
/// @param hookToRemove Hook to remove from to the Autopool
/// @param cleanupData Data to pass to the onUnregistered function of the hook
function removeHook(AutopoolState storage $, IStrategyHook hookToRemove, bytes calldata cleanupData) external {
Errors.verifyNotZero(address(hookToRemove), "hookToRemove");
bytes memory flags;
address hookStorage = $.hookStore;
if (hookStorage != address(0)) {
flags = SSTORE2.read(hookStorage);
} else {
revert HookNotSet(address(hookToRemove));
}
// We are OK with assumption that supported flags can't change between
// register and unregister
uint8 supportedHookFunctions = hookToRemove.getFnFlags();
Errors.verifyNotZero(supportedHookFunctions, "supportedHookFunctions");
uint256 fnToCheck = 1;
bytes memory newFlagsData;
uint256 bytesIndex = 0;
// Loop over all the functions hook slots
// build our replacement storage data, newFlagsData
// Replace the address with empty where needed
bytes memory empty = hex"0000000000000000000000000000000000000000";
while (fnToCheck < MAX_FN_FLAG) {
uint256 hookIx = 0;
bool needToRemove = supportedHookFunctions & fnToCheck == fnToCheck;
bool removed = false;
while (hookIx < NUM_HOOKS) {
// Get our existing value
address addrVal;
{
bytes memory val = LibBytes.slice(flags, bytesIndex, bytesIndex + 20);
assembly {
addrVal := shr(96, mload(add(val, 32)))
}
}
if (addrVal == address(hookToRemove)) {
// Don't put anything in its spot so that the remaining
// values will shift down. We'll tack an empty onto the end
removed = true;
} else {
// Not the value we're looking for, forward it on
newFlagsData = LibBytes.concat(newFlagsData, abi.encodePacked(addrVal));
}
unchecked {
++hookIx;
bytesIndex += 20;
}
}
if (needToRemove && !removed) {
revert FunctionNotSet(address(hookToRemove), fnToCheck);
}
// We don't support duplicates so we know it was only in once
// We will revert if didn't remove, so we know we removed one item
// Add an empty onto the end in its place
if (removed) {
newFlagsData = LibBytes.concat(newFlagsData, empty);
}
unchecked {
fnToCheck *= 2;
}
}
// We removed it so run the cleanup
hookToRemove.onUnregistered(cleanupData);
$.hookStore = SSTORE2.write(newFlagsData);
}
/// @notice Get hooks in a proper format
/// @dev Do not use in any executing code
/// @param $ Storage data of the calling Autopool
function getHooks(
AutopoolState storage $
) external view returns (HookConfiguration memory) {
bytes memory flags = getHookBytes($);
uint256 memSize = NUM_HOOKS * NUM_FUNCTIONS * 32;
assembly {
// Get our working space for building the struct
let structPtr := mload(0x40)
// Update pointer to new position
mstore(0x40, add(structPtr, memSize))
// Get the store of our addresses, first 32 is array size
let dataPtr := add(flags, 32)
let dataEnd := add(dataPtr, mload(flags))
let offset := 0
// Loop our data and set the addresses
for { } lt(dataPtr, dataEnd) { } {
let word := mload(dataPtr)
// Convert to address
word := shr(96, word)
// Add address to struct
mstore(add(structPtr, offset), word)
// Increment our counters
dataPtr := add(dataPtr, 20)
offset := add(offset, 32)
}
return(structPtr, memSize)
}
}
/// =====================================================
/// Functions - Public
/// =====================================================
/// @notice Get hooks in a proper format
/// @dev Do not use in any executing code
/// @param $ Storage data of the calling Autopool
function getHookBytes(
AutopoolState storage $
) public view returns (bytes memory) {
bytes memory flags;
address hookStorage = $.hookStore;
if (hookStorage != address(0)) {
flags = SSTORE2.read(hookStorage);
} else {
// If we haven't set any data initialize the array
// to all empty addresses
flags = new bytes(NUM_HOOKS * NUM_FUNCTIONS * 20);
}
return flags;
}
/// =====================================================
/// Functions - Private
/// =====================================================
/// @notice Add a hook to the Autopools configuration
/// @param flags Existing hook data
/// @param newHook Hook to add to the Autopool
/// @param configData Data to pass to the onRegistered function of the hook
/// @return New flags configuration storage data
function _addHook(
bytes memory flags,
IStrategyHook newHook,
bytes memory configData
) private returns (bytes memory) {
Errors.verifyNotZero(address(newHook), "newHook");
uint8 supportedHookFunctions = newHook.getFnFlags();
Errors.verifyNotZero(supportedHookFunctions, "supportedHookFunctions");
uint256 fnToCheck = 1;
bytes memory newFlagsData;
uint256 bytesIndex = 0;
// Loop over all the functions hook slots and
// build our replacement storage data, newFlagsData
// Splice in our new values where we need
while (fnToCheck < MAX_FN_FLAG) {
uint256 hookIx = 0;
bool needToSet = supportedHookFunctions & fnToCheck == fnToCheck;
bool set = false;
while (hookIx < NUM_HOOKS) {
// Get our existing value
bytes memory val = LibBytes.slice(flags, bytesIndex, bytesIndex + 20);
if (needToSet && !set) {
address addrVal;
assembly {
addrVal := shr(96, mload(add(val, 32)))
}
if (addrVal == address(newHook)) {
revert HookAlreadySet(addrVal, fnToCheck);
} else if (addrVal == address(0)) {
// If the space is empty we can use it
set = true;
newFlagsData = LibBytes.concat(newFlagsData, abi.encodePacked(newHook));
} else {
// Space isn't empty so forward the existing value
newFlagsData = LibBytes.concat(newFlagsData, val);
}
} else {
// We don't need to set, or already set, so just forward the current value
newFlagsData = LibBytes.concat(newFlagsData, val);
}
unchecked {
++hookIx;
bytesIndex += 20;
}
}
if (needToSet && !set) {
// We needed to configure the hook but couldn't. Means we couldn't find
// a spot which implies we have the maximum number already configured
revert MaxHooksSet();
}
unchecked {
fnToCheck *= 2;
}
}
// We were able to add it to the config, so set it up
newHook.onRegistered(configData);
return newFlagsData;
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { AutopoolDebt } from "src/vault/libs/AutopoolDebt.sol";
import { ProcessRebalanceParams } from "src/vault/libs/AutopoolState.sol";
// @dev Do not change the order of these, we rely on the index
enum HookFunctionIndex {
onRebalanceStart,
onRebalanceOutAssetsReady,
onRebalanceInAssetsReturned,
onRebalanceDestinationVaultUpdated,
onRebalanceFeeProfitHandlingComplete,
onRebalanceComplete,
onDestinationDebtReport,
onNavUpdate
}
interface IStrategyHook {
/// @notice Returns the flags that represent the functions to call on this hook
function getFnFlags() external view returns (uint8);
/// @notice Fires when the hook has been registered with an Autopool
/// @param registrationData Any data needed during registration such as initial configuration
function onRegistered(
bytes memory registrationData
) external;
/// @notice Fires when the hook as been unregistered with an Autopool
/// @param cleanupData Any information needed to run cleanup operations
function onUnregistered(
bytes memory cleanupData
) external;
/// =====================================================
/// Rebalance Flow
/// - Functions are defined in the order that they fire
/// - Any function may revert to stop a rebalance
/// - OnNavUpdate interjects at the end
/// =====================================================
/// Flag 1
/// @notice Fires at the start of a rebalance before any assets are moved
/// @dev No LP/Idle changes have occurred yet
/// @param params Rebalance parameters
/// @param solverCaller Solver who initiated the rebalance
function onRebalanceStart(ProcessRebalanceParams calldata params, address solverCaller) external;
/// Flag 2
/// @notice Fires when LP has been removed from a DestinationVault but before solver has control
/// @param params Rebalance parameters
/// @param solverCaller Solver who initiated the rebalance
/// @dev When this is an idle-out, the state of assets between here and Start() is the same
function onRebalanceOutAssetsReady(ProcessRebalanceParams calldata params, address solverCaller) external;
/// Flag 4
/// @notice Fires when LP/Idle has been returned from the Solver
/// @param params Rebalance parameters
/// @param solverCaller Solver who initiated the rebalance
/// @dev Solver has performed all market operations at this point
function onRebalanceInAssetsReturned(ProcessRebalanceParams calldata params, address solverCaller) external;
/// Flag 8
/// @notice Fires after assets have been deposited into DestinationVault
/// @param params Rebalance parameters
/// @param solverCaller Solver who initiated the rebalance
/// @dev When this is an idle-in, the state of assets doesn't change between InAssetsReturned() and here
function onRebalanceDestinationVaultUpdated(
ProcessRebalanceParams calldata params,
address solverCaller
) external;
/// Flag 16
/// @notice Fires after any fee and profit handling has occurred
/// @param params Rebalance parameters
/// @param solverCaller Solver who initiated the rebalance
/// @dev Autopool totalSupply() should be steady at this point
function onRebalanceFeeProfitHandlingComplete(
ProcessRebalanceParams calldata params,
address solverCaller
) external;
/// Flag 32
/// @notice Fires at the end of the rebalance
/// @param params Rebalance parameters
/// @param solverCaller Solver who initiated the rebalance
function onRebalanceComplete(ProcessRebalanceParams calldata params, address solverCaller) external;
/// =====================================================
/// Debt Reporting
/// - Functions should not revert
/// =====================================================
/// Flag 64
/// @notice Fires for any custom tracking of debt valuations
/// @dev Autopool has possession of auto-compounded rewards
/// @param destination Target DestinationVault address
/// @param debtResult Change in values due to debt reporting
function onDestinationDebtReport(address destination, AutopoolDebt.IdleDebtUpdates memory debtResult) external;
/// =====================================================
/// Nav Updates
/// - Functions should not revert
/// =====================================================
/// Flag 128
/// @notice Fires after any nav update
/// @param assetChanges New and old assets and totalSupply
/// @dev Also fires immediately before onRebalanceFeeProfitHandlingComplete()
function onNavUpdate(
AutopoolDebt.AssetChanges memory assetChanges
) external;
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { ReentrancyGuard } from "openzeppelin-contracts/security/ReentrancyGuard.sol";
import { EnumerableSet } from "openzeppelin-contracts/utils/structs/EnumerableSet.sol";
import { ISystemRegistry } from "src/interfaces/ISystemRegistry.sol";
import { IBaseRewarder } from "src/interfaces/rewarders/IBaseRewarder.sol";
import { IMainRewarder } from "src/interfaces/rewarders/IMainRewarder.sol";
import { IExtraRewarder } from "src/interfaces/rewarders/IExtraRewarder.sol";
import { AbstractRewarder } from "src/rewarders/AbstractRewarder.sol";
import { Errors } from "src/utils/Errors.sol";
/**
* @title MainRewarder
* @dev Contract is abstract to enforce proper role designation on construction
* @notice The MainRewarder contract extends the AbstractRewarder and
* manages the distribution of main rewards along with additional rewards
* from ExtraRewarder contracts.
*/
abstract contract MainRewarder is AbstractRewarder, IMainRewarder, ReentrancyGuard {
using EnumerableSet for EnumerableSet.AddressSet;
/// @notice Maximum amount of extra rewards addresses that can be registered
uint256 public constant MAX_EXTRA_REWARDS = 15;
/// @notice True if additional reward tokens/contracts are allowed to be added
/// @dev Destination Vaults should not allow extras. Autopool's should.
bool public immutable allowExtraRewards;
/// @notice Extra rewards can be added, but not removed, ref: https://github.com/Tokemak/v2-core/issues/659
EnumerableSet.AddressSet private _extraRewards;
uint256 private _totalSupply;
mapping(address => uint256) private _balances;
constructor(
ISystemRegistry _systemRegistry,
address _rewardToken,
uint256 _newRewardRatio,
uint256 _durationInBlock,
bytes32 _rewardRole,
bool _allowExtraRewards
) AbstractRewarder(_systemRegistry, _rewardToken, _newRewardRatio, _durationInBlock, _rewardRole) {
// slither-disable-next-line missing-zero-check
allowExtraRewards = _allowExtraRewards;
}
/// @inheritdoc IMainRewarder
function extraRewardsLength() external view returns (uint256) {
return _extraRewards.length();
}
/// @inheritdoc IMainRewarder
function addExtraReward(
address reward
) external hasRole(rewardRole) {
if (!allowExtraRewards) {
revert ExtraRewardsNotAllowed();
}
if (_extraRewards.length() >= MAX_EXTRA_REWARDS) {
revert MaxExtraRewardsReached();
}
Errors.verifyNotZero(reward, "reward");
if (!_extraRewards.add(reward)) {
revert Errors.ItemExists();
}
emit ExtraRewardAdded(reward);
}
/// @inheritdoc IMainRewarder
function getExtraRewarder(
uint256 index
) external view returns (IExtraRewarder rewarder) {
return IExtraRewarder(_extraRewards.at(index));
}
/// @inheritdoc IMainRewarder
function extraRewards() external view returns (address[] memory) {
return _extraRewards.values();
}
function _withdraw(address account, uint256 amount, bool claim) internal {
_updateReward(account);
_withdrawAbstractRewarder(account, amount);
uint256 length = _extraRewards.length();
for (uint256 i = 0; i < length; ++i) {
// No need to worry about reentrancy here
// slither-disable-next-line reentrancy-no-eth
IExtraRewarder(_extraRewards.at(i)).withdraw(account, amount);
}
if (claim) {
_processRewards(account, account, true);
}
// slither-disable-next-line events-maths
_totalSupply -= amount;
_balances[account] -= amount;
}
function _stake(address account, uint256 amount) internal {
_updateReward(account);
_stakeAbstractRewarder(account, amount);
uint256 length = _extraRewards.length();
for (uint256 i = 0; i < length; ++i) {
// No need to worry about reentrancy here
// slither-disable-next-line reentrancy-no-eth
IExtraRewarder(_extraRewards.at(i)).stake(account, amount);
}
// slither-disable-next-line events-maths
_totalSupply += amount;
_balances[account] += amount;
}
/// @inheritdoc IBaseRewarder
function getReward() external nonReentrant {
_updateReward(msg.sender);
_processRewards(msg.sender, msg.sender, true);
}
function _getReward(address account, address recipient, bool claimExtras) internal nonReentrant {
_updateReward(account);
_processRewards(account, recipient, claimExtras);
}
/// @inheritdoc IBaseRewarder
function totalSupply() public view override(AbstractRewarder, IBaseRewarder) returns (uint256) {
return _totalSupply;
}
/// @inheritdoc IBaseRewarder
function balanceOf(
address account
) public view override(AbstractRewarder, IBaseRewarder) returns (uint256) {
return _balances[account];
}
function _processRewards(address account, address recipient, bool claimExtras) internal {
_getReward(account, recipient);
uint256 length = _extraRewards.length();
//also get rewards from linked rewards
if (claimExtras) {
for (uint256 i = 0; i < length; ++i) {
IExtraRewarder(_extraRewards.at(i)).getReward(account, recipient);
}
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/draft-IERC20Permit.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in
* https://eips.ethereum.org/EIPS/eip-2612[EIP-2612].
*
* Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by
* presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't
* need to send a transaction, and thus is not required to hold Ether at all.
*/
interface IERC20Permit {
/**
* @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens,
* given ``owner``'s signed approval.
*
* IMPORTANT: The same issues {IERC20-approve} has related to transaction
* ordering also apply here.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `deadline` must be a timestamp in the future.
* - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`
* over the EIP712-formatted function arguments.
* - the signature must use ``owner``'s current nonce (see {nonces}).
*
* For more information on the signature format, see the
* https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP
* section].
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
/**
* @dev Returns the current nonce for `owner`. This value must be
* included whenever a signature is generated for {permit}.
*
* Every successful call to {permit} increases ``owner``'s nonce by one. This
* prevents a signature from being used multiple times.
*/
function nonces(address owner) external view returns (uint256);
/**
* @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}.
*/
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view returns (bytes32);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (proxy/ERC1967/ERC1967Proxy.sol)
pragma solidity ^0.8.0;
import "../Proxy.sol";
import "./ERC1967Upgrade.sol";
/**
* @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an
* implementation address that can be changed. This address is stored in storage in the location specified by
* https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the
* implementation behind the proxy.
*/
contract ERC1967Proxy is Proxy, ERC1967Upgrade {
/**
* @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`.
*
* If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded
* function call, and allows initializing the storage of the proxy like a Solidity constructor.
*/
constructor(address _logic, bytes memory _data) payable {
_upgradeToAndCall(_logic, _data, false);
}
/**
* @dev Returns the current implementation address.
*/
function _implementation() internal view virtual override returns (address impl) {
return ERC1967Upgrade._getImplementation();
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (access/IAccessControlEnumerable.sol)
pragma solidity ^0.8.0;
import "./IAccessControl.sol";
/**
* @dev External interface of AccessControlEnumerable declared to support ERC165 detection.
*/
interface IAccessControlEnumerable is IAccessControl {
/**
* @dev Returns one of the accounts that have `role`. `index` must be a
* value between 0 and {getRoleMemberCount}, non-inclusive.
*
* Role bearers are not sorted in any particular way, and their ordering may
* change at any point.
*
* WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
* you perform all queries on the same block. See the following
* https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
* for more information.
*/
function getRoleMember(bytes32 role, uint256 index) external view returns (address);
/**
* @dev Returns the number of accounts that have `role`. Can be used
* together with {getRoleMember} to enumerate all bearers of a role.
*/
function getRoleMemberCount(bytes32 role) external view returns (uint256);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { ISwapRouter } from "src/interfaces/swapper/ISwapRouter.sol";
interface ISyncSwapper {
error DataMismatch(string element);
error InvalidIndex();
/**
* @notice Returns address of swap router that can access SyncSwapper contract
*/
function router() external view returns (ISwapRouter);
/**
* @notice Swaps sellToken for buyToken
* @param pool The address of the pool for the swapper
* @param sellTokenAddress The address of the token to sell
* @param sellAmount The amount of sellToken to sell
* @param buyTokenAddress The address of the token to buy
* @param minBuyAmount The minimum amount of buyToken expected
* @param data Additional data used differently by the different swappers
* @return actualBuyAmount The actual amount received from the swap
*/
function swap(
address pool,
address sellTokenAddress,
uint256 sellAmount,
address buyTokenAddress,
uint256 minBuyAmount,
bytes memory data
) external returns (uint256 actualBuyAmount);
/**
* @notice Validates that the swapData contains the correct information, ensuring that the encoded data contains the
* correct 'fromAddress' and 'toAddress' (swapData.token), and verifies that these tokens are in the pool
* @dev This function should revert with a DataMismatch error if the swapData is invalid
* @param fromAddress The address from which the swap originates
* @param swapData The data associated with the swap that needs to be validated
*/
function validate(address fromAddress, ISwapRouter.SwapData memory swapData) external view;
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IAutopool } from "src/interfaces/vault/IAutopool.sol";
import { IMainRewarder } from "src/interfaces/rewarders/IMainRewarder.sol";
import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol";
/**
* @title AutopoolETH Router Base Interface
* @notice A canonical router between AutopoolETHs
*
* The base router is a multicall style router inspired by Uniswap v3 with built-in features for permit,
* WETH9 wrap/unwrap, and ERC20 token pulling/sweeping/approving. It includes methods for the four mutable
* ERC4626 functions deposit/mint/withdraw/redeem as well.
*
* These can all be arbitrarily composed using the multicall functionality of the router.
*
* NOTE the router is capable of pulling any approved token from your wallet. This is only possible when
* your address is msg.sender, but regardless be careful when interacting with the router or ERC4626 Vaults.
* The router makes no special considerations for unique ERC20 implementations such as fee on transfer.
* There are no built in protections for unexpected behavior beyond enforcing the minSharesOut is received.
*/
interface IAutopilotRouterBase {
/// @notice thrown when amount of assets received is below the min set by caller
error MinAmountError();
/// @notice thrown when amount of shares received is below the min set by caller
error MinSharesError();
/// @notice thrown when amount of assets received is above the max set by caller
error MaxAmountError();
/// @notice thrown when amount of shares received is above the max set by caller
error MaxSharesError();
/// @notice thrown when timestamp is too old
error TimestampTooOld();
/**
* @notice mint `shares` from an ERC4626 vault.
* @param vault The AutopoolETH to mint shares from.
* @param to The destination of ownership shares.
* @param shares The amount of shares to mint from `vault`.
* @param maxAmountIn The max amount of assets used to mint.
* @return amountIn the amount of assets used to mint by `to`.
* @dev throws MaxAmountError
*/
function mint(
IAutopool vault,
address to,
uint256 shares,
uint256 maxAmountIn
) external payable returns (uint256 amountIn);
/**
* @notice deposit `amount` to an ERC4626 vault.
* @param vault The AutopoolETH to deposit assets to.
* @param to The destination of ownership shares.
* @param amount The amount of assets to deposit to `vault`.
* @param minSharesOut The min amount of `vault` shares received by `to`.
* @return sharesOut the amount of shares received by `to`.
* @dev throws MinSharesError
*/
function deposit(
IAutopool vault,
address to,
uint256 amount,
uint256 minSharesOut
) external payable returns (uint256 sharesOut);
/**
* @notice withdraw `amount` from an ERC4626 vault.
* @param vault The AutopoolETH to withdraw assets from.
* @param to The destination of assets.
* @param amount The amount of assets to withdraw from vault.
* @param maxSharesOut The max amount of shares burned for assets requested.
* @return sharesOut the amount of shares received by `to`.
* @dev throws MaxSharesError
*/
function withdraw(
IAutopool vault,
address to,
uint256 amount,
uint256 maxSharesOut
) external payable returns (uint256 sharesOut);
/**
* @notice redeem `shares` shares from a AutopoolETH
* @param vault The AutopoolETH to redeem shares from.
* @param to The destination of assets.
* @param shares The amount of shares to redeem from vault.
* @param minAmountOut The min amount of assets received by `to`.
* @return amountOut the amount of assets received by `to`.
* @dev throws MinAmountError
*/
function redeem(
IAutopool vault,
address to,
uint256 shares,
uint256 minAmountOut
) external payable returns (uint256 amountOut);
/// @notice Stakes vault token to corresponding rewarder.
/// @param vault IERC20 instance of an Autopool to stake to.
/// @param maxAmount Maximum amount for user to stake. Amount > balanceOf(user) will stake all present tokens.
/// @return staked Returns total amount staked.
function stakeVaultToken(IERC20 vault, uint256 maxAmount) external payable returns (uint256 staked);
/// @notice Unstakes vault token from corresponding rewarder.
/// @param vault IAutopool instance of the vault token to withdraw.
/// @param rewarder Rewarder to withdraw from.
/// @param maxAmount Amount of vault token to withdraw Amount > balanceOf(user) will withdraw all owned tokens.
/// @param claim Claiming rewards or not on unstaking.
/// @return withdrawn Amount of vault token withdrawn.
function withdrawVaultToken(
IAutopool vault,
IMainRewarder rewarder,
uint256 maxAmount,
bool claim
) external payable returns (uint256 withdrawn);
/// @notice Claims rewards on user stake of vault token.
/// @param vault IAutopool instance of vault token to claim rewards for.
/// @param rewarder Rewarder to claim rewards from.
/// @param recipient Address to claim rewards for.
function claimAutopoolRewards(IAutopool vault, IMainRewarder rewarder, address recipient) external payable;
/// @notice Checks if timestamp is expired. Purpose is to check the execution deadline with the multicall.
/// @param timestamp Timestamp to check.
/// @dev throws TimestampTooOld. Payable to allow for multicall.
function expiration(
uint256 timestamp
) external payable;
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2024 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol";
/**
* @title Validates and distributes Vault token rewards based on the
* the signed and submitted payloads
*/
interface IRewards {
struct Recipient {
uint256 chainId;
uint256 cycle;
address wallet;
uint256 amount;
}
event SignerSet(address newSigner);
event Claimed(uint256 cycle, address recipient, uint256 amount);
/// @notice Get the underlying token rewards are paid in
/// @return Token address
function rewardToken() external view returns (IERC20);
/// @notice Get the current payload signer;
/// @return Signer address
function rewardsSigner() external view returns (address);
/// @notice Check the amount an account has already claimed
/// @param account Account to check
/// @return Amount already claimed
function claimedAmounts(
address account
) external view returns (uint256);
/// @notice Get the amount that is claimable based on the provided payload
/// @param recipient Published rewards payload
/// @return Amount claimable if the payload is signed
function getClaimableAmount(
Recipient calldata recipient
) external view returns (uint256);
/// @notice Change the signer used to validate payloads
/// @param newSigner The new address that will be signing rewards payloads
function setSigner(
address newSigner
) external;
/// @notice Claim your rewards
/// @param recipient Published rewards payload
/// @param v v component of the payload signature
/// @param r r component of the payload signature
/// @param s s component of the payload signature
function claim(Recipient calldata recipient, uint8 v, bytes32 r, bytes32 s) external returns (uint256);
/// @notice Claim rewards on behalf of another account , invoked primarily by the router
/// @param recipient Published rewards payload
/// @param v v component of the payload signature
/// @param r r component of the payload signature
/// @param s s component of the payload signature
function claimFor(Recipient calldata recipient, uint8 v, bytes32 r, bytes32 s) external returns (uint256);
/// @notice Generate the hash of the payload
/// @param recipient Published rewards payload
/// @return Hash of the payload
function genHash(
Recipient memory recipient
) external view returns (bytes32);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
struct SwapParams {
/// @dev The address of the token to be sold.
address sellTokenAddress;
/// @dev The amount of tokens to be sold.
uint256 sellAmount;
/// @dev The address of the token to be bought.
address buyTokenAddress;
/// @dev The expected minimum amount of tokens to be bought.
uint256 buyAmount;
/// @dev Data payload to be used for complex swap operations.
bytes data;
/// @dev Extra data payload reserved for future development. This field allows for additional information
/// or functionality to be added without changing the struct and interface.
bytes extraData;
/// @dev Execution deadline in timestamp format
uint256 deadline;
}
interface IAsyncSwapper {
error TokenAddressZero();
error SwapFailed();
error InsufficientBuyAmountReceived(uint256 buyTokenAmountReceived, uint256 buyAmount);
error InsufficientSellAmount();
error InsufficientBuyAmount();
error InsufficientBalance(uint256 balanceNeeded, uint256 balanceAvailable);
event Swapped(
address indexed sellTokenAddress,
address indexed buyTokenAddress,
uint256 sellAmount,
uint256 buyAmount,
uint256 buyTokenAmountReceived
);
/**
* @notice Swaps sellToken for buyToken
* @param swapParams Encoded swap data
* @return buyTokenAmountReceived The amount of buyToken received from the swap
*/
function swap(
SwapParams memory swapParams
) external returns (uint256 buyTokenAmountReceived);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { ISwapRouter } from "src/interfaces/swapper/ISwapRouter.sol";
interface ISwapRouterV2 is ISwapRouter {
struct UserSwapData {
address fromToken;
address toToken;
address target;
bytes data;
}
function initTransientSwap(
UserSwapData[] memory customRoutes
) external;
function exitTransientSwap() external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title IDestinationAdapter
* @dev This is a super-interface to unify different types of adapters to be registered in Destination Registry.
* Specific interface type is defined by extending from this interface.
*/
interface IDestinationAdapter {
error MustBeMoreThanZero();
error ArraysLengthMismatch();
error BalanceMustIncrease();
error MinLpAmountNotReached();
error LpTokenAmountMismatch();
error NoNonZeroAmountProvided();
error InvalidBalanceChange();
error InvalidAddress(address);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { ISystemComponent } from "src/interfaces/ISystemComponent.sol";
/// @notice Creates and registers Destination Vaults for the system
interface IDestinationVaultFactory is ISystemComponent {
/// @notice Creates a vault of the specified type
/// @dev vaultType will be bytes32 encoded and checked that a template is registered
/// @param vaultType human readable key of the vault template
/// @param baseAsset Base asset of the system. WETH/USDC/etc
/// @param underlyer Underlying asset the vault will wrap
/// @param incentiveCalculator Incentive calculator of the vault
/// @param additionalTrackedTokens Any tokens in addition to base and underlyer that should be tracked
/// @param salt Contracts are created via CREATE2 with this value
/// @param params params to be passed to vaults initialize function
/// @return vault address of the newly created destination vault
function create(
string memory vaultType,
address baseAsset,
address underlyer,
address incentiveCalculator,
address[] memory additionalTrackedTokens,
bytes32 salt,
bytes memory params
) external returns (address vault);
/// @notice Sets the default reward ratio
/// @param rewardRatio new default reward ratio
function setDefaultRewardRatio(
uint256 rewardRatio
) external;
/// @notice Sets the default reward block duration
/// @param blockDuration new default reward block duration
function setDefaultRewardBlockDuration(
uint256 blockDuration
) external;
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
/// @title Capture information about a pool or destination
interface IStatsCalculator {
/// @notice thrown when no snapshot is taken
error NoSnapshotTaken();
/// @notice The id for this instance of a calculator
function getAprId() external view returns (bytes32);
/// @notice The id of the underlying asset/pool/destination this calculator represents
/// @dev This may be a generated address
function getAddressId() external view returns (address);
/// @notice Setup the calculator after it has been copied
/// @dev Should only be executed one time
/// @param dependentAprIds apr ids that cover the dependencies of this calculator
/// @param initData setup data specific to this type of calculator
function initialize(bytes32[] calldata dependentAprIds, bytes calldata initData) external;
/// @notice Capture stat data about this setup
function snapshot() external;
/// @notice Indicates if a snapshot should be taken
/// @return takeSnapshot if true then a snapshot should be taken. If false, calling snapshot will do nothing
function shouldSnapshot() external view returns (bool takeSnapshot);
/// @dev Enum representing the snapshot status for a given rewarder (Convex and Aura) or reward token (Maverick)
enum SnapshotStatus {
noSnapshot, // Indicates that no snapshot has been taken yet for the rewarder.
tooSoon, // Indicates that it's too soon to take another snapshot since the last one.
shouldFinalize, // Indicates that the conditions are met for finalizing a snapshot.
shouldRestart // Indicates that the conditions are met for restarting a snapshot.
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IERC20Metadata as IERC20 } from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { IBaseAssetVault } from "src/interfaces/vault/IBaseAssetVault.sol";
import { IMainRewarder } from "src/interfaces/rewarders/IMainRewarder.sol";
import { IDexLSTStats } from "src/interfaces/stats/IDexLSTStats.sol";
import { ISystemComponent } from "src/interfaces/ISystemComponent.sol";
interface IDestinationVault is ISystemComponent, IBaseAssetVault, IERC20 {
enum VaultShutdownStatus {
Active,
Deprecated,
Exploit
}
error LogicDefect();
error BaseAmountReceived(uint256 amount);
/* ******************************** */
/* View */
/* ******************************** */
/// @notice A full unit of this vault
// solhint-disable-next-line func-name-mixedcase
function ONE() external view returns (uint256);
/// @notice The asset that is deposited into the vault
function underlying() external view returns (address);
/// @notice The total supply of the underlying asset
function underlyingTotalSupply() external view returns (uint256);
/// @notice The asset that rewards and withdrawals to the Autopool are denominated in
/// @inheritdoc IBaseAssetVault
function baseAsset() external view override returns (address);
/// @notice Debt balance of underlying asset that is in contract. This
/// value includes only assets that are known as debt by the rest of the
/// system (i.e. transferred in on rebalance), and does not include
/// extraneous amounts of underlyer that may have ended up in this contract.
function internalDebtBalance() external view returns (uint256);
/// @notice Debt balance of underlying asset staked externally. This value only
/// includes assets known as debt to the rest of the system, and does not include
/// any assets staked on behalf of the DV in external contracts.
function externalDebtBalance() external view returns (uint256);
/// @notice Returns true value of _underlyer in DV. Debt + tokens that may have
/// been transferred into the contract outside of rebalance.
function internalQueriedBalance() external view returns (uint256);
/// @notice Returns true value of staked _underlyer in external contract. This
/// will include any _underlyer that has been staked on behalf of the DV.
function externalQueriedBalance() external view returns (uint256);
/// @notice Balance of underlying debt, sum of `externalDebtBalance()` and `internalDebtBalance()`.
function balanceOfUnderlyingDebt() external view returns (uint256);
/// @notice Rewarder for this vault
function rewarder() external view returns (address);
/// @notice Exchange this destination vault points to
function exchangeName() external view returns (string memory);
/// @notice The type of pool associated with this vault
function poolType() external view returns (string memory);
/// @notice If the pool only deals in ETH when adding or removing liquidity
function poolDealInEth() external view returns (bool);
/// @notice Tokens that base asset can be swapped into
function underlyingTokens() external view returns (address[] memory);
/// @notice Gets the reserves of the underlying tokens
function underlyingReserves() external view returns (address[] memory tokens, uint256[] memory amounts);
/* ******************************** */
/* Events */
/* ******************************** */
event Donated(address sender, uint256 amount);
event Withdraw(
uint256 target, uint256 actual, uint256 debtLoss, uint256 claimLoss, uint256 fromIdle, uint256 fromDebt
);
event UpdateSignedMessage(bytes32 hash, bool flag);
/* ******************************** */
/* Errors */
/* ******************************** */
error ZeroAddress(string paramName);
error InvalidShutdownStatus(VaultShutdownStatus status);
/* ******************************** */
/* Functions */
/* ******************************** */
/// @notice Setup the contract. These will be cloned so no constructor
/// @param baseAsset_ Base asset of the system. WETH/USDC/etc
/// @param underlyer_ Underlying asset the vault will wrap
/// @param rewarder_ Reward tracker for this vault
/// @param incentiveCalculator_ Incentive calculator for this vault
/// @param additionalTrackedTokens_ Additional tokens that should be considered 'tracked'
/// @param params_ Any extra parameters needed to setup the contract
function initialize(
IERC20 baseAsset_,
IERC20 underlyer_,
IMainRewarder rewarder_,
address incentiveCalculator_,
address[] memory additionalTrackedTokens_,
bytes memory params_
) external;
function getRangePricesLP() external returns (uint256 spotPrice, uint256 safePrice, bool isSpotSafe);
/// @notice Calculates the current value of a portion of the debt based on shares
/// @dev Queries the current value of all tokens we have deployed, whether its a single place, multiple, staked, etc
/// @param shares The number of shares to value
/// @return value The current value of our debt in terms of the baseAsset
function debtValue(
uint256 shares
) external returns (uint256 value);
/// @notice Collects any earned rewards from staking, incentives, etc. Transfers to sender
/// @dev Should be limited to LIQUIDATOR_MANAGER. Rewards must be collected before claimed
/// @return amounts amount of rewards claimed for each token
/// @return tokens tokens claimed
function collectRewards() external returns (uint256[] memory amounts, address[] memory tokens);
/// @notice Pull any non-tracked token to the specified destination
/// @dev Should be limited to TOKEN_RECOVERY_MANAGER
function recover(address[] calldata tokens, uint256[] calldata amounts, address[] calldata destinations) external;
/// @notice Recovers any extra underlying both in DV and staked externally not tracked as debt.
/// @dev Should be limited to TOKEN_SAVER_ROLE.
/// @param destination The address to send excess underlyer to.
function recoverUnderlying(
address destination
) external;
/// @notice Deposit underlying to receive destination vault shares
/// @param amount amount of base lp asset to deposit
function depositUnderlying(
uint256 amount
) external returns (uint256 shares);
/// @notice Withdraw underlying by burning destination vault shares
/// @param shares amount of destination vault shares to burn
/// @param to destination of the underlying asset
/// @return amount underlyer amount 'to' received
function withdrawUnderlying(uint256 shares, address to) external returns (uint256 amount);
/// @notice Burn specified shares for underlyer swapped to base asset
/// @param shares amount of vault shares to burn
/// @param to destination of the base asset
/// @return amount base asset amount 'to' received
/// @return tokens the tokens burned to get the base asset
/// @return tokenAmounts the amount of the tokens burned to get the base asset
function withdrawBaseAsset(
uint256 shares,
address to
) external returns (uint256 amount, address[] memory tokens, uint256[] memory tokenAmounts);
/// @notice Mark this vault as shutdown so that autoPools can react
function shutdown(
VaultShutdownStatus reason
) external;
/// @notice True if the vault has been shutdown
function isShutdown() external view returns (bool);
/// @notice Returns the reason for shutdown (or `Active` if not shutdown)
function shutdownStatus() external view returns (VaultShutdownStatus);
/// @notice Stats contract for this vault
function getStats() external view returns (IDexLSTStats);
/// @notice get the marketplace rewards
/// @return rewardTokens list of reward token addresses
/// @return rewardRates list of reward rates
function getMarketplaceRewards() external returns (uint256[] memory rewardTokens, uint256[] memory rewardRates);
/// @notice Get the address of the underlying pool the vault points to
/// @return poolAddress address of the underlying pool
function getPool() external view returns (address poolAddress);
/// @notice Gets the spot price of the underlying LP token
/// @dev Price validated to be inside our tolerance against safe price. Will revert if outside.
/// @return price Value of 1 unit of the underlying LP token in terms of the base asset
function getValidatedSpotPrice() external returns (uint256 price);
/// @notice Gets the safe price of the underlying LP token
/// @dev Price validated to be inside our tolerance against spot price. Will revert if outside.
/// @return price Value of 1 unit of the underlying LP token in terms of the base asset
function getValidatedSafePrice() external returns (uint256 price);
/// @notice Get the lowest price we can get for the LP token
/// @dev This price can be attacked is not validate to be in any range
/// @return price Value of 1 unit of the underlying LP token in terms of the base asset
function getUnderlyerFloorPrice() external returns (uint256 price);
/// @notice Get the highest price we can get for the LP token
/// @dev This price can be attacked is not validate to be in any range
/// @return price Value of 1 unit of the underlying LP token in terms of the base asset
function getUnderlyerCeilingPrice() external returns (uint256 price);
/// @notice Set or unset a hash as a signed message
/// @dev Should be limited to DESTINATION_VAULTS_UPDATER. The set hash is used to validate a signature.
/// This signature can be potentially used to claim offchain rewards earned by Destination Vaults.
/// @param hash bytes32 hash of a payload
/// @param flag boolean flag to indicate a validity of hash
function setMessage(bytes32 hash, bool flag) external;
/// @notice Allows to change the incentive calculator of destination vault
/// @dev Only works when vault is shutdown, also validates the calculator before updating
/// @param incentiveCalculator address of the new incentive calculator
function setIncentiveCalculator(
address incentiveCalculator
) external;
/// @notice Allows to change the extension contract
/// @dev Should be limited to DESTINATION_VAULT_MANAGER
/// @param extension contract address
function setExtension(
address extension
) external;
/// @notice Calls the execute function of the extension contract
/// @dev Should be limited to DESTINATION_VAULT_MANAGER
/// @dev Special care should be taken to ensure that balances hasn't been manipulated
/// @param data any data that the extension contract needs
function executeExtension(
bytes calldata data
) external;
/// @notice Returns the max recoup credit given during the withdraw of an undervalued destination
function recoupMaxCredit() external view returns (uint256);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import { IERC20Metadata } from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol";
/// @dev Interface of the ERC4626 "Tokenized Vault Standard", as defined in https://eips.ethereum.org/EIPS/eip-4626
/// @dev Due to the nature of obtaining estimates for previewing withdraws and redeems, a few functions are not
/// view and therefore do not conform to eip 4626. These functions use state changing operations
/// to get accurate estimates, reverting after the preview amounts have been obtained.
interface IERC4626 is IERC20Metadata {
event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
event Withdraw(
address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares
);
/// @notice Returns the address of the underlying token used for the Vault for accounting, depositing, and
/// withdrawing.
/// @dev
/// - MUST be an ERC-20 token contract.
/// - MUST NOT revert.
function asset() external view returns (address assetTokenAddress);
/// @notice Returns the total amount of the underlying asset that is “managed” by Vault.
/// @dev
/// - SHOULD include any compounding that occurs from yield.
/// - MUST be inclusive of any fees that are charged against assets in the Vault.
/// - MUST NOT revert.
function totalAssets() external view returns (uint256 totalManagedAssets);
/// @notice Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an
/// ideal
/// scenario where all the conditions are met.
/// @dev
/// - MUST NOT be inclusive of any fees that are charged against assets in the Vault.
/// - MUST NOT show any variations depending on the caller.
/// - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.
/// - MUST NOT revert.
///
/// NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the
/// “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and
/// from.
function convertToShares(
uint256 assets
) external view returns (uint256 shares);
/// @notice Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an
/// ideal
/// scenario where all the conditions are met.
/// @dev
/// - MUST NOT be inclusive of any fees that are charged against assets in the Vault.
/// - MUST NOT show any variations depending on the caller.
/// - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.
/// - MUST NOT revert.
///
/// NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the
/// “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and
/// from.
function convertToAssets(
uint256 shares
) external view returns (uint256 assets);
/// @notice Returns the maximum amount of the underlying asset that can be deposited into the Vault for the
/// receiver,
/// through a deposit call.
/// @dev
/// - MUST return a limited value if receiver is subject to some deposit limit.
/// - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited.
/// - MUST NOT revert.
function maxDeposit(
address receiver
) external returns (uint256 maxAssets);
/// @notice Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block,
/// given
/// current on-chain conditions.
/// @dev
/// - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit
/// call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called
/// in the same transaction.
/// - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the
/// deposit would be accepted, regardless if the user has enough tokens approved, etc.
/// - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees.
/// - MUST NOT revert.
///
/// NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in
/// share price or some other type of condition, meaning the depositor will lose assets by depositing.
function previewDeposit(
uint256 assets
) external returns (uint256 shares);
/// @notice Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens.
/// @dev
/// - MUST emit the Deposit event.
/// - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the
/// deposit execution, and are accounted for during deposit.
/// - MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not
/// approving enough underlying tokens to the Vault contract, etc).
///
/// NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token.
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
/// @notice Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call.
/// @dev
/// - MUST return a limited value if receiver is subject to some mint limit.
/// - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted.
/// - MUST NOT revert.
function maxMint(
address receiver
) external returns (uint256 maxShares);
/// @notice Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given
/// current on-chain conditions.
/// @dev
/// - MUST return as close to and no fewer than the exact amount of assets that would be deposited in a mint call
/// in the same transaction. I.e. mint should return the same or fewer assets as previewMint if called in the
/// same transaction.
/// - MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint
/// would be accepted, regardless if the user has enough tokens approved, etc.
/// - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees.
/// - MUST NOT revert.
///
/// NOTE: any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in
/// share price or some other type of condition, meaning the depositor will lose assets by minting.
function previewMint(
uint256 shares
) external returns (uint256 assets);
/// @notice Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens.
/// @dev
/// - MUST emit the Deposit event.
/// - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint
/// execution, and are accounted for during mint.
/// - MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not
/// approving enough underlying tokens to the Vault contract, etc).
///
/// NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token.
function mint(uint256 shares, address receiver) external returns (uint256 assets);
/// @notice Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the
/// Vault, through a withdraw call.
/// @dev
/// - MUST return a limited value if owner is subject to some withdrawal limit or timelock.
/// - MUST NOT revert.
function maxWithdraw(
address owner
) external returns (uint256 maxAssets);
/// @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block,
/// given current on-chain conditions.
/// @dev
/// - MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw
/// call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if
/// called
/// in the same transaction.
/// - MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though
/// the withdrawal would be accepted, regardless if the user has enough shares, etc.
/// - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees.
/// - MUST NOT revert.
///
/// NOTE: any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in
/// share price or some other type of condition, meaning the depositor will lose assets by depositing.
function previewWithdraw(
uint256 assets
) external returns (uint256 shares);
/// @notice Burns shares from owner and sends exactly assets of underlying tokens to receiver.
/// @dev
/// - MUST emit the Withdraw event.
/// - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the
/// withdraw execution, and are accounted for during withdraw.
/// - MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner
/// not having enough shares, etc).
///
/// Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed.
/// Those methods should be performed separately.
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
/// @notice Returns the maximum amount of Vault shares that can be redeemed from the owner balance in the Vault,
/// through a redeem call.
/// @dev
/// - MUST return a limited value if owner is subject to some withdrawal limit or timelock.
/// - MUST return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock.
/// - MUST NOT revert.
function maxRedeem(
address owner
) external returns (uint256 maxShares);
/// @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block,
/// given current on-chain conditions.
/// @dev
/// - MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call
/// in the same transaction. I.e. redeem should return the same or more assets as previewRedeem if called in the
/// same transaction.
/// - MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the
/// redemption would be accepted, regardless if the user has enough shares, etc.
/// - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees.
/// - MUST NOT revert.
///
/// NOTE: any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in
/// share price or some other type of condition, meaning the depositor will lose assets by redeeming.
function previewRedeem(
uint256 shares
) external returns (uint256 assets);
/// @notice Burns exactly shares from owner and sends assets of underlying tokens to receiver.
/// @dev
/// - MUST emit the Withdraw event.
/// - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the
/// redeem execution, and are accounted for during redeem.
/// - MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner
/// not having enough shares, etc).
///
/// NOTE: some implementations will require pre-requesting to the Vault before a withdrawal may be performed.
/// Those methods should be performed separately.
function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/ECDSA.sol)
pragma solidity ^0.8.0;
import "../Strings.sol";
/**
* @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations.
*
* These functions can be used to verify that a message was signed by the holder
* of the private keys of a given address.
*/
library ECDSA {
enum RecoverError {
NoError,
InvalidSignature,
InvalidSignatureLength,
InvalidSignatureS,
InvalidSignatureV // Deprecated in v4.8
}
function _throwError(RecoverError error) private pure {
if (error == RecoverError.NoError) {
return; // no error: do nothing
} else if (error == RecoverError.InvalidSignature) {
revert("ECDSA: invalid signature");
} else if (error == RecoverError.InvalidSignatureLength) {
revert("ECDSA: invalid signature length");
} else if (error == RecoverError.InvalidSignatureS) {
revert("ECDSA: invalid signature 's' value");
}
}
/**
* @dev Returns the address that signed a hashed message (`hash`) with
* `signature` or error string. This address can then be used for verification purposes.
*
* The `ecrecover` EVM opcode allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {toEthSignedMessageHash} on it.
*
* Documentation for signature generation:
* - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js]
* - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers]
*
* _Available since v4.3._
*/
function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) {
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
/// @solidity memory-safe-assembly
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return tryRecover(hash, v, r, s);
} else {
return (address(0), RecoverError.InvalidSignatureLength);
}
}
/**
* @dev Returns the address that signed a hashed message (`hash`) with
* `signature`. This address can then be used for verification purposes.
*
* The `ecrecover` EVM opcode allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {toEthSignedMessageHash} on it.
*/
function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, signature);
_throwError(error);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately.
*
* See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures]
*
* _Available since v4.3._
*/
function tryRecover(
bytes32 hash,
bytes32 r,
bytes32 vs
) internal pure returns (address, RecoverError) {
bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
uint8 v = uint8((uint256(vs) >> 255) + 27);
return tryRecover(hash, v, r, s);
}
/**
* @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately.
*
* _Available since v4.2._
*/
function recover(
bytes32 hash,
bytes32 r,
bytes32 vs
) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, r, vs);
_throwError(error);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `v`,
* `r` and `s` signature fields separately.
*
* _Available since v4.3._
*/
function tryRecover(
bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s
) internal pure returns (address, RecoverError) {
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
//
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
// these malleable signatures as well.
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return (address(0), RecoverError.InvalidSignatureS);
}
// If the signature is valid (and not malleable), return the signer address
address signer = ecrecover(hash, v, r, s);
if (signer == address(0)) {
return (address(0), RecoverError.InvalidSignature);
}
return (signer, RecoverError.NoError);
}
/**
* @dev Overload of {ECDSA-recover} that receives the `v`,
* `r` and `s` signature fields separately.
*/
function recover(
bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s
) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, v, r, s);
_throwError(error);
return recovered;
}
/**
* @dev Returns an Ethereum Signed Message, created from a `hash`. This
* produces hash corresponding to the one signed with the
* https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
* JSON-RPC method as part of EIP-191.
*
* See {recover}.
*/
function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) {
// 32 is the length in bytes of hash,
// enforced by the type signature above
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}
/**
* @dev Returns an Ethereum Signed Message, created from `s`. This
* produces hash corresponding to the one signed with the
* https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
* JSON-RPC method as part of EIP-191.
*
* See {recover}.
*/
function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(s.length), s));
}
/**
* @dev Returns an Ethereum Signed Typed Data, created from a
* `domainSeparator` and a `structHash`. This produces hash corresponding
* to the one signed with the
* https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`]
* JSON-RPC method as part of EIP-712.
*
* See {recover}.
*/
function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
interface IBaseRewarder {
error RecoverDurationPending();
event RewardAdded(
uint256 reward,
uint256 rewardRate,
uint256 lastUpdateBlock,
uint256 periodInBlockFinish,
uint256 historicalRewards
);
event UserRewardUpdated(
address indexed user, uint256 amount, uint256 rewardPerTokenStored, uint256 lastUpdateBlock
);
event Staked(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
event RewardPaid(address indexed user, address indexed recipient, uint256 reward);
event QueuedRewardsUpdated(uint256 startingQueuedRewards, uint256 startingNewRewards, uint256 queuedRewards);
event AddedToWhitelist(address indexed wallet);
event RemovedFromWhitelist(address indexed wallet);
event TokeLockDurationUpdated(uint256 newDuration);
event Recovered(address token, address recipient, uint256 amount);
/**
* @notice Claims and transfers all rewards for the specified account
*/
function getReward() external;
/**
* @notice Stakes the specified amount of tokens for the specified account.
* @param account The address of the account to stake tokens for.
* @param amount The amount of tokens to stake.
*/
function stake(address account, uint256 amount) external;
/**
* @notice Calculate the earned rewards for an account.
* @param account Address of the account.
* @return The earned rewards for the given account.
*/
function earned(
address account
) external view returns (uint256);
/**
* @notice Calculates the rewards per token for the current block.
* @dev The total amount of rewards available in the system is fixed, and it needs to be distributed among the users
* based on their token balances and staking duration.
* Rewards per token represent the amount of rewards that each token is entitled to receive at the current block.
* The calculation takes into account the reward rate, the time duration since the last update,
* and the total supply of tokens in the staking pool.
* @return The updated rewards per token value for the current block.
*/
function rewardPerToken() external view returns (uint256);
/**
* @notice Get the current reward rate per block.
* @return The current reward rate per block.
*/
function rewardRate() external view returns (uint256);
/**
* @notice Get the current TOKE lock duration.
* @return The current TOKE lock duration.
*/
function tokeLockDuration() external view returns (uint256);
/**
* @notice Get the last block where rewards are applicable.
* @return The last block number where rewards are applicable.
*/
function lastBlockRewardApplicable() external view returns (uint256);
/**
* @notice The total amount of tokens staked
*/
function totalSupply() external view returns (uint256);
/**
* @notice The amount of tokens staked for the specified account
* @param account The address of the account to get the balance of
*/
function balanceOf(
address account
) external view returns (uint256);
/**
* @notice Queue new rewards to be distributed.
* @param newRewards The amount of new rewards to be queued.
*/
function queueNewRewards(
uint256 newRewards
) external;
/**
* @notice Token distributed as rewards
* @return reward token address
*/
function rewardToken() external view returns (address);
/**
* @notice Add an address to the whitelist.
* @param wallet The address to be added to the whitelist.
*/
function addToWhitelist(
address wallet
) external;
/**
* @notice Remove an address from the whitelist.
* @param wallet The address to be removed from the whitelist.
*/
function removeFromWhitelist(
address wallet
) external;
/**
* @notice Recovers tokens from the rewarder. However, a recovery duration of 1 year is applicable for reward token
* @param token Address of token
* @param recipient recipient Address of recipient
*/
function recover(address token, address recipient) external;
/**
* @notice Check if an address is whitelisted.
* @param wallet The address to be checked.
* @return bool indicating if the address is whitelisted.
*/
function isWhitelisted(
address wallet
) external view returns (bool);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IBaseRewarder } from "src/interfaces/rewarders/IBaseRewarder.sol";
interface IExtraRewarder is IBaseRewarder {
/**
* @notice Withdraws the specified amount of tokens from the vault for the specified account.
* @param account The address of the account to withdraw tokens for.
* @param amount The amount of tokens to withdraw.
*/
function withdraw(address account, uint256 amount) external;
/**
* @notice Claims and transfers all rewards for the specified account from this contract.
* @param account The address of the account to claim rewards for.
* @param recipient The address to send the rewards to.
*/
function getReward(address account, address recipient) external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// solhint-disable no-inline-assembly
/// @notice Read and write to persistent storage at a fraction of the cost.
/// @notice Copied at
/// https://github.com/Vectorized/solady/blob/ccaed15a964891aa729c9d22670304e88584a480/src/utils/SSTORE2.sol
/// @notice Pragma updated from 8.4 to 8.24
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/SSTORE2.sol)
/// @author Saw-mon-and-Natalie (https://github.com/Saw-mon-and-Natalie)
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SSTORE2.sol)
/// @author Modified from 0xSequence (https://github.com/0xSequence/sstore2/blob/master/contracts/SSTORE2.sol)
/// @author Modified from SSTORE3 (https://github.com/Philogy/sstore3)
library SSTORE2 {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTANTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The proxy initialization code.
uint256 private constant _CREATE3_PROXY_INITCODE = 0x67363d3d37363d34f03d5260086018f3;
/// @dev Hash of the `_CREATE3_PROXY_INITCODE`.
/// Equivalent to `keccak256(abi.encodePacked(hex"67363d3d37363d34f03d5260086018f3"))`.
bytes32 internal constant CREATE3_PROXY_INITCODE_HASH =
0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Unable to deploy the storage contract.
error DeploymentFailed();
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* WRITE LOGIC */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Writes `data` into the bytecode of a storage contract and returns its address.
function write(
bytes memory data
) internal returns (address pointer) {
/// @solidity memory-safe-assembly
assembly {
let n := mload(data) // Let `l` be `n + 1`. +1 as we prefix a STOP opcode.
/**
* ---------------------------------------------------+
* Opcode | Mnemonic | Stack | Memory |
* ---------------------------------------------------|
* 61 l | PUSH2 l | l | |
* 80 | DUP1 | l l | |
* 60 0xa | PUSH1 0xa | 0xa l l | |
* 3D | RETURNDATASIZE | 0 0xa l l | |
* 39 | CODECOPY | l | [0..l): code |
* 3D | RETURNDATASIZE | 0 l | [0..l): code |
* F3 | RETURN | | [0..l): code |
* 00 | STOP | | |
* ---------------------------------------------------+
* @dev Prefix the bytecode with a STOP opcode to ensure it cannot be called.
* Also PUSH2 is used since max contract size cap is 24,576 bytes which is less than 2 ** 16.
*/
// Do a out-of-gas revert if `n + 1` is more than 2 bytes.
mstore(add(data, gt(n, 0xfffe)), add(0xfe61000180600a3d393df300, shl(0x40, n)))
// Deploy a new contract with the generated creation code.
pointer := create(0, add(data, 0x15), add(n, 0xb))
if iszero(pointer) {
mstore(0x00, 0x30116425) // `DeploymentFailed()`.
revert(0x1c, 0x04)
}
mstore(data, n) // Restore the length of `data`.
}
}
/// @dev Writes `data` into the bytecode of a storage contract with `salt`
/// and returns its normal CREATE2 deterministic address.
function writeCounterfactual(bytes memory data, bytes32 salt) internal returns (address pointer) {
/// @solidity memory-safe-assembly
assembly {
let n := mload(data)
// Do a out-of-gas revert if `n + 1` is more than 2 bytes.
mstore(add(data, gt(n, 0xfffe)), add(0xfe61000180600a3d393df300, shl(0x40, n)))
// Deploy a new contract with the generated creation code.
pointer := create2(0, add(data, 0x15), add(n, 0xb), salt)
if iszero(pointer) {
mstore(0x00, 0x30116425) // `DeploymentFailed()`.
revert(0x1c, 0x04)
}
mstore(data, n) // Restore the length of `data`.
}
}
/// @dev Writes `data` into the bytecode of a storage contract and returns its address.
/// This uses the so-called "CREATE3" workflow,
/// which means that `pointer` is agnostic to `data, and only depends on `salt`.
function writeDeterministic(bytes memory data, bytes32 salt) internal returns (address pointer) {
/// @solidity memory-safe-assembly
assembly {
let n := mload(data)
mstore(0x00, _CREATE3_PROXY_INITCODE) // Store the `_PROXY_INITCODE`.
let proxy := create2(0, 0x10, 0x10, salt)
if iszero(proxy) {
mstore(0x00, 0x30116425) // `DeploymentFailed()`.
revert(0x1c, 0x04)
}
mstore(0x14, proxy) // Store the proxy's address.
// 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01).
// 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex).
mstore(0x00, 0xd694)
mstore8(0x34, 0x01) // Nonce of the proxy contract (1).
pointer := keccak256(0x1e, 0x17)
// Do a out-of-gas revert if `n + 1` is more than 2 bytes.
mstore(add(data, gt(n, 0xfffe)), add(0xfe61000180600a3d393df300, shl(0x40, n)))
if iszero(
mul( // The arguments of `mul` are evaluated last to first.
extcodesize(pointer), call(gas(), proxy, 0, add(data, 0x15), add(n, 0xb), codesize(), 0x00))
) {
mstore(0x00, 0x30116425) // `DeploymentFailed()`.
revert(0x1c, 0x04)
}
mstore(data, n) // Restore the length of `data`.
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ADDRESS CALCULATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Returns the initialization code hash of the storage contract for `data`.
/// Used for mining vanity addresses with create2crunch.
function initCodeHash(
bytes memory data
) internal pure returns (bytes32 hash) {
/// @solidity memory-safe-assembly
assembly {
let n := mload(data)
// Do a out-of-gas revert if `n + 1` is more than 2 bytes.
returndatacopy(returndatasize(), returndatasize(), gt(n, 0xfffe))
mstore(data, add(0x61000180600a3d393df300, shl(0x40, n)))
hash := keccak256(add(data, 0x15), add(n, 0xb))
mstore(data, n) // Restore the length of `data`.
}
}
/// @dev Equivalent to `predictCounterfactualAddress(data, salt, address(this))`
function predictCounterfactualAddress(bytes memory data, bytes32 salt) internal view returns (address pointer) {
pointer = predictCounterfactualAddress(data, salt, address(this));
}
/// @dev Returns the CREATE2 address of the storage contract for `data`
/// deployed with `salt` by `deployer`.
/// Note: The returned result has dirty upper 96 bits. Please clean if used in assembly.
function predictCounterfactualAddress(
bytes memory data,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
bytes32 hash = initCodeHash(data);
/// @solidity memory-safe-assembly
assembly {
// Compute and store the bytecode hash.
mstore8(0x00, 0xff) // Write the prefix.
mstore(0x35, hash)
mstore(0x01, shl(96, deployer))
mstore(0x15, salt)
predicted := keccak256(0x00, 0x55)
// Restore the part of the free memory pointer that has been overwritten.
mstore(0x35, 0)
}
}
/// @dev Equivalent to `predictDeterministicAddress(salt, address(this))`.
function predictDeterministicAddress(
bytes32 salt
) internal view returns (address pointer) {
pointer = predictDeterministicAddress(salt, address(this));
}
/// @dev Returns the "CREATE3" deterministic address for `salt` with `deployer`.
function predictDeterministicAddress(bytes32 salt, address deployer) internal pure returns (address pointer) {
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x00, deployer) // Store `deployer`.
mstore8(0x0b, 0xff) // Store the prefix.
mstore(0x20, salt) // Store the salt.
mstore(0x40, CREATE3_PROXY_INITCODE_HASH) // Store the bytecode hash.
mstore(0x14, keccak256(0x0b, 0x55)) // Store the proxy's address.
mstore(0x40, m) // Restore the free memory pointer.
// 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01).
// 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex).
mstore(0x00, 0xd694)
mstore8(0x34, 0x01) // Nonce of the proxy contract (1).
pointer := keccak256(0x1e, 0x17)
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* READ LOGIC */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Equivalent to `read(pointer, 0, 2 ** 256 - 1)`.
function read(
address pointer
) internal view returns (bytes memory data) {
/// @solidity memory-safe-assembly
assembly {
data := mload(0x40)
let n := and(0xffffffffff, sub(extcodesize(pointer), 0x01))
extcodecopy(pointer, add(data, 0x1f), 0x00, add(n, 0x21))
mstore(data, n) // Store the length.
mstore(0x40, add(n, add(data, 0x40))) // Allocate memory.
}
}
/// @dev Equivalent to `read(pointer, start, 2 ** 256 - 1)`.
function read(address pointer, uint256 start) internal view returns (bytes memory data) {
/// @solidity memory-safe-assembly
assembly {
data := mload(0x40)
let n := and(0xffffffffff, sub(extcodesize(pointer), 0x01))
let l := sub(n, and(0xffffff, mul(lt(start, n), start)))
extcodecopy(pointer, add(data, 0x1f), start, add(l, 0x21))
mstore(data, mul(sub(n, start), lt(start, n))) // Store the length.
mstore(0x40, add(data, add(0x40, mload(data)))) // Allocate memory.
}
}
/// @dev Returns a slice of the data on `pointer` from `start` to `end`.
/// `start` and `end` will be clamped to the range `[0, args.length]`.
/// The `pointer` MUST be deployed via the SSTORE2 write functions.
/// Otherwise, the behavior is undefined.
/// Out-of-gas reverts if `pointer` does not have any code.
function read(address pointer, uint256 start, uint256 end) internal view returns (bytes memory data) {
/// @solidity memory-safe-assembly
assembly {
data := mload(0x40)
if iszero(lt(end, 0xffff)) { end := 0xffff }
let d := mul(sub(end, start), lt(start, end))
extcodecopy(pointer, add(data, 0x1f), start, add(d, 0x01))
if iszero(and(0xff, mload(add(data, d)))) {
let n := sub(extcodesize(pointer), 0x01)
returndatacopy(returndatasize(), returndatasize(), shr(40, n))
d := mul(gt(n, start), sub(d, mul(gt(end, n), sub(end, n))))
}
mstore(data, d) // Store the length.
mstore(add(add(data, 0x20), d), 0) // Zeroize the slot after the bytes.
mstore(0x40, add(add(data, 0x40), d)) // Allocate memory.
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// solhint-disable no-inline-assembly
/// @notice Library for byte related operations.
/// @notice Copied at
/// https://github.com/Vectorized/solady/blob/ccaed15a964891aa729c9d22670304e88584a480/src/utils/LibBytes.sol
/// @notice Pragma updated from 8.4 to 8.24
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/LibBytes.sol)
library LibBytes {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STRUCTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Goated bytes storage struct that totally MOGs, no cap, fr.
/// Uses less gas and bytecode than Solidity's native bytes storage. It's meta af.
/// Packs length with the first 31 bytes if <255 bytes, so it’s mad tight.
struct BytesStorage {
bytes32 _spacer;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTANTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The constant returned when the `search` is not found in the bytes.
uint256 internal constant NOT_FOUND = type(uint256).max;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* BYTE STORAGE OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Sets the value of the bytes storage `$` to `s`.
function set(BytesStorage storage $, bytes memory s) internal {
/// @solidity memory-safe-assembly
assembly {
let n := mload(s)
let packed := or(0xff, shl(8, n))
for { let i := 0 } 1 { } {
if iszero(gt(n, 0xfe)) {
i := 0x1f
packed := or(n, shl(8, mload(add(s, i))))
if iszero(gt(n, i)) { break }
}
let o := add(s, 0x20)
mstore(0x00, $.slot)
for { let p := keccak256(0x00, 0x20) } 1 { } {
sstore(add(p, shr(5, i)), mload(add(o, i)))
i := add(i, 0x20)
if iszero(lt(i, n)) { break }
}
break
}
sstore($.slot, packed)
}
}
/// @dev Sets the value of the bytes storage `$` to `s`.
function setCalldata(BytesStorage storage $, bytes calldata s) internal {
/// @solidity memory-safe-assembly
assembly {
let packed := or(0xff, shl(8, s.length))
for { let i := 0 } 1 { } {
if iszero(gt(s.length, 0xfe)) {
i := 0x1f
packed := or(s.length, shl(8, shr(8, calldataload(s.offset))))
if iszero(gt(s.length, i)) { break }
}
mstore(0x00, $.slot)
for { let p := keccak256(0x00, 0x20) } 1 { } {
sstore(add(p, shr(5, i)), calldataload(add(s.offset, i)))
i := add(i, 0x20)
if iszero(lt(i, s.length)) { break }
}
break
}
sstore($.slot, packed)
}
}
/// @dev Sets the value of the bytes storage `$` to the empty bytes.
function clear(
BytesStorage storage $
) internal {
delete $._spacer;
}
/// @dev Returns whether the value stored is `$` is the empty bytes "".
function isEmpty(
BytesStorage storage $
) internal view returns (bool) {
return uint256($._spacer) & 0xff == uint256(0);
}
/// @dev Returns the length of the value stored in `$`.
function length(
BytesStorage storage $
) internal view returns (uint256 result) {
result = uint256($._spacer);
/// @solidity memory-safe-assembly
assembly {
let n := and(0xff, result)
result := or(mul(shr(8, result), eq(0xff, n)), mul(n, iszero(eq(0xff, n))))
}
}
/// @dev Returns the value stored in `$`.
function get(
BytesStorage storage $
) internal view returns (bytes memory result) {
/// @solidity memory-safe-assembly
assembly {
result := mload(0x40)
let o := add(result, 0x20)
let packed := sload($.slot)
let n := shr(8, packed)
for { let i := 0 } 1 { } {
if iszero(eq(or(packed, 0xff), packed)) {
mstore(o, packed)
n := and(0xff, packed)
i := 0x1f
if iszero(gt(n, i)) { break }
}
mstore(0x00, $.slot)
for { let p := keccak256(0x00, 0x20) } 1 { } {
mstore(add(o, i), sload(add(p, shr(5, i))))
i := add(i, 0x20)
if iszero(lt(i, n)) { break }
}
break
}
mstore(result, n) // Store the length of the memory.
mstore(add(o, n), 0) // Zeroize the slot after the bytes.
mstore(0x40, add(add(o, n), 0x20)) // Allocate memory.
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* BYTES OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Returns `subject` all occurrences of `needle` replaced with `replacement`.
function replace(
bytes memory subject,
bytes memory needle,
bytes memory replacement
) internal pure returns (bytes memory result) {
/// @solidity memory-safe-assembly
assembly {
result := mload(0x40)
let needleLen := mload(needle)
let replacementLen := mload(replacement)
let d := sub(result, subject) // Memory difference.
let i := add(subject, 0x20) // Subject bytes pointer.
mstore(0x00, add(i, mload(subject))) // End of subject.
if iszero(gt(needleLen, mload(subject))) {
let subjectSearchEnd := add(sub(mload(0x00), needleLen), 1)
let h := 0 // The hash of `needle`.
if iszero(lt(needleLen, 0x20)) { h := keccak256(add(needle, 0x20), needleLen) }
let s := mload(add(needle, 0x20))
for { let m := shl(3, sub(0x20, and(needleLen, 0x1f))) } 1 { } {
let t := mload(i)
// Whether the first `needleLen % 32` bytes of `subject` and `needle` matches.
if iszero(shr(m, xor(t, s))) {
if h {
if iszero(eq(keccak256(i, needleLen), h)) {
mstore(add(i, d), t)
i := add(i, 1)
if iszero(lt(i, subjectSearchEnd)) { break }
continue
}
}
// Copy the `replacement` one word at a time.
for { let j := 0 } 1 { } {
mstore(add(add(i, d), j), mload(add(add(replacement, 0x20), j)))
j := add(j, 0x20)
if iszero(lt(j, replacementLen)) { break }
}
d := sub(add(d, replacementLen), needleLen)
if needleLen {
i := add(i, needleLen)
if iszero(lt(i, subjectSearchEnd)) { break }
continue
}
}
mstore(add(i, d), t)
i := add(i, 1)
if iszero(lt(i, subjectSearchEnd)) { break }
}
}
let end := mload(0x00)
let n := add(sub(d, add(result, 0x20)), end)
// Copy the rest of the bytes one word at a time.
for { } lt(i, end) { i := add(i, 0x20) } { mstore(add(i, d), mload(i)) }
let o := add(i, d)
mstore(o, 0) // Zeroize the slot after the bytes.
mstore(0x40, add(o, 0x20)) // Allocate memory.
mstore(result, n) // Store the length.
}
}
/// @dev Returns the byte index of the first location of `needle` in `subject`,
/// needleing from left to right, starting from `from`.
/// Returns `NOT_FOUND` (i.e. `type(uint256).max`) if the `needle` is not found.
function indexOf(bytes memory subject, bytes memory needle, uint256 from) internal pure returns (uint256 result) {
/// @solidity memory-safe-assembly
assembly {
result := not(0) // Initialize to `NOT_FOUND`.
for { let subjectLen := mload(subject) } 1 { } {
if iszero(mload(needle)) {
result := from
if iszero(gt(from, subjectLen)) { break }
result := subjectLen
break
}
let needleLen := mload(needle)
let subjectStart := add(subject, 0x20)
subject := add(subjectStart, from)
let end := add(sub(add(subjectStart, subjectLen), needleLen), 1)
let m := shl(3, sub(0x20, and(needleLen, 0x1f)))
let s := mload(add(needle, 0x20))
if iszero(and(lt(subject, end), lt(from, subjectLen))) { break }
if iszero(lt(needleLen, 0x20)) {
for { let h := keccak256(add(needle, 0x20), needleLen) } 1 { } {
if iszero(shr(m, xor(mload(subject), s))) {
if eq(keccak256(subject, needleLen), h) {
result := sub(subject, subjectStart)
break
}
}
subject := add(subject, 1)
if iszero(lt(subject, end)) { break }
}
break
}
for { } 1 { } {
if iszero(shr(m, xor(mload(subject), s))) {
result := sub(subject, subjectStart)
break
}
subject := add(subject, 1)
if iszero(lt(subject, end)) { break }
}
break
}
}
}
/// @dev Returns the byte index of the first location of `needle` in `subject`,
/// needleing from left to right.
/// Returns `NOT_FOUND` (i.e. `type(uint256).max`) if the `needle` is not found.
function indexOf(bytes memory subject, bytes memory needle) internal pure returns (uint256) {
return indexOf(subject, needle, 0);
}
/// @dev Returns the byte index of the first location of `needle` in `subject`,
/// needleing from right to left, starting from `from`.
/// Returns `NOT_FOUND` (i.e. `type(uint256).max`) if the `needle` is not found.
function lastIndexOf(
bytes memory subject,
bytes memory needle,
uint256 from
) internal pure returns (uint256 result) {
/// @solidity memory-safe-assembly
assembly {
for { } 1 { } {
result := not(0) // Initialize to `NOT_FOUND`.
let needleLen := mload(needle)
if gt(needleLen, mload(subject)) { break }
let w := result
let fromMax := sub(mload(subject), needleLen)
if iszero(gt(fromMax, from)) { from := fromMax }
let end := add(add(subject, 0x20), w)
subject := add(add(subject, 0x20), from)
if iszero(gt(subject, end)) { break }
// As this function is not too often used,
// we shall simply use keccak256 for smaller bytecode size.
for { let h := keccak256(add(needle, 0x20), needleLen) } 1 { } {
if eq(keccak256(subject, needleLen), h) {
result := sub(subject, add(end, 1))
break
}
subject := add(subject, w) // `sub(subject, 1)`.
if iszero(gt(subject, end)) { break }
}
break
}
}
}
/// @dev Returns the byte index of the first location of `needle` in `subject`,
/// needleing from right to left.
/// Returns `NOT_FOUND` (i.e. `type(uint256).max`) if the `needle` is not found.
function lastIndexOf(bytes memory subject, bytes memory needle) internal pure returns (uint256) {
return lastIndexOf(subject, needle, type(uint256).max);
}
/// @dev Returns true if `needle` is found in `subject`, false otherwise.
function contains(bytes memory subject, bytes memory needle) internal pure returns (bool) {
return indexOf(subject, needle) != NOT_FOUND;
}
/// @dev Returns whether `subject` starts with `needle`.
function startsWith(bytes memory subject, bytes memory needle) internal pure returns (bool result) {
/// @solidity memory-safe-assembly
assembly {
let n := mload(needle)
// Just using keccak256 directly is actually cheaper.
let t := eq(keccak256(add(subject, 0x20), n), keccak256(add(needle, 0x20), n))
result := lt(gt(n, mload(subject)), t)
}
}
/// @dev Returns whether `subject` ends with `needle`.
function endsWith(bytes memory subject, bytes memory needle) internal pure returns (bool result) {
/// @solidity memory-safe-assembly
assembly {
let n := mload(needle)
let notInRange := gt(n, mload(subject))
// `subject + 0x20 + max(subject.length - needle.length, 0)`.
let t := add(add(subject, 0x20), mul(iszero(notInRange), sub(mload(subject), n)))
// Just using keccak256 directly is actually cheaper.
result := gt(eq(keccak256(t, n), keccak256(add(needle, 0x20), n)), notInRange)
}
}
/// @dev Returns `subject` repeated `times`.
function repeat(bytes memory subject, uint256 times) internal pure returns (bytes memory result) {
/// @solidity memory-safe-assembly
assembly {
let l := mload(subject) // Subject length.
if iszero(or(iszero(times), iszero(l))) {
result := mload(0x40)
subject := add(subject, 0x20)
let o := add(result, 0x20)
for { } 1 { } {
// Copy the `subject` one word at a time.
for { let j := 0 } 1 { } {
mstore(add(o, j), mload(add(subject, j)))
j := add(j, 0x20)
if iszero(lt(j, l)) { break }
}
o := add(o, l)
times := sub(times, 1)
if iszero(times) { break }
}
mstore(o, 0) // Zeroize the slot after the bytes.
mstore(0x40, add(o, 0x20)) // Allocate memory.
mstore(result, sub(o, add(result, 0x20))) // Store the length.
}
}
}
/// @dev Returns a copy of `subject` sliced from `start` to `end` (exclusive).
/// `start` and `end` are byte offsets.
function slice(bytes memory subject, uint256 start, uint256 end) internal pure returns (bytes memory result) {
/// @solidity memory-safe-assembly
assembly {
let l := mload(subject) // Subject length.
if iszero(gt(l, end)) { end := l }
if iszero(gt(l, start)) { start := l }
if lt(start, end) {
result := mload(0x40)
let n := sub(end, start)
let i := add(subject, start)
let w := not(0x1f)
// Copy the `subject` one word at a time, backwards.
for { let j := and(add(n, 0x1f), w) } 1 { } {
mstore(add(result, j), mload(add(i, j)))
j := add(j, w) // `sub(j, 0x20)`.
if iszero(j) { break }
}
let o := add(add(result, 0x20), n)
mstore(o, 0) // Zeroize the slot after the bytes.
mstore(0x40, add(o, 0x20)) // Allocate memory.
mstore(result, n) // Store the length.
}
}
}
/// @dev Returns a copy of `subject` sliced from `start` to the end of the bytes.
/// `start` is a byte offset.
function slice(bytes memory subject, uint256 start) internal pure returns (bytes memory result) {
result = slice(subject, start, type(uint256).max);
}
/// @dev Returns a copy of `subject` sliced from `start` to `end` (exclusive).
/// `start` and `end` are byte offsets. Faster than Solidity's native slicing.
function sliceCalldata(
bytes calldata subject,
uint256 start,
uint256 end
) internal pure returns (bytes calldata result) {
/// @solidity memory-safe-assembly
assembly {
end := xor(end, mul(xor(end, subject.length), lt(subject.length, end)))
start := xor(start, mul(xor(start, subject.length), lt(subject.length, start)))
result.offset := add(subject.offset, start)
result.length := mul(lt(start, end), sub(end, start))
}
}
/// @dev Returns a copy of `subject` sliced from `start` to the end of the bytes.
/// `start` is a byte offset. Faster than Solidity's native slicing.
function sliceCalldata(bytes calldata subject, uint256 start) internal pure returns (bytes calldata result) {
/// @solidity memory-safe-assembly
assembly {
start := xor(start, mul(xor(start, subject.length), lt(subject.length, start)))
result.offset := add(subject.offset, start)
result.length := mul(lt(start, subject.length), sub(subject.length, start))
}
}
/// @dev Reduces the size of `subject` to `n`.
/// If `n` is greater than the size of `subject`, this will be a no-op.
function truncate(bytes memory subject, uint256 n) internal pure returns (bytes memory result) {
/// @solidity memory-safe-assembly
assembly {
result := subject
mstore(mul(lt(n, mload(result)), result), n)
}
}
/// @dev Returns a copy of `subject`, with the length reduced to `n`.
/// If `n` is greater than the size of `subject`, this will be a no-op.
function truncatedCalldata(bytes calldata subject, uint256 n) internal pure returns (bytes calldata result) {
/// @solidity memory-safe-assembly
assembly {
result.offset := subject.offset
result.length := xor(n, mul(xor(n, subject.length), lt(subject.length, n)))
}
}
/// @dev Returns all the indices of `needle` in `subject`.
/// The indices are byte offsets.
function indicesOf(bytes memory subject, bytes memory needle) internal pure returns (uint256[] memory result) {
/// @solidity memory-safe-assembly
assembly {
let searchLen := mload(needle)
if iszero(gt(searchLen, mload(subject))) {
result := mload(0x40)
let i := add(subject, 0x20)
let o := add(result, 0x20)
let subjectSearchEnd := add(sub(add(i, mload(subject)), searchLen), 1)
let h := 0 // The hash of `needle`.
if iszero(lt(searchLen, 0x20)) { h := keccak256(add(needle, 0x20), searchLen) }
let s := mload(add(needle, 0x20))
for { let m := shl(3, sub(0x20, and(searchLen, 0x1f))) } 1 { } {
let t := mload(i)
// Whether the first `searchLen % 32` bytes of `subject` and `needle` matches.
if iszero(shr(m, xor(t, s))) {
if h {
if iszero(eq(keccak256(i, searchLen), h)) {
i := add(i, 1)
if iszero(lt(i, subjectSearchEnd)) { break }
continue
}
}
mstore(o, sub(i, add(subject, 0x20))) // Append to `result`.
o := add(o, 0x20)
i := add(i, searchLen) // Advance `i` by `searchLen`.
if searchLen {
if iszero(lt(i, subjectSearchEnd)) { break }
continue
}
}
i := add(i, 1)
if iszero(lt(i, subjectSearchEnd)) { break }
}
mstore(result, shr(5, sub(o, add(result, 0x20)))) // Store the length of `result`.
// Allocate memory for result.
// We allocate one more word, so this array can be recycled for {split}.
mstore(0x40, add(o, 0x20))
}
}
}
/// @dev Returns a arrays of bytess based on the `delimiter` inside of the `subject` bytes.
function split(bytes memory subject, bytes memory delimiter) internal pure returns (bytes[] memory result) {
uint256[] memory indices = indicesOf(subject, delimiter);
/// @solidity memory-safe-assembly
assembly {
let w := not(0x1f)
let indexPtr := add(indices, 0x20)
let indicesEnd := add(indexPtr, shl(5, add(mload(indices), 1)))
mstore(add(indicesEnd, w), mload(subject))
mstore(indices, add(mload(indices), 1))
for { let prevIndex := 0 } 1 { } {
let index := mload(indexPtr)
mstore(indexPtr, 0x60)
if iszero(eq(index, prevIndex)) {
let element := mload(0x40)
let l := sub(index, prevIndex)
mstore(element, l) // Store the length of the element.
// Copy the `subject` one word at a time, backwards.
for { let o := and(add(l, 0x1f), w) } 1 { } {
mstore(add(element, o), mload(add(add(subject, prevIndex), o)))
o := add(o, w) // `sub(o, 0x20)`.
if iszero(o) { break }
}
mstore(add(add(element, 0x20), l), 0) // Zeroize the slot after the bytes.
// Allocate memory for the length and the bytes, rounded up to a multiple of 32.
mstore(0x40, add(element, and(add(l, 0x3f), w)))
mstore(indexPtr, element) // Store the `element` into the array.
}
prevIndex := add(index, mload(delimiter))
indexPtr := add(indexPtr, 0x20)
if iszero(lt(indexPtr, indicesEnd)) { break }
}
result := indices
if iszero(mload(delimiter)) {
result := add(indices, 0x20)
mstore(result, sub(mload(indices), 2))
}
}
}
/// @dev Returns a concatenated bytes of `a` and `b`.
/// Cheaper than `bytes.concat()` and does not de-align the free memory pointer.
function concat(bytes memory a, bytes memory b) internal pure returns (bytes memory result) {
/// @solidity memory-safe-assembly
assembly {
result := mload(0x40)
let w := not(0x1f)
let aLen := mload(a)
// Copy `a` one word at a time, backwards.
for { let o := and(add(aLen, 0x20), w) } 1 { } {
mstore(add(result, o), mload(add(a, o)))
o := add(o, w) // `sub(o, 0x20)`.
if iszero(o) { break }
}
let bLen := mload(b)
let output := add(result, aLen)
// Copy `b` one word at a time, backwards.
for { let o := and(add(bLen, 0x20), w) } 1 { } {
mstore(add(output, o), mload(add(b, o)))
o := add(o, w) // `sub(o, 0x20)`.
if iszero(o) { break }
}
let totalLen := add(aLen, bLen)
let last := add(add(result, 0x20), totalLen)
mstore(last, 0) // Zeroize the slot after the bytes.
mstore(result, totalLen) // Store the length.
mstore(0x40, add(last, 0x20)) // Allocate memory.
}
}
/// @dev Returns whether `a` equals `b`.
function eq(bytes memory a, bytes memory b) internal pure returns (bool result) {
/// @solidity memory-safe-assembly
assembly {
result := eq(keccak256(add(a, 0x20), mload(a)), keccak256(add(b, 0x20), mload(b)))
}
}
/// @dev Returns whether `a` equals `b`, where `b` is a null-terminated small bytes.
function eqs(bytes memory a, bytes32 b) internal pure returns (bool result) {
/// @solidity memory-safe-assembly
assembly {
// These should be evaluated on compile time, as far as possible.
let m := not(shl(7, div(not(iszero(b)), 255))) // `0x7f7f ...`.
let x := not(or(m, or(b, add(m, and(b, m)))))
let r := shl(7, iszero(iszero(shr(128, x))))
r := or(r, shl(6, iszero(iszero(shr(64, shr(r, x))))))
r := or(r, shl(5, lt(0xffffffff, shr(r, x))))
r := or(r, shl(4, lt(0xffff, shr(r, x))))
r := or(r, shl(3, lt(0xff, shr(r, x))))
// forgefmt: disable-next-item
result := gt(eq(mload(a), add(iszero(x), xor(31, shr(3, r)))),
xor(shr(add(8, r), b), shr(add(8, r), mload(add(a, 0x20)))))
}
}
/// @dev Directly returns `a` without copying.
function directReturn(
bytes memory a
) internal pure {
assembly {
// Assumes that the bytes does not start from the scratch space.
let retStart := sub(a, 0x20)
let retUnpaddedSize := add(mload(a), 0x40)
// Right pad with zeroes. Just in case the bytes is produced
// by a method that doesn't zero right pad.
mstore(add(retStart, retUnpaddedSize), 0)
mstore(retStart, 0x20) // Store the return offset.
// End the transaction, returning the bytes.
return(retStart, and(not(0x1f), add(0x1f, retUnpaddedSize)))
}
}
/// @dev Directly returns `a` with minimal copying.
function directReturn(
bytes[] memory a
) internal pure {
assembly {
let n := mload(a) // `a.length`.
let o := add(a, 0x20) // Start of elements in `a`.
let u := a // Highest memory slot.
let w := not(0x1f)
for { let i := 0 } iszero(eq(i, n)) { i := add(i, 1) } {
let c := add(o, shl(5, i)) // Location of pointer to `a[i]`.
let s := mload(c) // `a[i]`.
let l := mload(s) // `a[i].length`.
let r := and(l, 0x1f) // `a[i].length % 32`.
let z := add(0x20, and(l, w)) // Offset of last word in `a[i]` from `s`.
// If `s` comes before `o`, or `s` is not zero right padded.
if iszero(lt(lt(s, o), or(iszero(r), iszero(shl(shl(3, r), mload(add(s, z))))))) {
let m := mload(0x40)
mstore(m, l) // Copy `a[i].length`.
for { } 1 { } {
mstore(add(m, z), mload(add(s, z))) // Copy `a[i]`, backwards.
z := add(z, w) // `sub(z, 0x20)`.
if iszero(z) { break }
}
let e := add(add(m, 0x20), l)
mstore(e, 0) // Zeroize the slot after the copied bytes.
mstore(0x40, add(e, 0x20)) // Allocate memory.
s := m
}
mstore(c, sub(s, o)) // Convert to calldata offset.
let t := add(l, add(s, 0x20))
if iszero(lt(t, u)) { u := t }
}
let retStart := add(a, w) // Assumes `a` doesn't start from scratch space.
mstore(retStart, 0x20) // Store the return offset.
return(retStart, add(0x40, sub(u, retStart))) // End the transaction.
}
}
/// @dev Returns the word at `offset`, without any bounds checks.
/// To load an address, you can use `address(bytes20(load(a, offset)))`.
function load(bytes memory a, uint256 offset) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
result := mload(add(add(a, 0x20), offset))
}
}
/// @dev Returns the word at `offset`, without any bounds checks.
/// To load an address, you can use `address(bytes20(loadCalldata(a, offset)))`.
function loadCalldata(bytes calldata a, uint256 offset) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
result := calldataload(add(a.offset, offset))
}
}
/// @dev Returns empty calldata bytes. For silencing the compiler.
function emptyCalldata() internal pure returns (bytes calldata result) {
/// @solidity memory-safe-assembly
assembly {
result.length := 0
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (security/ReentrancyGuard.sol)
pragma solidity ^0.8.0;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be _NOT_ENTERED
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import { Address } from "openzeppelin-contracts/utils/Address.sol";
import { ISystemRegistry } from "src/interfaces/ISystemRegistry.sol";
import { SecurityBase } from "src/security/SecurityBase.sol";
import { IBaseRewarder } from "src/interfaces/rewarders/IBaseRewarder.sol";
import { IAccToke } from "src/interfaces/staking/IAccToke.sol";
import { LibAdapter } from "src/libs/LibAdapter.sol";
import { Roles } from "src/libs/Roles.sol";
import { Errors } from "src/utils/Errors.sol";
/**
* @dev An abstract contract that serves as the base for rewarder contracts.
* It implements common functionalities for reward distribution, including calculating rewards per token,
* tracking user rewards, and handling stake-related operations.
* Inherited by rewarder contracts, such as MainRewarder and ExtraRewarder.
* The contract is inspired by the Convex contract but uses block-based duration instead of timestamp-based duration.
*/
abstract contract AbstractRewarder is IBaseRewarder, SecurityBase {
using SafeERC20 for IERC20;
/// @notice The minimum duration for recovering tokens (1 year).
uint256 public constant MINIMUM_RECOVER_DURATION = 31_536_000;
/// @notice The duration of the reward period in blocks.
uint256 public immutable durationInBlock;
/// @notice It is used to determine if the new rewards should be distributed immediately or queued for later. If
/// the ratio of current rewards to the sum of new and queued rewards is less than newRewardRatio, the new rewards
/// are distributed immediately; otherwise, they are added to the queue.
uint256 public immutable newRewardRatio;
/// @notice An instance of the system registry contract.
ISystemRegistry internal immutable systemRegistry;
/// @notice The address of the token to be distributed as rewards.
address public immutable rewardToken;
/// @notice The block number when the current reward period ends.
uint256 public periodInBlockFinish;
/// @notice The rate of reward distribution per block.
uint256 public rewardRate;
/// @notice The block number when rewards were last updated.
uint256 public lastUpdateBlock;
/// @notice The amount of rewards distributed per staked token stored.
uint256 public rewardPerTokenStored;
/// @notice The amount of rewards waiting in the queue to be distributed.
uint256 public queuedRewards;
/// @notice The amount of current rewards being distributed.
uint256 public currentRewards;
/// @notice The total amount of rewards distributed historically.
uint256 public historicalRewards;
/// @notice The amount of reward per token paid to each user.
mapping(address => uint256) public userRewardPerTokenPaid;
/// @notice The amount of rewards for each user.
mapping(address => uint256) public rewards;
/// @notice The duration for locking the Toke token rewards.
uint256 public tokeLockDuration;
/// @notice Whitelisted addresses for queuing new rewards.
mapping(address => bool) public whitelistedAddresses;
/// @notice Role that manages rewarder contract.
bytes32 internal immutable rewardRole;
/**
* @param _systemRegistry Address of the system registry.
* @param _rewardToken Address of the reward token.
* @param _newRewardRatio The new reward rate.
* @param _durationInBlock The duration of the reward period in blocks.
* @param _rewardRole Role that controls role based functions in Rewarder.
*/
constructor(
ISystemRegistry _systemRegistry,
address _rewardToken,
uint256 _newRewardRatio,
uint256 _durationInBlock,
bytes32 _rewardRole
) SecurityBase(address(_systemRegistry.accessController())) {
Errors.verifyNotZero(_rewardToken, "_rewardToken");
Errors.verifyNotZero(_durationInBlock, "_durationInBlock");
Errors.verifyNotZero(_newRewardRatio, "_newRewardRatio");
Errors.verifyNotZero(_rewardRole, "_rewardRole");
systemRegistry = _systemRegistry;
if (!systemRegistry.isRewardToken(_rewardToken)) {
revert Errors.InvalidParam("_rewardToken");
}
rewardToken = _rewardToken;
newRewardRatio = _newRewardRatio;
durationInBlock = _durationInBlock;
rewardRole = _rewardRole;
}
/// @notice Restricts access to whitelisted addresses or holders of the liquidator role.
modifier onlyWhitelisted() {
if (!whitelistedAddresses[msg.sender] && !_hasRole(Roles.LIQUIDATOR_MANAGER, msg.sender)) {
revert Errors.AccessDenied();
}
_;
}
/**
* @notice Internal function that updates the user's rewards.
* @param account The address of the user to update the rewards for.
*/
function _updateReward(
address account
) internal {
uint256 earnedRewards = 0;
rewardPerTokenStored = rewardPerToken();
lastUpdateBlock = lastBlockRewardApplicable();
if (rewardPerTokenStored > 0) {
if (account != address(0)) {
earnedRewards = earned(account);
rewards[account] = earnedRewards;
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
}
emit UserRewardUpdated(account, earnedRewards, rewardPerTokenStored, lastUpdateBlock);
}
/// @inheritdoc IBaseRewarder
function lastBlockRewardApplicable() public view returns (uint256) {
return block.number < periodInBlockFinish ? block.number : periodInBlockFinish;
}
/// @inheritdoc IBaseRewarder
function rewardPerToken() public view returns (uint256) {
uint256 total = totalSupply();
if (total == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + ((lastBlockRewardApplicable() - lastUpdateBlock) * rewardRate * 1e18 / total);
}
/**
* @inheritdoc IBaseRewarder
* @dev
* The function calculates the earned rewards based on the balance of the account,
* the total supply of the staked tokens, the rewards per token and the last reward rate
* the user has been paid at. The reward rate is determined by the `rewardPerToken`
* function and is a measure of the amount of rewards distributed per staked token
* per block.
*
* The amount of earned rewards is calculated as follows:
* - First, it calculates the difference between the current reward per token and
* the last reward rate the user was paid at, which gives the reward rate per token
* since the user last claimed rewards.
* - This difference is multiplied by the balance of the account to find the total
* amount of rewards the account has earned since it last claimed rewards.
* - Finally, the function adds the rewards that have not yet been claimed by the
* user to find the total amount of earned rewards.
*/
function earned(
address account
) public view returns (uint256) {
return (balanceOf(account) * (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18) + rewards[account];
}
/**
* @inheritdoc IBaseRewarder
* @dev The function transfers the new rewards from the caller to this contract,
* ensuring that the deposited amount matches the declared rewards.
* Irrespective of whether we're near the start or the end of a reward period, if the accrued rewards
* are too large relative to the new rewards (i.e., queuedRatio is greater than newRewardRatio), the new
* rewards will be added to the queue rather than being immediately distributed.
*/
function queueNewRewards(
uint256 newRewards
) external onlyWhitelisted {
uint256 startingQueuedRewards = queuedRewards;
uint256 startingNewRewards = newRewards;
newRewards += startingQueuedRewards;
if (block.number >= periodInBlockFinish) {
notifyRewardAmount(newRewards);
queuedRewards = 0;
} else {
uint256 elapsedBlock = block.number - (periodInBlockFinish - durationInBlock);
uint256 currentAtNow = rewardRate * elapsedBlock;
uint256 queuedRatio = currentAtNow * 1000 / newRewards;
if (queuedRatio < newRewardRatio) {
notifyRewardAmount(newRewards);
queuedRewards = 0;
} else {
queuedRewards = newRewards;
}
}
emit QueuedRewardsUpdated(startingQueuedRewards, startingNewRewards, queuedRewards);
// Transfer the new rewards from the caller to this contract.
IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), startingNewRewards);
}
/**
* @notice Notifies the contract about the amount of reward tokens to be distributed.
* @param reward The amount of reward tokens to be distributed.
* @dev The function updates the rewardRate, lastUpdateBlock, periodInBlockFinish, and historicalRewards.
* It calculates the remaining reward based on the current block number and adjusts the reward rate
* accordingly.
*
* If the current block number is within the reward period, the remaining reward is added to the reward queue
* and will be distributed gradually over the remaining duration.
* If the current block number exceeds the reward period, the remaining reward is distributed immediately.
*/
function notifyRewardAmount(
uint256 reward
) internal {
historicalRewards += reward;
// Correctly calculate leftover reward when totalSupply() is 0.
if (totalSupply() == 0) {
if (lastUpdateBlock < periodInBlockFinish) {
// slither-disable-next-line divide-before-multiply
reward += (periodInBlockFinish - lastUpdateBlock) * rewardRate;
}
} else if (block.number < periodInBlockFinish) {
uint256 remaining = periodInBlockFinish - block.number;
// slither-disable-next-line divide-before-multiply
uint256 leftover = remaining * rewardRate;
reward += leftover;
}
_updateReward(address(0));
// slither-disable-next-line divide-before-multiply
rewardRate = reward / durationInBlock;
// If `reward` < `durationInBlock`, it will result in a `rewardRate` of 0, which we want to prevent.
if (rewardRate <= 0) revert Errors.ZeroAmount();
currentRewards = reward;
lastUpdateBlock = block.number;
periodInBlockFinish = block.number + durationInBlock;
emit RewardAdded(reward, rewardRate, lastUpdateBlock, periodInBlockFinish, historicalRewards);
}
/**
* inheritdoc IBaseRewarder
* @dev If the lock duration is set to 0, it turns off the staking functionality for Toke tokens.
* @dev If the lock duration is greater than 0, it should be long enough to satisfy the minimum staking duration
* requirement of the accToke contract.
*/
function setTokeLockDuration(
uint256 _tokeLockDuration
) external hasRole(rewardRole) {
// if duration is not set to 0 (that would turn off functionality), make sure it's long enough for accToke
if (_tokeLockDuration > 0) {
Errors.verifyNotZero(address(systemRegistry.accToke()), "accToke");
if (_tokeLockDuration < systemRegistry.accToke().minStakeDuration()) {
revert IAccToke.StakingDurationTooShort();
}
}
tokeLockDuration = _tokeLockDuration;
emit TokeLockDurationUpdated(_tokeLockDuration);
}
/// @inheritdoc IBaseRewarder
function addToWhitelist(
address wallet
) external override hasRole(rewardRole) {
Errors.verifyNotZero(wallet, "wallet");
if (whitelistedAddresses[wallet]) {
revert Errors.ItemExists();
}
whitelistedAddresses[wallet] = true;
emit AddedToWhitelist(wallet);
}
/// @inheritdoc IBaseRewarder
function removeFromWhitelist(
address wallet
) external override hasRole(rewardRole) {
if (!whitelistedAddresses[wallet]) {
revert Errors.ItemNotFound();
}
whitelistedAddresses[wallet] = false;
emit RemovedFromWhitelist(wallet);
}
/// @inheritdoc IBaseRewarder
function isWhitelisted(
address wallet
) external view override returns (bool) {
return whitelistedAddresses[wallet];
}
/**
* @notice Internal function to distribute rewards to a specific account.
* @param account The address of the user to distribute rewards to.
* @param recipient The address to send the rewards to.
*/
function _getReward(address account, address recipient) internal {
Errors.verifyNotZero(account, "account");
Errors.verifyNotZero(recipient, "recipient");
uint256 reward = earned(account);
(IAccToke accToke, address tokeAddress) = (systemRegistry.accToke(), address(systemRegistry.toke()));
// slither-disable-next-line incorrect-equality
if (reward == 0) return;
// if NOT toke, or staking is turned off (by duration = 0), just send reward back
if (rewardToken != tokeAddress || tokeLockDuration == 0) {
rewards[account] = 0;
emit RewardPaid(account, recipient, reward);
IERC20(rewardToken).safeTransfer(recipient, reward);
} else if (accToke.isStakeableAmount(reward)) {
rewards[account] = 0;
emit RewardPaid(account, recipient, reward);
// authorize accToke to get our reward Toke
LibAdapter._approve(IERC20(tokeAddress), address(accToke), reward);
// stake Toke
accToke.stake(reward, tokeLockDuration, recipient);
}
}
/**
* @notice Internal function to handle withdrawals.
* @param account The address of the user to handle withdrawal.
* @dev This function primarily checks for valid parameters and emits an event.
* It adopts a pattern established by Convex. It helps with:
* - Identifying system errors (if a revert happens here, there is an issue within our system).
* - Enhancing system monitoring capabilities through emitted events.
* @param amount The amount to be withdrawn.
*/
function _withdrawAbstractRewarder(address account, uint256 amount) internal {
Errors.verifyNotZero(account, "account");
Errors.verifyNotZero(amount, "amount");
emit Withdrawn(account, amount);
}
/**
* @notice Internal function to handle staking.
* @dev This function primarily checks for valid parameters and emits an event.
* It adopts a pattern established by Convex. It helps with:
* - Identifying system errors (if a revert happens here, there is an issue within our system).
* - Enhancing system monitoring capabilities through emitted events.
* @param account The address of the user to handle staking.
* @param amount The amount to be staked.
*/
function _stakeAbstractRewarder(address account, uint256 amount) internal {
Errors.verifyNotZero(account, "account");
Errors.verifyNotZero(amount, "amount");
emit Staked(account, amount);
}
/// @inheritdoc IBaseRewarder
function totalSupply() public view virtual returns (uint256);
/// @inheritdoc IBaseRewarder
function recover(address token, address recipient) external override hasRole(Roles.TOKEN_RECOVERY_MANAGER) {
Errors.verifyNotZero(token, "token");
Errors.verifyNotZero(recipient, "recipient");
if (recipient == address(this)) revert Errors.InvalidAddress(recipient);
if (!canTokenBeRecovered(token)) revert Errors.AssetNotAllowed(token);
if (block.number < lastUpdateBlock + MINIMUM_RECOVER_DURATION && token == rewardToken) {
revert RecoverDurationPending();
}
if (token == LibAdapter.CURVE_REGISTRY_ETH_ADDRESS_POINTER) {
uint256 tokenBalance = address(this).balance;
if (tokenBalance > 0) {
emit Recovered(token, recipient, tokenBalance);
Address.sendValue(payable(recipient), tokenBalance);
}
} else {
uint256 tokenBalance = IERC20(token).balanceOf(address(this));
if (tokenBalance > 0) {
emit Recovered(token, recipient, tokenBalance);
IERC20(token).safeTransfer(recipient, tokenBalance);
}
}
}
/**
* @notice Check if a token is recoverable.
* @param token The address to be checked.
* @return bool indicating if the token is recoverable.
*/
function canTokenBeRecovered(
address token
) public view virtual returns (bool);
/// @inheritdoc IBaseRewarder
function balanceOf(
address account
) public view virtual returns (uint256);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (proxy/Proxy.sol)
pragma solidity ^0.8.0;
/**
* @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM
* instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to
* be specified by overriding the virtual {_implementation} function.
*
* Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a
* different contract through the {_delegate} function.
*
* The success and return data of the delegated call will be returned back to the caller of the proxy.
*/
abstract contract Proxy {
/**
* @dev Delegates the current call to `implementation`.
*
* This function does not return to its internal call site, it will return directly to the external caller.
*/
function _delegate(address implementation) internal virtual {
assembly {
// Copy msg.data. We take full control of memory in this inline assembly
// block because it will not return to Solidity code. We overwrite the
// Solidity scratch pad at memory position 0.
calldatacopy(0, 0, calldatasize())
// Call the implementation.
// out and outsize are 0 because we don't know the size yet.
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
// Copy the returned data.
returndatacopy(0, 0, returndatasize())
switch result
// delegatecall returns 0 on error.
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
/**
* @dev This is a virtual function that should be overridden so it returns the address to which the fallback function
* and {_fallback} should delegate.
*/
function _implementation() internal view virtual returns (address);
/**
* @dev Delegates the current call to the address returned by `_implementation()`.
*
* This function does not return to its internal call site, it will return directly to the external caller.
*/
function _fallback() internal virtual {
_beforeFallback();
_delegate(_implementation());
}
/**
* @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other
* function in the contract matches the call data.
*/
fallback() external payable virtual {
_fallback();
}
/**
* @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data
* is empty.
*/
receive() external payable virtual {
_fallback();
}
/**
* @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback`
* call, or as part of the Solidity `fallback` or `receive` functions.
*
* If overridden should call `super._beforeFallback()`.
*/
function _beforeFallback() internal virtual {}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (proxy/ERC1967/ERC1967Upgrade.sol)
pragma solidity ^0.8.2;
import "../beacon/IBeacon.sol";
import "../../interfaces/draft-IERC1822.sol";
import "../../utils/Address.sol";
import "../../utils/StorageSlot.sol";
/**
* @dev This abstract contract provides getters and event emitting update functions for
* https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots.
*
* _Available since v4.1._
*
* @custom:oz-upgrades-unsafe-allow delegatecall
*/
abstract contract ERC1967Upgrade {
// This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1
bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;
/**
* @dev Storage slot with the address of the current implementation.
* This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is
* validated in the constructor.
*/
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
/**
* @dev Emitted when the implementation is upgraded.
*/
event Upgraded(address indexed implementation);
/**
* @dev Returns the current implementation address.
*/
function _getImplementation() internal view returns (address) {
return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
}
/**
* @dev Stores a new address in the EIP1967 implementation slot.
*/
function _setImplementation(address newImplementation) private {
require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
}
/**
* @dev Perform implementation upgrade
*
* Emits an {Upgraded} event.
*/
function _upgradeTo(address newImplementation) internal {
_setImplementation(newImplementation);
emit Upgraded(newImplementation);
}
/**
* @dev Perform implementation upgrade with additional setup call.
*
* Emits an {Upgraded} event.
*/
function _upgradeToAndCall(
address newImplementation,
bytes memory data,
bool forceCall
) internal {
_upgradeTo(newImplementation);
if (data.length > 0 || forceCall) {
Address.functionDelegateCall(newImplementation, data);
}
}
/**
* @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call.
*
* Emits an {Upgraded} event.
*/
function _upgradeToAndCallUUPS(
address newImplementation,
bytes memory data,
bool forceCall
) internal {
// Upgrades from old implementations will perform a rollback test. This test requires the new
// implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing
// this special case will break upgrade paths from old UUPS implementation to new ones.
if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) {
_setImplementation(newImplementation);
} else {
try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) {
require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID");
} catch {
revert("ERC1967Upgrade: new implementation is not UUPS");
}
_upgradeToAndCall(newImplementation, data, forceCall);
}
}
/**
* @dev Storage slot with the admin of the contract.
* This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is
* validated in the constructor.
*/
bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
/**
* @dev Emitted when the admin account has changed.
*/
event AdminChanged(address previousAdmin, address newAdmin);
/**
* @dev Returns the current admin.
*/
function _getAdmin() internal view returns (address) {
return StorageSlot.getAddressSlot(_ADMIN_SLOT).value;
}
/**
* @dev Stores a new address in the EIP1967 admin slot.
*/
function _setAdmin(address newAdmin) private {
require(newAdmin != address(0), "ERC1967: new admin is the zero address");
StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;
}
/**
* @dev Changes the admin of the proxy.
*
* Emits an {AdminChanged} event.
*/
function _changeAdmin(address newAdmin) internal {
emit AdminChanged(_getAdmin(), newAdmin);
_setAdmin(newAdmin);
}
/**
* @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy.
* This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor.
*/
bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;
/**
* @dev Emitted when the beacon is upgraded.
*/
event BeaconUpgraded(address indexed beacon);
/**
* @dev Returns the current beacon.
*/
function _getBeacon() internal view returns (address) {
return StorageSlot.getAddressSlot(_BEACON_SLOT).value;
}
/**
* @dev Stores a new beacon in the EIP1967 beacon slot.
*/
function _setBeacon(address newBeacon) private {
require(Address.isContract(newBeacon), "ERC1967: new beacon is not a contract");
require(
Address.isContract(IBeacon(newBeacon).implementation()),
"ERC1967: beacon implementation is not a contract"
);
StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon;
}
/**
* @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does
* not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that).
*
* Emits a {BeaconUpgraded} event.
*/
function _upgradeBeaconToAndCall(
address newBeacon,
bytes memory data,
bool forceCall
) internal {
_setBeacon(newBeacon);
emit BeaconUpgraded(newBeacon);
if (data.length > 0 || forceCall) {
Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (access/IAccessControl.sol)
pragma solidity ^0.8.0;
/**
* @dev External interface of AccessControl declared to support ERC165 detection.
*/
interface IAccessControl {
/**
* @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.
*
* _Available since v3.1._
*/
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, an admin role
* bearer except when using {AccessControl-_setupRole}.
*/
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 `account`.
*/
function renounceRole(bytes32 role, address account) external;
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
interface IBaseAssetVault {
/// @notice Asset that this Vault primarily manages
/// @dev Vault decimals should be the same as the baseAsset
function baseAsset() external view returns (address);
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
import { ILSTStats } from "src/interfaces/stats/ILSTStats.sol";
/// @title Return stats DEXs with LSTs
interface IDexLSTStats {
event DexSnapshotTaken(uint256 snapshotTimestamp, uint256 priorFeeApr, uint256 newFeeApr, uint256 unfilteredFeeApr);
struct StakingIncentiveStats {
// time-weighted average total supply to prevent spikes/attacks from impacting rebalancing
uint256 safeTotalSupply;
// rewardTokens, annualizedRewardAmounts, and periodFinishForRewards will match indexes
// they are split to workaround an issue with forge having nested structs
// address of the reward tokens
address[] rewardTokens;
// the annualized reward rate for the reward token
uint256[] annualizedRewardAmounts;
// the timestamp for when the rewards are set to terminate
uint40[] periodFinishForRewards;
// incentive rewards score. max 48, min 0
uint8 incentiveCredits;
}
struct DexLSTStatsData {
uint256 lastSnapshotTimestamp;
uint256 feeApr;
uint256[] reservesInEth;
StakingIncentiveStats stakingIncentiveStats;
ILSTStats.LSTStatsData[] lstStatsData;
}
/// @notice Get the current stats for the DEX with underlying LST tokens
/// @dev Returned data is a combination of current data and filtered snapshots
/// @return dexLSTStatsData current data on the DEX
function current() external returns (DexLSTStatsData memory dexLSTStatsData);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (utils/Strings.sol)
pragma solidity ^0.8.0;
import "./math/Math.sol";
/**
* @dev String operations.
*/
library Strings {
bytes16 private constant _SYMBOLS = "0123456789abcdef";
uint8 private constant _ADDRESS_LENGTH = 20;
/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/
function toString(uint256 value) internal pure returns (string memory) {
unchecked {
uint256 length = Math.log10(value) + 1;
string memory buffer = new string(length);
uint256 ptr;
/// @solidity memory-safe-assembly
assembly {
ptr := add(buffer, add(32, length))
}
while (true) {
ptr--;
/// @solidity memory-safe-assembly
assembly {
mstore8(ptr, byte(mod(value, 10), _SYMBOLS))
}
value /= 10;
if (value == 0) break;
}
return buffer;
}
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/
function toHexString(uint256 value) internal pure returns (string memory) {
unchecked {
return toHexString(value, Math.log256(value) + 1);
}
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
*/
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = _SYMBOLS[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
return string(buffer);
}
/**
* @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation.
*/
function toHexString(address addr) internal pure returns (string memory) {
return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (proxy/beacon/IBeacon.sol)
pragma solidity ^0.8.0;
/**
* @dev This is the interface that {BeaconProxy} expects of its beacon.
*/
interface IBeacon {
/**
* @dev Must return an address that can be used as a delegate call target.
*
* {BeaconProxy} will check that this address is a contract.
*/
function implementation() external view returns (address);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (interfaces/draft-IERC1822.sol)
pragma solidity ^0.8.0;
/**
* @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified
* proxy whose upgrades are fully controlled by the current implementation.
*/
interface IERC1822Proxiable {
/**
* @dev Returns the storage slot that the proxiable contract assumes is being used to store the implementation
* address.
*
* IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks
* bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this
* function revert if invoked through a proxy.
*/
function proxiableUUID() external view returns (bytes32);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (utils/StorageSlot.sol)
pragma solidity ^0.8.0;
/**
* @dev Library for reading and writing primitive types to specific storage slots.
*
* Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts.
* This library helps with reading and writing to such slots without the need for inline assembly.
*
* The functions in this library return Slot structs that contain a `value` member that can be used to read or write.
*
* Example usage to set ERC1967 implementation slot:
* ```
* contract ERC1967 {
* bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
*
* function _getImplementation() internal view returns (address) {
* return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
* }
*
* function _setImplementation(address newImplementation) internal {
* require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
* StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
* }
* }
* ```
*
* _Available since v4.1 for `address`, `bool`, `bytes32`, and `uint256`._
*/
library StorageSlot {
struct AddressSlot {
address value;
}
struct BooleanSlot {
bool value;
}
struct Bytes32Slot {
bytes32 value;
}
struct Uint256Slot {
uint256 value;
}
/**
* @dev Returns an `AddressSlot` with member `value` located at `slot`.
*/
function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `BooleanSlot` with member `value` located at `slot`.
*/
function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `Bytes32Slot` with member `value` located at `slot`.
*/
function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `Uint256Slot` with member `value` located at `slot`.
*/
function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
}
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity ^0.8.24;
/// @title Return stats on base LSTs
interface ILSTStats {
struct LSTStatsData {
uint256 lastSnapshotTimestamp;
uint256 baseApr;
int256 discount; // positive number is a discount, negative is a premium
uint24[10] discountHistory; // 7 decimal precision
uint40 discountTimestampByPercent; // timestamp that the token reached 1pct discount
}
/// @notice Get the current stats for the LST
/// @dev Returned data is a combination of current data and filtered snapshots
/// @return lstStatsData current data on the LST
function current() external returns (LSTStatsData memory lstStatsData);
/// @notice Get the EthPerToken (or Share) for the LST
/// @return ethPerShare the backing eth for the LST
function calculateEthPerToken() external view returns (uint256 ethPerShare);
/// @notice Returns whether to use the market price when calculating discount
/// @dev Will be true for rebasing tokens and other non-standard tokens
function usePriceAsDiscount() external view returns (bool useAsDiscount);
}