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/single-sided-lp/CurveConvex2Token.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { IWithdrawRequestManager } from "../interfaces/IWithdrawRequestManager.sol";
import { AbstractSingleSidedLP, BaseLPLib } from "./AbstractSingleSidedLP.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { TokenUtils } from "../utils/TokenUtils.sol";
import { ETH_ADDRESS, ALT_ETH_ADDRESS, WETH, CHAIN_ID_MAINNET } from "../utils/Constants.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {
ICurvePool,
ICurve2TokenPoolV1,
ICurve2TokenPoolV2,
ICurveStableSwapNG,
ICurveGauge,
CurveInterface
} from "../interfaces/Curve/ICurve.sol";
import { IConvexBooster, IConvexRewardPool } from "../interfaces/Curve/IConvex.sol";
struct DeploymentParams {
address pool;
address poolToken;
address gauge;
address convexRewardPool;
CurveInterface curveInterface;
}
contract CurveConvex2Token is AbstractSingleSidedLP {
uint256 internal constant _NUM_TOKENS = 2;
ERC20 public immutable CURVE_POOL_TOKEN;
uint8 internal immutable _PRIMARY_INDEX;
address internal immutable TOKEN_1;
address internal immutable TOKEN_2;
function NUM_TOKENS() internal pure override returns (uint256) {
return _NUM_TOKENS;
}
function PRIMARY_INDEX() public view override returns (uint256) {
return _PRIMARY_INDEX;
}
function TOKENS() public view override returns (ERC20[] memory) {
ERC20[] memory tokens = new ERC20[](_NUM_TOKENS);
tokens[0] = ERC20(TOKEN_1);
tokens[1] = ERC20(TOKEN_2);
return tokens;
}
constructor(
uint256 _maxPoolShare,
address _asset,
address _yieldToken,
uint256 _feeRate,
address _rewardManager,
DeploymentParams memory params
)
AbstractSingleSidedLP(_maxPoolShare, _asset, _yieldToken, _feeRate, _rewardManager, 18)
{
CURVE_POOL_TOKEN = ERC20(params.poolToken);
// We interact with curve pools directly so we never pass the token addresses back
// to the curve pools. The amounts are passed back based on indexes instead. Therefore
// we can rewrite the token addresses from ALT Eth (0xeeee...) back to (0x0000...) which
// is used by the vault internally to represent ETH.
TOKEN_1 = _rewriteAltETH(ICurvePool(params.pool).coins(0));
TOKEN_2 = _rewriteAltETH(ICurvePool(params.pool).coins(1));
// Assets may be WETH, so we need to unwrap it in this case.
_PRIMARY_INDEX = (TOKEN_1 == _asset || (TOKEN_1 == ETH_ADDRESS && _asset == address(WETH)))
? 0
: (TOKEN_2 == _asset || (TOKEN_2 == ETH_ADDRESS && _asset == address(WETH)))
? 1
// Otherwise the primary index is not set and we will not be able to enter or exit
// single sided.
: type(uint8).max;
LP_LIB = address(new CurveConvexLib(TOKEN_1, TOKEN_2, _asset, _PRIMARY_INDEX, params));
// Validate that withdraw request managers are either all address(0) or all non-zero
_validateWithdrawRequestManagers();
}
function strategy() public pure override returns (string memory) {
return "CurveConvex2Token";
}
function _rewriteAltETH(address token) private pure returns (address) {
return token == address(ALT_ETH_ADDRESS) ? ETH_ADDRESS : address(token);
}
function _transferYieldTokenToOwner(address owner, uint256 yieldTokens) internal override {
_delegateCall(
LP_LIB, abi.encodeWithSelector(CurveConvexLib.transferYieldTokenToOwner.selector, owner, yieldTokens)
);
}
function _totalPoolSupply() internal view override returns (uint256) {
return CURVE_POOL_TOKEN.totalSupply();
}
function _checkReentrancyContext() internal override {
CurveConvexLib(payable(LP_LIB)).checkReentrancyContext();
}
}
contract CurveConvexLib is BaseLPLib {
using SafeERC20 for ERC20;
using TokenUtils for ERC20;
uint256 internal constant _NUM_TOKENS = 2;
address internal immutable CURVE_POOL;
ERC20 internal immutable CURVE_POOL_TOKEN;
/// @dev Curve gauge contract used when there is no convex reward pool
address internal immutable CURVE_GAUGE;
/// @dev Convex booster contract used for staking BPT
address internal immutable CONVEX_BOOSTER;
/// @dev Convex reward pool contract used for unstaking and claiming reward tokens
address internal immutable CONVEX_REWARD_POOL;
uint256 internal immutable CONVEX_POOL_ID;
uint8 internal immutable _PRIMARY_INDEX;
address internal immutable ASSET;
address internal immutable TOKEN_1;
address internal immutable TOKEN_2;
CurveInterface internal immutable CURVE_INTERFACE;
// Payable is required for the CurveV1 interface which will execute a transfer
// when the remove_liquidity function is called, it only will be done to this contract
// during the checkReentrancyContext function.
receive() external payable { }
constructor(address _token1, address _token2, address _asset, uint8 _primaryIndex, DeploymentParams memory params) {
TOKEN_1 = _token1;
TOKEN_2 = _token2;
ASSET = _asset;
_PRIMARY_INDEX = _primaryIndex;
CURVE_POOL = params.pool;
CURVE_GAUGE = params.gauge;
CURVE_POOL_TOKEN = ERC20(params.poolToken);
CURVE_INTERFACE = params.curveInterface;
// If the convex reward pool is set then get the booster and pool id, if not then
// we will stake on the curve gauge directly.
CONVEX_REWARD_POOL = params.convexRewardPool;
address convexBooster;
uint256 poolId;
if (block.chainid == CHAIN_ID_MAINNET && CONVEX_REWARD_POOL != address(0)) {
convexBooster = IConvexRewardPool(CONVEX_REWARD_POOL).operator();
poolId = IConvexRewardPool(CONVEX_REWARD_POOL).pid();
}
CONVEX_POOL_ID = poolId;
CONVEX_BOOSTER = convexBooster;
}
function checkReentrancyContext() external {
uint256[2] memory minAmounts;
if (CURVE_INTERFACE == CurveInterface.V1) {
ICurve2TokenPoolV1(CURVE_POOL).remove_liquidity(0, minAmounts);
} else if (CURVE_INTERFACE == CurveInterface.StableSwapNG) {
// Total supply on stable swap has a non-reentrant lock
ICurveStableSwapNG(CURVE_POOL).totalSupply();
} else if (CURVE_INTERFACE == CurveInterface.V2) {
ICurve2TokenPoolV2(CURVE_POOL).claim_admin_fees();
} else {
revert();
}
}
function TOKENS() internal view override returns (ERC20[] memory) {
ERC20[] memory tokens = new ERC20[](_NUM_TOKENS);
tokens[0] = ERC20(TOKEN_1);
tokens[1] = ERC20(TOKEN_2);
return tokens;
}
function initialApproveTokens() external {
if (CONVEX_BOOSTER != address(0)) {
CURVE_POOL_TOKEN.checkApprove(address(CONVEX_BOOSTER), type(uint256).max);
} else {
CURVE_POOL_TOKEN.checkApprove(address(CURVE_GAUGE), type(uint256).max);
}
}
function joinPoolAndStake(uint256[] memory _amounts, uint256 minPoolClaim) external {
// Although Curve uses ALT_ETH to represent native ETH, it is rewritten in the Curve2TokenPoolMixin
// to the Deployments.ETH_ADDRESS which we use internally.
uint256 msgValue;
if (TOKEN_1 == ETH_ADDRESS) {
msgValue = _amounts[0];
} else if (TOKEN_2 == ETH_ADDRESS) {
msgValue = _amounts[1];
}
if (msgValue > 0) WETH.withdraw(msgValue);
uint256 lpTokens = _enterPool(_amounts, minPoolClaim, msgValue);
_stakeLpTokens(lpTokens);
}
function unstakeAndExitPool(
uint256 poolClaim,
uint256[] memory _minAmounts,
bool isSingleSided
)
external
returns (uint256[] memory exitBalances, ERC20[] memory tokens)
{
_unstakeLpTokens(poolClaim);
exitBalances = _exitPool(poolClaim, _minAmounts, isSingleSided);
tokens = TOKENS();
// Any ETH received from exit pool needs to be wrapped back into WETH. Change the
// reported token array to WETH accordingly. This also allows us to use a WETH withdraw
// request manager.
if (ASSET == address(WETH)) {
if (TOKEN_1 == ETH_ADDRESS) {
WETH.deposit{ value: exitBalances[0] }();
tokens[0] = ERC20(address(WETH));
} else if (TOKEN_2 == ETH_ADDRESS) {
WETH.deposit{ value: exitBalances[1] }();
tokens[1] = ERC20(address(WETH));
}
}
}
function transferYieldTokenToOwner(address owner, uint256 yieldTokens) external {
_unstakeLpTokens(yieldTokens);
CURVE_POOL_TOKEN.safeTransfer(owner, yieldTokens);
}
function _enterPool(uint256[] memory _amounts, uint256 minPoolClaim, uint256 msgValue) internal returns (uint256) {
if (0 < _amounts[0]) ERC20(TOKEN_1).checkApprove(address(CURVE_POOL), _amounts[0]);
if (0 < _amounts[1]) ERC20(TOKEN_2).checkApprove(address(CURVE_POOL), _amounts[1]);
if (CURVE_INTERFACE == CurveInterface.StableSwapNG) {
return ICurveStableSwapNG(CURVE_POOL).add_liquidity{ value: msgValue }(_amounts, minPoolClaim);
}
uint256[2] memory amounts;
amounts[0] = _amounts[0];
amounts[1] = _amounts[1];
if (CURVE_INTERFACE == CurveInterface.V1) {
return ICurve2TokenPoolV1(CURVE_POOL).add_liquidity{ value: msgValue }(amounts, minPoolClaim);
} else if (CURVE_INTERFACE == CurveInterface.V2) {
return ICurve2TokenPoolV2(CURVE_POOL).add_liquidity{ value: msgValue }(
amounts,
minPoolClaim,
0 < msgValue // use_eth = true if msgValue > 0
);
}
revert();
}
function _exitPool(
uint256 poolClaim,
uint256[] memory _minAmounts,
bool isSingleSided
)
internal
returns (uint256[] memory exitBalances)
{
if (isSingleSided) {
exitBalances = new uint256[](_NUM_TOKENS);
if (CURVE_INTERFACE == CurveInterface.V1 || CURVE_INTERFACE == CurveInterface.StableSwapNG) {
// Method signature is the same for v1 and stable swap ng
exitBalances[_PRIMARY_INDEX] = ICurve2TokenPoolV1(CURVE_POOL).remove_liquidity_one_coin(
poolClaim, int8(_PRIMARY_INDEX), _minAmounts[_PRIMARY_INDEX]
);
} else {
exitBalances[_PRIMARY_INDEX] = ICurve2TokenPoolV2(CURVE_POOL).remove_liquidity_one_coin(
// Last two parameters are useEth and receiver = this contract. Check if one of the tokens
// is ETH and have it return ETH in this case so that we can wrap it back into WETH if required.
poolClaim,
_PRIMARY_INDEX,
_minAmounts[_PRIMARY_INDEX],
TOKEN_1 == ETH_ADDRESS || TOKEN_2 == ETH_ADDRESS,
address(this)
);
}
} else {
// Two sided exit
if (CURVE_INTERFACE == CurveInterface.StableSwapNG) {
return ICurveStableSwapNG(CURVE_POOL).remove_liquidity(poolClaim, _minAmounts);
}
// Redeem proportionally, min amounts are rewritten to a fixed length array
uint256[2] memory minAmounts;
minAmounts[0] = _minAmounts[0];
minAmounts[1] = _minAmounts[1];
exitBalances = new uint256[](_NUM_TOKENS);
if (CURVE_INTERFACE == CurveInterface.V1) {
uint256[2] memory _exitBalances = ICurve2TokenPoolV1(CURVE_POOL).remove_liquidity(poolClaim, minAmounts);
exitBalances[0] = _exitBalances[0];
exitBalances[1] = _exitBalances[1];
} else {
exitBalances[0] = TokenUtils.tokenBalance(TOKEN_1);
exitBalances[1] = TokenUtils.tokenBalance(TOKEN_2);
// Remove liquidity on CurveV2 does not return the exit amounts so we have to measure
// them before and after.
ICurve2TokenPoolV2(CURVE_POOL).remove_liquidity(
// Last two parameters are useEth and receiver = this contract. Check if one of the tokens
// is ETH and have it return ETH in this case so that we can wrap it back into WETH if required.
poolClaim,
minAmounts,
TOKEN_1 == ETH_ADDRESS || TOKEN_2 == ETH_ADDRESS,
address(this)
);
exitBalances[0] = TokenUtils.tokenBalance(TOKEN_1) - exitBalances[0];
exitBalances[1] = TokenUtils.tokenBalance(TOKEN_2) - exitBalances[1];
}
}
}
function _stakeLpTokens(uint256 lpTokens) internal {
if (CONVEX_BOOSTER != address(0)) {
bool success = IConvexBooster(CONVEX_BOOSTER).deposit(CONVEX_POOL_ID, lpTokens, true);
require(success);
} else {
ICurveGauge(CURVE_GAUGE).deposit(lpTokens);
}
}
function _unstakeLpTokens(uint256 poolClaim) internal {
if (CONVEX_REWARD_POOL != address(0)) {
bool success = IConvexRewardPool(CONVEX_REWARD_POOL).withdrawAndUnwrap(poolClaim, false);
require(success);
} else {
ICurveGauge(CURVE_GAUGE).withdraw(poolClaim);
}
}
}
"
},
"src/interfaces/IWithdrawRequestManager.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { TradeType } from "./ITradingModule.sol";
/// Each withdraw request manager contract is responsible for managing withdraws of a token
/// from a specific token (i.e. wstETH, weETH, sUSDe, etc). Each yield strategy can call the
/// appropriate withdraw request manager to initiate a withdraw of a given yield token.
struct StakingTradeParams {
TradeType tradeType;
uint256 minPurchaseAmount;
bytes exchangeData;
uint16 dexId;
bytes stakeData;
}
struct WithdrawRequest {
uint256 requestId;
uint120 yieldTokenAmount;
uint120 sharesAmount;
}
struct TokenizedWithdrawRequest {
uint120 totalYieldTokenAmount;
uint120 totalWithdraw;
bool finalized;
}
interface IWithdrawRequestManager {
event ApprovedVault(address indexed vault, bool indexed isApproved);
event InitiateWithdrawRequest(
address indexed account,
address indexed vault,
uint256 yieldTokenAmount,
uint256 sharesAmount,
uint256 requestId
);
event WithdrawRequestTokenized(
address indexed from, address indexed to, address indexed vault, uint256 requestId, uint256 sharesAmount
);
event WithdrawRequestFinalized(
address indexed vault, address indexed account, uint256 requestId, uint256 totalWithdraw
);
event WithdrawRequestRedeemed(
address indexed vault,
address indexed account,
uint256 requestId,
uint256 withdrawYieldTokenAmount,
uint256 sharesBurned,
bool isCleared
);
/// @notice Returns the token that will be the result of staking
/// @return yieldToken the yield token of the withdraw request manager
function YIELD_TOKEN() external view returns (address);
/// @notice Returns the token that will be the result of the withdraw request
/// @return withdrawToken the withdraw token of the withdraw request manager
function WITHDRAW_TOKEN() external view returns (address);
/// @notice Returns the token that will be used to stake
/// @return stakingToken the staking token of the withdraw request manager
function STAKING_TOKEN() external view returns (address);
/// @notice Returns whether a vault is approved to initiate withdraw requests
/// @param vault the vault to check the approval for
/// @return isApproved whether the vault is approved
function isApprovedVault(address vault) external view returns (bool);
/// @notice Returns whether a vault has a pending withdraw request
/// @param vault the vault to check the pending withdraw request for
/// @param account the account to check the pending withdraw request for
/// @return isPending whether the vault has a pending withdraw request
function isPendingWithdrawRequest(address vault, address account) external view returns (bool);
/// @notice Sets whether a vault is approved to initiate withdraw requests
/// @param vault the vault to set the approval for
/// @param isApproved whether the vault is approved
function setApprovedVault(address vault, bool isApproved) external;
/// @notice Stakes the deposit token to the yield token and transfers it back to the vault
/// @dev Only approved vaults can stake tokens
/// @param depositToken the token to stake, will be transferred from the vault
/// @param amount the amount of tokens to stake
/// @param data additional data for the stake
function stakeTokens(
address depositToken,
uint256 amount,
bytes calldata data
)
external
returns (uint256 yieldTokensMinted);
/// @notice Initiates a withdraw request
/// @dev Only approved vaults can initiate withdraw requests
/// @param account the account to initiate the withdraw request for
/// @param yieldTokenAmount the amount of yield tokens to withdraw
/// @param sharesAmount the amount of shares to withdraw, used to mark the shares to
/// yield token ratio at the time of the withdraw request
/// @param data additional data for the withdraw request
/// @return requestId the request id of the withdraw request
function initiateWithdraw(
address account,
uint256 yieldTokenAmount,
uint256 sharesAmount,
bytes calldata data,
address forceWithdrawFrom
)
external
returns (uint256 requestId);
/// @notice Attempts to redeem active withdraw requests during vault exit
/// @dev Will revert if the withdraw request is not finalized
/// @param account the account to finalize and redeem the withdraw request for
/// @param withdrawYieldTokenAmount the amount of yield tokens to withdraw
/// @param sharesToBurn the amount of shares to burn for the yield token
/// @return tokensWithdrawn amount of withdraw tokens redeemed from the withdraw requests
function finalizeAndRedeemWithdrawRequest(
address account,
uint256 withdrawYieldTokenAmount,
uint256 sharesToBurn
)
external
returns (uint256 tokensWithdrawn);
/// @notice Finalizes withdraw requests outside of a vault exit. This may be required in cases if an
/// account is negligent in exiting their vault position and letting the withdraw request sit idle
/// could result in losses. The withdraw request is finalized and stored in a tokenized withdraw request
/// where the account has the full claim on the withdraw.
/// @dev No access control is enforced on this function but no tokens are transferred off the request
/// manager either.
/// @dev Will revert if the withdraw request is not finalized
function finalizeRequestManual(address vault, address account) external returns (uint256 tokensWithdrawn);
/// @notice If an account has an illiquid withdraw request, this method will tokenize their
/// claim on it during liquidation.
/// @dev Only approved vaults can tokenize withdraw requests
/// @param from the account that is being liquidated
/// @param to the liquidator
/// @param sharesAmount the amount of shares to the liquidator
function tokenizeWithdrawRequest(
address from,
address to,
uint256 sharesAmount
)
external
returns (bool didTokenize);
/// @notice Allows the emergency exit role to rescue tokens from the withdraw request manager
/// @param cooldownHolder the cooldown holder to rescue tokens from
/// @param token the token to rescue
/// @param receiver the receiver of the rescued tokens
/// @param amount the amount of tokens to rescue
function rescueTokens(address cooldownHolder, address token, address receiver, uint256 amount) external;
/// @notice Returns whether a withdraw request can be finalized, only used off chain
/// to determine if a withdraw request can be finalized.
/// @param requestId the request id of the withdraw request
/// @return canFinalize whether the withdraw request can be finalized
function canFinalizeWithdrawRequest(uint256 requestId) external view returns (bool);
/// @notice Returns the withdraw request and tokenized withdraw request for an account
/// @param vault the vault to get the withdraw request for
/// @param account the account to get the withdraw request for
/// @return w the withdraw request
/// @return s the tokenized withdraw request
function getWithdrawRequest(
address vault,
address account
)
external
view
returns (WithdrawRequest memory w, TokenizedWithdrawRequest memory s);
/// @notice Returns the value of a withdraw request in terms of the asset
/// @param vault the vault to get the withdraw request for
/// @param account the account to get the withdraw request for
/// @param asset the asset to get the value for
/// @param shares the amount of shares to get the value for
/// @return hasRequest whether the account has a withdraw request
/// @return value the value of the withdraw request in terms of the asset
function getWithdrawRequestValue(
address vault,
address account,
address asset,
uint256 shares
)
external
view
returns (bool hasRequest, uint256 value);
/// @notice Returns the protocol reported exchange rate between the yield token
/// and then withdraw token.
/// @return exchangeRate the exchange rate of the yield token to the withdraw token
function getExchangeRate() external view returns (uint256 exchangeRate);
/// @notice Returns the known amount of withdraw tokens for a withdraw request
/// @param requestId the request id of the withdraw request
/// @return hasKnownAmount whether the amount is known
/// @return amount the amount of withdraw tokens
function getKnownWithdrawTokenAmount(uint256 requestId)
external
view
returns (bool hasKnownAmount, uint256 amount);
}
"
},
"src/single-sided-lp/AbstractSingleSidedLP.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { AbstractYieldStrategy } from "../AbstractYieldStrategy.sol";
import { DEFAULT_PRECISION, ADDRESS_REGISTRY, WETH } from "../utils/Constants.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { Trade, TradeType } from "../interfaces/ITradingModule.sol";
import { RewardManagerMixin } from "../rewards/RewardManagerMixin.sol";
import { IWithdrawRequestManager, WithdrawRequest } from "../interfaces/IWithdrawRequestManager.sol";
import { TokenUtils } from "../utils/TokenUtils.sol";
import { PoolShareTooHigh, AssetRemaining } from "../interfaces/Errors.sol";
import { ILPLib, TradeParams, DepositParams, RedeemParams, WithdrawParams } from "../interfaces/ISingleSidedLP.sol";
/**
* @notice Base contract for the SingleSidedLP strategy. This strategy deposits into an LP
* pool given a single borrowed currency. Allows for users to trade via external exchanges
* during entry and exit, but the general expected behavior is single sided entries and
* exits. Inheriting contracts will fill in the implementation details for integration with
* the external DEX pool.
*/
abstract contract AbstractSingleSidedLP is RewardManagerMixin {
using TokenUtils for ERC20;
uint256 public immutable MAX_POOL_SHARE;
address internal immutable LP_LIB;
/**
*
* VIRTUAL FUNCTIONS *
* These virtual functions are used to isolate implementation specific *
* behavior. *
*
*/
/// @notice Total number of tokens held by the LP token
function NUM_TOKENS() internal view virtual returns (uint256);
/// @notice Addresses of tokens held and decimal places of each token. ETH will always be
/// recorded in this array as address(0)
function TOKENS() public view virtual returns (ERC20[] memory);
/// @notice Index of the TOKENS() array that refers to the primary borrowed currency by the
/// leveraged vault. All valuations are done in terms of this currency.
function PRIMARY_INDEX() public view virtual returns (uint256);
/// @notice Returns the total supply of the pool token. Is a virtual function because
/// ComposableStablePools use a "virtual supply" and a different method must be called
/// to get the actual total supply.
function _totalPoolSupply() internal view virtual returns (uint256);
/// @dev Checks that the reentrancy context is valid
function _checkReentrancyContext() internal virtual;
/// @notice Called once during initialization to set the initial token approvals.
function _initialApproveTokens() internal virtual {
_delegateCall(LP_LIB, abi.encodeWithSelector(ILPLib.initialApproveTokens.selector));
}
/// @notice Implementation specific wrapper for joining a pool with the given amounts. Will also
/// stake on the relevant booster protocol.
function _joinPoolAndStake(uint256[] memory amounts, uint256 minPoolClaim) internal virtual {
_delegateCall(LP_LIB, abi.encodeWithSelector(ILPLib.joinPoolAndStake.selector, amounts, minPoolClaim));
}
/// @notice Implementation specific wrapper for unstaking from the booster protocol and withdrawing
/// funds from the LP pool
function _unstakeAndExitPool(
uint256 poolClaim,
uint256[] memory minAmounts,
bool isSingleSided
)
internal
virtual
returns (uint256[] memory exitBalances, ERC20[] memory tokens)
{
bytes memory result = _delegateCall(
LP_LIB, abi.encodeWithSelector(ILPLib.unstakeAndExitPool.selector, poolClaim, minAmounts, isSingleSided)
);
(exitBalances, tokens) = abi.decode(result, (uint256[], ERC20[]));
}
/**
*
* CLASS FUNCTIONS *
* Below are class functions that represent the base implementation *
* of the Single Sided LP strategy. *
*
*/
constructor(
uint256 _maxPoolShare,
address _asset,
address _yieldToken,
uint256 _feeRate,
address _rewardManager,
uint8 _yieldTokenDecimals
)
RewardManagerMixin(_asset, _yieldToken, _feeRate, _rewardManager, _yieldTokenDecimals)
{
MAX_POOL_SHARE = _maxPoolShare;
}
/// @notice Validates that all withdraw request managers are either all address(0) or all non-zero
/// @dev this must be called in the child contract constructor since the tokens are not yet
/// initialized in this constructor.
function _validateWithdrawRequestManagers() internal view {
ERC20[] memory tokens = TOKENS();
bool hasNonZeroManager = false;
bool hasZeroManager = false;
for (uint256 i; i < tokens.length; i++) {
address token = address(tokens[i]);
// If the token is address(0), then we use WETH as the withdraw request token
if (token == address(0)) token = address(WETH);
address withdrawRequestManager = address(ADDRESS_REGISTRY.getWithdrawRequestManager(address(token)));
if (withdrawRequestManager == address(0)) {
hasZeroManager = true;
} else {
hasNonZeroManager = true;
}
// If we have both zero and non-zero managers, revert
if (hasZeroManager && hasNonZeroManager) {
revert("Inconsistent withdraw request managers: must be all zero or all non-zero");
}
}
}
function _initialize(bytes calldata data) internal override {
super._initialize(data);
_initialApproveTokens();
}
function _mintYieldTokens(uint256 assets, address, /* receiver */ bytes memory depositData) internal override {
DepositParams memory params = abi.decode(depositData, (DepositParams));
uint256[] memory amounts = new uint256[](NUM_TOKENS());
// If depositTrades are specified, then parts of the initial deposit are traded
// for corresponding amounts of the other pool tokens via external exchanges. If
// these amounts are not specified then the pool will just be joined single sided.
// Deposit trades are not automatically enabled on vaults since the trading module
// requires explicit permission for every token that can be sold by an address.
if (params.depositTrades.length > 0) {
// NOTE: amounts is modified in place
_executeDepositTrades(assets, amounts, params.depositTrades);
} else {
// This is a single sided entry, will revert if index is out of bounds
amounts[PRIMARY_INDEX()] = assets;
}
_joinPoolAndStake(amounts, params.minPoolClaim);
_checkPoolShare();
}
function _checkPoolShare() internal view virtual {
// Checks that the vault does not own too large of a portion of the pool. If this is the case,
// single sided exits may have a detrimental effect on the liquidity.
uint256 maxSupplyThreshold = (_totalPoolSupply() * MAX_POOL_SHARE) / DEFAULT_PRECISION;
// This is incumbent on a 1-1 ratio between the lpToken and the yieldToken, if that is not the
// case then this function must be overridden.
uint256 poolClaim = ERC20(yieldToken).balanceOf(address(this));
if (maxSupplyThreshold < poolClaim) revert PoolShareTooHigh(poolClaim, maxSupplyThreshold);
}
function _redeemShares(
uint256 sharesToRedeem,
address sharesOwner,
bool isEscrowed,
bytes memory redeemData
)
internal
override
{
RedeemParams memory params = abi.decode(redeemData, (RedeemParams));
// Stores the amount of each token that has been withdrawn from the pool.
uint256[] memory exitBalances;
bool isSingleSided;
ERC20[] memory tokens;
if (isEscrowed) {
// Attempt to withdraw all pending requests, tokens may be different if there
// is a withdraw request.
(exitBalances, tokens) = _withdrawPendingRequests(sharesOwner, sharesToRedeem);
// If there are pending requests, then we are not single sided by definition
isSingleSided = false;
} else {
isSingleSided = params.redemptionTrades.length == 0;
uint256 yieldTokensBurned = convertSharesToYieldToken(sharesToRedeem);
(exitBalances, tokens) = _unstakeAndExitPool(yieldTokensBurned, params.minAmounts, isSingleSided);
}
if (!isSingleSided) {
// If not a single sided trade, will execute trades back to the primary token on
// external exchanges. This method will execute EXACT_IN trades to ensure that
// all of the balance in the other tokens is sold for primary.
// Redemption trades are not automatically enabled on vaults since the trading module
// requires explicit permission for every token that can be sold by an address.
_executeRedemptionTrades(tokens, exitBalances, params.redemptionTrades);
}
}
/// @dev Trades the amount of primary token into other secondary tokens prior to entering a pool.
function _executeDepositTrades(
uint256 assets,
uint256[] memory amounts,
TradeParams[] memory depositTrades
)
internal
{
ERC20[] memory tokens = TOKENS();
Trade memory trade;
uint256 assetRemaining = assets;
for (uint256 i; i < amounts.length; i++) {
if (i == PRIMARY_INDEX()) continue;
TradeParams memory t = depositTrades[i];
if (t.tradeAmount > 0) {
if (t.tradeType != TradeType.EXACT_IN_SINGLE && t.tradeType != TradeType.STAKE_TOKEN) revert();
trade = Trade({
tradeType: t.tradeType,
sellToken: address(asset),
buyToken: address(tokens[i]),
amount: t.tradeAmount,
limit: t.minPurchaseAmount,
deadline: block.timestamp,
exchangeData: t.exchangeData
});
// Always selling the primaryToken and buying the secondary token.
(uint256 amountSold, uint256 amountBought) = _executeTrade(trade, t.dexId);
amounts[i] = amountBought;
// Will revert on underflow if over-selling the primary borrowed
assetRemaining -= amountSold;
}
}
if (PRIMARY_INDEX() < amounts.length) {
amounts[PRIMARY_INDEX()] = assetRemaining;
} else if (0 < assetRemaining) {
// This can happen if the asset is not in the pool and we need to trade all
// of the remaining asset for tokens in the pool.
revert AssetRemaining(assetRemaining);
}
}
/// @dev Trades the amount of secondary tokens into the primary token after exiting a pool.
function _executeRedemptionTrades(
ERC20[] memory tokens,
uint256[] memory exitBalances,
TradeParams[] memory redemptionTrades
)
internal
returns (uint256 finalPrimaryBalance)
{
for (uint256 i; i < exitBalances.length; i++) {
if (address(tokens[i]) == address(asset)) {
finalPrimaryBalance += exitBalances[i];
continue;
}
TradeParams memory t = redemptionTrades[i];
// Always sell the entire exit balance to the primary token
if (exitBalances[i] > 0) {
Trade memory trade = Trade({
tradeType: TradeType.EXACT_IN_SINGLE,
sellToken: address(tokens[i]),
buyToken: address(asset),
amount: exitBalances[i],
limit: t.minPurchaseAmount,
deadline: block.timestamp,
exchangeData: t.exchangeData
});
( /* */ , uint256 amountBought) = _executeTrade(trade, t.dexId);
finalPrimaryBalance += amountBought;
}
}
}
function _preLiquidation(
address liquidateAccount,
address liquidator,
uint256 sharesToLiquidate,
uint256 accountSharesHeld
)
internal
override
{
_checkReentrancyContext();
return super._preLiquidation(liquidateAccount, liquidator, sharesToLiquidate, accountSharesHeld);
}
function __postLiquidation(
address liquidator,
address liquidateAccount,
uint256 sharesToLiquidator
)
internal
override
returns (bool didTokenize)
{
bytes memory result = _delegateCall(
LP_LIB,
abi.encodeWithSelector(
ILPLib.tokenizeWithdrawRequest.selector, liquidateAccount, liquidator, sharesToLiquidator
)
);
didTokenize = abi.decode(result, (bool));
}
function __initiateWithdraw(
address account,
uint256 yieldTokenAmount,
uint256 sharesHeld,
bytes memory data,
address forceWithdrawFrom
)
internal
override
returns (uint256 requestId)
{
WithdrawParams memory params = abi.decode(data, (WithdrawParams));
(uint256[] memory exitBalances, /* */ ) = _unstakeAndExitPool({
poolClaim: yieldTokenAmount,
minAmounts: params.minAmounts,
// When initiating a withdraw, we always exit proportionally
isSingleSided: false
});
bytes memory result = _delegateCall(
LP_LIB,
abi.encodeWithSelector(
ILPLib.initiateWithdraw.selector,
account,
sharesHeld,
exitBalances,
params.withdrawData,
forceWithdrawFrom
)
);
uint256[] memory requestIds = abi.decode(result, (uint256[]));
for (uint256 i; i < requestIds.length; i++) {
// Return the first non-zero request id since the base function requires it.
if (requestIds[i] > 0) return requestIds[i];
}
// Revert if there are no non-zero request ids.
revert();
}
function _withdrawPendingRequests(
address sharesOwner,
uint256 sharesToRedeem
)
internal
returns (uint256[] memory exitBalances, ERC20[] memory tokens)
{
bytes memory result = _delegateCall(
LP_LIB,
abi.encodeWithSelector(ILPLib.finalizeAndRedeemWithdrawRequest.selector, sharesOwner, sharesToRedeem)
);
(exitBalances, tokens) = abi.decode(result, (uint256[], ERC20[]));
}
/// @notice Returns the total value in terms of the borrowed token of the account's position
function convertToAssets(uint256 shares) public view override returns (uint256) {
if (t_CurrentAccount != address(0) && _isWithdrawRequestPending(t_CurrentAccount)) {
return ILPLib(LP_LIB).getWithdrawRequestValue(t_CurrentAccount, asset, shares);
}
return super.convertToAssets(shares);
}
function _isWithdrawRequestPending(address account) internal view override returns (bool) {
return ILPLib(LP_LIB).hasPendingWithdrawals(account);
}
}
abstract contract BaseLPLib is ILPLib {
using TokenUtils for ERC20;
function TOKENS() internal view virtual returns (ERC20[] memory);
function _tokensForWithdrawRequest() internal view returns (ERC20[] memory) {
ERC20[] memory tokens = TOKENS();
for (uint256 i; i < tokens.length; i++) {
// In withdraw requests, ETH is always wrapped to WETH.
if (address(tokens[i]) == address(0)) tokens[i] = ERC20(address(WETH));
}
return tokens;
}
/// @inheritdoc ILPLib
function getWithdrawRequestValue(
address account,
address asset,
uint256 shares
)
external
view
returns (uint256 totalValue)
{
ERC20[] memory tokens = _tokensForWithdrawRequest();
for (uint256 i; i < tokens.length; i++) {
IWithdrawRequestManager manager = ADDRESS_REGISTRY.getWithdrawRequestManager(address(tokens[i]));
// This is called as a view function, not a delegate call so use the msg.sender to get
// the correct vault address
(bool hasRequest, uint256 value) = manager.getWithdrawRequestValue(msg.sender, account, asset, shares);
// Ensure that this is true so that we do not lose any value.
require(hasRequest);
totalValue += value;
}
}
/// @inheritdoc ILPLib
function hasPendingWithdrawals(address account) external view override returns (bool) {
ERC20[] memory tokens = _tokensForWithdrawRequest();
for (uint256 i; i < tokens.length; i++) {
IWithdrawRequestManager manager = ADDRESS_REGISTRY.getWithdrawRequestManager(address(tokens[i]));
// If there is no withdraw request manager for the first token then there are no withdraw
// requests to check for. There must be a withdraw request manager for each token.
if (address(manager) == address(0)) return false;
// This is called as a view function, not a delegate call so use the msg.sender to get
// the correct vault address
(WithdrawRequest memory w, /* */ ) = manager.getWithdrawRequest(msg.sender, account);
if (w.requestId != 0) return true;
}
return false;
}
/// @inheritdoc ILPLib
function initiateWithdraw(
address account,
uint256 sharesHeld,
uint256[] calldata exitBalances,
bytes[] calldata withdrawData,
address forceWithdrawFrom
)
external
override
returns (uint256[] memory requestIds)
{
ERC20[] memory tokens = _tokensForWithdrawRequest();
requestIds = new uint256[](exitBalances.length);
for (uint256 i; i < exitBalances.length; i++) {
// For liquidity curve based pools (non-UniswapV3 tick based vaults), it is exceedingly
// difficult to push one of the tokens to a very low balance such that the exitBalance
// actually returns zero. Having a zero balance withdraw request will cause a lot of issues
// so instead we revert here.
if (exitBalances[i] == 0) revert();
IWithdrawRequestManager manager = ADDRESS_REGISTRY.getWithdrawRequestManager(address(tokens[i]));
tokens[i].checkApprove(address(manager), exitBalances[i]);
// Will revert if there is already a pending withdraw
requestIds[i] = manager.initiateWithdraw({
account: account,
yieldTokenAmount: exitBalances[i],
sharesAmount: sharesHeld,
data: withdrawData[i],
forceWithdrawFrom: forceWithdrawFrom
});
}
}
/// @inheritdoc ILPLib
function finalizeAndRedeemWithdrawRequest(
address sharesOwner,
uint256 sharesToRedeem
)
external
override
returns (uint256[] memory exitBalances, ERC20[] memory withdrawTokens)
{
ERC20[] memory tokens = _tokensForWithdrawRequest();
exitBalances = new uint256[](tokens.length);
withdrawTokens = new ERC20[](tokens.length);
WithdrawRequest memory w;
for (uint256 i; i < tokens.length; i++) {
IWithdrawRequestManager manager = ADDRESS_REGISTRY.getWithdrawRequestManager(address(tokens[i]));
(w, /* */ ) = manager.getWithdrawRequest(address(this), sharesOwner);
withdrawTokens[i] = ERC20(manager.WITHDRAW_TOKEN());
// There must be a corresponding withdraw request for each token.
if (w.sharesAmount == 0 || w.requestId == 0) revert();
uint256 yieldTokensBurned = uint256(w.yieldTokenAmount) * sharesToRedeem / w.sharesAmount;
exitBalances[i] = manager.finalizeAndRedeemWithdrawRequest({
account: sharesOwner,
withdrawYieldTokenAmount: yieldTokensBurned,
sharesToBurn: sharesToRedeem
});
}
}
/// @inheritdoc ILPLib
function tokenizeWithdrawRequest(
address liquidateAccount,
address liquidator,
uint256 sharesToLiquidator
)
external
override
returns (bool didTokenize)
{
ERC20[] memory tokens = _tokensForWithdrawRequest();
for (uint256 i; i < tokens.length; i++) {
IWithdrawRequestManager manager = ADDRESS_REGISTRY.getWithdrawRequestManager(address(tokens[i]));
// If there is no withdraw request manager for the first token then there are no
// withdraw requests to tokenize. There must be a withdraw request manager for each token.
if (address(manager) == address(0)) return false;
// If there is no withdraw request then this will be a noop, make sure to OR with the previous result
// to ensure that the result is always set but it is done after so the tokenizeWithdrawRequest call
// is not short circuited.
didTokenize =
manager.tokenizeWithdrawRequest(liquidateAccount, liquidator, sharesToLiquidator) || didTokenize;
}
}
}
"
},
"node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "./IERC20.sol";
import {IERC20Metadata} from "./extensions/IERC20Metadata.sol";
import {Context} from "../../utils/Context.sol";
import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol";
/**
* @dev Implementation of the {IERC20} interface.
*
* This implementation is agnostic to the way tokens are created. This means
* that a supply mechanism has to be added in a derived contract using {_mint}.
*
* TIP: For a detailed writeup see our guide
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
* to implement supply mechanisms].
*
* The default value of {decimals} is 18. To change this, you should override
* this function so it returns a different value.
*
* We have followed general OpenZeppelin Contracts guidelines: functions revert
* instead returning `false` on failure. This behavior is nonetheless
* conventional and does not conflict with the expectations of ERC-20
* applications.
*/
abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
mapping(address account => uint256) private _balances;
mapping(address account => mapping(address spender => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
/**
* @dev Sets the values for {name} and {symbol}.
*
* All two of these values are immutable: they can only be set once during
* construction.
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/**
* @dev Returns the name of the token.
*/
function name() public view virtual returns (string memory) {
return _name;
}
/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view virtual returns (string memory) {
return _symbol;
}
/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei. This is the default value returned by this function, unless
* it's overridden.
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view virtual returns (uint8) {
return 18;
}
/**
* @dev See {IERC20-totalSupply}.
*/
function totalSupply() public view virtual returns (uint256) {
return _totalSupply;
}
/**
* @dev See {IERC20-balanceOf}.
*/
function balanceOf(address account) public view virtual returns (uint256) {
return _balances[account];
}
/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - the caller must have a balance of at least `value`.
*/
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual returns (uint256) {
return _allowances[owner][spender];
}
/**
* @dev See {IERC20-approve}.
*
* NOTE: If `value` is the maximum `uint256`, the allowance is not updated on
* `transferFrom`. This is semantically equivalent to an infinite approval.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, value);
return true;
}
/**
* @dev See {IERC20-transferFrom}.
*
* Skips emitting an {Approval} event indicating an allowance update. This is not
* required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve].
*
* NOTE: Does not update the allowance if the current allowance
* is the maximum `uint256`.
*
* Requirements:
*
* - `from` and `to` cannot be the zero address.
* - `from` must have a balance of at least `value`.
* - the caller must have allowance for ``from``'s tokens of at least
* `value`.
*/
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}
/**
* @dev Moves a `value` amount of tokens from `from` to `to`.
*
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}
/**
* @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
* (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
* this function.
*
* Emits a {Transfer} event.
*/
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// Overflow check required: The rest of the code assumes that totalSupply never overflows
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
// Overflow not possible: value <= fromBalance <= totalSupply.
_balances[from] = fromBalance - value;
}
}
if (to == address(0)) {
unchecked {
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
_totalSupply -= value;
}
} else {
unchecked {
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
_balances[to] += value;
}
}
emit Transfer(from, to, value);
}
/**
* @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).
* Relies on the `_update` mechanism
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _mint(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(address(0), account, value);
}
/**
* @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.
* Relies on the `_update` mechanism.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead
*/
function _burn(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidSender(address(0));
}
_update(account, address(0), value);
}
/**
* @dev Sets `value` as the allowance of `spender` over the `owner` s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*
* Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.
*/
function _approve(address owner, address spender, uint256 value) internal {
_approve(owner, spender, value, true);
}
/**
* @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.
*
* By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by
* `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any
* `Approval` event during `transferFrom` operations.
*
* Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to
* true using the following override:
*
* ```solidity
* function _approve(address owner, address spender, uint256 value, bool) internal virtual override {
* super._approve(owner, spender, value, true);
* }
* ```
*
* Requirements are the same as {_approve}.
*/
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
_allowances[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}
/**
* @dev Updates `owner` s allowance for `spender` based on spent `value`.
*
* Does not update the allowance value in case of infinite allowance.
* Revert if not enough allowance is available.
*
* Does not emit an {Approval} event.
*/
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance < type(uint256).max) {
if (currentAllowance < value) {
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
}
unchecked {
_approve(owner, spender, currentAllowance - value, false);
}
}
}
}
"
},
"src/utils/TokenUtils.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ETH_ADDRESS, ALT_ETH_ADDRESS } from "./Constants.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
library TokenUtils {
using SafeERC20 for ERC20;
function getDecimals(address token) internal view returns (uint8 decimals) {
decimals = (token == ETH_ADDRESS || token == ALT_ETH_ADDRESS) ? 18 : ERC20(token).decimals();
require(decimals <= 18);
}
function tokenBalance(address token) internal view returns (uint256) {
return token == ETH_ADDRESS ? address(this).balance : ERC20(token).balanceOf(address(this));
}
function checkApprove(ERC20 token, address spender, uint256 amount) internal {
if (address(token) == address(0)) return;
token.forceApprove(spender, amount);
}
function checkRevoke(ERC20 token, address spender) internal {
if (address(token) == address(0)) return;
token.forceApprove(spender, 0);
}
function checkReturnCode() internal pure returns (bool success) {
uint256[1] memory result;
assembly {
switch returndatasize()
case 0 {
// This is a non-standard ERC-20
success := 1 // set success to true
}
case 32 {
// This is a compliant ERC-20
returndatacopy(result, 0, 32)
success := mload(result) // Set `success = returndata` of external call
}
default {
// This is an excessively non-compliant ERC-20, revert.
revert(0, 0)
}
}
}
}
"
},
"src/utils/Constants.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { WETH9 } from "../interfaces/IWETH.sol";
import { AddressRegistry } from "../proxy/AddressRegistry.sol";
address constant ETH_ADDRESS = address(0);
address constant ALT_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
uint256 constant DEFAULT_PRECISION = 1e18;
uint256 constant DEFAULT_DECIMALS = 18;
uint256 constant SHARE_PRECISION = 1e24;
uint256 constant VIRTUAL_SHARES = 1e6;
uint256 constant COOLDOWN_PERIOD = 5 minutes;
uint256 constant YEAR = 365 days;
// Will move these to a deployment file when we go to multiple chains
uint256 constant CHAIN_ID_MAINNET = 1;
WETH9 constant WETH = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
AddressRegistry constant ADDRESS_REGISTRY = AddressRegistry(0xe335d314BD4eF7DD44F103dC124FEFb7Ce63eC95);
"
},
"node_modules/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.0) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC-20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
/**
* @dev An operation with an ERC-20 token failed.
*/
error SafeERC20FailedOperation(address token);
/**
* @dev Indicates a failed `decreaseAllowance` request.
*/
error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
/**
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
}
/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
*/
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 oldAllowance = token.allowance(address(this), spender);
forceApprove(token, spender, oldAllowance + value);
}
/**
* @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
* value, non-reverting calls are assumed to be successful.
*
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
*/
function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
unchecked {
uint256 currentAllowance = token.allowance(address(this), spender);
if (currentAllowance < requestedDecrease) {
revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
}
forceApprove(token, spender, currentAllowance - requestedDecrease);
}
}
/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
* to be set to zero before setting it to a non-zero value, such as USDT.
*
* NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
* only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
* set here.
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));
if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
_callOptionalReturn(token, approvalCall);
}
}
/**
* @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
Submitted on: 2025-09-30 10:08:39
Comments
Log in to comment.
No comments yet.