From 6b996901d6ae742763cf622379436a9c048fdb5a Mon Sep 17 00:00:00 2001 From: mcclurejt Date: Tue, 14 Feb 2023 14:30:50 -0600 Subject: [PATCH] create an ERC4626 Asset Proxy using the YVaultAssetProxy as a base --- contracts/ERC4626AssetProxy.sol | 183 ++++++++++++++++++++++++++++++ contracts/interfaces/IERC4626.sol | 92 +++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 contracts/ERC4626AssetProxy.sol create mode 100644 contracts/interfaces/IERC4626.sol diff --git a/contracts/ERC4626AssetProxy.sol b/contracts/ERC4626AssetProxy.sol new file mode 100644 index 00000000..f5419708 --- /dev/null +++ b/contracts/ERC4626AssetProxy.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { IERC20 } from "./interfaces/IERC20.sol"; +import { IERC4626Vault } from "./interfaces/IERC4626.sol"; +import "./WrappedPosition.sol"; +import "./libraries/Authorizable.sol"; + +/// SECURITY - This contract has an owner address which can migrate funds to a new yearn vault [or other contract +/// with compatible interface] as well as pause deposits and withdraws. This means that any deposited funds +/// have the same security as that address. + +/// @author mcclurejt +/// @title ERC4626 Asset Proxy +contract ERC4626AssetProxy is WrappedPosition, Authorizable { + // The addresses of the current yearn vault + IERC4626Vault public vault; + // 18 decimal fractional form of the multiplier which is applied after + // a vault upgrade. 0 when no upgrade has happened + uint88 public conversionRate; + // Bool packed into the same storage slot as vault and conversion rate + bool public paused; + uint8 public immutable vaultDecimals; + + /// @notice Constructs this contract and stores needed data + /// @param vault_ The erc4626 vault + /// @param _token The underlying token. + /// This token should revert in the event of a transfer failure. + /// @param _name The name of the token created + /// @param _symbol The symbol of the token created + /// @param _governance The address which can upgrade the yearn vault + /// @param _pauser address which can pause this contract + constructor( + address vault_, + IERC20 _token, + string memory _name, + string memory _symbol, + address _governance, + address _pauser + ) WrappedPosition(_token, _name, _symbol) Authorizable() { + // Authorize the pauser + _authorize(_pauser); + // set the owner + setOwner(_governance); + // Set the vault + vault = IERC4626Vault(vault_); + // Approve the vault so it can pull tokens from this address + _token.approve(vault_, type(uint256).max); + // Load the decimals and set them as an immutable + uint8 localVaultDecimals = IERC20(vault_).decimals(); + vaultDecimals = localVaultDecimals; + require( + uint8(_token.decimals()) == localVaultDecimals, + "Inconsistent decimals" + ); + } + + /// @notice Checks that the contract has not been paused + modifier notPaused() { + require(!paused, "Paused"); + _; + } + + /// @notice Makes the actual deposit into the ERC4626 vault + /// @return Tuple (the shares minted, amount underlying used) + function _deposit() internal override notPaused returns (uint256, uint256) { + // Get the amount deposited + uint256 amount = token.balanceOf(address(this)); + + // Deposit and get the shares that were minted to this + uint256 shares = vault.deposit(amount, address(this)); + + // If we have migrated our shares are no longer 1 - 1 with the vault shares + if (conversionRate != 0) { + // conversionRate is the fraction of yearnSharePrice1/yearnSharePrices2 at time of migration + // and so this multiplication will convert between yearn shares in the new vault and + // those in the old vault + shares = (shares * conversionRate) / 1e18; + } + + // Return the amount of shares the user has produced, and the amount used for it. + return (shares, amount); + } + + /// @notice Withdraw the number of shares + /// @param _shares The number of wrapped position shares to withdraw + /// @param _destination The address to send the output funds + // @param _underlyingPerShare The possibly precomputed underlying per share + /// @return returns the amount of funds freed by doing a yearn withdraw + function _withdraw( + uint256 _shares, + address _destination, + uint256 + ) internal override notPaused returns (uint256) { + // If the conversion rate is non-zero we have upgraded and so our wrapped shares are + // not one to one with the original shares. + if (conversionRate != 0) { + // Then since conversion rate is yearnSharePrice1/yearnSharePrices2 we divide the + // wrapped position shares by it because they are equivalent to the first yearn vault shares + _shares = (_shares * 1e18) / conversionRate; + } + // Withdraws shares from the vault. Max loss is set at 100% as + // the minimum output value is enforced by the calling + // function in the WrappedPosition contract. + uint256 amountReceived = vault.withdraw( + _shares, + _destination, + address(this) + ); + + // Return the amount of underlying + return amountReceived; + } + + /// @notice Get the underlying amount of tokens per shares given + /// @param _amount The amount of shares you want to know the value of + /// @return Value of shares in underlying token + function _underlying(uint256 _amount) + internal + view + override + returns (uint256) + { + return vault.previewRedeem(_amount); + } + + /// @notice Get the price per share in the vault by checking the cost of minting a single share + /// @return The price per share in units of underlying; + function _pricePerShare() internal view returns (uint256) { + return vault.previewMint(1); + } + + /// @notice Function to reset approvals for the proxy + function approve() external { + token.approve(address(vault), 0); + token.approve(address(vault), type(uint256).max); + } + + /// @notice Allows an authorized address or the owner to pause this contract + /// @param pauseStatus true for paused, false for not paused + /// @dev the caller must be authorized + function pause(bool pauseStatus) external onlyAuthorized { + paused = pauseStatus; + } + + /// @notice Function to transition between two yearn vaults + /// @param newVault The address of the new vault + /// @param minOutputShares The min of the new yearn vault's shares the wp will receive + /// @dev WARNING - This function has the capacity to steal all user funds from this + /// contract and so it should be ensured that the owner is a high quorum + /// governance vote through the time lock. + function transition(IERC4626Vault newVault, uint256 minOutputShares) + external + onlyOwner + { + // Load the current vault's price per share + uint256 currentPricePerShare = _pricePerShare(); + // Load the new vault's price per share + uint256 newPricePerShare = newVault.previewMint(1); + // Load the current conversion rate or set it to 1 + uint256 newConversionRate = conversionRate == 0 ? 1e18 : conversionRate; + // Calculate the new conversion rate, note by multiplying by the old + // conversion rate here we implicitly support more than 1 upgrade + newConversionRate = + (newConversionRate * newPricePerShare) / + currentPricePerShare; + // We now withdraw from the old yearn vault using max shares + // Note - Vaults should be checked in the future that they still have this behavior + vault.withdraw(type(uint256).max, address(this), address(this)); + // Approve the new vault + token.approve(address(newVault), type(uint256).max); + // Then we deposit into the new vault + uint256 currentBalance = token.balanceOf(address(this)); + uint256 outputShares = newVault.deposit(currentBalance, address(this)); + // We enforce a min output + require(outputShares >= minOutputShares, "Not enough output"); + // Change the stored variables + vault = newVault; + // because of the truncation yearn vaults can't have a larger diff than ~ billion + // times larger + conversionRate = uint88(newConversionRate); + } +} diff --git a/contracts/interfaces/IERC4626.sol b/contracts/interfaces/IERC4626.sol new file mode 100644 index 00000000..a022a5e5 --- /dev/null +++ b/contracts/interfaces/IERC4626.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IERC4626Vault is IERC20 { + // input asset + function asset() external view returns (address assetTokenAddress); + + function totalAssets() external view returns (uint256 totalManagedAssets); + + // conversions + function convertToShares(uint256 assets) + external + view + returns (uint256 shares); + + function convertToAssets(uint256 shares) + external + view + returns (uint256 assets); + + // deposit + function maxDeposit(address caller) + external + view + returns (uint256 maxAssets); + + function previewDeposit(uint256 assets) + external + view + returns (uint256 shares); + + function deposit(uint256 assets, address receiver) + external + returns (uint256 shares); + + // mint + function maxMint(address caller) external view returns (uint256 maxShares); + + function previewMint(uint256 shares) external view returns (uint256 assets); + + function mint(uint256 shares, address receiver) + external + returns (uint256 assets); + + // withdraw + function maxWithdraw(address owner) + external + view + returns (uint256 maxAssets); + + function previewWithdraw(uint256 assets) + external + view + returns (uint256 shares); + + function withdraw( + uint256 assets, + address receiver, + address owner + ) external returns (uint256 shares); + + // redeem + function maxRedeem(address owner) external view returns (uint256 maxShares); + + function previewRedeem(uint256 shares) + external + view + returns (uint256 assets); + + function redeem( + uint256 shares, + address receiver, + address owner + ) external returns (uint256 assets); + + // events + event Deposit( + address indexed caller, + address indexed owner, + uint256 assets, + uint256 shares + ); + event Withdraw( + address indexed caller, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); +}