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/staking/StakingStrategy.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { AbstractStakingStrategy } from "./AbstractStakingStrategy.sol";
import { ADDRESS_REGISTRY, CHAIN_ID_MAINNET } from "../utils/Constants.sol";
contract StakingStrategy is AbstractStakingStrategy {
constructor(
address _asset,
address _yieldToken,
uint256 _feeRate
)
AbstractStakingStrategy(_asset, _yieldToken, _feeRate, ADDRESS_REGISTRY.getWithdrawRequestManager(_yieldToken))
{
require(block.chainid == CHAIN_ID_MAINNET);
}
function strategy() public pure override returns (string memory) {
return "Staking";
}
}
"
},
"src/staking/AbstractStakingStrategy.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { AbstractYieldStrategy } from "../AbstractYieldStrategy.sol";
import { IWithdrawRequestManager, WithdrawRequest } from "../interfaces/IWithdrawRequestManager.sol";
import { Trade, TradeType } from "../interfaces/ITradingModule.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { TokenUtils } from "../utils/TokenUtils.sol";
struct RedeemParams {
uint8 dexId;
uint256 minPurchaseAmount;
bytes exchangeData;
}
struct DepositParams {
uint8 dexId;
uint256 minPurchaseAmount;
bytes exchangeData;
}
/**
* Supports vaults that borrow a token and stake it into a token that earns yield but may
* require some illiquid redemption period.
*/
abstract contract AbstractStakingStrategy is AbstractYieldStrategy {
using TokenUtils for ERC20;
address internal immutable withdrawToken;
constructor(
address _asset,
address _yieldToken,
uint256 _feeRate,
IWithdrawRequestManager _withdrawRequestManager
)
AbstractYieldStrategy(_asset, _yieldToken, _feeRate, ERC20(_yieldToken).decimals())
{
// For Pendle PT the yield token does not define the withdraw request manager,
// it is the token out sy
withdrawRequestManager = _withdrawRequestManager;
if (address(withdrawRequestManager) != address(0)) {
accountingAsset = withdrawRequestManager.STAKING_TOKEN();
withdrawToken = withdrawRequestManager.WITHDRAW_TOKEN();
} else {
withdrawToken = address(0);
// Accounting asset will be set to the asset itself if no withdraw
// request manager is set. For Pendle PT strategies this will be
// set to the token in sy.
}
}
/// @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)) {
(bool hasRequest, uint256 value) =
withdrawRequestManager.getWithdrawRequestValue(address(this), t_CurrentAccount, asset, shares);
// If the account does not have a withdraw request then this will fall through
// to the super implementation.
if (hasRequest) return value;
}
return super.convertToAssets(shares);
}
function _initiateWithdraw(
address account,
uint256 yieldTokenAmount,
uint256 sharesHeld,
bytes memory data,
address forceWithdrawFrom
)
internal
virtual
override
returns (uint256 requestId)
{
ERC20(yieldToken).checkApprove(address(withdrawRequestManager), yieldTokenAmount);
requestId = withdrawRequestManager.initiateWithdraw({
account: account,
yieldTokenAmount: yieldTokenAmount,
sharesAmount: sharesHeld,
data: data,
forceWithdrawFrom: forceWithdrawFrom
});
}
/// @dev By default we can use the withdraw request manager to stake the tokens
function _mintYieldTokens(
uint256 assets,
address, /* receiver */
bytes memory depositData
)
internal
virtual
override
{
ERC20(asset).checkApprove(address(withdrawRequestManager), assets);
withdrawRequestManager.stakeTokens(address(asset), assets, depositData);
}
function _redeemShares(
uint256 sharesToRedeem,
address sharesOwner,
bool isEscrowed,
bytes memory redeemData
)
internal
override
{
if (isEscrowed) {
(WithdrawRequest memory w, /* */ ) = withdrawRequestManager.getWithdrawRequest(address(this), sharesOwner);
uint256 yieldTokensBurned = uint256(w.yieldTokenAmount) * sharesToRedeem / w.sharesAmount;
uint256 tokensClaimed = withdrawRequestManager.finalizeAndRedeemWithdrawRequest({
account: sharesOwner,
withdrawYieldTokenAmount: yieldTokensBurned,
sharesToBurn: sharesToRedeem
});
// Trades may be required here if the borrowed token is not the same as what is
// received when redeeming.
if (asset != withdrawToken) {
RedeemParams memory params = abi.decode(redeemData, (RedeemParams));
Trade memory trade = Trade({
tradeType: TradeType.EXACT_IN_SINGLE,
sellToken: address(withdrawToken),
buyToken: address(asset),
amount: tokensClaimed,
limit: params.minPurchaseAmount,
deadline: block.timestamp,
exchangeData: params.exchangeData
});
_executeTrade(trade, params.dexId);
}
} else {
uint256 yieldTokensBurned = convertSharesToYieldToken(sharesToRedeem);
_executeInstantRedemption(yieldTokensBurned, redeemData);
}
}
/// @notice Default implementation for an instant redemption is to sell the staking token to the
/// borrow token through the trading module. Can be overridden if required for different implementations.
function _executeInstantRedemption(
uint256 yieldTokensToRedeem,
bytes memory redeemData
)
internal
virtual
returns (uint256 assetsPurchased)
{
RedeemParams memory params = abi.decode(redeemData, (RedeemParams));
Trade memory trade = Trade({
tradeType: TradeType.EXACT_IN_SINGLE,
sellToken: address(yieldToken),
buyToken: address(asset),
amount: yieldTokensToRedeem,
limit: params.minPurchaseAmount,
deadline: block.timestamp,
exchangeData: params.exchangeData
});
// Executes a trade on the given Dex, the vault must have permissions set for
// each dex and token it wants to sell.
( /* */ , assetsPurchased) = _executeTrade(trade, params.dexId);
}
/* solhint-disable no-empty-blocks */
function _preLiquidation(address, address, uint256, uint256) internal override { /* no-op */ }
/* solhint-enable no-empty-blocks */
function _postLiquidation(
address liquidator,
address liquidateAccount,
uint256 sharesToLiquidator
)
internal
override
returns (bool didTokenize)
{
if (address(withdrawRequestManager) != address(0)) {
// No need to accrue fees because neither the total supply or total yield token balance is changing. If
// there
// is no withdraw request then this will be a noop.
didTokenize =
withdrawRequestManager.tokenizeWithdrawRequest(liquidateAccount, liquidator, sharesToLiquidator);
}
}
}
"
},
"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);
"
},
"src/AbstractYieldStrategy.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
import { DEFAULT_DECIMALS, DEFAULT_PRECISION, YEAR, ADDRESS_REGISTRY, VIRTUAL_SHARES } from "./utils/Constants.sol";
import {
Unauthorized,
UnauthorizedLendingMarketTransfer,
InsufficientSharesHeld,
CannotEnterPosition,
CurrentAccountAlreadySet
} from "./interfaces/Errors.sol";
import { IYieldStrategy } from "./interfaces/IYieldStrategy.sol";
import { IOracle } from "./interfaces/Morpho/IOracle.sol";
import { TokenUtils } from "./utils/TokenUtils.sol";
import { Trade, TradeType, TRADING_MODULE, nProxy } from "./interfaces/ITradingModule.sol";
import { IWithdrawRequestManager } from "./interfaces/IWithdrawRequestManager.sol";
import { Initializable } from "./proxy/Initializable.sol";
import { MORPHO } from "./interfaces/Morpho/IMorpho.sol";
/// @title AbstractYieldStrategy
/// @notice This is the base contract for all yield strategies, it implements the core logic for
/// minting, burning and the valuation of tokens.
abstract contract AbstractYieldStrategy is Initializable, ERC20, ReentrancyGuardTransient, IYieldStrategy {
using TokenUtils for ERC20;
using SafeERC20 for ERC20;
uint256 internal constant VIRTUAL_YIELD_TOKENS = 1;
uint256 internal constant SHARE_PRECISION = DEFAULT_PRECISION * VIRTUAL_SHARES;
/// @inheritdoc IYieldStrategy
address public immutable override asset;
/// @inheritdoc IYieldStrategy
address public immutable override yieldToken;
/// @inheritdoc IYieldStrategy
uint256 public immutable override feeRate;
/// @inheritdoc IYieldStrategy
address public immutable override accountingAsset;
IWithdrawRequestManager internal immutable withdrawRequestManager;
uint8 internal immutable _yieldTokenDecimals;
uint8 internal immutable _assetDecimals;
uint256 internal immutable _feeAdjustmentPrecision;
/**
* Storage Variables ********
*/
string private s_name;
string private s_symbol;
uint32 private s_lastFeeAccrualTime;
uint256 private s_accruedFeesInYieldToken;
uint256 private s_escrowedShares;
uint256 internal s_yieldTokenBalance;
/**
* End Storage Variables *****
*/
/**
* Transient Variables ********
*/
// Used to adjust the valuation call of price(), is set on some methods and
// cleared by the lending router using clearCurrentAccount(). This is required to
// ensure that the variable is set throughout the entire context of the lending router
// call.
address internal transient t_CurrentAccount;
// Set and cleared on every call to a lending router authorized method
address internal transient t_CurrentLendingRouter;
// Used to authorize transfers off of the lending market
address internal transient t_AllowTransfer_To;
uint256 internal transient t_AllowTransfer_Amount;
/**
* End Transient Variables *****
*/
constructor(address _asset, address _yieldToken, uint256 _feeRate, uint8 __yieldTokenDecimals) ERC20("", "") {
feeRate = _feeRate;
asset = address(_asset);
yieldToken = address(_yieldToken);
// Not all yield tokens have a decimals() function (i.e. Convex staked tokens), so we
// do have to pass in the decimals as a parameter.
_yieldTokenDecimals = __yieldTokenDecimals;
_assetDecimals = TokenUtils.getDecimals(_asset);
accountingAsset = address(_asset);
if (_yieldTokenDecimals < 18) {
_feeAdjustmentPrecision = 10 ** (18 - _yieldTokenDecimals);
} else {
_feeAdjustmentPrecision = 1;
}
}
function name() public view override(ERC20, IERC20Metadata) returns (string memory) {
return s_name;
}
function symbol() public view override(ERC20, IERC20Metadata) returns (string memory) {
return s_symbol;
}
function decimals() public view override(ERC20, IERC20Metadata) returns (uint8) {
return _yieldTokenDecimals + 6;
}
/**
* Valuation and Conversion Functions **
*/
/// @inheritdoc IYieldStrategy
function convertSharesToYieldToken(uint256 shares) public view override returns (uint256) {
// NOTE: rounds down on division
return (shares * (s_yieldTokenBalance - feesAccrued() + VIRTUAL_YIELD_TOKENS)) / (effectiveSupply());
}
/// @inheritdoc IYieldStrategy
function convertYieldTokenToShares(uint256 yieldTokens) public view returns (uint256) {
// NOTE: rounds down on division
return (yieldTokens * effectiveSupply()) / (s_yieldTokenBalance - feesAccrued() + VIRTUAL_YIELD_TOKENS);
}
/// @inheritdoc IYieldStrategy
function convertToShares(uint256 assets) public view override returns (uint256) {
// NOTE: rounds down on division
uint256 yieldTokens = assets * (10 ** (_yieldTokenDecimals + DEFAULT_DECIMALS))
/ (convertYieldTokenToAsset() * (10 ** _assetDecimals));
return convertYieldTokenToShares(yieldTokens);
}
/// @inheritdoc IOracle
function price() public view override returns (uint256) {
require(_reentrancyGuardEntered() == false);
// Disable direct borrowing from Morpho, but allow any other callers to see
// the proper price.
if (msg.sender == address(MORPHO) && t_CurrentAccount == address(0)) return 0;
return convertToAssets(SHARE_PRECISION) * (10 ** (36 - 24));
}
/// @inheritdoc IYieldStrategy
function price(address borrower) external override nonReentrant returns (uint256) {
// Do not change the current account in this method since this method is not
// authenticated and we do not want to have any unexpected side effects.
address prevCurrentAccount = t_CurrentAccount;
t_CurrentAccount = borrower;
uint256 p = convertToAssets(SHARE_PRECISION) * (10 ** (36 - 24));
t_CurrentAccount = prevCurrentAccount;
return p;
}
/// @inheritdoc IYieldStrategy
function totalAssets() public view override returns (uint256) {
return convertToAssets(totalSupply());
}
/// @inheritdoc IYieldStrategy
function convertYieldTokenToAsset() public view returns (uint256) {
// The trading module always returns a positive rate in 18 decimals so we can safely
// cast to uint256
(int256 rate, /* */ ) = TRADING_MODULE.getOraclePrice(yieldToken, asset);
return uint256(rate);
}
/// @inheritdoc IYieldStrategy
function effectiveSupply() public view returns (uint256) {
return (totalSupply() - s_escrowedShares + VIRTUAL_SHARES);
}
function getInternalStorage()
external
view
returns (
uint256 yieldTokenBalance,
uint256 escrowedShares,
uint256 accruedFeesInYieldToken,
uint256 lastFeeAccrualTime
)
{
return (s_yieldTokenBalance, s_escrowedShares, s_accruedFeesInYieldToken, s_lastFeeAccrualTime);
}
/**
* Fee Methods **
*/
/// @inheritdoc IYieldStrategy
function feesAccrued() public view override returns (uint256 feesAccruedInYieldToken) {
return (s_accruedFeesInYieldToken + _calculateAdditionalFeesInYieldToken()) / _feeAdjustmentPrecision;
}
/// @inheritdoc IYieldStrategy
function collectFees() external override nonReentrant returns (uint256 feesCollected) {
_accrueFees();
feesCollected = s_accruedFeesInYieldToken / _feeAdjustmentPrecision;
s_yieldTokenBalance -= feesCollected;
s_accruedFeesInYieldToken -= (feesCollected * _feeAdjustmentPrecision);
_transferYieldTokenToOwner(ADDRESS_REGISTRY.feeReceiver(), feesCollected);
emit FeesCollected(feesCollected);
}
/**
* Core Functions **
*/
modifier onlyLendingRouter() {
if (ADDRESS_REGISTRY.isLendingRouter(msg.sender) == false) revert Unauthorized(msg.sender);
t_CurrentLendingRouter = msg.sender;
_;
delete t_CurrentLendingRouter;
}
modifier setCurrentAccount(address onBehalf) {
if (t_CurrentAccount == address(0)) {
t_CurrentAccount = onBehalf;
} else if (t_CurrentAccount != onBehalf) {
revert CurrentAccountAlreadySet();
}
_;
}
/// @inheritdoc IYieldStrategy
function clearCurrentAccount() external override onlyLendingRouter {
delete t_CurrentAccount;
}
function mintShares(
uint256 assetAmount,
address receiver,
bytes calldata depositData
)
external
override
onlyLendingRouter
setCurrentAccount(receiver)
nonReentrant
returns (uint256 sharesMinted)
{
// Cannot mint shares if the receiver has an active withdraw request
if (_isWithdrawRequestPending(receiver)) revert CannotEnterPosition();
ERC20(asset).safeTransferFrom(t_CurrentLendingRouter, address(this), assetAmount);
sharesMinted = _mintSharesGivenAssets(assetAmount, depositData, receiver);
t_AllowTransfer_To = t_CurrentLendingRouter;
t_AllowTransfer_Amount = sharesMinted;
// Transfer the shares to the lending router so it can supply collateral
_transfer(receiver, t_CurrentLendingRouter, sharesMinted);
_checkInvariant();
}
function burnShares(
address sharesOwner,
uint256 sharesToBurn,
uint256 sharesHeld,
bytes calldata redeemData
)
external
override
onlyLendingRouter
setCurrentAccount(sharesOwner)
nonReentrant
returns (uint256 assetsWithdrawn)
{
assetsWithdrawn = _burnShares(sharesToBurn, sharesHeld, redeemData, sharesOwner);
// Send all the assets back to the lending router
ERC20(asset).safeTransfer(t_CurrentLendingRouter, assetsWithdrawn);
_checkInvariant();
}
function allowTransfer(
address to,
uint256 amount,
address currentAccount
)
external
setCurrentAccount(currentAccount)
onlyLendingRouter
{
// Sets the transient variables to allow the lending market to transfer shares on exit position
// or liquidation.
t_AllowTransfer_To = to;
t_AllowTransfer_Amount = amount;
}
function preLiquidation(
address liquidator,
address liquidateAccount,
uint256 sharesToLiquidate,
uint256 accountSharesHeld
)
external
onlyLendingRouter
nonReentrant
{
t_CurrentAccount = liquidateAccount;
// Liquidator cannot liquidate if they have an active withdraw request, including a tokenized
// withdraw request.
if (_isWithdrawRequestPending(liquidator)) revert CannotEnterPosition();
// Cannot receive a pending withdraw request if the liquidator has a balanceOf
if (_isWithdrawRequestPending(liquidateAccount) && balanceOf(liquidator) > 0) {
revert CannotEnterPosition();
}
_preLiquidation(liquidateAccount, liquidator, sharesToLiquidate, accountSharesHeld);
// Allow transfers to the lending router which will proxy the call to liquidate.
t_AllowTransfer_To = msg.sender;
t_AllowTransfer_Amount = sharesToLiquidate;
}
function postLiquidation(
address liquidator,
address liquidateAccount,
uint256 sharesToLiquidator
)
external
onlyLendingRouter
nonReentrant
{
t_AllowTransfer_To = liquidator;
t_AllowTransfer_Amount = sharesToLiquidator;
// Transfer the shares to the liquidator from the lending router
_transfer(t_CurrentLendingRouter, liquidator, sharesToLiquidator);
_postLiquidation(liquidator, liquidateAccount, sharesToLiquidator);
// Clear the transient variables to prevent re-use in a future call.
delete t_CurrentAccount;
ADDRESS_REGISTRY.emitAccountNativePosition(liquidator, false);
_checkInvariant();
}
/// @inheritdoc IYieldStrategy
/// @dev We do not set the current account here because valuation is not done in this method.
/// A native balance does not require a collateral check.
function redeemNative(
uint256 sharesToRedeem,
bytes memory redeemData
)
external
override
nonReentrant
returns (uint256 assetsWithdrawn)
{
uint256 sharesHeld = balanceOf(msg.sender);
if (sharesHeld == 0) revert InsufficientSharesHeld();
assetsWithdrawn = _burnShares(sharesToRedeem, sharesHeld, redeemData, msg.sender);
ERC20(asset).safeTransfer(msg.sender, assetsWithdrawn);
if (sharesHeld == sharesToRedeem) ADDRESS_REGISTRY.emitAccountNativePosition(msg.sender, true);
_checkInvariant();
}
/// @inheritdoc IYieldStrategy
function initiateWithdraw(
address account,
uint256 sharesHeld,
bytes calldata data,
address forceWithdrawFrom
)
external
override
nonReentrant
onlyLendingRouter
setCurrentAccount(account)
returns (uint256 requestId)
{
requestId = _withdraw(account, sharesHeld, data, forceWithdrawFrom);
_checkInvariant();
}
/// @inheritdoc IYieldStrategy
/// @dev We do not set the current account here because valuation is not done in this method. A
/// native balance does not require a collateral check.
function initiateWithdrawNative(bytes memory data) external override nonReentrant returns (uint256 requestId) {
requestId = _withdraw(msg.sender, balanceOf(msg.sender), data, address(0));
_checkInvariant();
}
function _withdraw(
address account,
uint256 sharesHeld,
bytes memory data,
address forceWithdrawFrom
)
internal
returns (uint256 requestId)
{
if (sharesHeld == 0) revert InsufficientSharesHeld();
// Accrue fees before initiating a withdraw since it will change the effective supply
_accrueFees();
uint256 yieldTokenAmount = convertSharesToYieldToken(sharesHeld);
requestId = _initiateWithdraw(account, yieldTokenAmount, sharesHeld, data, forceWithdrawFrom);
// Revert in the edge case that the withdraw request is not created.
require(requestId > 0);
// Escrow the shares after the withdraw since it will change the effective supply
// during reward claims when using the RewardManagerMixin.
s_escrowedShares += sharesHeld;
s_yieldTokenBalance -= yieldTokenAmount;
}
/**
* Private Functions **
*/
function _calculateAdditionalFeesInYieldToken() private view returns (uint256 additionalFeesInYieldToken) {
uint256 timeSinceLastFeeAccrual = block.timestamp - s_lastFeeAccrualTime;
// e ^ (feeRate * timeSinceLastFeeAccrual / YEAR)
uint256 x = (feeRate * timeSinceLastFeeAccrual) / YEAR;
if (x == 0) return 0;
// Don't accrue fees on a dust balance of yield tokens.
if (s_yieldTokenBalance < VIRTUAL_SHARES) return 0;
uint256 preFeeUserHeldYieldTokens = s_yieldTokenBalance * _feeAdjustmentPrecision - s_accruedFeesInYieldToken;
// Taylor approximation of e ^ x = 1 + x + x^2 / 2! + x^3 / 3! + ...
uint256 eToTheX = DEFAULT_PRECISION + x + (x * x) / (2 * DEFAULT_PRECISION)
+ (x * x * x) / (6 * DEFAULT_PRECISION * DEFAULT_PRECISION);
// Decay the user's yield tokens by e ^ (feeRate * timeSinceLastFeeAccrual / YEAR)
uint256 postFeeUserHeldYieldTokens = preFeeUserHeldYieldTokens * DEFAULT_PRECISION / eToTheX;
additionalFeesInYieldToken = preFeeUserHeldYieldTokens - postFeeUserHeldYieldTokens;
}
function _accrueFees() private {
if (s_lastFeeAccrualTime == block.timestamp) return;
// NOTE: this has to be called before any mints or burns.
s_accruedFeesInYieldToken += _calculateAdditionalFeesInYieldToken();
s_lastFeeAccrualTime = uint32(block.timestamp);
}
function _update(address from, address to, uint256 value) internal override {
if (from != address(0) && to != address(0)) {
// Any transfers off of the lending market must be authorized here, this means that native balances
// held cannot be transferred.
if (t_AllowTransfer_To != to) revert UnauthorizedLendingMarketTransfer(from, to, value);
if (t_AllowTransfer_Amount < value) revert UnauthorizedLendingMarketTransfer(from, to, value);
delete t_AllowTransfer_To;
delete t_AllowTransfer_Amount;
}
super._update(from, to, value);
}
/**
* Internal Helper Functions **
*/
function _checkInvariant() internal view {
require(s_yieldTokenBalance <= ERC20(yieldToken).balanceOf(address(this)));
}
function _isWithdrawRequestPending(address account) internal view virtual returns (bool) {
return address(withdrawRequestManager) != address(0)
&& withdrawRequestManager.isPendingWithdrawRequest(address(this), account);
}
/// @dev Can be used to delegate call to the TradingModule's implementation in order to execute a trade
function _executeTrade(
Trade memory trade,
uint16 dexId
)
internal
returns (uint256 amountSold, uint256 amountBought)
{
if (trade.tradeType == TradeType.STAKE_TOKEN) {
IWithdrawRequestManager wrm = ADDRESS_REGISTRY.getWithdrawRequestManager(trade.buyToken);
ERC20(trade.sellToken).checkApprove(address(wrm), trade.amount);
amountBought = wrm.stakeTokens(trade.sellToken, trade.amount, trade.exchangeData);
emit TradeExecuted(trade.sellToken, trade.buyToken, trade.amount, amountBought);
return (trade.amount, amountBought);
} else {
address implementation = nProxy(payable(address(TRADING_MODULE))).getImplementation();
bytes memory result = _delegateCall(
implementation, abi.encodeWithSelector(TRADING_MODULE.executeTrade.selector, dexId, trade)
);
(amountSold, amountBought) = abi.decode(result, (uint256, uint256));
}
}
function _delegateCall(address target, bytes memory data) internal returns (bytes memory result) {
bool success;
(success, result) = target.delegatecall(data);
if (!success) {
assembly {
// Copy the return data to memory
returndatacopy(0, 0, returndatasize())
// Revert with the return data
revert(0, returndatasize())
}
}
}
/**
* Virtual Functions **
*/
function _initialize(bytes calldata data) internal virtual override {
(string memory _name, string memory _symbol) = abi.decode(data, (string, string));
s_name = _name;
s_symbol = _symbol;
s_lastFeeAccrualTime = uint32(block.timestamp);
emit VaultCreated(address(this));
}
/// @dev Marked as virtual to allow for RewardManagerMixin to override
function _mintSharesGivenAssets(
uint256 assets,
bytes memory depositData,
address receiver
)
internal
virtual
returns (uint256 sharesMinted)
{
if (assets == 0) return 0;
// First accrue fees on the yield token
_accrueFees();
uint256 yieldTokensBefore = ERC20(yieldToken).balanceOf(address(this));
_mintYieldTokens(assets, receiver, depositData);
uint256 yieldTokensAfter = ERC20(yieldToken).balanceOf(address(this));
uint256 yieldTokensMinted = yieldTokensAfter - yieldTokensBefore;
sharesMinted =
(yieldTokensMinted * effectiveSupply()) / (s_yieldTokenBalance - feesAccrued() + VIRTUAL_YIELD_TOKENS);
s_yieldTokenBalance += yieldTokensMinted;
_mint(receiver, sharesMinted);
}
/// @dev Marked as virtual to allow for RewardManagerMixin to override
function _burnShares(
uint256 sharesToBurn,
uint256, /* sharesHeld */
bytes memory redeemData,
address sharesOwner
)
internal
virtual
returns (uint256 assetsWithdrawn)
{
if (sharesToBurn == 0) return 0;
bool isEscrowed = _isWithdrawRequestPending(sharesOwner);
uint256 initialAssetBalance = TokenUtils.tokenBalance(asset);
// First accrue fees on the yield token
_accrueFees();
uint256 yieldTokensBefore = ERC20(yieldToken).balanceOf(address(this));
_redeemShares(sharesToBurn, sharesOwner, isEscrowed, redeemData);
if (isEscrowed) s_escrowedShares -= sharesToBurn;
uint256 yieldTokensAfter = ERC20(yieldToken).balanceOf(address(this));
uint256 yieldTokensRedeemed = yieldTokensBefore - yieldTokensAfter;
s_yieldTokenBalance -= yieldTokensRedeemed;
uint256 finalAssetBalance = TokenUtils.tokenBalance(asset);
assetsWithdrawn = finalAssetBalance - initialAssetBalance;
// This burns the shares from the sharesOwner's balance
_burn(sharesOwner, sharesToBurn);
}
/// @dev Some yield tokens (such as Convex staked tokens) cannot be transferred, so we may need
/// to override this function.
function _transferYieldTokenToOwner(address owner, uint256 yieldTokens) internal virtual {
ERC20(yieldToken).safeTransfer(owner, yieldTokens);
}
/// @dev Returns the maximum number of shares that can be liquidated. Allows the strategy to override the
/// underlying lending market's liquidation logic.
function _preLiquidation(
address liquidateAccount,
address liquidator,
uint256 sharesToLiquidate,
uint256 accountSharesHeld
)
internal
virtual;
/// @dev Called after liquidation
function _postLiquidation(
address liquidator,
address liquidateAccount,
uint256 sharesToLiquidator
)
internal
virtual
returns (bool didTokenize);
/// @dev Mints yield tokens given a number of assets.
function _mintYieldTokens(uint256 assets, address receiver, bytes memory depositData) internal virtual;
/// @dev Redeems shares
function _redeemShares(
uint256 sharesToRedeem,
address sharesOwner,
bool isEscrowed,
bytes memory redeemData
)
internal
virtual;
function _initiateWithdraw(
address account,
uint256 yieldTokenAmount,
uint256 sharesHeld,
bytes memory data,
address forceWithdrawFrom
)
internal
virtual
returns (uint256 requestId);
/// @inheritdoc IYieldStrategy
function convertToAssets(uint256 shares) public view virtual override returns (uint256) {
uint256 yieldTokens = convertSharesToYieldToken(shares);
// NOTE: rounds down on division
return (yieldTokens * convertYieldTokenToAsset() * (10 ** _assetDecimals))
/ (10 ** (_yieldTokenDecimals + DEFAULT_DECIMALS));
}
}
"
},
"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/interfaces/ITradingModule.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity >=0.8.28;
import { AggregatorV2V3Interface } from "./AggregatorV2V3Interface.sol";
enum DexId {
_UNUSED, // flag = 1, enum = 0
UNISWAP_V2, // flag = 2, enum = 1
UNISWAP_V3, // flag = 4, enum = 2
ZERO_EX, // flag = 8, enum = 3
BALANCER_V2, // flag = 16, enum = 4
// NOTE: this id is unused in the TradingModule
CURVE, // flag = 32, enum = 5
NOTIONAL_VAULT, // flag = 64, enum = 6
CURVE_V2, // flag = 128, enum = 7
CAMELOT_V3 // flag = 256, enum = 8
}
enum TradeType {
EXACT_IN_SINGLE, // flag = 1
EXACT_OUT_SINGLE, // flag = 2
EXACT_IN_BATCH, // flag = 4
EXACT_OUT_BATCH, // flag = 8
STAKE_TOKEN // flag = 16
}
struct UniV3SingleData {
uint24 fee;
}
// Path is packed encoding `token, fee, token, fee, outToken`
struct UniV3BatchData {
bytes path;
}
struct CurveV2SingleData {
// Address of the pool to use for the swap
address pool;
int128 fromIndex;
int128 toIndex;
}
struct CurveV2BatchData {
// Array of [initial token, pool, token, pool, token, ...]
// The array is iterated until a pool address of 0x00, then the last
// given token is transferred to `_receiver`
address[9] route;
// Multidimensional array of [i, j, swap type] where i and j are the correct
// values for the n'th pool in `_route`. The swap type should be
// 1 for a stableswap `exchange`,
// 2 for stableswap `exchange_underlying`,
// 3 for a cryptoswap `exchange`,
// 4 for a cryptoswap `exchange_underlying`,
// 5 for factory metapools with lending base pool `exchange_underlying`,
// 6 for factory crypto-meta pools underlying exchange (`exchange` method in zap),
// 7-11 for wrapped coin (underlying for lending or fake pool) -> LP token "exchange" (actually `add_liquidity`),
// 12-14 for LP token -> wrapped coin (underlying for lending pool) "exchange" (actually
// `remove_liquidity_one_coin`)
// 15 for WETH -> ETH "exchange" (actually deposit/withdraw)
uint256[3][4] swapParams;
}
struct Trade {
TradeType tradeType;
address sellToken;
address buyToken;
uint256 amount;
/// minBuyAmount or maxSellAmount
uint256 limit;
uint256 deadline;
bytes exchangeData;
}
error InvalidTrade();
error DynamicTradeFailed();
error TradeFailed();
interface nProxy {
function getImplementation() external view returns (address);
}
interface ITradingModule {
struct TokenPermissions {
bool allowSell;
/// @notice allowed DEXes
uint32 dexFlags;
/// @notice allowed trade types
uint32 tradeTypeFlags;
}
event TradeExecuted(address indexed sellToken, address indexed buyToken, uint256 sellAmount, uint256 buyAmount);
event PriceOracleUpdated(address token, address oracle);
event MaxOracleFreshnessUpdated(uint32 currentValue, uint32 newValue);
event TokenPermissionsUpdated(address sender, address token, TokenPermissions permissions);
function tokenWhitelist(
address spender,
address token
)
external
view
returns (bool allowSell, uint32 dexFlags, uint32 tradeTypeFlags);
function priceOracles(address token) external view returns (AggregatorV2V3Interface oracle, uint8 rateDecimals);
function getExecutionData(
uint16 dexId,
address from,
Trade calldata trade
)
external
view
returns (address spender, address target, uint256 value, bytes memory params);
function setMaxOracleFreshness(uint32 newMaxOracleFreshnessInSeconds) external;
function setPriceOracle(address token, AggregatorV2V3Interface oracle) external;
function setTokenPermissions(address sender, address token, TokenPermissions calldata permissions) external;
function getOraclePrice(address inToken, address outToken) external view returns (int256 answer, int256 decimals);
function executeTrade(
uint16 dexId,
Trade calldata trade
)
external
payable
returns (uint256 amountSold, uint256 amountBought);
function executeTradeWithDynamicSlippage(
uint16 dexId,
Trade memory trade,
uint32 dynamicSlippageLimit
)
external
payable
returns (uint256 amountSold, uint256 amountBought);
function getLimitAmount(
address from,
TradeType tradeType,
address sellToken,
address buyToken,
uint256 amount,
uint32 slippageLimit
)
external
view
returns (uint256 limitAmount);
function canExecuteTrade(address from, uint16 dexId, Trade calldata trade) external view returns (bool);
}
ITradingModule constant TRADING_MODULE = ITradingModule(0x594734c7e06C3D483466ADBCe401C6Bd269746C8);
"
},
"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/interfaces/IWETH.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface WETH9 is IERC20 {
function deposit() external payable;
function withdraw(uint256 wad) external;
}
"
},
"src/proxy/AddressRegistry.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { Unauthorized, CannotEnterPosition, InvalidVault } from "../interfaces/Errors.sol";
import { IWithdrawRequestManager } from "../interfaces/IWithdrawRequestManager.sol";
import { VaultPosition } from "../interfaces/ILendingRouter.sol";
import { Initializable } from "./Initializable.sol";
/// @notice Registry for the addresses for different components of the protocol.
contract AddressRegistry is Initializable {
event PendingUpgradeAdminSet(address indexed newPendingUpgradeAdmin);
event UpgradeAdminTransferred(address indexed newUpgradeAdmin);
event PendingPauseAdminSet(address indexed newPendingPauseAdmin);
event PauseAdminTransferred(address indexed newPauseAdmin);
event FeeReceiverTransferred(address indexed newFeeReceiver);
event WithdrawRequestManagerSet(address indexed yieldToken, address indexed withdrawRequestManager);
event LendingRouterSet(address indexed lendingRouter);
event AccountPositionCreated(address indexed account, address indexed vault, address indexed lendingRouter);
event AccountPositionCleared(address indexed account, address indexed vault, address indexed lendingRouter);
event WhitelistedVault(address indexed vault, bool isWhitelisted);
/// @notice Address of the admin that is allowed to:
/// - Upgrade TimelockUpgradeableProxy contracts given a 7 day timelock
/// - Transfer the upgrade admin role
/// - Set the pause admin
/// - Set the fee receiver
/// - Add reward tokens to the RewardManager
/// - Set the WithdrawRequestManager for a yield token
/// - Whitelist vaults for the WithdrawRequestManager
/// - Whitelist new lending routers
address public upgradeAdmin;
address public pendingUpgradeAdmin;
/// @notice Address of the admin that is allowed to selectively pause or unpause
/// TimelockUpgradeableProxy contracts
address public pauseAdmin;
address public pendingPauseAdmin;
/// @notice Address of the account that receives the protocol fees
address public feeReceiver;
/// @notice Mapping of yield token to WithdrawRequestManager
mapping(address token => address withdrawRequestManager) public withdrawRequestManagers;
/// @notice Mapping of lending router to boolean indicating if it is whitelisted
mapping(address lendingRouter => bool isLendingRouter) public lendingRouters;
/// @notice Mapping to whitelisted vaults
mapping(address vault => bool isWhitelisted) public whitelistedVaults;
/// @notice Mapping of accounts to their existing position on a given vault
mapping(address account => mapping(address vault => VaultPosition)) internal accountPositions;
function _initialize(bytes calldata data) internal override {
(address _upgradeAdmin, address _pauseAdmin, address _feeReceiver) =
abi.decode(data, (address, address, address));
upgradeAdmin = _upgradeAdmin;
pauseAdmin = _pauseAdmin;
feeReceiver = _feeReceiver;
}
modifier onlyUpgradeAdmin() {
if (msg.sender != upgradeAdmin) revert Unauthorized(msg.sender);
_;
}
function transferUpgradeAdmin(address _newUpgradeAdmin) external onlyUpgradeAdmin {
pendingUpgradeAdmin = _newUpgradeAdmin;
emit PendingUpgradeAdminSet(_newUpgradeAdmin);
}
function acceptUpgradeOwnership() external {
if (msg.sender != pendingUpgradeAdmin) revert Unauthorized(msg.sender);
upgradeAdmin = pendingUpgradeAdmin;
delete pendingUpgradeAdmin;
emit UpgradeAdminTransferred(upgradeAdmin);
}
function transferPauseAdmin(address _newPauseAdmin) external onlyUpgradeAdmin {
pendingPauseAdmin = _newPauseAdmin;
emit PendingPauseAdminSet(_newPauseAdmin);
}
function acceptPauseAdmin() external {
if (msg.sender != pendingPauseAdmin) revert Unauthorized(msg.sender);
pauseAdmin
Submitted on: 2025-10-01 12:39:33
Comments
Log in to comment.
No comments yet.