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/withdraws/Origin.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { AbstractWithdrawRequestManager } from "./AbstractWithdrawRequestManager.sol";
import { WETH } from "../utils/Constants.sol";
import { IOriginVault, OriginVault, oETH, wOETH } from "../interfaces/IOrigin.sol";
contract OriginWithdrawRequestManager is AbstractWithdrawRequestManager {
bool internal immutable IS_WRAPPED_OETH;
constructor(address yieldToken) AbstractWithdrawRequestManager(address(WETH), yieldToken, address(WETH)) {
IS_WRAPPED_OETH = YIELD_TOKEN == address(wOETH);
if (!IS_WRAPPED_OETH) require(yieldToken == address(oETH), "Invalid yield token");
}
function _initiateWithdrawImpl(
address, /* account */
uint256 amountToWithdraw,
bytes calldata, /* data */
address /* forceWithdrawFrom */
)
internal
override
returns (uint256 requestId)
{
if (IS_WRAPPED_OETH) {
amountToWithdraw = wOETH.redeem(amountToWithdraw, address(this), address(this));
}
oETH.approve(address(OriginVault), amountToWithdraw);
(requestId,) = OriginVault.requestWithdrawal(amountToWithdraw);
}
function _stakeTokens(uint256 amount, bytes memory stakeData) internal override {
uint256 minAmountOut;
if (stakeData.length > 0) (minAmountOut) = abi.decode(stakeData, (uint256));
WETH.approve(address(OriginVault), amount);
uint256 oethBefore = oETH.balanceOf(address(this));
OriginVault.mint(address(WETH), amount, minAmountOut);
uint256 oethAfter = oETH.balanceOf(address(this));
if (IS_WRAPPED_OETH) {
oETH.approve(address(wOETH), oethAfter - oethBefore);
wOETH.deposit(oethAfter - oethBefore, address(this));
}
}
function _finalizeWithdrawImpl(
address, /* account */
uint256 requestId
)
internal
override
returns (uint256 tokensClaimed)
{
uint256 balanceBefore = WETH.balanceOf(address(this));
OriginVault.claimWithdrawal(requestId);
tokensClaimed = WETH.balanceOf(address(this)) - balanceBefore;
}
function canFinalizeWithdrawRequest(uint256 requestId) public view returns (bool) {
IOriginVault.WithdrawalRequest memory request = OriginVault.withdrawalRequests(requestId);
IOriginVault.WithdrawalQueueMetadata memory queue = OriginVault.withdrawalQueueMetadata();
uint256 withdrawalClaimDelay = OriginVault.withdrawalClaimDelay();
bool claimDelayMet = request.timestamp + withdrawalClaimDelay <= block.timestamp;
bool queueLiquidityAvailable = request.queued <= queue.claimable;
bool notClaimed = request.claimed == false;
return claimDelayMet && queueLiquidityAvailable && notClaimed;
}
function getExchangeRate() public view override returns (uint256) {
if (IS_WRAPPED_OETH) return super.getExchangeRate();
// If the yield token is oETH then we can just return 1e18 for a 1-1 exchange rate
return 1e18;
}
}
"
},
"src/withdraws/AbstractWithdrawRequestManager.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import {
IWithdrawRequestManager,
WithdrawRequest,
TokenizedWithdrawRequest,
StakingTradeParams
} from "../interfaces/IWithdrawRequestManager.sol";
import { Initializable } from "../proxy/Initializable.sol";
import { ClonedCoolDownHolder } from "./ClonedCoolDownHolder.sol";
import {
Unauthorized,
ExistingWithdrawRequest,
NoWithdrawRequest,
InvalidWithdrawRequestTokenization
} from "../interfaces/Errors.sol";
import { TypeConvert } from "../utils/TypeConvert.sol";
import { TokenUtils } from "../utils/TokenUtils.sol";
import { ADDRESS_REGISTRY, DEFAULT_PRECISION } from "../utils/Constants.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Trade, TradeType, TRADING_MODULE, nProxy, ITradingModule } from "../interfaces/ITradingModule.sol";
import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
/**
* Library to handle potentially illiquid withdraw requests of staking tokens where there
* is some indeterminate lock up time before tokens can be redeemed. Examples would be withdraws
* of staked or restaked ETH, tokens like sUSDe or stkAave which have cooldown periods before they
* can be withdrawn.
*
* Primarily, this library tracks the withdraw request and an associated identifier for the withdraw
* request. It also allows for the withdraw request to be "tokenized" so that shares of the withdraw
* request can be liquidated.
*/
abstract contract AbstractWithdrawRequestManager is IWithdrawRequestManager, Initializable, ReentrancyGuardTransient {
using SafeERC20 for ERC20;
using TypeConvert for uint256;
/// @inheritdoc IWithdrawRequestManager
address public immutable override YIELD_TOKEN;
/// @inheritdoc IWithdrawRequestManager
address public immutable override WITHDRAW_TOKEN;
/// @inheritdoc IWithdrawRequestManager
address public immutable override STAKING_TOKEN;
mapping(address vault => bool isApproved) public override isApprovedVault;
mapping(address vault => mapping(address account => WithdrawRequest)) private s_accountWithdrawRequest;
mapping(uint256 requestId => TokenizedWithdrawRequest tokenizedWithdrawRequest) private s_tokenizedWithdrawRequest;
constructor(address _withdrawToken, address _yieldToken, address _stakingToken) Initializable() {
WITHDRAW_TOKEN = _withdrawToken;
YIELD_TOKEN = _yieldToken;
STAKING_TOKEN = _stakingToken;
}
modifier onlyOwner() {
if (msg.sender != ADDRESS_REGISTRY.upgradeAdmin()) revert Unauthorized(msg.sender);
_;
}
/// @dev Ensures that only approved vaults can initiate withdraw requests.
modifier onlyApprovedVault() {
if (!isApprovedVault[msg.sender]) revert Unauthorized(msg.sender);
_;
}
/// @inheritdoc IWithdrawRequestManager
function getWithdrawRequest(
address vault,
address account
)
public
view
override
returns (WithdrawRequest memory w, TokenizedWithdrawRequest memory s)
{
w = s_accountWithdrawRequest[vault][account];
s = s_tokenizedWithdrawRequest[w.requestId];
}
/// @inheritdoc IWithdrawRequestManager
function isPendingWithdrawRequest(address vault, address account) public view override returns (bool) {
return s_accountWithdrawRequest[vault][account].requestId != 0;
}
/// @inheritdoc IWithdrawRequestManager
function setApprovedVault(address vault, bool isApproved) external override onlyOwner {
isApprovedVault[vault] = isApproved;
emit ApprovedVault(vault, isApproved);
}
/// @inheritdoc IWithdrawRequestManager
function stakeTokens(
address depositToken,
uint256 amount,
bytes calldata data
)
external
override
onlyApprovedVault
nonReentrant
returns (uint256 yieldTokensMinted)
{
uint256 initialYieldTokenBalance = ERC20(YIELD_TOKEN).balanceOf(address(this));
ERC20(depositToken).safeTransferFrom(msg.sender, address(this), amount);
(uint256 stakeTokenAmount, bytes memory stakeData) = _preStakingTrade(depositToken, amount, data);
_stakeTokens(stakeTokenAmount, stakeData);
yieldTokensMinted = ERC20(YIELD_TOKEN).balanceOf(address(this)) - initialYieldTokenBalance;
ERC20(YIELD_TOKEN).safeTransfer(msg.sender, yieldTokensMinted);
// Emits the amount of staking tokens for the yield token at this point in time.
emit ITradingModule.TradeExecuted(STAKING_TOKEN, YIELD_TOKEN, stakeTokenAmount, yieldTokensMinted);
}
/// @inheritdoc IWithdrawRequestManager
function initiateWithdraw(
address account,
uint256 yieldTokenAmount,
uint256 sharesAmount,
bytes calldata data,
address forceWithdrawFrom
)
external
override
onlyApprovedVault
nonReentrant
returns (uint256 requestId)
{
WithdrawRequest storage accountWithdraw = s_accountWithdrawRequest[msg.sender][account];
if (accountWithdraw.requestId != 0) {
revert ExistingWithdrawRequest(msg.sender, account, accountWithdraw.requestId);
}
// Receive the requested amount of yield tokens from the approved vault.
ERC20(YIELD_TOKEN).safeTransferFrom(msg.sender, address(this), yieldTokenAmount);
requestId = _initiateWithdrawImpl(account, yieldTokenAmount, data, forceWithdrawFrom);
accountWithdraw.requestId = requestId;
accountWithdraw.yieldTokenAmount = yieldTokenAmount.toUint120();
accountWithdraw.sharesAmount = sharesAmount.toUint120();
s_tokenizedWithdrawRequest[requestId] = TokenizedWithdrawRequest({
totalYieldTokenAmount: yieldTokenAmount.toUint120(),
totalWithdraw: 0,
finalized: false
});
emit InitiateWithdrawRequest(account, msg.sender, yieldTokenAmount, sharesAmount, requestId);
}
/// @inheritdoc IWithdrawRequestManager
function finalizeAndRedeemWithdrawRequest(
address account,
uint256 withdrawYieldTokenAmount,
uint256 sharesToBurn
)
external
override
onlyApprovedVault
nonReentrant
returns (uint256 tokensWithdrawn)
{
WithdrawRequest storage s_withdraw = s_accountWithdrawRequest[msg.sender][account];
if (s_withdraw.requestId == 0) return 0;
tokensWithdrawn = _finalizeWithdraw(account, msg.sender, s_withdraw);
// Allows for partial withdrawal of yield tokens
uint256 requestId = s_withdraw.requestId;
bool isCleared = false;
if (withdrawYieldTokenAmount < s_withdraw.yieldTokenAmount) {
tokensWithdrawn = tokensWithdrawn * withdrawYieldTokenAmount / s_withdraw.yieldTokenAmount;
s_withdraw.sharesAmount -= sharesToBurn.toUint120();
s_withdraw.yieldTokenAmount -= withdrawYieldTokenAmount.toUint120();
} else {
require(s_withdraw.yieldTokenAmount == withdrawYieldTokenAmount);
delete s_accountWithdrawRequest[msg.sender][account];
isCleared = true;
}
emit WithdrawRequestRedeemed(msg.sender, account, requestId, withdrawYieldTokenAmount, sharesToBurn, isCleared);
ERC20(WITHDRAW_TOKEN).safeTransfer(msg.sender, tokensWithdrawn);
}
/// @inheritdoc IWithdrawRequestManager
function finalizeRequestManual(
address vault,
address account
)
external
override
nonReentrant
returns (uint256 tokensWithdrawn)
{
WithdrawRequest storage s_withdraw = s_accountWithdrawRequest[vault][account];
if (s_withdraw.requestId == 0) revert NoWithdrawRequest(vault, account);
// Do not transfer any tokens off of this method here. Withdrawn tokens will be held in the
// tokenized withdraw request until the vault calls this contract to withdraw the tokens.
tokensWithdrawn = _finalizeWithdraw(account, vault, s_withdraw);
}
/// @inheritdoc IWithdrawRequestManager
function tokenizeWithdrawRequest(
address _from,
address _to,
uint256 sharesAmount
)
external
override
onlyApprovedVault
nonReentrant
returns (bool didTokenize)
{
if (_from == _to) revert();
WithdrawRequest storage s_withdraw = s_accountWithdrawRequest[msg.sender][_from];
uint256 requestId = s_withdraw.requestId;
if (requestId == 0 || sharesAmount == 0) return false;
// Ensure that no withdraw request gets overridden, the _to account always receives their withdraw
// request in the account withdraw slot. All storage is updated prior to changes to the `w` storage
// variable below.
WithdrawRequest storage toWithdraw = s_accountWithdrawRequest[msg.sender][_to];
if (toWithdraw.requestId != 0 && toWithdraw.requestId != requestId) {
revert ExistingWithdrawRequest(msg.sender, _to, toWithdraw.requestId);
}
toWithdraw.requestId = requestId;
if (s_withdraw.sharesAmount < sharesAmount) {
// This should never occur given the checks below.
revert InvalidWithdrawRequestTokenization();
} else if (s_withdraw.sharesAmount == sharesAmount) {
// If the resulting vault shares is zero, then delete the request. The _from account's
// withdraw request is fully transferred to _to. In this case, the _to account receives
// the full amount of the _from account's withdraw request.
toWithdraw.yieldTokenAmount = toWithdraw.yieldTokenAmount + s_withdraw.yieldTokenAmount;
toWithdraw.sharesAmount = toWithdraw.sharesAmount + s_withdraw.sharesAmount;
delete s_accountWithdrawRequest[msg.sender][_from];
} else {
// In this case, the amount of yield tokens is transferred from one account to the other.
uint256 yieldTokenAmount = s_withdraw.yieldTokenAmount * sharesAmount / s_withdraw.sharesAmount;
toWithdraw.yieldTokenAmount = (toWithdraw.yieldTokenAmount + yieldTokenAmount).toUint120();
toWithdraw.sharesAmount = (toWithdraw.sharesAmount + sharesAmount).toUint120();
s_withdraw.yieldTokenAmount = (s_withdraw.yieldTokenAmount - yieldTokenAmount).toUint120();
s_withdraw.sharesAmount = (s_withdraw.sharesAmount - sharesAmount).toUint120();
}
emit WithdrawRequestTokenized(_from, _to, msg.sender, requestId, sharesAmount);
return true;
}
/// @inheritdoc IWithdrawRequestManager
function rescueTokens(
address cooldownHolder,
address token,
address receiver,
uint256 amount
)
external
override
onlyOwner
{
ClonedCoolDownHolder(cooldownHolder).rescueTokens(ERC20(token), receiver, amount);
}
/// @notice Finalizes a withdraw request and updates the account required to determine how many
/// tokens the account has a claim over.
function _finalizeWithdraw(
address account,
address vault,
WithdrawRequest memory w
)
internal
returns (uint256 tokensWithdrawn)
{
TokenizedWithdrawRequest storage s = s_tokenizedWithdrawRequest[w.requestId];
// If the tokenized request was already finalized in a different transaction
// then return the values here and we can short circuit the withdraw impl
if (s.finalized) {
return uint256(s.totalWithdraw) * uint256(w.yieldTokenAmount) / uint256(s.totalYieldTokenAmount);
}
// These values are the total tokens claimed from the withdraw request, does not
// account for potential tokenization.
tokensWithdrawn = _finalizeWithdrawImpl(account, w.requestId);
s.totalWithdraw = tokensWithdrawn.toUint120();
// Safety check to ensure that we do not override a finalized tokenized withdraw request
require(s.finalized == false);
s.finalized = true;
tokensWithdrawn = uint256(s.totalWithdraw) * uint256(w.yieldTokenAmount) / uint256(s.totalYieldTokenAmount);
emit WithdrawRequestFinalized(vault, account, w.requestId, s.totalWithdraw);
}
/// @notice Required implementation to begin the withdraw request
/// @return requestId some identifier of the withdraw request
function _initiateWithdrawImpl(
address account,
uint256 yieldTokenAmount,
bytes calldata data,
address forceWithdrawFrom
)
internal
virtual
returns (uint256 requestId);
/// @notice Required implementation to finalize the withdraw
/// @dev Must revert if the withdraw request is not finalized
/// @return tokensWithdrawn total tokens claimed as a result of the withdraw, does not
/// necessarily represent the tokens that go to the account if the request has been
/// tokenized due to liquidation
function _finalizeWithdrawImpl(
address account,
uint256 requestId
)
internal
virtual
returns (uint256 tokensWithdrawn);
/// @notice Required implementation to stake the deposit token to the yield token
function _stakeTokens(uint256 amount, bytes memory stakeData) internal virtual;
/// @dev Allows for the deposit token to be traded into the staking token prior to staking, i.e.
/// enables USDC to USDe trades before staking into sUSDe.
function _preStakingTrade(
address depositToken,
uint256 depositAmount,
bytes calldata data
)
internal
returns (uint256 amountBought, bytes memory stakeData)
{
if (depositToken == STAKING_TOKEN) {
amountBought = depositAmount;
stakeData = data;
} else {
StakingTradeParams memory params = abi.decode(data, (StakingTradeParams));
stakeData = params.stakeData;
( /* */ , amountBought) = _executeTrade(
Trade({
tradeType: TradeType.EXACT_IN_SINGLE,
sellToken: depositToken,
buyToken: STAKING_TOKEN,
amount: depositAmount,
exchangeData: params.exchangeData,
limit: params.minPurchaseAmount,
deadline: block.timestamp
}),
params.dexId
);
}
}
function _executeTrade(
Trade memory trade,
uint16 dexId
)
internal
returns (uint256 amountSold, uint256 amountBought)
{
(bool success, bytes memory result) = nProxy(payable(address(TRADING_MODULE))).getImplementation().delegatecall(
abi.encodeWithSelector(TRADING_MODULE.executeTrade.selector, dexId, trade)
);
if (!success) {
assembly {
// Copy the return data to memory
returndatacopy(0, 0, returndatasize())
// Revert with the return data
revert(0, returndatasize())
}
}
(amountSold, amountBought) = abi.decode(result, (uint256, uint256));
}
function getKnownWithdrawTokenAmount(uint256 /* requestId */ )
public
view
virtual
override
returns (bool hasKnownAmount, uint256 amount)
{
return (false, 0);
}
/// @inheritdoc IWithdrawRequestManager
function getWithdrawRequestValue(
address vault,
address account,
address asset,
uint256 shares
)
external
view
override
returns (bool hasRequest, uint256 valueInAsset)
{
WithdrawRequest memory w = s_accountWithdrawRequest[vault][account];
if (w.requestId == 0) return (false, 0);
TokenizedWithdrawRequest memory s = s_tokenizedWithdrawRequest[w.requestId];
(bool hasKnownAmount, uint256 knownAmount) = getKnownWithdrawTokenAmount(w.requestId);
int256 tokenRate;
uint256 tokenAmount;
uint256 tokenDecimals;
uint256 assetDecimals = TokenUtils.getDecimals(asset);
if (s.finalized || hasKnownAmount) {
// If finalized the withdraw request is locked to the tokens withdrawn
(tokenRate, /* */ ) = TRADING_MODULE.getOraclePrice(WITHDRAW_TOKEN, asset);
tokenDecimals = TokenUtils.getDecimals(WITHDRAW_TOKEN);
uint256 totalWithdraw = s.finalized ? uint256(s.totalWithdraw) : knownAmount;
tokenAmount = (uint256(w.yieldTokenAmount) * totalWithdraw) / uint256(s.totalYieldTokenAmount);
} else {
// Otherwise we use the yield token rate
(tokenRate, /* */ ) = TRADING_MODULE.getOraclePrice(YIELD_TOKEN, asset);
tokenDecimals = TokenUtils.getDecimals(YIELD_TOKEN);
tokenAmount = w.yieldTokenAmount;
}
// The trading module always returns a positive rate in 18 decimals so we can safely
// cast to uint256
uint256 totalValue =
(uint256(tokenRate) * tokenAmount * (10 ** assetDecimals)) / ((10 ** tokenDecimals) * DEFAULT_PRECISION);
// NOTE: returns the normalized value given the shares input
return (true, totalValue * shares / w.sharesAmount);
}
/// @inheritdoc IWithdrawRequestManager
function getExchangeRate() public view virtual override returns (uint256) {
// Covers the base case where the yield token is an ERC4626 vault and returns
// the exchange rate of the yield token back to the staking token.
return IERC4626(YIELD_TOKEN).convertToAssets(10 ** ERC20(YIELD_TOKEN).decimals());
}
}
"
},
"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/interfaces/IOrigin.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
interface IOriginVault {
struct WithdrawalQueueMetadata {
// cumulative total of all withdrawal requests included the ones that have already been claimed
uint128 queued;
// cumulative total of all the requests that can be claimed including the ones that have already been claimed
uint128 claimable;
// total of all the requests that have been claimed
uint128 claimed;
// index of the next withdrawal request starting at 0
uint128 nextWithdrawalIndex;
}
struct WithdrawalRequest {
address withdrawer;
bool claimed;
uint40 timestamp; // timestamp of the withdrawal request
// Amount of oTokens to redeem. eg OETH
uint128 amount;
// cumulative total of all withdrawal requests including this one.
// this request can be claimed when this queued amount is less than or equal to the queue's claimable amount.
uint128 queued;
}
function withdrawalRequests(uint256 requestId) external view returns (WithdrawalRequest memory);
function withdrawalClaimDelay() external view returns (uint256);
function withdrawalQueueMetadata() external view returns (WithdrawalQueueMetadata memory);
function requestWithdrawal(uint256 amount) external returns (uint256 requestId, uint256 queued);
function mint(address token, uint256 amount, uint256 minAmountOut) external;
function claimWithdrawal(uint256 requestId) external returns (uint256 amount);
function addWithdrawalQueueLiquidity() external;
}
IOriginVault constant OriginVault = IOriginVault(0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab);
ERC20 constant oETH = ERC20(0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3);
IERC4626 constant wOETH = IERC4626(0xDcEe70654261AF21C44c093C300eD3Bb97b78192);
"
},
"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/proxy/Initializable.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { InvalidInitialization } from "../interfaces/Errors.sol";
contract Initializable {
bool private initialized;
constructor() {
initialized = true;
}
function initialize(bytes calldata data) external {
if (initialized) revert InvalidInitialization();
initialized = true;
_initialize(data);
}
/* solhint-disable no-empty-blocks */
function _initialize(bytes calldata data) internal virtual { }
/* solhint-enable no-empty-blocks */
}
"
},
"src/withdraws/ClonedCoolDownHolder.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";
/**
* @notice Used for withdraws where only one cooldown period can exist per address,
* this contract will receive the staked token and initiate a cooldown
*/
abstract contract ClonedCoolDownHolder {
using SafeERC20 for ERC20;
/// @notice The manager contract that is responsible for managing the cooldown period.
address public immutable manager;
constructor(address _manager) {
manager = _manager;
}
modifier onlyManager() {
require(msg.sender == manager);
_;
}
/// @notice If anything ever goes wrong, allows the manager to recover lost tokens.
function rescueTokens(ERC20 token, address receiver, uint256 amount) external onlyManager {
token.safeTransfer(receiver, amount);
}
// External methods with authentication
function startCooldown(uint256 cooldownBalance) external onlyManager {
_startCooldown(cooldownBalance);
}
function stopCooldown() external onlyManager {
_stopCooldown();
}
function finalizeCooldown() external onlyManager returns (uint256 tokensWithdrawn, bool finalized) {
return _finalizeCooldown();
}
// Internal implementation methods
function _startCooldown(uint256 cooldownBalance) internal virtual;
function _stopCooldown() internal virtual;
function _finalizeCooldown() internal virtual returns (uint256 tokensWithdrawn, bool finalized);
}
"
},
"src/interfaces/Errors.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.28;
error NotAuthorized(address operator, address user);
error Unauthorized(address caller);
error UnauthorizedLendingMarketTransfer(address from, address to, uint256 value);
error InsufficientYieldTokenBalance();
error InsufficientAssetsForRepayment(uint256 assetsToRepay, uint256 assetsWithdrawn);
error CannotLiquidate(uint256 maxLiquidateShares, uint256 seizedAssets);
error CannotLiquidateZeroShares();
error Paused();
error CannotExitPositionWithinCooldownPeriod();
error CannotTokenizeWithdrawRequest();
error CurrentAccountAlreadySet();
error InvalidVault(address vault);
error WithdrawRequestNotFinalized(uint256 requestId);
error CannotInitiateWithdraw(address account);
error CannotForceWithdraw(address account);
error InsufficientSharesHeld();
error SlippageTooHigh(uint256 actualTokensOut, uint256 minTokensOut);
error CannotEnterPosition();
error NoExistingPosition();
error LiquidatorHasPosition();
error InvalidUpgrade();
error InvalidInitialization();
error InvalidLendingRouter();
error ExistingWithdrawRequest(address vault, address account, uint256 requestId);
error NoWithdrawRequest(address vault, address account);
error InvalidWithdrawRequestTokenization();
error InvalidPrice(uint256 oraclePrice, uint256 spotPrice);
error PoolShareTooHigh(uint256 poolClaim, uint256 maxSupplyThreshold);
error AssetRemaining(uint256 assetRemaining);
"
},
"src/utils/TypeConvert.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
library TypeConvert {
function toUint(int256 x) internal pure returns (uint256) {
require(x >= 0);
return uint256(x);
}
function toInt(uint256 x) internal pure returns (int256) {
require(x <= uint256(type(int256).max)); // dev: toInt overflow
return int256(x);
}
function toUint128(uint256 x) internal pure returns (uint128) {
require(x <= uint128(type(uint128).max)); // dev: toUint128 overflow
return uint128(x);
}
function toUint120(uint256 x) internal pure returns (uint120) {
require(x <= uint120(type(uint120).max)); // dev: toUint120 overflow
return uint120(x);
}
}
"
},
"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)
}
}
}
}
"
},
"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);
}
}
}
}
"
},
"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) {
safeTransfer(token, to, value);
} else if (!token.transferAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
* has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
function transferFromAndCallRelaxed(
IERC1363 token,
address from,
address to,
uint256 value,
bytes memory data
) internal {
if (to.code.length == 0) {
safeTransferFrom(token, from, to, value);
} else if (!token.transferFromAndCall(from, to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
* Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
* once without retrying, and relies on the returned value to be true.
*
* Reverts if the returned value is other than `true`.
*/
function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
forceApprove(token, to, value);
} else if (!token.approveAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
// bubble errors
if iszero(success) {
let ptr := mload(0x40)
returndatacopy(ptr, 0, returndatasize())
revert(ptr, returndatasize())
}
returnSize := returndatasize()
returnValue := mload(0)
}
if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
bool success;
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
returnSize := returndatasize()
returnValue := mload(0)
}
return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
}
}
"
},
"src/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(
Submitted on: 2025-09-26 10:05:36
Comments
Log in to comment.
No comments yet.