Skip to content

Commit

Permalink
VaultRouter cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
RedVeil committed Nov 11, 2024
1 parent d9ad9e4 commit bfaeae1
Show file tree
Hide file tree
Showing 8 changed files with 477 additions and 667 deletions.
19 changes: 19 additions & 0 deletions src/interfaces/IAsyncVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-3.0
// Docgen-SOLC: 0.8.25

pragma solidity ^0.8.25;

import {IERC4626, IERC20} from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import {IERC7540Redeem} from "ERC-7540/interfaces/IERC7540.sol";

interface IAsyncVault is IERC7540Redeem {
function fulfillRedeem(
uint256 shares,
address controller
) external returns (uint256 total);

function fulfillMultipleRedeems(
uint256[] memory shares,
address[] memory controllers
) external returns (uint256 total);
}
201 changes: 85 additions & 116 deletions src/utils/VaultRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pragma solidity ^0.8.25;
import {IERC4626, IERC20} from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import {ICurveGauge} from "src/interfaces/external/curve/ICurveGauge.sol";
import {IERC7540Redeem} from "ERC-7540/interfaces/IERC7540.sol";
import {IAsyncVault} from "src/interfaces/IAsyncVault.sol";

/**
* @title VaultRouter
Expand All @@ -24,37 +24,53 @@ contract VaultRouter {

error SlippageTooHigh();

/**
* @notice Deposit and stake assets in a vault and gauge (Works both for ERC4626 and ERC7540 vaults)
* @param vault The vault to deposit and stake in
* @param gauge The gauge to stake in
* @param assets The amount of assets to deposit and stake
* @param minOut The minimum amount of shares to receive
* @param receiver The receiver of the shares
*/
function depositAndStake(
address vault,
address gauge,
uint256 assetAmount,
uint256 assets,
uint256 minOut,
address receiver
) external {
// Here to reduce user input
IERC20 asset = IERC20(IERC4626(vault).asset());
asset.safeTransferFrom(msg.sender, address(this), assetAmount);
asset.approve(address(vault), assetAmount);

uint256 shares = IERC4626(vault).deposit(assetAmount, address(this));
asset.safeTransferFrom(msg.sender, address(this), assets);
asset.approve(address(vault), assets);

uint256 shares = IERC4626(vault).deposit(assets, address(this));

if (shares < minOut) revert SlippageTooHigh();

IERC4626(vault).approve(gauge, shares);
ICurveGauge(gauge).deposit(shares, receiver);
}

/**
* @notice Unstake and withdraw assets from a gauge and vault (ONLY works both for ERC4626)
* @param vault The vault to deposit and stake in
* @param gauge The gauge to stake in
* @param shares The amount of shares to burn
* @param minOut The minimum amount of assets to receive
* @param receiver The receiver of the assets
*/
function unstakeAndWithdraw(
address vault,
address gauge,
uint256 burnAmount,
uint256 shares,
uint256 minOut,
address receiver
) external {
uint256 preBal = IERC4626(vault).balanceOf(address(this));

IERC20(gauge).safeTransferFrom(msg.sender, address(this), burnAmount);

ICurveGauge(gauge).withdraw(burnAmount);
_unstake(gauge, shares);

uint256 postBal = IERC4626(vault).balanceOf(address(this));

Expand All @@ -67,153 +83,106 @@ contract VaultRouter {
if (assets < minOut) revert SlippageTooHigh();
}

/// @notice Internal function to unstake shares from a gauge
function _unstake(address gauge, uint256 shares) internal {
IERC20(gauge).safeTransferFrom(msg.sender, address(this), shares);
ICurveGauge(gauge).withdraw(shares);
}

/*//////////////////////////////////////////////////////////////
ASYNCHRONOUS INTERACTION LOGIC
//////////////////////////////////////////////////////////////*/

event WithdrawalRequested(
address indexed vault,
address indexed asset,
address indexed receiver,
address caller,
uint256 amount
);
event WithdrawalFullfilled(
address indexed vault,
address indexed asset,
address indexed receiver,
uint256 amount
);
event WithdrawalClaimed(
address indexed asset,
address indexed receiver,
uint256 amount
);
event WithdrawalCancelled(
address indexed vault,
address indexed receiver,
uint256 amount
);

error ArrayMismatch();
// Vault Receiver Amount
mapping(address => mapping(address => uint256)) public requestShares;
// Vault Receiver Amount
mapping(address => mapping(address => uint256)) public claimableAssets;

/**
* @notice Unstake and request withdrawal from a vault (ONLY works for ERC7540 vaults)
* @param vault The vault to request withdrawal from
* @param gauge The gauge to unstake from
* @param shares The amount of shares to unstake
* @param receiver The receiver of the shares
*/
function unstakeAndRequestWithdrawal(
address gauge,
address vault,
address receiver,
uint256 shares
) external {
_unstake(gauge, shares);
_requestWithdrawal(vault, receiver, shares);
}

/**
* @notice Instantly unstake and withdraw your assets from a vault with all the necessary request and fulfill logic (ONLY works for ERC7540 vaults)
* @param gauge The gauge to unstake from
* @param vault The vault to request withdrawal from
* @param receiver The receiver of the shares
* @param shares The amount of shares to unstake
* @dev This function will unstake from the gauge, request a withdrawal from the vault, and immediately fulfill the withdrawal before redeeming the shares making the vault effectivly instant and synchronous
* @dev This router must be enabled as `controller` on the vault by the `receiver`
*/
function unstakeRequestFulfillWithdraw(
address gauge,
address vault,
address receiver,
uint256 shares
) external {
IERC20(gauge).safeTransferFrom(msg.sender, address(this), shares);

ICurveGauge(gauge).withdraw(shares);

_requestWithdrawal(vault, receiver, shares);
_requestFulfillWithdraw(vault, receiver, shares);
}

function requestWithdrawal(
/**
* @notice Instantly withdraw your assets from a vault with all the necessary request and fulfill logic (ONLY works for ERC7540 vaults)
* @param vault The vault to request withdrawal from
* @param receiver The receiver of the shares
* @param shares The amount of shares to unstake
* @dev This function will request a withdrawal from the vault, and immediately fulfill the withdrawal before redeeming the shares making the vault effectivly instant and synchronous
* @dev This router must be enabled as `controller` on the vault by the `receiver`
*/
function requestFulfillWithdraw(
address vault,
address receiver,
uint256 shares
) external {
IERC20(vault).safeTransferFrom(msg.sender, address(this), shares);

_requestWithdrawal(vault, receiver, shares);
_requestFulfillWithdraw(vault, receiver, shares);
}

function _requestWithdrawal(
/// @notice Internal function to request and fulfill and execute a withdrawal from a vault
function _requestFulfillWithdraw(
address vault,
address receiver,
uint256 shares
) internal {
requestShares[vault][receiver] += shares;

// allow vault to pull shares
IERC20(vault).safeIncreaseAllowance(vault, shares);

// request redeem - send shares to vault
IERC7540Redeem(vault).requestRedeem(shares, receiver, address(this));

emit WithdrawalRequested(
vault,
IERC4626(vault).asset(),
receiver,
msg.sender,
shares
);
}

// anyone can claim for a receiver
function claimWithdrawal(address vault, address receiver) external {
uint256 amount = claimableAssets[vault][receiver];
claimableAssets[vault][receiver] = 0;
_requestWithdrawal(vault, receiver, shares);

// claim asset with receiver shares
IERC4626(vault).withdraw(amount, receiver, receiver);
IAsyncVault(vault).fulfillRedeem(shares, receiver);

emit WithdrawalClaimed(vault, receiver, amount);
IERC4626(vault).redeem(shares, receiver, receiver);
}

// anyone can fullfil a withdrawal for a receiver
function fullfillWithdrawal(
/// @notice Internal function to fulfill a withdrawal from a vault
function _fulfillWithdrawal(
address vault,
address receiver,
uint256 shares
) external {
IERC20 asset = IERC20(IERC4626(vault).asset());

_fullfillWithdrawal(vault, receiver, asset, shares);
}

function fullfillWithdrawals(
address vault,
address[] memory receivers,
uint256[] memory shares
) external {
uint256 len = receivers.length;
if (len != shares.length) revert ArrayMismatch();

IERC20 asset = IERC20(IERC4626(vault).asset());

for (uint256 i; i < len; i++) {
_fullfillWithdrawal(vault, receivers[i], asset, shares[i]);
}
) internal {
IAsyncVault(vault).fulfillRedeem(shares, receiver);
IERC4626(vault).redeem(shares, receiver, receiver);
}

function _fullfillWithdrawal(
/// @notice Internal function to request a withdrawal from a vault
function _requestWithdrawal(
address vault,
address receiver,
IERC20 asset,
uint256 shares
) internal {
requestShares[vault][receiver] -= shares;

// fulfill redeem of pending shares for receiver
uint256 assetAmount = IERC7540Redeem(vault).fulfillRedeem(shares, receiver);

// assets are claimable now
claimableAssets[vault][receiver] += assetAmount;

emit WithdrawalFullfilled(vault, address(asset), receiver, shares);
}

error ZeroRequestShares();

// only receiver is able to cancel a request
function cancelRequest(address vault) external {
uint256 sharesCancelled = requestShares[vault][msg.sender];

if(sharesCancelled == 0)
revert ZeroRequestShares();

requestShares[vault][msg.sender] = 0;
IERC20(vault).safeTransferFrom(msg.sender, address(this), shares);

// cancel request and receive pending shares back
IERC7540Redeem(vault).cancelRedeemRequest(msg.sender);
// allow vault to pull shares
IERC20(vault).safeIncreaseAllowance(vault, shares);

emit WithdrawalCancelled(vault, msg.sender, sharesCancelled);
// request redeem - send shares to vault
IAsyncVault(vault).requestRedeem(shares, receiver, address(this));
}
}
Loading

0 comments on commit bfaeae1

Please sign in to comment.