// SPDX-License-Identifier: Apache-2.0 // https://docs.soliditylang.org/en/v0.8.10/style-guide.html pragma solidity 0.8.11; import {MathUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import {ImpactVaultManager} from "src/vaults/ImpactVaultManager.sol"; /** * @title ImpactVault * @author douglasqian * @notice This contract implements a new token vault standard inspired by * ERC-4626. Key difference is that ImpactVault ERC20 tokens do not * entitle depositors to a portion of the yield earned on the vault. * Instead, shares of yield is tracked to mint a proportional amount of * governance tokens to determine how the vault's yield will be deployed. * * Note: this vault should always be initialized with an ERC20 token * (ex: CELO) and a non-rebasing yield token (ex: stCELO). */ abstract contract ImpactVault is ERC20Upgradeable, PausableUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgradeable { using MathUpgradeable for uint256; using SafeERC20Upgradeable for IERC20Upgradeable; error ZeroDeposit(); error ZeroWithdraw(); event Deposit(uint256 _amount, address _receiver); event DepositWithYield(uint256 _amount, address _receiver, uint256 _yield); event WithdrawAsset(uint256 _amount, address _owner, address _receiver); event WithdrawYieldAsset( uint256 _amount, address _owner, address _receiver ); event TransferYieldToManager( address _owner, uint256 _amountYieldAsset, uint256 _amountCUSD ); IERC20Upgradeable internal asset; IERC20Upgradeable internal yieldAsset; address public impactVaultManager; /** * @dev Data structure that allows us to keep track of how much yield * each depositor in the vault is generating. For every depositor, this * is updated on deposits & withdraws. * * yield{t, t-1} = total_value{t} - total_value{t-1} */ struct YieldIndex { // Tracks the amount of yield assets held at last update. // This is important to track because yield generated is calculated // based on how much this share of the vault has appreciated. uint256 amountYieldAssetAtLastUpdate; // Tracks the total value of yield assets associated with a depositor // in the vault at last update. Denominated in "asset" uint256 totalAssetValueAtLastUpdate; // Tracks the total amount of yield accumulated into vault. // Denominated in "asset". uint256 accumulatedYield; } mapping(address => YieldIndex) public yieldIndexMap; /** * @dev Set the underlying asset contracts. Checks invariant: * convertToAsset(convertToYieldAsset(asset)) == asset */ function __ImpactVault_init( IERC20Upgradeable _asset, IERC20Upgradeable _yieldAsset, address _impactVaultManager ) internal onlyInitializing { __ImpactVault_init_unchained(_asset, _yieldAsset, _impactVaultManager); } function __ImpactVault_init_unchained( IERC20Upgradeable _asset, IERC20Upgradeable _yieldAsset, address _impactVaultManager ) internal onlyInitializing { asset = _asset; yieldAsset = _yieldAsset; impactVaultManager = _impactVaultManager; } /** * @notice Returns total asset value of vault. */ function totalAssets() public view virtual returns (uint256) { return asset.balanceOf(address(this)) + convertToAsset(yieldAsset.balanceOf(address(this))); } /** * DEPOSIT */ /** * @notice After asset are deposited in the vault, we stake it in the * underlying staked asset and mint new vault tokens. */ function deposit(uint256 _amount, address _receiver) public virtual whenNotPaused nonReentrant { if (_amount == 0) { revert ZeroDeposit(); } // Using SafeERC20Upgradeable // slither-disable-next-line unchecked-transfer asset.transferFrom(_msgSender(), address(this), _amount); _stake(_amount); _mint(_receiver, _amount); _updateYieldIndexSinceLastUpdate(_receiver, _amount, true); emit Deposit(_amount, _receiver); } /** * @dev For initial migration from V0 staking vaults only. Mint tokens * proportional to principal but stake all of the underlying asset that * can be transferred. Can only be called by a V0 vault. */ function depositWithYield( uint256 _amount, address _receiver, uint256 _yield ) external virtual whenNotPaused nonReentrant onlyV0Vault(_msgSender()) { if (_amount + _yield == 0) { revert ZeroDeposit(); } // Using SafeERC20Upgradeable // slither-disable-next-line unchecked-transfer asset.transferFrom(_msgSender(), address(this), _amount + _yield); _stake(_amount + _yield); _mint(_receiver, _amount); _updateYieldIndexSinceLastUpdate(_receiver, _amount + _yield, true); yieldIndexMap[_receiver].accumulatedYield += _yield; emit DepositWithYield(_amount, _receiver, _yield); } modifier onlyV0Vault(address _vault) { require( ImpactVaultManager(payable(impactVaultManager)).isV0Vault(_vault), "NOT_V0_VAULT" ); _; } /** * WITHDRAW */ /** * @notice Withdraws underlying asset by converting equivalent value in * staked asset and transferring it to the receiver. * @dev Burn vault tokens before withdrawing. */ function withdraw( uint256 _amount, address _receiver, address _owner ) public virtual whenNotPaused nonReentrant { // Capture assets associated with owner before burn. _beforeWithdraw(_amount, _owner); _withdraw(_receiver, _amount); _updateYieldIndexSinceLastUpdate(_owner, _amount, false); emit WithdrawAsset(_amount, _owner, _receiver); } function withdrawAll(address _receiver, address _owner) external virtual { withdraw(balanceOf(_owner), _receiver, _owner); } /** * @notice Withdraws yield asset from owner balance to receiver. * @param _amountAsset Amount to withdraw, denominated in asset. */ function withdrawYieldAsset( uint256 _amountAsset, address _receiver, address _owner ) public virtual whenNotPaused nonReentrant { // Capture assets associated with owner before burn. _beforeWithdraw(_amountAsset, _owner); uint256 amountYieldAssetToWithdraw = convertToYieldAsset(_amountAsset); yieldAsset.transfer(_receiver, amountYieldAssetToWithdraw); _updateYieldIndexSinceLastUpdate(_owner, _amountAsset, false); emit WithdrawYieldAsset(amountYieldAssetToWithdraw, _owner, _receiver); } function withdrawAllYieldAsset(address _receiver, address _owner) external virtual { withdrawYieldAsset(balanceOf(_owner), _receiver, _owner); } /** * @notice Transfers yield associated with a given address to the * ImpactVaultManager and updates their yield index. This can only be * triggered on the vault manager by the owner of the underlying asset. * Returns the amount of yield assets withdrawn from the vault in cUSD. */ function transferYieldToManager(address _address) external virtual whenNotPaused nonReentrant onlyVaultManager returns (uint256 amountToTransferCUSD) { // Withdraw total yield value in cUSD to ImpactVaultManager uint256 amountToTransferYieldAsset = convertToYieldAsset( getYield(_address) ); yieldAsset.transfer(_msgSender(), amountToTransferYieldAsset); // Reset yield index YieldIndex memory yIndex = yieldIndexMap[_address]; yIndex.accumulatedYield = 0; yIndex.amountYieldAssetAtLastUpdate = 0; yIndex.totalAssetValueAtLastUpdate = balanceOf(_address); // just assets yieldIndexMap[_address] = yIndex; amountToTransferCUSD = convertToUSD( convertToAsset(amountToTransferYieldAsset) ); emit TransferYieldToManager( _address, amountToTransferYieldAsset, amountToTransferCUSD ); return amountToTransferCUSD; } modifier onlyVaultManager() { require(_msgSender() == impactVaultManager); _; } /** * @dev Common hook called before all withdrawal flows. */ function _beforeWithdraw(uint256 _amount, address _owner) internal virtual { if (_amount == 0) { revert ZeroWithdraw(); } address caller = _msgSender(); if (caller != _owner) { _spendAllowance(_owner, caller, _amount); } _burn(_owner, _amount); } /** * YIELD INDEX */ /** * @dev Updates the yield index for a given address. Yield values * should not change before & after this (invariant). * * @param _address Address of the depositor * @param _amount Amount of asset being deposited/withdrawn * @param _isDeposit True if deposit otherwise withdraw */ function _updateYieldIndexSinceLastUpdate( address _address, uint256 _amount, bool _isDeposit ) internal virtual { uint256 yieldBeforeUpdate = getYield(_address); // Adjust the yield asset balance associated with this address. YieldIndex memory yIndex = yieldIndexMap[_address]; if (_isDeposit) { yIndex.amountYieldAssetAtLastUpdate += convertToYieldAsset(_amount); } else { yIndex.amountYieldAssetAtLastUpdate -= convertToYieldAsset(_amount); } // Update total value of yield asset (denominated in asset). yIndex.totalAssetValueAtLastUpdate = convertToAsset( yIndex.amountYieldAssetAtLastUpdate ); yIndex.accumulatedYield += _yieldEarnedSinceLastUpdate(_address); yieldIndexMap[_address] = yIndex; uint256 yieldAfterUpdate = getYield(_address); require(yieldBeforeUpdate == yieldAfterUpdate, "YIELD_SHOULDNT_CHANGE"); } /** * @dev Computes the yield earned by yield assets since the last index update. */ function _yieldEarnedSinceLastUpdate(address _address) internal view returns (uint256) { uint256 assetValueNow = convertToAsset( yieldIndexMap[_address].amountYieldAssetAtLastUpdate ); uint256 assetValueAtLastUpdate = yieldIndexMap[_address] .totalAssetValueAtLastUpdate; // Capped at current value to prevent potential underflow from rounding errors. return assetValueNow - MathUpgradeable.min(assetValueNow, assetValueAtLastUpdate); } /** * @notice Returns total yield generated on vault in the underlying asset. */ function totalYield() public view virtual returns (uint256) { return totalAssets() - totalSupply(); } function totalYieldUSD() public view virtual returns (uint256) { return convertToUSD(totalYield()); } /** * @notice Returns yield in vault associated with a depositor in underlying asset. */ function getYield(address _address) public view virtual returns (uint256) { return yieldIndexMap[_address].accumulatedYield + _yieldEarnedSinceLastUpdate(_address); } /** * @notice Returns yield in vault associated with a depositor in cUSD. */ function getYieldUSD(address _address) public view virtual returns (uint256) { return convertToUSD(getYield(_address)); } function pause() external onlyOwner { _pause(); } function unpause() external onlyOwner { _unpause(); } /** * TO BE IMPLEMENTED */ /** * @notice Converts an amount of the underlying asset to its value in cUSD. */ function convertToUSD(uint256 _assetAmount) public view virtual returns (uint256); /** * @dev Converts amount of yield asset to asset. */ function convertToAsset(uint256 _amountYieldAsset) public view virtual returns (uint256); /** * @dev Converts amount of asset to yield asset. */ function convertToYieldAsset(uint256 _amountAsset) public view virtual returns (uint256); /** * @dev Post-deposit hook to stake assets deposited and store in vault. */ function _stake(uint256 _assets) internal virtual; /** * @dev Core logic for withdrawing from staked asset contract to * receive underlying asset that we send back to receiver. */ function _withdraw(address _receiver, uint256 _amount) internal virtual; }