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/routers/MorphoLendingRouter.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { InsufficientAssetsForRepayment } from "../interfaces/Errors.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { TokenUtils } from "../utils/TokenUtils.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IYieldStrategy } from "../interfaces/IYieldStrategy.sol";
import {
IMorphoLiquidateCallback,
IMorphoFlashLoanCallback,
IMorphoRepayCallback
} from "../interfaces/Morpho/IMorphoCallbacks.sol";
import { ADDRESS_REGISTRY } from "../utils/Constants.sol";
import { AbstractLendingRouter } from "./AbstractLendingRouter.sol";
import {
MORPHO, MarketParams, Id, Position, Market, Withdrawal, PUBLIC_ALLOCATOR
} from "../interfaces/Morpho/IMorpho.sol";
struct MorphoParams {
address irm;
uint256 lltv;
}
struct MorphoAllocation {
address vault;
uint256 feeAmount;
Withdrawal[] withdrawals;
}
contract MorphoLendingRouter is
AbstractLendingRouter,
IMorphoLiquidateCallback,
IMorphoFlashLoanCallback,
IMorphoRepayCallback
{
using SafeERC20 for ERC20;
using TokenUtils for ERC20;
mapping(address vault => MorphoParams params) private s_morphoParams;
uint256 private transient t_vaultSharesReceived;
uint256 private transient t_borrowShares;
uint256 private transient t_profitsWithdrawn;
// Used for the health factor calculation to replicate Morpho's behavior
uint256 private constant VIRTUAL_ASSETS = 1;
uint256 private constant VIRTUAL_SHARES = 1e6;
function name() external pure override returns (string memory) {
return "Morpho";
}
function initializeMarket(address vault, address irm, uint256 lltv) external {
require(ADDRESS_REGISTRY.upgradeAdmin() == msg.sender);
// Cannot override parameters once they are set
require(s_morphoParams[vault].irm == address(0));
require(s_morphoParams[vault].lltv == 0);
s_morphoParams[vault] = MorphoParams({ irm: irm, lltv: lltv });
// If the market already exists this call will revert. This is okay because there should
// be no reason that the market would already exist unless something has gone wrong. In that
// case we would want to assess why the market was created and perhaps change the market
// parameters in order to fix the issue.
MORPHO.createMarket(marketParams(vault));
}
function marketParams(address vault) public view returns (MarketParams memory) {
return marketParams(vault, IYieldStrategy(vault).asset());
}
function marketParams(address vault, address asset) internal view returns (MarketParams memory) {
MorphoParams memory params = s_morphoParams[vault];
return MarketParams({
loanToken: asset,
collateralToken: vault,
oracle: vault,
irm: params.irm,
lltv: params.lltv
});
}
function morphoId(MarketParams memory m) internal pure returns (Id) {
return Id.wrap(keccak256(abi.encode(m)));
}
/// @dev Allows integration with the public allocator so that accounts can
/// ensure there is sufficient liquidity in the lending market before entering
function _allocate(address vault, MorphoAllocation[] calldata allocationData) internal {
MarketParams memory m = marketParams(vault);
uint256 totalFeeAmount;
for (uint256 i = 0; i < allocationData.length; i++) {
PUBLIC_ALLOCATOR.reallocateTo{ value: allocationData[i].feeAmount }(
allocationData[i].vault, allocationData[i].withdrawals, m
);
totalFeeAmount += allocationData[i].feeAmount;
}
require(msg.value == totalFeeAmount, "Insufficient fee amount");
}
function allocateAndEnterPosition(
address onBehalf,
address vault,
uint256 depositAssetAmount,
uint256 borrowAmount,
bytes calldata depositData,
MorphoAllocation[] calldata allocationData
)
external
payable
isAuthorized(onBehalf, vault)
nonReentrant
{
_allocate(vault, allocationData);
_enterPosition(onBehalf, vault, depositAssetAmount, borrowAmount, depositData, address(0));
}
function allocateAndMigratePosition(
address onBehalf,
address vault,
address migrateFrom,
MorphoAllocation[] calldata allocationData
)
external
payable
isAuthorized(onBehalf, vault)
nonReentrant
{
_allocate(vault, allocationData);
_migratePosition(onBehalf, vault, migrateFrom);
}
function _flashBorrowAndEnter(
address onBehalf,
address vault,
address asset,
uint256 depositAssetAmount,
uint256 borrowAmount,
bytes memory depositData,
address migrateFrom
)
internal
override
returns (uint256 vaultSharesReceived, uint256 borrowShares)
{
// At this point we will flash borrow funds from the lending market and then
// receive control in a different function on a callback.
bytes memory flashLoanData = abi.encode(onBehalf, vault, asset, depositAssetAmount, depositData, migrateFrom);
MORPHO.flashLoan(asset, borrowAmount, flashLoanData);
// These are only used to get these values back from the flash loan callback
// so that we can emit the event with the correct values
vaultSharesReceived = t_vaultSharesReceived;
borrowShares = t_borrowShares;
}
function onMorphoFlashLoan(uint256 assets, bytes calldata data) external override {
require(msg.sender == address(MORPHO));
(
address onBehalf,
address vault,
address asset,
uint256 depositAssetAmount,
bytes memory depositData,
address migrateFrom
) = abi.decode(data, (address, address, address, uint256, bytes, address));
t_vaultSharesReceived =
_enterOrMigrate(onBehalf, vault, asset, assets + depositAssetAmount, depositData, migrateFrom);
MarketParams memory m = marketParams(vault, asset);
// Borrow the assets in order to repay the flash loan
( /* */ , t_borrowShares) = MORPHO.borrow(m, assets, 0, onBehalf, address(this));
// Allow for flash loan to be repaid
ERC20(asset).checkApprove(address(MORPHO), assets);
}
function _supplyCollateral(
address onBehalf,
address vault,
address asset,
uint256 sharesReceived
)
internal
override
{
MarketParams memory m = marketParams(vault, asset);
// Allows the transfer from the lending market to the Morpho contract
IYieldStrategy(vault).allowTransfer(address(MORPHO), sharesReceived, onBehalf);
// We should receive shares in return
ERC20(vault).approve(address(MORPHO), sharesReceived);
MORPHO.supplyCollateral(m, sharesReceived, onBehalf, "");
}
function _withdrawCollateral(
address vault,
address asset,
uint256 sharesToRedeem,
address sharesOwner,
address receiver
)
internal
override
{
MarketParams memory m = marketParams(vault, asset);
MORPHO.withdrawCollateral(m, sharesToRedeem, sharesOwner, receiver);
}
function _exitWithRepay(
address onBehalf,
address vault,
address asset,
address receiver,
uint256 sharesToRedeem,
uint256 assetToRepay,
bytes memory redeemData
)
internal
override
returns (uint256 borrowSharesRepaid, uint256 profitsWithdrawn)
{
uint256 sharesToRepay;
if (assetToRepay == type(uint256).max) {
// If assetToRepay is uint256.max then get the morpho borrow shares amount to
// get a full exit.
sharesToRepay = balanceOfBorrowShares(onBehalf, vault);
assetToRepay = 0;
}
if (assetToRepay == 0 && sharesToRepay == 0) {
// Allows migration in the edge case where the user has no debt but
// still wants to migrate their position.
require(_isMigrate(receiver));
profitsWithdrawn = _redeemShares(onBehalf, vault, asset, receiver, sharesToRedeem, redeemData);
} else {
bytes memory repayData =
abi.encode(onBehalf, vault, asset, receiver, sharesToRedeem, redeemData, _isMigrate(receiver));
// Will trigger a callback to onMorphoRepay
borrowSharesRepaid = _repay(vault, asset, assetToRepay, sharesToRepay, onBehalf, repayData);
profitsWithdrawn = t_profitsWithdrawn;
}
}
function _repay(
address vault,
address asset,
uint256 assetToRepay,
uint256 sharesToRepay,
address onBehalf,
bytes memory repayData
)
internal
returns (uint256 borrowSharesRepaid)
{
MarketParams memory m = marketParams(vault, asset);
( /* */ , borrowSharesRepaid) = MORPHO.repay(m, assetToRepay, sharesToRepay, onBehalf, repayData);
}
function onMorphoRepay(uint256 assetToRepay, bytes calldata data) external override {
require(msg.sender == address(MORPHO));
(
address sharesOwner,
address vault,
address asset,
address receiver,
uint256 sharesToRedeem,
bytes memory redeemData,
bool isMigrate
) = abi.decode(data, (address, address, address, address, uint256, bytes, bool));
uint256 assetsWithdrawn =
_redeemShares(sharesOwner, vault, asset, isMigrate ? receiver : address(0), sharesToRedeem, redeemData);
if (isMigrate) {
// When migrating we do not withdraw any assets and we must repay the entire debt
// from the previous lending router.
if (0 < assetToRepay) ERC20(asset).safeTransferFrom(receiver, address(this), assetToRepay);
assetsWithdrawn = assetToRepay;
}
// Transfer any profits to the receiver
if (assetsWithdrawn < assetToRepay) {
// We have to revert in this case because we've already redeemed the yield tokens
revert InsufficientAssetsForRepayment(assetToRepay, assetsWithdrawn);
}
uint256 profitsWithdrawn;
unchecked {
profitsWithdrawn = assetsWithdrawn - assetToRepay;
}
if (0 < profitsWithdrawn) ERC20(asset).safeTransfer(receiver, profitsWithdrawn);
// Allow morpho to repay the debt
ERC20(asset).checkApprove(address(MORPHO), assetToRepay);
// Set the transient variable to be used for later event emission
t_profitsWithdrawn = profitsWithdrawn;
}
function _liquidate(
address liquidator,
address vault,
address liquidateAccount,
uint256 sharesToLiquidate,
uint256 borrowSharesToRepay
)
internal
override
returns (uint256 sharesToLiquidator, uint256 borrowSharesRepaid)
{
MarketParams memory m = marketParams(vault);
uint256 borrowSharesBefore = balanceOfBorrowShares(liquidateAccount, vault);
// If the account's borrow shares are less than when the liquidator is trying to repay,
// set it to the account's borrow shares to prevent an underflow inside Morpho.
if (borrowSharesBefore < borrowSharesToRepay) borrowSharesToRepay = borrowSharesBefore;
// This does not return borrow shares repaid so we have to calculate it manually
(sharesToLiquidator, /* */ ) = MORPHO.liquidate(
m, liquidateAccount, sharesToLiquidate, borrowSharesToRepay, abi.encode(m.loanToken, liquidator)
);
borrowSharesRepaid = borrowSharesBefore - balanceOfBorrowShares(liquidateAccount, vault);
}
function onMorphoLiquidate(uint256 repaidAssets, bytes calldata data) external override {
require(msg.sender == address(MORPHO));
(address asset, address liquidator) = abi.decode(data, (address, address));
ERC20(asset).safeTransferFrom(liquidator, address(this), repaidAssets);
ERC20(asset).checkApprove(address(MORPHO), repaidAssets);
}
function balanceOfCollateral(
address account,
address vault
)
public
view
override
returns (uint256 collateralBalance)
{
MarketParams memory m = marketParams(vault);
collateralBalance = MORPHO.position(morphoId(m), account).collateral;
}
function balanceOfBorrowShares(
address account,
address vault
)
public
view
override
returns (uint256 borrowShares)
{
MarketParams memory m = marketParams(vault);
borrowShares = MORPHO.position(morphoId(m), account).borrowShares;
}
function convertBorrowSharesToAssets(
address vault,
uint256 borrowShares
)
external
override
returns (uint256 assets)
{
MarketParams memory m = marketParams(vault);
MORPHO.accrueInterest(m);
Market memory market = MORPHO.market(morphoId(m));
return (borrowShares * uint256(market.totalBorrowAssets)) / uint256(market.totalBorrowShares);
}
function _mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) {
return (x * y + (d - 1)) / d;
}
function _toAssetsUp(
uint256 shares,
uint256 totalShares,
uint256 totalAssets
)
internal
pure
returns (uint256 assets)
{
return _mulDivUp(shares, totalAssets + VIRTUAL_ASSETS, totalShares + VIRTUAL_SHARES);
}
function healthFactor(
address borrower,
address vault
)
public
override
returns (uint256 borrowed, uint256 collateralValue, uint256 maxBorrow)
{
MarketParams memory m = marketParams(vault);
Id id = morphoId(m);
// Ensure interest is accrued before calculating health factor
MORPHO.accrueInterest(m);
Position memory position = MORPHO.position(id, borrower);
Market memory market = MORPHO.market(id);
if (position.borrowShares > 0) {
borrowed = _toAssetsUp(position.borrowShares, market.totalBorrowShares, market.totalBorrowAssets);
} else {
borrowed = 0;
}
collateralValue = (uint256(position.collateral) * IYieldStrategy(vault).price(borrower)) / 1e36;
maxBorrow = collateralValue * m.lltv / 1e18;
}
}
"
},
"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);
"
},
"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)
}
}
}
}
"
},
"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/IYieldStrategy.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
import { IOracle } from "./Morpho/IOracle.sol";
/**
* @notice A strategy vault that is specifically designed for leveraged yield
* strategies. Minting and burning shares are restricted to the `enterPosition`
* and `exitPosition` functions respectively. This means that shares will be
* exclusively held on lending markets as collateral unless the LendingMarket is
* set to NONE. In this case, the user will just be holding the yield token without
* any leverage.
*
* The `transfer` function is non-standard in that transfers off of a lending market
* are restricted to ensure that liquidation conditions are met.
*
* This contract also serves as its own oracle.
*/
interface IYieldStrategy is IERC20, IERC20Metadata, IOracle {
event VaultCreated(address indexed vault);
// These can be emitted by the reward manager
event VaultRewardTransfer(address indexed token, address indexed account, uint256 amount);
event VaultRewardUpdate(address indexed rewardToken, uint128 emissionRatePerYear, uint32 endTime);
// This is emitted by the trading module
event TradeExecuted(address indexed sellToken, address indexed buyToken, uint256 sellAmount, uint256 buyAmount);
event FeesCollected(uint256 feesCollected);
/**
* @dev Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing.
*
* - MUST be an ERC-20 token contract.
* - MUST NOT revert.
*/
function asset() external view returns (address assetTokenAddress);
/**
* @dev Returns the address of the accounting asset used for the
* to mark the price of the yield token excluding any market profit and loss.
* This is only used for off chain accounting.
*/
function accountingAsset() external view returns (address accountingAssetAddress);
/**
* @dev Returns the name of the strategy.
*/
function strategy() external view returns (string memory strategyName);
/**
* @dev Returns the address of the yield token held by the vault. Does not equal the share token,
* which represents each user's share of the yield tokens held by the vault.
*
* - MUST be an ERC-20 token contract.
* - MUST NOT revert.
*/
function yieldToken() external view returns (address yieldTokenAddress);
/**
* @dev Returns the total amount of the underlying asset that is “managed” by Vault.
*
* - SHOULD include any compounding that occurs from yield.
* - MUST be inclusive of any fees that are charged against assets in the Vault.
* - MUST NOT revert.
*/
function totalAssets() external view returns (uint256 totalManagedAssets);
/**
* @dev Returns the effective supply which excludes any escrowed shares.
*/
function effectiveSupply() external view returns (uint256);
/**
* @dev Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal
* scenario where all the conditions are met.
*
* - MUST NOT be inclusive of any fees that are charged against assets in the Vault.
* - MUST NOT show any variations depending on the caller.
* - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.
* - MUST NOT revert.
*
* NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the
* “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and
* from.
*/
function convertToShares(uint256 assets) external view returns (uint256 shares);
/**
* @dev Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal
* scenario where all the conditions are met.
*
* - MUST NOT be inclusive of any fees that are charged against assets in the Vault.
* - MUST NOT show any variations depending on the caller.
* - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.
*/
function convertToAssets(uint256 shares) external view returns (uint256 assets);
/**
* @dev Returns the amount of yield tokens that the Vault would exchange for the amount of shares provided, in an
* ideal
* scenario where all the conditions are met.
*/
function convertSharesToYieldToken(uint256 shares) external view returns (uint256 yieldTokens);
/**
* @dev Returns the amount of yield tokens that the account would receive for the amount of shares provided.
*/
function convertYieldTokenToShares(uint256 shares) external view returns (uint256 yieldTokens);
/**
* @dev Returns the oracle price of a yield token in terms of the asset token.
*/
function convertYieldTokenToAsset() external view returns (uint256 price);
/**
* @dev Returns the fee rate of the vault where 100% = 1e18.
*/
function feeRate() external view returns (uint256 feeRate);
/**
* @dev Returns the balance of yield tokens accrued by the vault.
*/
function feesAccrued() external view returns (uint256 feesAccruedInYieldToken);
/**
* @dev Collects the fees accrued by the vault. Only callable by the owner.
*/
function collectFees() external returns (uint256 feesCollected);
/**
* @dev Returns the price of a yield token in terms of the asset token for the
* given borrower taking into account withdrawals.
*/
function price(address borrower) external returns (uint256 price);
/**
* @notice Mints shares for a given number of assets.
*
* @param assets The amount of assets to mint shares for.
* @param receiver The address to mint the shares to.
* @param depositData calldata used to deposit the assets.
*/
function mintShares(
uint256 assets,
address receiver,
bytes memory depositData
)
external
returns (uint256 sharesMinted);
/**
* @notice Burns shares for a given number of shares.
*
* @param sharesOwner The address of the account to burn the shares for.
* @param sharesToBurn The amount of shares to burn.
* @param redeemData calldata used to redeem the yield token.
*/
function burnShares(
address sharesOwner,
uint256 sharesToBurn,
uint256 sharesHeld,
bytes memory redeemData
)
external
returns (uint256 assetsWithdrawn);
/**
* @notice Allows the lending market to transfer shares on exit position
* or liquidation.
*
* @param to The address to allow the transfer to.
* @param amount The amount of shares to allow the transfer of.
* @param currentAccount The address of the current account.
*/
function allowTransfer(address to, uint256 amount, address currentAccount) external;
/**
* @notice Pre-liquidation function.
*
* @param liquidator The address of the liquidator.
* @param liquidateAccount The address of the account to liquidate.
* @param sharesToLiquidate The amount of shares to liquidate.
* @param accountSharesHeld The amount of shares the account holds.
*/
function preLiquidation(
address liquidator,
address liquidateAccount,
uint256 sharesToLiquidate,
uint256 accountSharesHeld
)
external;
/**
* @notice Post-liquidation function.
*
* @param liquidator The address of the liquidator.
* @param liquidateAccount The address of the account to liquidate.
* @param sharesToLiquidator The amount of shares to liquidate.
*/
function postLiquidation(address liquidator, address liquidateAccount, uint256 sharesToLiquidator) external;
/**
* @notice Redeems shares for assets for a native token.
*
* @param sharesToRedeem The amount of shares to redeem.
* @param redeemData calldata used to redeem the yield token.
*/
function redeemNative(uint256 sharesToRedeem, bytes memory redeemData) external returns (uint256 assetsWithdrawn);
/**
* @notice Initiates a withdraw for a given number of shares.
*
* @param account The address of the account to initiate the withdraw for.
* @param sharesHeld The number of shares the account holds.
* @param data calldata used to initiate the withdraw.
*/
function initiateWithdraw(
address account,
uint256 sharesHeld,
bytes calldata data,
address forceWithdrawFrom
)
external
returns (uint256 requestId);
/**
* @notice Initiates a withdraw for the native balance of the account.
*
* @param data calldata used to initiate the withdraw.
*/
function initiateWithdrawNative(bytes calldata data) external returns (uint256 requestId);
/**
* @notice Clears the current account.
*/
function clearCurrentAccount() external;
}
"
},
"src/interfaces/Morpho/IMorphoCallbacks.sol": {
"content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.8.28;
/// @title IMorphoLiquidateCallback
/// @notice Interface that liquidators willing to use `liquidate`'s callback must implement.
interface IMorphoLiquidateCallback {
/// @notice Callback called when a liquidation occurs.
/// @dev The callback is called only if data is not empty.
/// @param repaidAssets The amount of repaid assets.
/// @param data Arbitrary data passed to the `liquidate` function.
function onMorphoLiquidate(uint256 repaidAssets, bytes calldata data) external;
}
/// @title IMorphoRepayCallback
/// @notice Interface that users willing to use `repay`'s callback must implement.
interface IMorphoRepayCallback {
/// @notice Callback called when a repayment occurs.
/// @dev The callback is called only if data is not empty.
/// @param assets The amount of repaid assets.
/// @param data Arbitrary data passed to the `repay` function.
function onMorphoRepay(uint256 assets, bytes calldata data) external;
}
/// @title IMorphoSupplyCallback
/// @notice Interface that users willing to use `supply`'s callback must implement.
interface IMorphoSupplyCallback {
/// @notice Callback called when a supply occurs.
/// @dev The callback is called only if data is not empty.
/// @param assets The amount of supplied assets.
/// @param data Arbitrary data passed to the `supply` function.
function onMorphoSupply(uint256 assets, bytes calldata data) external;
}
/// @title IMorphoSupplyCollateralCallback
/// @notice Interface that users willing to use `supplyCollateral`'s callback must implement.
interface IMorphoSupplyCollateralCallback {
/// @notice Callback called when a supply of collateral occurs.
/// @dev The callback is called only if data is not empty.
/// @param assets The amount of supplied collateral.
/// @param data Arbitrary data passed to the `supplyCollateral` function.
function onMorphoSupplyCollateral(uint256 assets, bytes calldata data) external;
}
/// @title IMorphoFlashLoanCallback
/// @notice Interface that users willing to use `flashLoan`'s callback must implement.
interface IMorphoFlashLoanCallback {
/// @notice Callback called when a flash loan occurs.
/// @dev The callback is called only if data is not empty.
/// @param assets The amount of assets that was flash loaned.
/// @param data Arbitrary data passed to the `flashLoan` function.
function onMorphoFlashLoan(uint256 assets, bytes calldata data) external;
}
"
},
"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/routers/AbstractLendingRouter.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;
import { ILendingRouter, VaultPosition } from "../interfaces/ILendingRouter.sol";
import {
NotAuthorized,
CannotExitPositionWithinCooldownPeriod,
CannotForceWithdraw,
InvalidLendingRouter,
NoExistingPosition,
LiquidatorHasPosition,
CannotEnterPosition,
CannotLiquidateZeroShares,
InsufficientSharesHeld
} from "../interfaces/Errors.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { TokenUtils } from "../utils/TokenUtils.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IYieldStrategy } from "../interfaces/IYieldStrategy.sol";
import { RewardManagerMixin } from "../rewards/RewardManagerMixin.sol";
import { ADDRESS_REGISTRY, COOLDOWN_PERIOD } from "../utils/Constants.sol";
import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
abstract contract AbstractLendingRouter is ILendingRouter, ReentrancyGuardTransient {
using SafeERC20 for ERC20;
using TokenUtils for ERC20;
mapping(address user => mapping(address operator => bool approved)) private s_isApproved;
/**
* Authorization Methods **
*/
modifier isAuthorized(address onBehalf, address vault) {
// In this case msg.sender is the operator
if (msg.sender != onBehalf && !isApproved(onBehalf, msg.sender)) {
revert NotAuthorized(msg.sender, onBehalf);
}
_;
// Clear the current account after the transaction is finished
IYieldStrategy(vault).clearCurrentAccount();
}
/// @inheritdoc ILendingRouter
function setApproval(address operator, bool approved) external override {
if (operator == msg.sender) revert NotAuthorized(msg.sender, operator);
s_isApproved[msg.sender][operator] = approved;
emit ApprovalUpdated(msg.sender, operator, approved);
}
/// @inheritdoc ILendingRouter
function isApproved(address user, address operator) public view override returns (bool) {
return s_isApproved[user][operator];
}
/// @inheritdoc ILendingRouter
function enterPosition(
address onBehalf,
address vault,
uint256 depositAssetAmount,
uint256 borrowAmount,
bytes calldata depositData
)
public
override
isAuthorized(onBehalf, vault)
nonReentrant
{
_enterPosition(onBehalf, vault, depositAssetAmount, borrowAmount, depositData, address(0));
}
/// @inheritdoc ILendingRouter
function migratePosition(
address onBehalf,
address vault,
address migrateFrom
)
public
override
isAuthorized(onBehalf, vault)
nonReentrant
{
_migratePosition(onBehalf, vault, migrateFrom);
}
function _migratePosition(address onBehalf, address vault, address migrateFrom) internal {
if (!ADDRESS_REGISTRY.isLendingRouter(migrateFrom)) revert InvalidLendingRouter();
// Borrow amount is set to the amount of debt owed to the previous lending router
(uint256 borrowAmount, /* */, /* */ ) = ILendingRouter(migrateFrom).healthFactor(onBehalf, vault);
_enterPosition(onBehalf, vault, 0, borrowAmount, bytes(""), migrateFrom);
}
function _enterPosition(
address onBehalf,
address vault,
uint256 depositAssetAmount,
uint256 borrowAmount,
bytes memory depositData,
address migrateFrom
)
internal
{
address asset = IYieldStrategy(vault).asset();
// Cannot enter a position if the account already has a native share balance
if (IYieldStrategy(vault).balanceOf(onBehalf) > 0) revert CannotEnterPosition();
if (depositAssetAmount > 0) {
// Take any margin deposit from the sender initially
ERC20(asset).safeTransferFrom(msg.sender, address(this), depositAssetAmount);
}
uint256 borrowShares;
uint256 vaultSharesReceived;
if (borrowAmount > 0) {
(vaultSharesReceived, borrowShares) =
_flashBorrowAndEnter(onBehalf, vault, asset, depositAssetAmount, borrowAmount, depositData, migrateFrom);
} else {
vaultSharesReceived = _enterOrMigrate(onBehalf, vault, asset, depositAssetAmount, depositData, migrateFrom);
}
ADDRESS_REGISTRY.setPosition(onBehalf, vault);
emit EnterPosition(
onBehalf, vault, depositAssetAmount, borrowShares, vaultSharesReceived, migrateFrom != address(0)
);
}
/// @inheritdoc ILendingRouter
function exitPosition(
address onBehalf,
address vault,
address receiver,
uint256 sharesToRedeem,
uint256 assetToRepay,
bytes calldata redeemData
)
external
override
isAuthorized(onBehalf, vault)
nonReentrant
{
_checkExit(onBehalf, vault);
address asset = IYieldStrategy(vault).asset();
uint256 borrowSharesRepaid;
uint256 profitsWithdrawn;
if (0 < assetToRepay) {
(borrowSharesRepaid, profitsWithdrawn) =
_exitWithRepay(onBehalf, vault, asset, receiver, sharesToRedeem, assetToRepay, redeemData);
} else {
// Migrate to is always set to address(0) since assetToRepay is always set to uint256.max
profitsWithdrawn = _redeemShares(onBehalf, vault, asset, address(0), sharesToRedeem, redeemData);
if (0 < profitsWithdrawn) ERC20(asset).safeTransfer(receiver, profitsWithdrawn);
}
if (balanceOfCollateral(onBehalf, vault) == 0) {
ADDRESS_REGISTRY.clearPosition(onBehalf, vault);
}
emit ExitPosition(onBehalf, vault, borrowSharesRepaid, sharesToRedeem, profitsWithdrawn);
}
/// @inheritdoc ILendingRouter
function liquidate(
address liquidateAccount,
address vault,
uint256 sharesToLiquidate,
uint256 debtToRepay
)
external
override
nonReentrant
returns (uint256 sharesToLiquidator)
{
if (sharesToLiquidate == 0) revert CannotLiquidateZeroShares();
address liquidator = msg.sender;
VaultPosition memory position = ADDRESS_REGISTRY.getVaultPosition(liquidator, vault);
// If the liquidator has a position then they cannot liquidate or they will have
// a native balance and a balance on the lending market.
if (position.lendingRouter != address(0)) revert LiquidatorHasPosition();
uint256 balanceBefore = balanceOfCollateral(liquidateAccount, vault);
if (balanceBefore == 0) revert InsufficientSharesHeld();
// Runs any checks on the vault to ensure that the liquidation can proceed, whitelists the lending platform
// to transfer collateral to the lending router. The current account is set in this method.
IYieldStrategy(vault).preLiquidation(liquidator, liquidateAccount, sharesToLiquidate, balanceBefore);
// After this call, address(this) will have the liquidated shares
uint256 borrowSharesRepaid;
(sharesToLiquidator, borrowSharesRepaid) =
_liquidate(liquidator, vault, liquidateAccount, sharesToLiquidate, debtToRepay);
// Transfers the shares to the liquidator from the lending router and does any post liquidation logic. The
// current account is cleared in this method.
IYieldStrategy(vault).postLiquidation(liquidator, liquidateAccount, sharesToLiquidator);
// The liquidator will receive shares in their native balance and then they can call redeem
// on the yield strategy to get the assets.
// Clear the position if the liquidator has taken all the shares, in the case of an insolvency,
// the account's position will just be left on the lending market with zero collateral. The account
// would be able to create a new position on this lending router or a new position on a different
// lending router. If they do create a new position on an insolvent account their old debt may
// be applied to their new position.
if (sharesToLiquidator == balanceBefore) ADDRESS_REGISTRY.clearPosition(liquidateAccount, vault);
emit LiquidatePosition(liquidator, liquidateAccount, vault, borrowSharesRepaid, sharesToLiquidator);
}
/// @inheritdoc ILendingRouter
function initiateWithdraw(
address onBehalf,
address vault,
bytes calldata data
)
external
override
isAuthorized(onBehalf, vault)
nonReentrant
returns (uint256 requestId)
{
requestId = _initiateWithdraw(vault, onBehalf, data, false);
}
/// @inheritdoc ILendingRouter
function forceWithdraw(
address account,
address vault,
bytes calldata data
)
external
nonReentrant
returns (uint256 requestId)
{
// Can only force a withdraw if health factor is negative, this allows a liquidator to
// force a withdraw and liquidate a position at a later time.
(uint256 borrowed, /* */, uint256 maxBorrow) = healthFactor(account, vault);
if (borrowed <= maxBorrow) revert CannotForceWithdraw(account);
requestId = _initiateWithdraw(vault, account, data, true);
// Clear the current account since this method is not called using isAuthorized
IYieldStrategy(vault).clearCurrentAccount();
emit ForceWithdraw(account, vault, requestId);
}
/// @inheritdoc ILendingRouter
function claimRewards(
address onBehalf,
address vault
)
external
override
isAuthorized(onBehalf, vault)
nonReentrant
returns (uint256[] memory rewards)
{
return RewardManagerMixin(vault).claimAccountRewards(onBehalf, balanceOfCollateral(onBehalf, vault));
}
/// @inheritdoc ILendingRouter
function healthFactor(
address borrower,
address vault
)
public
virtual
override
returns (uint256 borrowed, uint256 collateralValue, uint256 maxBorrow);
/// @inheritdoc ILendingRouter
function balanceOfCollateral(
address account,
address vault
)
public
view
virtual
override
returns (uint256 collateralBalance);
/**
* Internal Methods **
*/
function _checkExit(address onBehalf, address vault) internal view {
VaultPosition memory position = ADDRESS_REGISTRY.getVaultPosition(onBehalf, vault);
if (position.lendingRouter != address(this)) revert NoExistingPosition();
if (block.timestamp - position.lastEntryTime < COOLDOWN_PERIOD) {
revert CannotExitPositionWithinCooldownPeriod();
}
}
/// @dev Checks if an exitPosition call is a migration, this would be called via a lending router
function _isMigrate(address receiver) internal view returns (bool) {
return receiver == msg.sender && ADDRESS_REGISTRY.isLendingRouter(msg.sender);
}
/// @dev Enters a position or migrates shares from a previous lending router
function _enterOrMigrate(
address onBehalf,
address vault,
address asset,
uint256 assetAmount,
bytes memory depositData,
address migrateFrom
)
internal
returns (uint256 sharesReceived)
{
if (migrateFrom != address(0)) {
// Allow the previous lending router to repay the debt from assets held here.
ERC20(asset).checkApprove(migrateFrom, assetAmount);
sharesReceived = ILendingRouter(migrateFrom).balanceOfCollateral(onBehalf, vault);
// Must migrate the entire position
ILendingRouter(migrateFrom).exitPosition(
onBehalf, vault, address(this), sharesReceived, type(uint256).max, bytes("")
);
} else {
ERC20(asset).checkApprove(vault, assetAmoun
Submitted on: 2025-09-27 10:49:35
Comments
Log in to comment.
No comments yet.