Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/contracts/EtherFiARM.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {AbstractARM} from "./AbstractARM.sol";
import {IERC20, IWETH, IEETHWithdrawal, IEETHWithdrawalNFT, IEETHRedemptionManager} from "./Interfaces.sol";
/**
* @title EtherFi (eETH) Automated Redemption Manager (ARM)
* @dev This implementation supports multiple Liquidity Providers (LPs) with single buy and sell prices.
* It also integrates to a CapManager contract that caps the amount of assets a liquidity provider
* can deposit and caps the ARM's total assets.
* A performance fee is also collected on increases in the ARM's total assets.
* @author Origin Protocol Inc
*/
contract EtherFiARM is Initializable, AbstractARM, IERC721Receiver {
/// @notice The address of the EtherFi eETH token
IERC20 public immutable eeth;
/// @notice The address of the Wrapped ETH (WETH) token
IWETH public immutable weth;
/// @notice The address of the EtherFi Withdrawal Queue contract
IEETHWithdrawal public immutable etherfiWithdrawalQueue;
/// @notice The address of the EtherFi Withdrawal NFT contract
IEETHWithdrawalNFT public immutable etherfiWithdrawalNFT;
/// @notice The address of the EtherFi Redemption Manager contract
IEETHRedemptionManager public immutable etherfiRedemptionManager;
/// @notice The amount of eETH in the EtherFi Withdrawal Queue
uint256 public etherfiWithdrawalQueueAmount;
/// @notice stores the requested amount for each EtherFi withdrawal
mapping(uint256 id => uint256 amount) public etherfiWithdrawalRequests;
event RequestEtherFiWithdrawal(uint256 amount, uint256 requestId);
event ClaimEtherFiWithdrawals(uint256[] requestIds);
event RegisterEtherFiWithdrawalRequests(uint256[] requestIds, uint256 totalAmountRequested);
/// @param _eeth The address of the eETH token
/// @param _weth The address of the WETH token
/// @param _etherfiWithdrawalQueue The address of the EtherFi's withdrawal queue contract
/// @param _claimDelay The delay in seconds before a user can claim a redeem from the request
/// @param _minSharesToRedeem The minimum amount of shares to redeem from the active lending market
/// @param _allocateThreshold The minimum amount of liquidity assets in excess of the ARM buffer before
/// the ARM can allocate to a active lending market.
constructor(
address _eeth,
address _weth,
address _etherfiWithdrawalQueue,
uint256 _claimDelay,
uint256 _minSharesToRedeem,
int256 _allocateThreshold,
address _etherfiWithdrawalNFT,
address _etherfiRedemptionManager
) AbstractARM(_weth, _eeth, _weth, _claimDelay, _minSharesToRedeem, _allocateThreshold) {
eeth = IERC20(_eeth);
weth = IWETH(_weth);
etherfiWithdrawalQueue = IEETHWithdrawal(_etherfiWithdrawalQueue);
etherfiWithdrawalNFT = IEETHWithdrawalNFT(_etherfiWithdrawalNFT);
etherfiRedemptionManager = IEETHRedemptionManager(_etherfiRedemptionManager);
_disableInitializers();
}
/// @notice Initialize the storage variables stored in the proxy contract.
/// The deployer that calls initialize has to approve the ARM's proxy contract to transfer 1e12 WETH.
/// @param _name The name of the liquidity provider (LP) token.
/// @param _symbol The symbol of the liquidity provider (LP) token.
/// @param _operator The address of the account that can request and claim EtherFi withdrawals.
/// @param _fee The performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent).
/// 10,000 = 100% performance fee
/// 1,500 = 15% performance fee
/// @param _feeCollector The account that can collect the performance fee
/// @param _capManager The address of the CapManager contract
function initialize(
string calldata _name,
string calldata _symbol,
address _operator,
uint256 _fee,
address _feeCollector,
address _capManager
) external initializer {
_initARM(_operator, _name, _symbol, _fee, _feeCollector, _capManager);
// Approve the EtherFi withdrawal queue contract. Used for redemption requests.
eeth.approve(address(etherfiWithdrawalQueue), type(uint256).max);
}
/**
* @notice Request an eETH for ETH withdrawal.
* Reference: https://etherfi.gitbook.io/etherfi/contracts-and-integrations/how-to
*/
function requestEtherFiWithdrawal(uint256 amount) external onlyOperatorOrOwner returns (uint256 requestId) {
// Request the withdrawal from the EtherFi Withdrawal Queue.
requestId = etherfiWithdrawalQueue.requestWithdraw(address(this), amount);
// Store the requested amount from storage
etherfiWithdrawalRequests[requestId] = amount;
// Increase the Ether outstanding from the EtherFi Withdrawal Queue
etherfiWithdrawalQueueAmount += amount;
// Emit event for the request
emit RequestEtherFiWithdrawal(amount, requestId);
}
/**
* @notice Claim the ETH owed from the redemption requests and convert it to WETH.
* Before calling this method, caller should check on the request NFTs to ensure the withdrawal was processed.
* @param requestIds The request IDs of the withdrawal requests.
* Call `findCheckpointHints` on the EtherFi withdrawal queue contract to get the hint IDs.
*/
function claimEtherFiWithdrawals(uint256[] calldata requestIds) external {
// Claim the NFTs for ETH.
etherfiWithdrawalNFT.batchClaimWithdraw(requestIds);
// Reduce the amount outstanding from the EtherFi Withdrawal Queue.
// The amount of ETH claimed from the EtherFi Withdrawal Queue can be less than the requested amount
// in the event of a mass slashing event of EtherFi validators.
uint256 totalAmountRequested = 0;
for (uint256 i = 0; i < requestIds.length; i++) {
// Read the requested amount from storage
uint256 requestAmount = etherfiWithdrawalRequests[requestIds[i]];
// Validate the request came from this EtherFi ARM contract and not
// transferred in from another account.
require(requestAmount > 0, "EtherFiARM: invalid request");
totalAmountRequested += requestAmount;
}
// Store the reduced outstanding withdrawals from the EtherFi Withdrawal Queue
if (etherfiWithdrawalQueueAmount < totalAmountRequested) {
// This can happen if a EtherFi withdrawal request was transferred to the ARM contract
etherfiWithdrawalQueueAmount = 0;
} else {
etherfiWithdrawalQueueAmount -= totalAmountRequested;
}
// Wrap all the received ETH to WETH.
weth.deposit{value: address(this).balance}();
emit ClaimEtherFiWithdrawals(requestIds);
}
/**
* @dev Calculates the amount of eETH in the EtherFi Withdrawal Queue.
*/
function _externalWithdrawQueue() internal view override returns (uint256) {
return etherfiWithdrawalQueueAmount;
}
/// @notice This payable method is necessary for receiving ETH claimed from the EtherFi withdrawal queue.
receive() external payable {}
/// @notice To be able to receive the NFTs from the EtherFi withdrawal queue contract.
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data)
external
pure
override
returns (bytes4)
{
return IERC721Receiver.onERC721Received.selector;
}
}
"
},
"dependencies/@openzeppelin-contracts-upgradeable-5.0.2-5.0.2/contracts/proxy/utils/Initializable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (proxy/utils/Initializable.sol)
pragma solidity ^0.8.20;
/**
* @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]
* ```solidity
* 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 Storage of the initializable contract.
*
* It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions
* when using with upgradeable contracts.
*
* @custom:storage-location erc7201:openzeppelin.storage.Initializable
*/
struct InitializableStorage {
/**
* @dev Indicates that the contract has been initialized.
*/
uint64 _initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool _initializing;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00;
/**
* @dev The contract is already initialized.
*/
error InvalidInitialization();
/**
* @dev The contract is not initializing.
*/
error NotInitializing();
/**
* @dev Triggered when the contract has been initialized or reinitialized.
*/
event Initialized(uint64 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 in the context of a constructor an `initializer` may be invoked any
* number of times. This behavior in the constructor can be useful during testing and is not expected to be used in
* production.
*
* Emits an {Initialized} event.
*/
modifier initializer() {
// solhint-disable-next-line var-name-mixedcase
InitializableStorage storage $ = _getInitializableStorage();
// Cache values to avoid duplicated sloads
bool isTopLevelCall = !$._initializing;
uint64 initialized = $._initialized;
// Allowed calls:
// - initialSetup: the contract is not in the initializing state and no previous version was
// initialized
// - construction: the contract is initialized at version 1 (no reininitialization) and the
// current contract is just being deployed
bool initialSetup = initialized == 0 && isTopLevelCall;
bool construction = initialized == 1 && address(this).code.length == 0;
if (!initialSetup && !construction) {
revert InvalidInitialization();
}
$._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 2**64 - 1 will prevent any future reinitialization.
*
* Emits an {Initialized} event.
*/
modifier reinitializer(uint64 version) {
// solhint-disable-next-line var-name-mixedcase
InitializableStorage storage $ = _getInitializableStorage();
if ($._initializing || $._initialized >= version) {
revert InvalidInitialization();
}
$._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() {
_checkInitializing();
_;
}
/**
* @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}.
*/
function _checkInitializing() internal view virtual {
if (!_isInitializing()) {
revert NotInitializing();
}
}
/**
* @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 {
// solhint-disable-next-line var-name-mixedcase
InitializableStorage storage $ = _getInitializableStorage();
if ($._initializing) {
revert InvalidInitialization();
}
if ($._initialized != type(uint64).max) {
$._initialized = type(uint64).max;
emit Initialized(type(uint64).max);
}
}
/**
* @dev Returns the highest version that has been initialized. See {reinitializer}.
*/
function _getInitializedVersion() internal view returns (uint64) {
return _getInitializableStorage()._initialized;
}
/**
* @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
*/
function _isInitializing() internal view returns (bool) {
return _getInitializableStorage()._initializing;
}
/**
* @dev Returns a pointer to the storage namespace.
*/
// solhint-disable-next-line var-name-mixedcase
function _getInitializableStorage() private pure returns (InitializableStorage storage $) {
assembly {
$.slot := INITIALIZABLE_STORAGE
}
}
}
"
},
"dependencies/@openzeppelin-contracts-5.0.2-5.0.2/contracts/token/ERC721/IERC721Receiver.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/IERC721Receiver.sol)
pragma solidity ^0.8.20;
/**
* @title ERC721 token receiver interface
* @dev Interface for any contract that wants to support safeTransfers
* from ERC721 asset contracts.
*/
interface IERC721Receiver {
/**
* @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
* by `operator` from `from`, this function is called.
*
* It must return its Solidity selector to confirm the token transfer.
* If any other value is returned or the interface is not implemented by the recipient, the transfer will be
* reverted.
*
* The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`.
*/
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
"
},
"src/contracts/AbstractARM.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;
import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {OwnableOperable} from "./OwnableOperable.sol";
import {IERC20, ICapManager} from "./Interfaces.sol";
/**
* @title Generic Automated Redemption Manager (ARM)
* @author Origin Protocol Inc
*/
abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
////////////////////////////////////////////////////
/// Constants
////////////////////////////////////////////////////
/// @notice Maximum amount the Owner can set the cross price below 1 scaled to 36 decimals.
/// 20e32 is a 0.2% deviation, or 20 basis points.
uint256 public constant MAX_CROSS_PRICE_DEVIATION = 20e32;
/// @notice Scale of the prices.
uint256 public constant PRICE_SCALE = 1e36;
/// @notice The amount of shares that are minted to a dead address on initialization
uint256 internal constant MIN_TOTAL_SUPPLY = 1e12;
/// @notice The address with no known private key that the initial shares are minted to
address internal constant DEAD_ACCOUNT = 0x000000000000000000000000000000000000dEaD;
/// @notice The scale of the performance fee
/// 10,000 = 100% performance fee
uint256 public constant FEE_SCALE = 10000;
////////////////////////////////////////////////////
/// Immutable Variables
////////////////////////////////////////////////////
/// @notice The minimum amount of shares that can be redeemed from the active market.
uint256 public immutable minSharesToRedeem;
/// @notice The minimum amount of liquidity assets in excess of the ARM buffer before
/// the ARM can allocate to a active lending market.
/// This should be close to zero.
/// @dev This prevents allocate flipping between depositing/withdrawing to/from the active market
int256 public immutable allocateThreshold;
/// @notice The address of the asset that is used to add and remove liquidity. eg WETH
/// This is also the quote asset when the prices are set.
/// eg the stETH/WETH price has a base asset of stETH and quote asset of WETH.
address public immutable liquidityAsset;
/// @notice The asset being purchased by the ARM and put in the withdrawal queue. eg stETH
address public immutable baseAsset;
/// @notice The swap input token that is transferred to this contract.
/// From a User perspective, this is the token being sold.
/// token0 is also compatible with the Uniswap V2 Router interface.
IERC20 public immutable token0;
/// @notice The swap output token that is transferred from this contract.
/// From a User perspective, this is the token being bought.
/// token1 is also compatible with the Uniswap V2 Router interface.
IERC20 public immutable token1;
/// @notice The delay before a withdrawal request can be claimed in seconds. eg 600 is 10 minutes.
uint256 public immutable claimDelay;
////////////////////////////////////////////////////
/// Storage Variables
////////////////////////////////////////////////////
/**
* @notice For one `token0` from a Trader, how many `token1` does the pool send.
* For example, if `token0` is WETH and `token1` is stETH then
* `traderate0` is the WETH/stETH price.
* From a Trader's perspective, this is the buy price.
* From the ARM's perspective, this is the sell price.
* Rate is to 36 decimals (1e36).
* To convert to a stETH/WETH price, use `PRICE_SCALE * PRICE_SCALE / traderate0`.
*/
uint256 public traderate0;
/**
* @notice For one `token1` from a Trader, how many `token0` does the pool send.
* For example, if `token0` is WETH and `token1` is stETH then
* `traderate1` is the stETH/WETH price.
* From a Trader's perspective, this is the sell price.
* From a ARM's perspective, this is the buy price.
* Rate is to 36 decimals (1e36).
*/
uint256 public traderate1;
/// @notice The price that buy and sell prices can not cross scaled to 36 decimals.
/// This is also the price the base assets, eg stETH, in the ARM contract are priced at in `totalAssets`.
uint256 public crossPrice;
/// @notice Cumulative total of all withdrawal requests including the ones that have already been claimed.
uint128 public withdrawsQueued;
/// @notice Total of all the withdrawal requests that have been claimed.
uint128 public withdrawsClaimed;
/// @notice Index of the next withdrawal request starting at 0.
uint256 public nextWithdrawalIndex;
struct WithdrawalRequest {
address withdrawer;
bool claimed;
// When the withdrawal can be claimed
uint40 claimTimestamp;
// Amount of liquidity assets to withdraw. eg WETH
uint128 assets;
// Cumulative total of all withdrawal requests including this one when the redeem request was made.
uint128 queued;
}
/// @notice Mapping of withdrawal request indices to the user withdrawal request data.
mapping(uint256 requestId => WithdrawalRequest) public withdrawalRequests;
/// @notice Performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent).
/// 10,000 = 100% performance fee
/// 2,000 = 20% performance fee
/// 500 = 5% performance fee
uint16 public fee;
/// @notice The available assets the last time the performance fees were collected and adjusted
/// for liquidity assets (WETH) deposited and redeemed.
/// This can be negative if there were asset gains and then all the liquidity providers redeemed.
int128 public lastAvailableAssets;
/// @notice The account or contract that can collect the performance fee.
address public feeCollector;
/// @notice The address of the CapManager contract used to manage the ARM's liquidity provider and total assets caps.
address public capManager;
/// @notice The address of the active lending market.
address public activeMarket;
/// @notice Lending markets that can be used by the ARM.
mapping(address market => bool supported) public supportedMarkets;
/// @notice Percentage of available liquid assets to keep in the ARM. 100% = 1e18.
uint256 public armBuffer;
uint256[38] private _gap;
////////////////////////////////////////////////////
/// Events
////////////////////////////////////////////////////
event TraderateChanged(uint256 traderate0, uint256 traderate1);
event CrossPriceUpdated(uint256 crossPrice);
event Deposit(address indexed owner, uint256 assets, uint256 shares);
event RedeemRequested(
address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued, uint256 claimTimestamp
);
event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets);
event FeeCollected(address indexed feeCollector, uint256 fee);
event FeeUpdated(uint256 fee);
event FeeCollectorUpdated(address indexed newFeeCollector);
event CapManagerUpdated(address indexed capManager);
event ActiveMarketUpdated(address indexed market);
event MarketAdded(address indexed market);
event MarketRemoved(address indexed market);
event ARMBufferUpdated(uint256 armBuffer);
event Allocated(address indexed market, int256 assets);
constructor(
address _token0,
address _token1,
address _liquidityAsset,
uint256 _claimDelay,
uint256 _minSharesToRedeem,
int256 _allocateThreshold
) {
require(IERC20(_token0).decimals() == 18);
require(IERC20(_token1).decimals() == 18);
token0 = IERC20(_token0);
token1 = IERC20(_token1);
claimDelay = _claimDelay;
_setOwner(address(0)); // Revoke owner for implementation contract at deployment
require(_liquidityAsset == address(token0) || _liquidityAsset == address(token1), "invalid liquidity asset");
liquidityAsset = _liquidityAsset;
// The base asset, eg stETH, is not the liquidity asset, eg WETH
baseAsset = _liquidityAsset == _token0 ? _token1 : _token0;
minSharesToRedeem = _minSharesToRedeem;
require(_allocateThreshold >= 0, "invalid allocate threshold");
allocateThreshold = _allocateThreshold;
}
/// @notice Initialize the contract.
/// The deployer that calls initialize has to approve the this ARM's proxy contract to transfer 1e12 WETH.
/// @param _operator The address of the account that can request and claim Lido withdrawals.
/// @param _name The name of the liquidity provider (LP) token.
/// @param _symbol The symbol of the liquidity provider (LP) token.
/// @param _fee The performance fee that is collected by the feeCollector measured in basis points (1/100th of a percent).
/// 10,000 = 100% performance fee
/// 500 = 5% performance fee
/// @param _feeCollector The account that can collect the performance fee
/// @param _capManager The address of the CapManager contract
function _initARM(
address _operator,
string calldata _name,
string calldata _symbol,
uint256 _fee,
address _feeCollector,
address _capManager
) internal {
_initOwnableOperable(_operator);
__ERC20_init(_name, _symbol);
// Transfer a small bit of liquidity from the initializer to this contract
IERC20(liquidityAsset).transferFrom(msg.sender, address(this), MIN_TOTAL_SUPPLY);
// mint a small amount of shares to a dead account so the total supply can never be zero
// This avoids donation attacks when there are no assets in the ARM contract
_mint(DEAD_ACCOUNT, MIN_TOTAL_SUPPLY);
// Set the sell price to its highest value. 1.0
traderate0 = PRICE_SCALE;
// Set the buy price to its lowest value. 0.998
traderate1 = PRICE_SCALE - MAX_CROSS_PRICE_DEVIATION;
emit TraderateChanged(traderate0, traderate1);
// Initialize the last available assets to the current available assets
// This ensures no performance fee is accrued when the performance fee is calculated when the fee is set
(uint256 availableAssets,) = _availableAssets();
lastAvailableAssets = SafeCast.toInt128(SafeCast.toInt256(availableAssets));
_setFee(_fee);
_setFeeCollector(_feeCollector);
capManager = _capManager;
emit CapManagerUpdated(_capManager);
crossPrice = PRICE_SCALE;
emit CrossPriceUpdated(PRICE_SCALE);
}
////////////////////////////////////////////////////
/// Swap Functions
////////////////////////////////////////////////////
/**
* @notice Swaps an exact amount of input tokens for as many output tokens as possible.
* msg.sender should have already given the ARM contract an allowance of
* at least amountIn on the input token.
*
* @param inToken Input token.
* @param outToken Output token.
* @param amountIn The amount of input tokens to send.
* @param amountOutMin The minimum amount of output tokens that must be received for the transaction not to revert.
* @param to Recipient of the output tokens.
*/
function swapExactTokensForTokens(
IERC20 inToken,
IERC20 outToken,
uint256 amountIn,
uint256 amountOutMin,
address to
) external virtual {
uint256 amountOut = _swapExactTokensForTokens(inToken, outToken, amountIn, to);
require(amountOut >= amountOutMin, "ARM: Insufficient output amount");
}
/**
* @notice Uniswap V2 Router compatible interface. Swaps an exact amount of
* input tokens for as many output tokens as possible.
* msg.sender should have already given the ARM contract an allowance of
* at least amountIn on the input token.
*
* @param amountIn The amount of input tokens to send.
* @param amountOutMin The minimum amount of output tokens that must be received for the transaction not to revert.
* @param path The input and output token addresses.
* @param to Recipient of the output tokens.
* @param deadline Unix timestamp after which the transaction will revert.
* @return amounts The input and output token amounts.
*/
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external virtual returns (uint256[] memory amounts) {
require(path.length == 2, "ARM: Invalid path length");
_inDeadline(deadline);
IERC20 inToken = IERC20(path[0]);
IERC20 outToken = IERC20(path[1]);
uint256 amountOut = _swapExactTokensForTokens(inToken, outToken, amountIn, to);
require(amountOut >= amountOutMin, "ARM: Insufficient output amount");
amounts = new uint256[](2);
amounts[0] = amountIn;
amounts[1] = amountOut;
}
/**
* @notice Receive an exact amount of output tokens for as few input tokens as possible.
* msg.sender should have already given the router an allowance of
* at least amountInMax on the input token.
*
* @param inToken Input token.
* @param outToken Output token.
* @param amountOut The amount of output tokens to receive.
* @param amountInMax The maximum amount of input tokens that can be required before the transaction reverts.
* @param to Recipient of the output tokens.
*/
function swapTokensForExactTokens(
IERC20 inToken,
IERC20 outToken,
uint256 amountOut,
uint256 amountInMax,
address to
) external virtual {
uint256 amountIn = _swapTokensForExactTokens(inToken, outToken, amountOut, to);
require(amountIn <= amountInMax, "ARM: Excess input amount");
}
/**
* @notice Uniswap V2 Router compatible interface. Receive an exact amount of
* output tokens for as few input tokens as possible.
* msg.sender should have already given the router an allowance of
* at least amountInMax on the input token.
*
* @param amountOut The amount of output tokens to receive.
* @param amountInMax The maximum amount of input tokens that can be required before the transaction reverts.
* @param path The input and output token addresses.
* @param to Recipient of the output tokens.
* @param deadline Unix timestamp after which the transaction will revert.
* @return amounts The input and output token amounts.
*/
function swapTokensForExactTokens(
uint256 amountOut,
uint256 amountInMax,
address[] calldata path,
address to,
uint256 deadline
) external virtual returns (uint256[] memory amounts) {
require(path.length == 2, "ARM: Invalid path length");
_inDeadline(deadline);
IERC20 inToken = IERC20(path[0]);
IERC20 outToken = IERC20(path[1]);
uint256 amountIn = _swapTokensForExactTokens(inToken, outToken, amountOut, to);
require(amountIn <= amountInMax, "ARM: Excess input amount");
amounts = new uint256[](2);
amounts[0] = amountIn;
amounts[1] = amountOut;
}
function _inDeadline(uint256 deadline) internal view {
require(deadline >= block.timestamp, "ARM: Deadline expired");
}
/// @dev Ensure any liquidity assets reserved for the withdrawal queue are not used
/// in swaps that send liquidity assets out of the ARM
function _transferAsset(address asset, address to, uint256 amount) internal virtual {
if (asset == liquidityAsset) _requireLiquidityAvailable(amount);
IERC20(asset).transfer(to, amount);
}
/// @dev Hook to transfer assets into the ARM contract
function _transferAssetFrom(address asset, address from, address to, uint256 amount) internal virtual {
IERC20(asset).transferFrom(from, to, amount);
}
function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, address to)
internal
virtual
returns (uint256 amountOut)
{
uint256 price;
if (inToken == token0) {
require(outToken == token1, "ARM: Invalid out token");
price = traderate0;
} else if (inToken == token1) {
require(outToken == token0, "ARM: Invalid out token");
price = traderate1;
} else {
revert("ARM: Invalid in token");
}
amountOut = amountIn * price / PRICE_SCALE;
// Transfer the input tokens from the caller to this ARM contract
_transferAssetFrom(address(inToken), msg.sender, address(this), amountIn);
// Transfer the output tokens to the recipient
_transferAsset(address(outToken), to, amountOut);
}
function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountOut, address to)
internal
virtual
returns (uint256 amountIn)
{
uint256 price;
if (inToken == token0) {
require(outToken == token1, "ARM: Invalid out token");
price = traderate0;
} else if (inToken == token1) {
require(outToken == token0, "ARM: Invalid out token");
price = traderate1;
} else {
revert("ARM: Invalid in token");
}
// always round in our favor
// +1 for truncation when dividing integers
// +2 to cover stETH transfers being up to 2 wei short of the requested transfer amount
amountIn = ((amountOut * PRICE_SCALE) / price) + 3;
// Transfer the input tokens from the caller to this ARM contract
_transferAssetFrom(address(inToken), msg.sender, address(this), amountIn);
// Transfer the output tokens to the recipient
_transferAsset(address(outToken), to, amountOut);
}
/// @notice Get the available liquidity for a each token in the ARM.
/// @return reserve0 The available liquidity for token0
/// @return reserve1 The available liquidity for token1
function getReserves() external view returns (uint256 reserve0, uint256 reserve1) {
// The amount of liquidity assets (WETH) that is still to be claimed in the withdrawal queue
uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed;
uint256 liquidityAssetBalance = IERC20(liquidityAsset).balanceOf(address(this));
uint256 baseAssetBalance = IERC20(baseAsset).balanceOf(address(this));
// Ensure there is no negative reserves when there are more outstanding withdrawals than liquidity assets in the ARM
reserve0 = outstandingWithdrawals > liquidityAssetBalance ? 0 : liquidityAssetBalance - outstandingWithdrawals;
reserve1 = baseAssetBalance;
// The previous assignment assumed token0 is be the liquidity asset.
// If not, swap the reserves
if (address(token0) == baseAsset) (reserve0, reserve1) = (reserve1, reserve0);
}
/**
* @notice Set exchange rates from an operator account from the ARM's perspective.
* If token 0 is WETH and token 1 is stETH, then both prices will be set using the stETH/WETH price.
* @param buyT1 The price the ARM buys Token 1 (stETH) from the Trader, denominated in Token 0 (WETH), scaled to 36 decimals.
* From the Trader's perspective, this is the sell price.
* @param sellT1 The price the ARM sells Token 1 (stETH) to the Trader, denominated in Token 0 (WETH), scaled to 36 decimals.
* From the Trader's perspective, this is the buy price.
*/
function setPrices(uint256 buyT1, uint256 sellT1) external onlyOperatorOrOwner {
// Ensure buy price is always below past sell prices
require(sellT1 >= crossPrice, "ARM: sell price too low");
require(buyT1 < crossPrice, "ARM: buy price too high");
traderate0 = PRICE_SCALE * PRICE_SCALE / sellT1; // quote (t0) -> base (t1); eg WETH -> stETH
traderate1 = buyT1; // base (t1) -> quote (t0). eg stETH -> WETH
emit TraderateChanged(traderate0, traderate1);
}
/**
* @notice set the price that buy and sell prices can not cross.
* That is, the buy prices must be below the cross price
* and the sell prices must be above the cross price.
* If the cross price is being lowered, there can not be a significant amount of base assets in the ARM. eg stETH.
* This prevents the ARM making a loss when the base asset is sold at a lower price than it was bought
* before the cross price was lowered.
* The base assets should be sent to the withdrawal queue before the cross price can be lowered. For example, the
* `Owner` should construct a tx that calls `requestLidoWithdrawals` before `setCrossPrice` for the Lido ARM
* when the cross price is being lowered.
* The cross price can be increased with assets in the ARM.
* @param newCrossPrice The new cross price scaled to 36 decimals.
*/
function setCrossPrice(uint256 newCrossPrice) external onlyOwner {
require(newCrossPrice >= PRICE_SCALE - MAX_CROSS_PRICE_DEVIATION, "ARM: cross price too low");
require(newCrossPrice <= PRICE_SCALE, "ARM: cross price too high");
// The exiting sell price must be greater than or equal to the new cross price
require(PRICE_SCALE * PRICE_SCALE / traderate0 >= newCrossPrice, "ARM: sell price too low");
// The existing buy price must be less than the new cross price
require(traderate1 < newCrossPrice, "ARM: buy price too high");
// If the cross price is being lowered, there can not be a significant amount of base assets in the ARM. eg stETH.
// This prevents the ARM making a loss when the base asset is sold at a lower price than it was bought
// before the cross price was lowered.
if (newCrossPrice < crossPrice) {
// Check there is not a significant amount of base assets in the ARM
require(IERC20(baseAsset).balanceOf(address(this)) < MIN_TOTAL_SUPPLY, "ARM: too many base assets");
}
// Save the new cross price to storage
crossPrice = newCrossPrice;
emit CrossPriceUpdated(newCrossPrice);
}
////////////////////////////////////////////////////
/// Liquidity Provider Functions
////////////////////////////////////////////////////
/// @notice Preview the amount of shares that would be minted for a given amount of assets
/// @param assets The amount of liquidity assets to deposit
/// @return shares The amount of shares that would be minted
function previewDeposit(uint256 assets) external view returns (uint256 shares) {
shares = convertToShares(assets);
}
/// @notice deposit liquidity assets in exchange for liquidity provider (LP) shares.
/// The caller needs to have approved the contract to transfer the assets.
/// @param assets The amount of liquidity assets to deposit
/// @return shares The amount of shares that were minted
function deposit(uint256 assets) external returns (uint256 shares) {
shares = _deposit(assets, msg.sender);
}
/// @notice deposit liquidity assets in exchange for liquidity provider (LP) shares.
/// Funds will be transferred from msg.sender.
/// @param assets The amount of liquidity assets to deposit
/// @param receiver The address that will receive shares.
/// @return shares The amount of shares that were minted
function deposit(uint256 assets, address receiver) external returns (uint256 shares) {
shares = _deposit(assets, receiver);
}
/// @dev Internal logic for depositing liquidity assets in exchange for liquidity provider (LP) shares.
function _deposit(uint256 assets, address receiver) internal returns (uint256 shares) {
// Calculate the amount of shares to mint after the performance fees have been accrued
// which reduces the available assets, and before new assets are deposited.
shares = convertToShares(assets);
// Add the deposited assets to the last available assets
lastAvailableAssets += SafeCast.toInt128(SafeCast.toInt256(assets));
// Transfer the liquidity asset from the sender to this contract
IERC20(liquidityAsset).transferFrom(msg.sender, address(this), assets);
// mint shares
_mint(receiver, shares);
// Check the liquidity provider caps after the new assets have been deposited
if (capManager != address(0)) {
ICapManager(capManager).postDepositHook(receiver, assets);
}
emit Deposit(receiver, assets, shares);
}
/// @notice Preview the amount of assets that would be received for burning a given amount of shares
/// @param shares The amount of shares to burn
/// @return assets The amount of liquidity assets that would be received
function previewRedeem(uint256 shares) external view returns (uint256 assets) {
assets = convertToAssets(shares);
}
/// @notice Request to redeem liquidity provider shares for liquidity assets
/// @param shares The amount of shares the redeemer wants to burn for liquidity assets
/// @return requestId The index of the withdrawal request
/// @return assets The amount of liquidity assets that will be claimable by the redeemer
function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) {
// Calculate the amount of assets to transfer to the redeemer
assets = convertToAssets(shares);
requestId = nextWithdrawalIndex;
// Store the next withdrawal request
nextWithdrawalIndex = requestId + 1;
uint128 queued = SafeCast.toUint128(withdrawsQueued + assets);
// Store the updated queued amount which reserves liquidity assets (WETH) in the withdrawal queue
withdrawsQueued = queued;
uint40 claimTimestamp = uint40(block.timestamp + claimDelay);
// Store requests
withdrawalRequests[requestId] = WithdrawalRequest({
withdrawer: msg.sender,
claimed: false,
claimTimestamp: claimTimestamp,
assets: SafeCast.toUint128(assets),
queued: queued
});
// burn redeemer's shares
_burn(msg.sender, shares);
// Remove the redeemed assets from the last available assets
lastAvailableAssets -= SafeCast.toInt128(SafeCast.toInt256(assets));
emit RedeemRequested(msg.sender, requestId, assets, queued, claimTimestamp);
}
/// @notice Claim liquidity assets from a previous withdrawal request after the claim delay has passed.
/// This will withdraw from the active lending market if there are not enough liquidity assets in the ARM.
/// @param requestId The index of the withdrawal request
/// @return assets The amount of liquidity assets that were transferred to the redeemer
function claimRedeem(uint256 requestId) external returns (uint256 assets) {
// Load the struct from storage into memory
WithdrawalRequest memory request = withdrawalRequests[requestId];
require(request.claimTimestamp <= block.timestamp, "Claim delay not met");
// Is there enough liquidity to claim this request?
// This includes liquidity assets in the ARM and the the active lending market
require(request.queued <= claimable(), "Queue pending liquidity");
require(request.withdrawer == msg.sender, "Not requester");
require(request.claimed == false, "Already claimed");
assets = request.assets;
// Store the request as claimed
withdrawalRequests[requestId].claimed = true;
// Store the updated claimed amount
withdrawsClaimed += SafeCast.toUint128(assets);
// If there is not enough liquidity assets in the ARM, get from the active market if one is configured.
// Read the active market address from storage once to save gas.
address activeMarketMem = activeMarket;
if (activeMarketMem != address(0)) {
uint256 liquidityInARM = IERC20(liquidityAsset).balanceOf(address(this));
if (assets > liquidityInARM) {
uint256 liquidityFromMarket = assets - liquidityInARM;
// This should work as we have checked earlier the claimable() amount which includes the active market
IERC4626(activeMarketMem).withdraw(liquidityFromMarket, address(this), address(this));
}
}
// transfer the liquidity asset to the withdrawer
IERC20(liquidityAsset).transfer(msg.sender, assets);
emit RedeemClaimed(msg.sender, requestId, assets);
}
/// @notice Used to work out if an ARM's withdrawal request can be claimed.
/// If the withdrawal request's `queued` amount is less than the returned `claimable` amount, then it can be claimed.
/// The `claimable` amount is the all the withdrawals already claimed plus the liquidity assets in the ARM
/// and active lending market.
/// @return claimableAmount The amount of liquidity assets that can be claimed
function claimable() public view returns (uint256 claimableAmount) {
claimableAmount = withdrawsClaimed + IERC20(liquidityAsset).balanceOf(address(this));
// if there is an active lending market, add to the claimable amount
address activeMarketMem = activeMarket;
if (activeMarketMem != address(0)) {
claimableAmount += IERC4626(activeMarketMem).maxWithdraw(address(this));
}
}
////////////////////////////////////////////////////
/// Asset amount functions
////////////////////////////////////////////////////
/// @dev Checks if there is enough liquidity asset (WETH) in the ARM is not reserved for the withdrawal queue.
// That is, the amount of liquidity assets (WETH) that is available to be swapped or collected as fees.
// If no outstanding withdrawals, no check will be done of the amount against the balance of the liquidity assets in the ARM.
// This is a gas optimization for swaps.
// The ARM can swap out liquidity assets (WETH) that has been accrued from the performance fee for the fee collector.
// There is no liquidity guarantee for the fee collector. If there is not enough liquidity assets (WETH) in
// the ARM to collect the accrued fees, then the fee collector will have to wait until there is enough liquidity assets.
function _requireLiquidityAvailable(uint256 amount) internal view {
// The amount of liquidity assets (WETH) that is still to be claimed in the withdrawal queue
uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed;
// Save gas on an external balanceOf call if there are no outstanding withdrawals
if (outstandingWithdrawals == 0) return;
// If there is not enough liquidity assets in the ARM to cover the outstanding withdrawals and the amount
require(
amount + outstandingWithdrawals <= IERC20(liquidityAsset).balanceOf(address(this)),
"ARM: Insufficient liquidity"
);
}
/// @notice The total amount of assets in the ARM, active lending market and external withdrawal queue,
/// less the liquidity assets reserved for the ARM's withdrawal queue and accrued fees.
/// @return The total amount of assets in the ARM
function totalAssets() public view virtual returns (uint256) {
(uint256 fees, uint256 newAvailableAssets) = _feesAccrued();
// total assets should only go up from the initial deposit amount that is burnt
// but in case of something unforeseen, return at least MIN_TOTAL_SUPPLY.
if (fees + MIN_TOTAL_SUPPLY >= newAvailableAssets) return MIN_TOTAL_SUPPLY;
// Remove the performance fee from the available assets
return newAvailableAssets - fees;
}
/// @notice The liquidity asset used for deposits and redeems. eg WETH or wS
/// Used for compatibility with ERC-4626
/// @return The address of the liquidity asset
function asset() external view virtual returns (address) {
return liquidityAsset;
}
/// @dev Calculate the available assets which is the assets in the ARM, external withdrawal queue,
/// and active lending market, less liquidity assets reserved for the ARM's withdrawal queue.
/// This does not exclude any accrued performance fees.
function _availableAssets() internal view returns (uint256 availableAssets, uint256 outstandingWithdrawals) {
// Liquidity assets, eg WETH, in the ARM and lending markets are priced at 1.0
// Base assets, eg stETH, in the withdrawal queue are also priced at 1.0
// Base assets, eg stETH, in the ARM are priced at the cross price which is a discounted price
uint256 assets = IERC20(liquidityAsset).balanceOf(address(this)) + _externalWithdrawQueue()
+ IERC20(baseAsset).balanceOf(address(this)) * crossPrice / PRICE_SCALE;
address activeMarketMem = activeMarket;
if (activeMarketMem != address(0)) {
// Get all the active lending market shares owned by this ARM contract
uint256 allShares = IERC4626(activeMarketMem).balanceOf(address(this));
// Add all the assets in the active lending market.
// previewRedeem is used instead of maxWithdraw as maxWithdraw will return less if the market
// is highly utilized or has a temporary pause.
assets += IERC4626(activeMarketMem).previewRedeem(allShares);
}
// The amount of liquidity assets, eg WETH, that is still to be claimed in the withdrawal queue
outstandingWithdrawals = withdrawsQueued - withdrawsClaimed;
// If the ARM becomes insolvent enough that the available assets in the ARM and external withdrawal queue
// is less than the outstanding withdrawals and accrued fees.
if (assets < outstandingWithdrawals) {
return (0, outstandingWithdrawals);
}
// Need to remove the liquidity assets that have been reserved for the withdrawal queue
availableAssets = assets - outstandingWithdrawals;
}
/// @dev Hook for calculating the amount of assets in an external withdrawal queue like Lido or OETH
/// This is not the ARM's withdrawal queue
function _externalWithdrawQueue() internal view virtual returns (uint256 assets);
/// @notice Calculates the amount of shares for a given amount of liquidity assets
/// @dev Total assets can't be zero. The lowest it can be is MIN_TOTAL_SUPPLY
/// @param assets The amount of liquidity assets to convert to shares
/// @return shares The amount of shares that would be minted for the given assets
function convertToShares(uint256 assets) public view returns (uint256 shares) {
shares = assets * totalSupply() / totalAssets();
}
/// @notice Calculates the amount of liquidity assets for a given amount of shares
/// @dev Total supply can't be zero. The lowest it can be is MIN_TOTAL_SUPPLY
/// @param shares The amount of shares to convert to assets
/// @return assets The amount of liquidity assets that would be received for the given shares
function convertToAssets(uint256 shares) public view returns (uint256 assets) {
assets = (shares * totalAssets()) / totalSupply();
}
////////////////////////////////////////////////////
/// Performance Fee Functions
////////////////////////////////////////////////////
/// @notice Owner sets the performance fee on increased assets
/// @param _fee The performance fee measured in basis points (1/100th of a percent)
/// 10,000 = 100% performance fee
/// 500 = 5% performance fee
/// The max allowed performance fee is 50% (5000)
function setFee(uint256 _fee) external onlyOwner {
_setFee(_fee);
}
/// @notice Owner sets the account/contract that receives the performance fee
/// @param _feeCollector The address of the fee collector
function setFeeCollector(address _feeCollector) external onlyOwner {
_setFeeCollector(_feeCollector);
}
function _setFee(uint256 _fee) internal {
require(_fee <= FEE_SCALE / 2, "ARM: fee too high");
// Collect any performance fees up to this point using the old fee
collectFees();
fee = SafeCast.toUint16(_fee);
emit FeeUpdated(_fee);
}
function _setFeeCollector(address _feeCollector) internal {
require(_feeCollector != address(0), "ARM: invalid fee collector");
feeCollector = _feeCollector;
emit FeeCollectorUpdated(_feeCollector);
}
/// @notice Transfer accrued performance fees to the fee collector
/// This requires enough liquidity assets (WETH) in the ARM that are not reserved
/// for the withdrawal queue to cover the accrued fees.
/// @return fees The amount of performance fees collected
function collectFees() public returns (uint256 fees) {
uint256 newAvailableAssets;
// Accrue any performance fees up to this point
(fees, newAvailableAssets) = _feesAccrued();
// Save the new available assets back to storage less the collected fees.
// This needs to be done before the fees == 0 check to cover the scenario where the performance fee is zero
// and there has been an increase in assets since the last time fees were collected.
lastAvailableAssets = SafeCast.toInt128(SafeCast.toInt256(newAvailableAssets) - SafeCast.toInt256(fees));
if (fees == 0) return 0;
// Check there is enough liquidity assets (WETH) that are not reserved for the withdrawal queue
// to cover the fee being collected.
_requireLiquidityAvailable(fees);
// _requireLiquidityAvailable() is optimized for swaps so will not revert if there are no outstanding withdrawals.
// We need to check there is enough liquidity assets to cover the fees being collect from this ARM contract.
// We could try the transfer and let it revert if there are not enough assets, but there is no error message with
// a failed WETH transfer so we spend the extra gas to check and give a meaningful error message.
require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity");
IERC20(liquidityAsset).transfer(feeCollector, fees);
emit FeeCollected(feeCollector, fees);
}
/// @notice Calculates the performance fees accrued since the last time fees were collected
/// @param fees The amount of performance fees accrued
function feesAccrued() external view returns (uint256 fees) {
(fees,) = _feesAccrued();
}
function _feesAccrued() internal view returns (uint256 fees, uint256 newAvailableAssets) {
(newAvailableAssets,) = _availableAssets();
// Calculate the increase in assets since the last time fees were calculated
int256 assetIncrease = SafeCast.toInt256(newAvailableAssets) - lastAvailableAssets;
// Do not accrued a performance fee if the available assets has decreased
if (assetIncrease <= 0) return (0, newAvailableAssets);
fees = SafeCast.toUint256(assetIncrease) * fee / FEE_SCALE;
}
////////////////////////////////////////////////////
/// Lending Market Functions
////////////////////////////////////////////////////
/// @notice Owner adds supported lending market to the ARM.
/// In order to be a safe lending market for the ARM, it must be:
/// 1. up only exchange rate
/// 2. no slippage
/// 3. no fees.
/// @param _markets The addresses of the lending markets to add
function addMarkets(address[] calldata _markets) external onlyOwner {
for (uint256 i = 0; i < _markets.length; ++i) {
address market = _markets[i];
require(market != address(0), "ARM: invalid market");
require(!supportedMarkets[market], "ARM: market already supported");
require(IERC4626(market).asset() == liquidityAsset, "ARM: invalid market asset");
supportedMarkets[market] = true;
emit MarketAdded(market);
}
}
/// @notice Owner removes a supported lending market from the ARM.
/// This can not be the active market.
/// @param _market The address of the lending market to remove
function removeMarket(address _market) external onlyOwner {
require(_market != address(0), "ARM: invalid market");
require(supportedMarkets[_market], "ARM: market not supported");
require(_market != activeMarket, "ARM: market in active");
supportedMarkets[_market] = false;
emit MarketRemoved(_market);
}
/// @notice set a new active lending market for the ARM.
/// This can be set to address(0) to disable the use of a lending market.
/// @param _market The address of the lending market to set as active
function setActiveMarket(address _market) external onlyOperatorOrOwner {
require(_market == address(0) || supportedMarkets[_market], "ARM: market not supported");
// Read once from storage to save gas and make it clear this is the previous active market
address previousActiveMarket = activeMarket;
// Don't revert if the previous active market is the same as the new one
if (previousActiveMarket == _market) return;
if (previousActiveMarket != address(0)) {
// Redeem all shares from the previous active lending market.
// balanceOf is used instead of maxRedeem to ensure all shares are redeemed.
// maxRedeem can return a smaller amount of shares than balanceOf if the market is highly utilized.
uint256 shares = IERC4626(previousActiveMarket).balanceOf(address(this));
if (shares > 0) {
// This could fail if the market has high utilization. In this case, the Operator needs
// to wait until the utilization drops before setting a new active market.
// The redeem can also fail if the ARM has a dust amount of shares left. eg 100 wei.
// If that happens, the Operator can transfer a tiny amount of active market shares
// to the ARM so the following redeem will not fail.
IERC4626(previousActiveMarket).redeem(shares, address(this), address(this));
}
}
activeMarket = _market;
emit ActiveMarketUpdated(_market);
// Exit if no new active market
if (_market == address(0)) return;
_allocate();
}
/// @notice Deposit or withdraw liquidity assets to/from the active lending market
/// to match the ARM's liquidity buffer which is a percentage of the available assets.
/// Will revert if there is no active lending market set.
/// @return liquidityDelta The actual liquidity less target liquidity before
/// the deposit/withdrawal to/from the active lending market.
function allocate() external returns (int256 liquidityDelta) {
require(activeMarket != address(0), "ARM: no active market");
liquidityDelta = _allocate();
}
function _allocate() internal returns (int256 liquidityDelta) {
(uint256 availableAssets, uint256 outstandingWithdrawals) = _availableAssets();
if (availableAssets == 0) return 0;
int256 armLiquidity = SafeCast.toInt256(IERC20(liquidityAsset).balanceOf(address(this)))
- SafeCast.toInt256(outstandingWithdrawals);
uint256 targetArmLiquidity = availableAssets * armBuffer / 1e18;
liquidityDelta = armLiquidity - SafeCast.toInt256(targetArmLiquidity);
// Load the active lending market address from storage to save gas
address activeMarketMem = activeMarket;
// The allocateThreshold prevents the ARM from constantly depositing and withdrawing if there are rounding issues
if (liquidityDelta > allocateThreshold) {
// We have too much liquidity in the ARM, we need to deposit some to the active lending market
uint256 depositAmount = SafeCast.toUint256(liquidityDelta);
IERC20(liquidityAsset).approve(activeMarketMem, depositAmount);
IERC4626(activeMarketMem).deposit(depositAmount, address(this));
} else if (liquidityDelta < 0) {
// We have too little liquidity in the ARM, we need to withdraw some from the active lending market
uint256 availableMarketAssets = IERC4626(activeMarketMem).maxWithdraw(address(this));
uint256 desiredWithdrawAmount = SafeCast.toUint256(-liquidityDelta);
if (availableMarketAssets < desiredWithdrawAmount) {
// Not enough assets in the market so redeem as much as possible.
// maxRedeem is used instead of balanceOf as we want to redeem as much as possible without failing.
// redeem of the ARM's balance can fail if the lending market is highly utilized or temporarily paused.
// Redeem and not withdrawal is used to avoid leaving a small amount of assets in the market.
uint256 shares = IERC4626(activeMarketMem).maxRedeem(address(this));
if (shares <= minSharesToRedeem) return liquidityDelta;
// This should not fail
Submitted on: 2025-10-30 14:07:15
Comments
Log in to comment.
No comments yet.