diff --git a/script/Contracts.sol b/script/Contracts.sol index 2ca38af..53a1f8e 100644 --- a/script/Contracts.sol +++ b/script/Contracts.sol @@ -1,6 +1,14 @@ // SPDX-License-Identifier: BSD-3-Clause pragma solidity ^0.8.24; +contract MainnetContracts { + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address public constant STETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; + address public constant METH = 0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa; + address public constant OETH = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3; + address public constant RETH = 0xae78736Cd615f374D3085123A210448E74Fc6393; +} + contract ChapelContracts { address public constant ACTORS = 0xbA02225f0fdB684c80ad1e829FC31f048c416Ce6; address public constant VAULT_FACTORY = 0x964C6d4050e052D627b8234CAD9CdF0981E40EB3; diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol deleted file mode 100644 index 3e4ccfb..0000000 --- a/script/Deploy.s.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.24; - -import "lib/forge-std/src/Script.sol"; - -import {VaultFactory} from "src/VaultFactory.sol"; -import {IVaultFactory} from "src/IVaultFactory.sol"; -import {AnvilActors, HoleskyActors, ChapelActors, BscActors, IActors} from "script/Actors.sol"; -import {SingleVault} from "src/SingleVault.sol"; -import {TransparentUpgradeableProxy, TimelockController} from "src/Common.sol"; - -contract DeployVaultFactory is Script { - function run() public { - if (block.chainid == 31337) { - vm.startBroadcast(); - AnvilActors actors = new AnvilActors(); - uint256 minDelay = 10; // seconds - deployVaultFactory(actors, minDelay); - } - - if (block.chainid == 17000) { - vm.startBroadcast(); - HoleskyActors actors = new HoleskyActors(); - uint256 minDelay = 10; // seconds - deployVaultFactory(actors, minDelay); - } - - if (block.chainid == 97) { - vm.startBroadcast(); - ChapelActors actors = new ChapelActors(); - uint256 minDelay = 10; // seconds - deployVaultFactory(actors, minDelay); - } - - if (block.chainid == 56) { - vm.startBroadcast(); - BscActors actors = new BscActors(); - uint256 minDelay = 86400; // 24 hours in seconds - deployVaultFactory(actors, minDelay); - } - } - - function deployVaultFactory(IActors actors, uint256 minDelay) public returns (address) { - address vaultFactoryImpl = address(new VaultFactory()); - address singleVaultImpl = address(new SingleVault()); - - address[] memory proposers = new address[](2); - proposers[0] = actors.PROPOSER_1(); - proposers[1] = actors.PROPOSER_2(); - - address[] memory executors = new address[](2); - executors[0] = actors.EXECUTOR_1(); - executors[1] = actors.EXECUTOR_2(); - - address admin = actors.ADMIN(); - - string memory funcSig = "initialize(address,address,address)"; - - TimelockController timelock = new TimelockController(minDelay, proposers, executors, admin); - - TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( - vaultFactoryImpl, - address(timelock), - abi.encodeWithSignature(funcSig, singleVaultImpl, admin, address(timelock)) - ); - return address(proxy); - } -} diff --git a/src/Common.sol b/src/Common.sol index 8f97cf4..0e80bb6 100644 --- a/src/Common.sol +++ b/src/Common.sol @@ -5,20 +5,21 @@ import {AccessControlUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol"; import {Address} from "lib/openzeppelin-contracts/contracts/utils/Address.sol"; import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {ERC4626Upgradeable} from - "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol"; -import {ERC20PermitUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import {ERC20PermitUpgradeable} from + "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import {ETHRateProvider} from "src/module/ETHRateProvider.sol"; import {IAccessControl} from "lib/openzeppelin-contracts/contracts/access/IAccessControl.sol"; import {IERC20} from "lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol"; -import {IERC4626} from "lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; -import {IERC20Permit} from "lib/openzeppelin-contract/contracts/interfaces/IERC20Permit.sol"; +import {IERC20Metadata} from "lib/openzeppelin-contracts/contracts/interfaces/IERC20Metadata.sol"; +import {IERC20Permit} from "lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; import {IStakeManager} from "lib/synclub-contracts/contracts/interfaces/IStakeManager.sol"; +import {IRateProvider} from "src/interface/IRateProvider.sol"; +import {IVault} from "src/interface/IVault.sol"; import {Math} from "lib/openzeppelin-contracts/contracts/utils/math/Math.sol"; import {ProxyAdmin} from "lib/openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; import {ReentrancyGuardUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol"; import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import {StorageSlot} from "lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol"; import {TimelockController} from "lib/openzeppelin-contracts/contracts/governance/TimelockController.sol"; import {TimelockControllerUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/governance/TimelockControllerUpgradeable.sol"; @@ -26,6 +27,5 @@ import {TransparentUpgradeableProxy} from "lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {Storage} from "src/Storage.sol"; -import {Module} from "src/Module.sol"; contract Common {} diff --git a/src/ETHRateProvider.sol b/src/ETHRateProvider.sol new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/ETHRateProvider.sol @@ -0,0 +1 @@ + diff --git a/src/Module.sol b/src/Module.sol deleted file mode 100644 index f7ddc0c..0000000 --- a/src/Module.sol +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.24; - -import {IERC20Metadata, ERC20Upgradeable, Storage} from "src/Common.sol"; - -contract Module is IVault, Storage { - - - function _asset() internal view virtual returns (address) { - VaultStorage storage $ = _getVaultStorage(); - return $.baseAsset; - } - - function _assets() internal view returns (address[] memory assets_) { - AssetStorage storage $ = _getAssetStorage(); - uint256 assetListLength = $.assetList.length; - assets_ = new address[](assetListLength); - for (uint256 i = 0; i < assetListLength; i++) { - assets_[i] = $.assetList[i]; - } - } - - function _decimals() internal view virtual override(IERC20Metadata, ERC20Upgradeable) returns (uint8) { - VaultStorage storage $ = Storage.getVaultStorage(); - return $.baseDecimals + _decimalsOffset(); - } - - function _totalAssets() internal view virtual returns (uint256) { - AssetStorage storage $ = Storage.getVaultStorage(); - return $.totalAssets; - } - - function _maxDeposit(address) internal view virtual returns (uint256) { - return type(uint256).max; - } - - function _maxMint(address) internal view virtual returns (uint256) { - return type(uint256).max; - } - - function _maxWithdraw(address owner) internal view virtual returns (uint256) { - return _convertToAssets(balanceOf(owner), Math.Rounding.Floor); - } - - function _maxRedeem(address owner) public view virtual returns (uint256) { - ERC20Storage storage $ = Storage._getERC20Storage(); - return $.balanceOf(owner); - } - - function previewDeposit(uint256 assets) public view virtual returns (uint256) { - return _convertToShares(assets, Math.Rounding.Floor); - } - - function previewMint(uint256 shares) public view virtual returns (uint256) { - return _convertToAssets(shares, Math.Rounding.Ceil); - } - - function previewWithdraw(uint256 assets) public view virtual returns (uint256) { - return _convertToShares(assets, Math.Rounding.Ceil); - } - - function previewRedeem(uint256 shares) public view virtual returns (uint256) { - return _convertToAssets(shares, Math.Rounding.Floor); - } - - /** @dev See {IERC4626-deposit}. */ - function deposit(uint256 assets, address receiver) public virtual returns (uint256) { - // uint256 maxAssets = maxDeposit(receiver); - // if (assets > maxAssets) { - // revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets); - // } - - // uint256 shares = previewDeposit(assets); - // _deposit(_msgSender(), receiver, assets, shares); - - // return shares; - } - - /** @dev See {IAssetVault-depositAsset}. */ - function depositAsset(address asset, uint256 amount, address receiver) public virtual returns (uint256) { - // uint256 maxAssets = maxDeposit(receiver); - // if (assets > maxAssets) { - // revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets); - // } - - // uint256 shares = previewDeposit(assets); - // _deposit(_msgSender(), receiver, assets, shares); - - // return shares; - } - - /** @dev See {IERC4626-mint}. - * - * As opposed to {deposit}, minting is allowed even if the vault is in a state where the price of a share is zero. - * In this case, the shares will be minted without requiring any assets to be deposited. - */ - function mint(uint256 shares, address receiver) public virtual returns (uint256) { - // uint256 maxShares = maxMint(receiver); - // if (shares > maxShares) { - // revert ERC4626ExceededMaxMint(receiver, shares, maxShares); - // } - - // uint256 assets = previewMint(shares); - // _deposit(_msgSender(), receiver, assets, shares); - - // return assets; - } - - /** @dev See {IERC4626-withdraw}. */ - function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) { - // uint256 maxAssets = maxWithdraw(owner); - // if (assets > maxAssets) { - // revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); - // } - - // uint256 shares = previewWithdraw(assets); - // _withdraw(_msgSender(), receiver, owner, assets, shares); - - // return shares; - } - - /** @dev See {IERC4626-redeem}. */ - function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) { - // uint256 maxShares = maxRedeem(owner); - // if (shares > maxShares) { - // revert ERC4626ExceededMaxRedeem(owner, shares, maxShares); - // } - - // uint256 assets = previewRedeem(shares); - // _withdraw(_msgSender(), receiver, owner, assets, shares); - - // return assets; - } - - /** - * @dev Internal conversion function (from assets to shares) with support for rounding direction. - */ - function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) { - return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding); - } - - /** - * @dev Internal conversion function (from shares to assets) with support for rounding direction. - */ - function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) { - // return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding); - } - - /** - * @dev Deposit/mint common workflow. - */ - function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual { - // ERC4626Storage storage $ = _getERC4626Storage(); - // // If _asset is ERC777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the - // // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer, - // // calls the vault, which is assumed not malicious. - // // - // // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the - // // assets are transferred and before the shares are minted, which is a valid state. - // // slither-disable-next-line reentrancy-no-eth - // SafeERC20.safeTransferFrom($._asset, caller, address(this), assets); - // _mint(receiver, shares); - - // emit Deposit(caller, receiver, assets, shares); - } - - /** - * @dev Withdraw/redeem common workflow. - */ - function _withdraw( - address caller, - address receiver, - address owner, - uint256 assets, - uint256 shares - ) internal virtual { - // ERC4626Storage storage $ = _getERC4626Storage(); - // if (caller != owner) { - // _spendAllowance(owner, caller, shares); - // } - - // // If _asset is ERC777, `transfer` can trigger a reentrancy AFTER the transfer happens through the - // // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer, - // // calls the vault, which is assumed not malicious. - // // - // // Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the - // // shares are burned and after the assets are transferred, which is a valid state. - // _burn(owner, shares); - // SafeERC20.safeTransfer($._asset, receiver, assets); - - // emit Withdraw(caller, receiver, owner, assets, shares); - } - - function _decimalsOffset() internal view virtual returns (uint8) { - return 0; - } - - function _getStrategyWithLowestBalance() internal view returns (address strategyAddress, uint256 lowestBalance) { - StrategyStorage storage $ = Storage.getStrategyStorage(); - uint256 lowestBalanceFound = type(uint256).max; - address strategyWithLowestBalance; - - for (uint256 i = 0; i < $.strategyList.length; i++) { - address strategy = $.strategyList[i]; - uint256 currentBalance = $.strategies[strategy].currentBalance; - if (currentBalance < lowestBalanceFound) { - lowestBalanceFound = currentBalance; - strategyWithLowestBalance = strategy; - } - } - - return (strategyWithLowestBalance, lowestBalanceFound); - } - - function processAccounting() public { - // get the balances of the assets - AssetStorage storage assetStorage = _getAssetStorage(); - - for (uint256 i = 0; i < assetsStorage.list.length; i++) { - address asset = assets[i]; - idleBalance = asset.balanceOf(address(vault)); - } - // get the balances of the strategies - - // call convertToAssets on the strategie? - - - - // NOTE: Get the loops out of the public calls - } -} \ No newline at end of file diff --git a/src/Storage.sol b/src/Storage.sol index 53759da..4168127 100644 --- a/src/Storage.sol +++ b/src/Storage.sol @@ -1,18 +1,16 @@ // SPDX-License-Identifier: BSD-3-Clause pragma solidity ^0.8.24; -import {IVault} from "src/Common.sol"; +import {IVault} from "src/interface/IVault.sol"; library Storage { - bytes32 private constant VAULT_STORAGE_POSITION = keccak256("yieldnest.storage.vault"); bytes32 private constant ASSET_STORAGE_POSITION = keccak256("yieldnest.storage.asset"); bytes32 private constant STRAT_STORAGE_POSITION = keccak256("yieldnest.storage.strat"); - - bytes32 private constant ERC20_STORAGE_POSITION = keccak256( - abi.encode(uint256(keccak256("openzeppelin.storage.ERC20")) - 1) - ) & ~bytes32(uint256(0xff)); - + + bytes32 private constant ERC20_STORAGE_POSITION = + keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC20")) - 1)) & ~bytes32(uint256(0xff)); + function _getVaultStorage() internal pure returns (IVault.VaultStorage storage $) { assembly { $.slot := VAULT_STORAGE_POSITION @@ -25,7 +23,7 @@ library Storage { } } - function _getStrategyStorage() internal pure returns (StrategyStorage storage $) { + function _getStrategyStorage() internal pure returns (IVault.StrategyStorage storage $) { assembly { $.slot := STRAT_STORAGE_POSITION } @@ -36,4 +34,4 @@ library Storage { $.slot := ERC20_STORAGE_POSITION } } -} \ No newline at end of file +} diff --git a/src/Vault.sol b/src/Vault.sol index 7f95a9a..fedf435 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -1,42 +1,47 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BSD Clause-3 pragma solidity ^0.8.24; import { Address, IERC20, IVault, + Math, + IRateProvider, + IERC20Metadata, SafeERC20, Storage, - ERC20PermitUpgradeable + AccessControlUpgradeable, + ERC20PermitUpgradeable, + ReentrancyGuardUpgradeable } from "./Common.sol"; -contract Vault is IVault, Storage, ERC20PermitUpgradeable { - +contract Vault is IVault, ERC20PermitUpgradeable, AccessControlUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; using Address for address; function asset() public view virtual returns (address) { - VaultStorage storage $ = _getVaultStorage(); - return $.baseAsset; + AssetStorage storage assetStorage = Storage._getAssetStorage(); + return assetStorage.list[0]; } - + function assets() public view returns (address[] memory assets_) { - AssetStorage storage $ = _getAssetStorage(); - uint256 assetListLength = $.assetList.length; + AssetStorage storage assetStorage = Storage._getAssetStorage(); + uint256 assetListLength = assetStorage.list.length; assets_ = new address[](assetListLength); for (uint256 i = 0; i < assetListLength; i++) { - assets_[i] = $.assetList[i]; + assets_[i] = assetStorage.list[i]; } } - function decimals() public view virtual override(IERC20Metadata, ERC20Upgradeable) returns (uint8) { - VaultStorage storage $ = Storage.getVaultStorage(); - return $.baseDecimals + _decimalsOffset(); + function decimals() public view virtual override(IERC20Metadata, ERC20PermitUpgradeable) returns (uint8) { + AssetStorage storage assetStorage = Storage._getAssetStorage(); + // QUESTION: Do we need this decimals offset? + return assetStorage.assets[assetStorage.list[0]].decimals + _decimalsOffset(); } function totalAssets() public view virtual returns (uint256) { - AssetStorage storage $ = Storage.getVaultStorage(); - return $.totalAssets; + VaultStorage storage vaultStorage = Storage._getVaultStorage(); + return vaultStorage.totalAssets; } function maxDeposit(address) public view virtual returns (uint256) { @@ -47,53 +52,70 @@ contract Vault is IVault, Storage, ERC20PermitUpgradeable { return type(uint256).max; } + // QUESTION: How to handle this in v1 with async withdraws. function maxWithdraw(address owner) public view virtual returns (uint256) { - return _convertToAssets(balanceOf(owner), Math.Rounding.Floor); + AssetStorage storage assetStorage = Storage._getAssetStorage(); + return _convertToAssets(assetStorage.list[0], balanceOf(owner), Math.Rounding.Floor); } + // QUESTION: How to handle this in v1 with async withdraws. function maxRedeem(address owner) public view virtual returns (uint256) { - ERC20Storage storage $ = Storage._getERC20Storage(); - return $.balanceOf(owner); + AssetStorage storage assetStorage = Storage._getAssetStorage(); + return assetStorage.list[0].balanceOf(owner); } - function previewDeposit(uint256 assets) public view virtual returns (uint256) { - return _convertToShares(assets, Math.Rounding.Floor); + function previewDeposit(uint256 assetAmount) public view virtual returns (uint256) { + AssetStorage storage assetStorage = Storage._getAssetStorage(); + return _convertToShares(assetStorage.list[0], assetAmount, Math.Rounding.Floor); } - function previewDepositAsset(uint256 assets) public view virtual returns (uint256) { - return _convertToShares(assets, Math.Rounding.Floor); - } + function previewDepositAsset(address assetAddress, uint256 assetAmount) public view virtual returns (uint256) { + return _convertToShares(assetAddress, assetAmount, Math.Rounding.Floor); + } function previewMint(uint256 shares) public view virtual returns (uint256) { - return _convertToAssets(shares, Math.Rounding.Ceil); + VaultStorage storage vaultStorage = Storage._getVaultStorage(); + return _convertToAssets(vaultStorage.asset, shares, Math.Rounding.Ceil); } - function previewMintAsset(uint256 shares) public view virtual returns (uint256) { - return _convertToAssets(shares, Math.Rounding.Ceil); + function previewMintAsset(address assetAddress, uint256 shareAmount) public view virtual returns (uint256) { + return _convertToAssets(assetAddress, shareAmount, Math.Rounding.Ceil); } - function previewWithdraw(uint256 assets) public view virtual returns (uint256) { - return _convertToShares(assets, Math.Rounding.Ceil); + + // QUESTION: How to handle this? Start disabled, come back later + // This would have to be it's own Liquidity and Risk Module + // that calculates the asset ratios and figure out the debt ratio + function previewWithdraw(uint256 assetAmount) public view virtual returns (uint256) { + VaultStorage storage vaultStorage = Storage._getVaultStorage(); + return _convertToShares(vaultStorage.asset, assetAmount, Math.Rounding.Ceil); } function previewRedeem(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Floor); } - /** @dev See {IERC4626-deposit}. */ - function deposit(uint256 assets, address receiver) public virtual returns (uint256) { + /** + * @dev See {IERC4626-deposit}. + */ + function deposit(uint256 assetAmount, address receiver) public virtual returns (uint256) { + VaultStorage storage vaultStorage = Storage._getVaultStorage(); + if (vaultStorage.paused) revert Paused(); + uint256 maxAssets = maxDeposit(receiver); - if (assets > maxAssets) { - revert ExceededMaxDeposit(receiver, assets, maxAssets); + if (assetAmount > maxAssets) { + revert ExceededMaxDeposit(receiver, assetAmount, maxAssets); } - uint256 shares = previewDeposit(assets); - _deposit(_msgSender(), receiver, assets, shares); + uint256 shares = previewDeposit(assetAmount); + _deposit(vaultStorage.asset, _msgSender(), receiver, assetAmount, shares); return shares; } - /** @dev See {IAssetVault-depositAsset}. */ - function depositAsset(address asset, uint256 amount, address receiver) public virtual returns (uint256) { + /** + * @dev See {IAssetVault-depositAsset}. + */ + function depositAsset(address assetAddress, uint256 amount, address receiver) public virtual returns (uint256) { // uint256 maxAssets = maxDeposit(receiver); // if (assets > maxAssets) { // revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets); @@ -103,9 +125,10 @@ contract Vault is IVault, Storage, ERC20PermitUpgradeable { // _deposit(_msgSender(), receiver, assets, shares); // return shares; - } + } - /** @dev See {IERC4626-mint}. + /** + * @dev See {IERC4626-mint}. * * As opposed to {deposit}, minting is allowed even if the vault is in a state where the price of a share is zero. * In this case, the shares will be minted without requiring any assets to be deposited. @@ -122,8 +145,10 @@ contract Vault is IVault, Storage, ERC20PermitUpgradeable { // return assets; } - /** @dev See {IERC4626-withdraw}. */ - function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) { + /** + * @dev See {IERC4626-withdraw}. + */ + function withdraw(uint256 assetAmount, address receiver, address owner) public virtual returns (uint256) { // uint256 maxAssets = maxWithdraw(owner); // if (assets > maxAssets) { // revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); @@ -135,7 +160,9 @@ contract Vault is IVault, Storage, ERC20PermitUpgradeable { // return shares; } - /** @dev See {IERC4626-redeem}. */ + /** + * @dev See {IERC4626-redeem}. + */ function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) { // uint256 maxShares = maxRedeem(owner); // if (shares > maxShares) { @@ -149,50 +176,81 @@ contract Vault is IVault, Storage, ERC20PermitUpgradeable { } /** - * @dev Internal conversion function (from assets to shares) with support for rounding direction. + * @dev Converts assets to shares with support for rounding direction. + * + * This function first converts the assets to a base asset value using `_convertAssetToBase`, then scales this value + * based on the total supply and total assets of the vault, with the option to round up or down. + * + * @param asset The address of the asset to convert. + * @param assets The amount of assets to convert. + * @param rounding The rounding direction to use for the conversion. + * @return The converted share value. */ - function _convertToShares(address asset, uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) { - - uint256 convertedAssets = _convertToBasePrice(asset, assets); - - return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding); + function _convertToShares(address assetAddress, uint256 assetAmount, Math.Rounding rounding) + internal + view + virtual + returns (uint256) + { + uint256 convertedAssets = _convertAssetsToBase(assetAddress, assetAmount); + return convertedAssets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding); + } + + function _convertAssetsToBase(address assetAddress, uint256 assetAmount) internal view returns (uint256) { + VaultStorage storage vaultStorage = Storage._getVaultStorage(); + uint256 rate = IRateProvider(vaultStorage).rateProvider.getRate(assetAddress); + return (assetAmount * rate) / 1e18; } /** - * @dev Internal conversion function (from shares to assets) with support for rounding direction. + * @dev Converts shares to assets with support for rounding direction. + * + * This function takes an asset address, a number of shares, and a rounding direction as input. + * It first converts the shares to a base asset value using `_convertBaseToAssets`, then scales this value + * based on the total supply and total assets of the vault, with the option to round up or down. + * + * @param asset The address of the asset to convert shares to. + * @param shares The number of shares to convert. + * @param rounding The rounding direction to use for the conversion. + * @return The converted asset value. */ - function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) { - // return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding); + function _convertToAssets(address assetAddress, uint256 shareAmount, Math.Rounding rounding) + internal + view + virtual + returns (uint256) + { + uint256 convertedAssets = _convertBaseToAssets(assetAddress, shareAmount); + return convertedAssets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding); + } + + function _convertBaseToAssets(address assetAddress, uint256 baseAmount) internal view returns (uint256) { + VaultStorage storage vaultStorage = Storage._getVaultStorage(); + uint256 rate = IRateProvider(vaultStorage.rateProvider).getRate(assetAddress); + return (baseAmount * 1e18) / rate; } /** - * @dev Deposit/mint common workflow. + * @dev Being Multi asset, we need to add the asset param here to deposit the user's asset accordingly. */ - function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual { - // ERC4626Storage storage $ = _getERC4626Storage(); - // // If _asset is ERC777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the - // // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer, - // // calls the vault, which is assumed not malicious. - // // - // // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the - // // assets are transferred and before the shares are minted, which is a valid state. - // // slither-disable-next-line reentrancy-no-eth - // SafeERC20.safeTransferFrom($._asset, caller, address(this), assets); - // _mint(receiver, shares); - - // emit Deposit(caller, receiver, assets, shares); + function _deposit(address assetAddress, address caller, address receiver, uint256 assetAmount, uint256 shares) + internal + virtual + { + VaultStorage storage vaultStorage = Storage._getVaultStorage(); + vaultStorage.totalAssets += _convertAssetsToBase(assetAddress, assetAmount); + SafeERC20.safeTransferFrom(assetAddress, caller, address(this), assetAmount); + _mint(receiver, shares); + emit Deposit(caller, receiver, assetAmount, shares); } /** * @dev Withdraw/redeem common workflow. */ - function _withdraw( - address caller, - address receiver, - address owner, - uint256 assets, - uint256 shares - ) internal virtual { + function _withdraw(address caller, address receiver, address owner, uint256 assetAmount, uint256 shares) + internal + virtual + { // ERC4626Storage storage $ = _getERC4626Storage(); // if (caller != owner) { // _spendAllowance(owner, caller, shares); @@ -214,37 +272,89 @@ contract Vault is IVault, Storage, ERC20PermitUpgradeable { return 0; } - function _getStrategyWithLowestBalance() internal view returns (address strategyAddress, uint256 lowestBalance) { - StrategyStorage storage $ = Storage.getStrategyStorage(); - uint256 lowestBalanceFound = type(uint256).max; - address strategyWithLowestBalance; + // Admin functions - for (uint256 i = 0; i < $.strategyList.length; i++) { - address strategy = $.strategyList[i]; - uint256 currentBalance = $.strategies[strategy].currentBalance; - if (currentBalance < lowestBalanceFound) { - lowestBalanceFound = currentBalance; - strategyWithLowestBalance = strategy; - } - } + // QUESTION: Start with Strategies or add them later + // vault starts paused because the rate provider and assets / strategies haven't been set + function initialize(address admin, string memory name, string memory symbol) public initializer { + // Initialize the vault + __ERC20_init(name, symbol); + __AccessControl_init(); + __ReentrancyGuard_init(); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + + VaultStorage storage vaultStorage = Storage._getVaultStorage(); + vaultStorage.paused = true; + } + + // QUESTION: Measure the gas difference between IERC20 or address when casting / saving to storage + function setRateProvider(address rateProvider) public onlyRole(DEFAULT_ADMIN_ROLE) { + VaultStorage storage vaultStorage = Storage._getVaultStorage(); + vaultStorage.rateProvider = rateProvider; + emit SetRateProvider(rateProvider); + } + + function addAsset(address assetAddress, uint256 assetDecimals) public onlyRole(DEFAULT_ADMIN_ROLE) { + if (assetAddress == address(0)) revert ZeroAddress(); + if (assetDecimals > 18) revert InvalidDecimals(); + + AssetStorage storage assetStorage = Storage._getAssetStorage(); + if (assetStorage.assets[assetAddress].asset != address(0)) revert InvalidAsset(); + + uint256 newIndex = assetStorage.list.length; + + assetStorage.assets[assetAddress] = AssetParams({ + asset: assetAddress, + active: true, + index: newIndex, + decimals: assetDecimals, + idleAssets: 0, + deployedAssets: 0 + }); + + assetStorage.list.push(assetAddress); + + emit AddAsset(assetAddress, assetDecimals, newIndex); + } + + function toggleAsset(address asset_, bool active) public onlyRole(DEFAULT_ADMIN_ROLE) { + AssetStorage storage assetStorage = Storage._getAssetStorage(); + if (assetStorage.assets[asset_].asset == address(0)) revert AssetNotFound(); + assetStorage.assets[asset_].active = active; + emit ToggleAsset(asset_, active); + } - return (strategyWithLowestBalance, lowestBalanceFound); + function pause(bool paused) public onlyRole(DEFAULT_ADMIN_ROLE) { + VaultStorage storage vaultStorage = Storage._getVaultStorage(); + vaultStorage.paused = paused; + emit Pause(paused); } - + + // QUESTION: What params should be used here? strategy, asset, etc. function processAccounting() public { // get the balances of the assets - AssetStorage storage assetStorage = _getAssetStorage(); - - for (uint256 i = 0; i < assetsStorage.list.length; i++) { - address asset = assets[i]; - idleBalance = asset.balanceOf(address(vault)); - } + // AssetStorage storage assetStorage = _getAssetStorage(); + + // for (uint256 i = 0; i < assetsStorage.list.length; i++) { + // address asset = assets[i]; + // idleBalance = asset.balanceOf(address(vault)); + // } // get the balances of the strategies // call convertToAssets on the strategie? - + // QUESTION: Keep the balances for the assets, or keep that balances in base price // NOTE: Get the loops out of the public calls - } -} \ No newline at end of file + + //yv3 + // what if you want to udpate a single strategy, but what if you want to update one strat + // the debt value is used to separate the deposited value, you know what the rewards + // balance is based on the the debt less the the rewards. + // allows you to not have to grind through everything?? + } + + constructor() { + _disableInitializers(); + } +} diff --git a/src/interface/IRateProvider.sol b/src/interface/IRateProvider.sol new file mode 100644 index 0000000..c347828 --- /dev/null +++ b/src/interface/IRateProvider.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.24; + +interface IRateProvider { + function getRate(address asset) external view returns (uint256); +} diff --git a/src/interface/ISingleVault.sol b/src/interface/ISingleVault.sol deleted file mode 100644 index cd3b401..0000000 --- a/src/interface/ISingleVault.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.24; - -import {IERC20, IERC4626, IAccessControl} from "src/Common.sol"; - -interface ISingleVault is IERC20, IERC4626, IAccessControl { - error AssetZeroAddress(); - error NameEmpty(); - error SymbolEmpty(); - error AdminZeroAddress(); - error ProposersEmpty(); - error ExecutorsEmpty(); - - function initialize( - IERC20 asset_, - string calldata name_, - string calldata symbol_, - address admin_, - uint256 minDelay_, - address[] calldata proposers_, - address[] calldata executors_ - ) external; -} diff --git a/src/interface/IVault.sol b/src/interface/IVault.sol index 5014f73..409a502 100644 --- a/src/interface/IVault.sol +++ b/src/interface/IVault.sol @@ -1,52 +1,54 @@ // SPDX-License-Identifier: BSD-3-Clause pragma solidity ^0.8.24; -import {IERC4626, IAccessControl} from "../Common.sol"; - -interface IVault is IERC4626, IAccessControl { - +interface IVault { error ZeroAddress(); error InvalidString(); error InvalidArray(); error ExceededMaxDeposit(); - + error InvalidAsset(); + error InvalidDecimals(); + error AssetNotFound(); + error Paused(); + event DepositAsset(address indexed asset, address indexed vault, uint256 amount, address indexed receiver); + event SetRateProvider(address indexed rateProvider); + event AddAsset(address indexed asset, uint256 decimals, uint256 index); + event ToggleAsset(address indexed asset, bool active); + event Pause(bool paused); + event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares); // Internal storage vs balanceOf storage // QUESTION: What issues are there with lending markets or other issues struct VaultStorage { - // Version - uint8 version; - // Base underlying asset of the Vault - address asset; // WETH - // Decimals of the Vault token - uint8 underlyingDecimals; // Balance of total assets priced in base asset uint256 totalAssets; + // The price provider for asset conversions to the base asset + address rateProvider; + // If the vault is paused or not + bool paused; } // QUESTION: Update the rate instead of the totalAssets? // QUESTION: How to avoid recalculating everything everytime struct AssetParams { - // ERC20 asset token - address asset; // Activated or Deactivated bool active; // Index of this asset in the mapping - uint8 index; + uint256 index; // The decmials of the asset - uint8 decimals; + uint256 decimals; // Current vault deposit balance of this asset uint256 idleAssets; // deployedBalance - // QUESTION: Is this required? We are counting this balance in + // QUESTION: Is this required? We are counting this balance in // the strategy.assets uint256 deployedAssets; } struct AssetStorage { mapping(address => AssetParams) assets; - address[] list; + IERC20[] list; } struct StrategyParams { @@ -55,10 +57,10 @@ interface IVault is IERC4626, IAccessControl { // Activated or Deactivated bool active; // The index of this strategy in the map - uint8 index; + uint256 index; // The percent in 100 * 100 Basis // QUESTION: Should this be on the Vault storage? - uint8 ratio; + uint256 ratio; // The Asset Address and allocated balance. mapping(address => uint256) assets; } @@ -68,58 +70,53 @@ interface IVault is IERC4626, IAccessControl { address[] list; } - // taken from oz ERC20Upgradeable - struct ERC20Storage { - mapping(address account => uint256) _balances; - - mapping(address account => mapping(address spender => uint256)) _allowances; - - uint256 _totalSupply; - - string _name; - string _symbol; - } - - // read - function assets() external view returns (address[] memory assets_); + // 4626 + function asset() external view returns (address); function totalAssets() external view returns (uint256); function convertToShares(uint256 assets) external view returns (uint256); function convertToAssets(uint256 shares) external view returns (uint256); - function maxDeposit(address) external view returns (uint256); function maxMint(address) external view returns (uint256); function maxWithdraw(address owner) external view returns (uint256); function maxRedeem(address owner) external view returns (uint256); - - function previewDepositAsset(uint256 assets) external view returns (uint256); - function previewMintAsset(uint256 shares) external view returns (uint256); function previewWithdraw(uint256 assets) external view returns (uint256); function previewRedeem(uint256 shares) external view returns (uint256); - function getStrategies() external view returns (address[] memory); - function isStrategyActive(address strategy) external view returns (bool); - - // New function signatures for structs - function getVaultStorage() external view returns (VaultStorage memory); - function getAssetParams(address asset) external view returns (AssetParams memory); - function getAssetStorage() external view returns (AssetStorage memory); - function getStrategyParams(address strategy) external view returns (StrategyParams memory); - function getStrategyStorage() external view returns (StrategyStorage memory); - - // write function deposit(uint256 assets, address receiver) external returns (uint256); - function depositAsset(address asset, uint256 assets, address receiver) external returns (uint256); - function mint(uint256 shares, address receiver) external returns (uint256); - function mintAsset(address asset, uint25 shares, address receiver) external returns (uint256); - function withdraw(uint256 assets, address receiver, address owner) external returns (uint256); function redeem(uint256 shares, address receiver, address owner) external returns (uint256); + // multi asset + function assets() external view returns (address[] memory assets_); + function convertToSharesAsset(address, uint256 assets) external view returns (uint256); + function convertAssetToAssets(address, uint256 shares) external view returns (uint256); + function maxDepositAsset(address, address) external view returns (uint256); + function maxMintAsset(address, address) external view returns (uint256); + function previewDepositAsset(uint256 assets) external view returns (uint256); + function previewMintAsset(uint256 shares) external view returns (uint256); + + function depositAsset(address asset, uint256 assets, address receiver) external returns (uint256); + function mintAsset(address asset, uint256 shares, address receiver) external returns (uint256); + + function getStrategies() external view returns (address[] memory); + function getAssets() external view returns (address[] memory); + function isStrategyActive(address strategy) external view returns (bool); + // admin - function initialize(IERC20[] memory assets_, address admin_, string memory name_, string memory symbol_) external; + function initialize(address admin_, string memory name_, string memory symbol_) external; function addStrategy(address strategy) external; function removeStrategy(address strategy) external; - function processAccounting() external returns (uint256); + function setRateProvider(address rateProvider) external; + function addAsset(address assetAddress, uint256 assetDecimals) external; + function toggleAsset(address asset, bool active) external; + + struct ERC20Storage { + mapping(address => uint256) balances; + mapping(address => mapping(address => uint256)) allowances; + uint256 totalSupply; + string name; + string symbol; + } } diff --git a/src/interface/IVaultFactory.sol b/src/interface/IVaultFactory.sol deleted file mode 100644 index 817a254..0000000 --- a/src/interface/IVaultFactory.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.24; - -import {IERC20, IERC4626, IAccessControl} from "src/Common.sol"; - -interface IVaultFactory is IAccessControl { - enum VaultType { - SingleAsset, - MetaVault, - Strategy - } - - /** - * @dev Represents a vault with its timelock, name, symbol, and type. - * @param timelock The address of the timelock controller for the vault. - * @param name The name of the vault. - * @param symbol The symbol of the vault. - * @param vaultType The type of the vault, either SingleAsset or MultiAsset. - */ - struct Vault { - address timelock; - string name; - string symbol; - VaultType vaultType; - } - - function timelock() external view returns (address); - - function singleVaultImpl() external view returns (address); - - function multiVaultImpl() external view returns (address); - - function initialize(address singleVaultImpl_, address admin, address timelock_) external; - - function createSingleVault( - IERC20 asset_, - string memory name_, - string memory symbol_, - address admin_, - uint256 minDelay_, - address[] memory proposers_, - address[] memory executors_ - ) external returns (address); - - function setVaultVersion(address implementation_, VaultType vaultType) external; -} diff --git a/src/module/ETHRateProvider.sol b/src/module/ETHRateProvider.sol new file mode 100644 index 0000000..da4d5fe --- /dev/null +++ b/src/module/ETHRateProvider.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.24; + +import {IRateProvider} from "src/interface/IRateProvider.sol"; +import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; + +interface IStETH { + function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256); +} + +interface IMETH { + function ratio() external view returns (uint256); +} + +interface IOETH { + function assetToEth(uint256 _assetAmount) external view returns (uint256); +} + +interface IRETH { + function getExchangeRate() external view returns (uint256); +} + +contract ETHRateProvider is IRateProvider, Ownable { + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address public constant STETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; + address public constant METH = 0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa; + address public constant OETH = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3; + address public constant RETH = 0xae78736Cd615f374D3085123A210448E74Fc6393; + + mapping(address => uint256) private _manualRates; + + error UnsupportedAsset(address asset); + + constructor() Ownable(msg.sender) {} + + function getRate(address asset) external view override returns (uint256) { + if (asset == WETH) { + return 1e18; // WETH is 1:1 with ETH + } else if (asset == STETH) { + return _getStETHRate(); + } else if (asset == METH) { + return _getMETHRate(); + } else if (asset == OETH) { + return _getOETHRate(); + } else if (asset == RETH) { + return _getRETHRate(); + } else if (_manualRates[asset] != 0) { + return _manualRates[asset]; + } + revert UnsupportedAsset(asset); + } + + function _getStETHRate() internal view returns (uint256) { + return IStETH(STETH).getPooledEthByShares(1e18); + } + + function _getMETHRate() internal view returns (uint256) { + return IMETH(METH).ratio(); + } + + function _getOETHRate() internal view returns (uint256) { + return IOETH(OETH).assetToEth(1e18); + } + + function _getRETHRate() internal view returns (uint256) { + return IRETH(RETH).getExchangeRate(); + } + + function setManualRate(address asset, uint256 rate) external onlyOwner { + _manualRates[asset] = rate; + } +} diff --git a/test/factory/create.t.sol b/test/factory/create.t.sol deleted file mode 100644 index fc092f4..0000000 --- a/test/factory/create.t.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.24; - -import "lib/forge-std/src/Test.sol"; -import {IERC20, ProxyAdmin} from "src/Common.sol"; -import {MockERC20} from "test/mocks/MockERC20.sol"; -import {LocalActors, IActors} from "script/Actors.sol"; -import {TestConstants} from "test/helpers/Constants.sol"; -import {SingleVault} from "src/SingleVault.sol"; -import {VaultFactory} from "src/VaultFactory.sol"; -import {DeployVaultFactory} from "script/Deploy.s.sol"; -import {Etches} from "test/helpers/Etches.sol"; - -contract CreateTest is Test, LocalActors, TestConstants { - VaultFactory public factory; - IERC20 public asset; - IActors public actors; - address[] proposers; - address[] executors; - - function setUp() public { - vm.startPrank(ADMIN); - asset = IERC20(address(new MockERC20(ASSET_NAME, ASSET_SYMBOL))); - actors = new LocalActors(); - - Etches etches = new Etches(); - etches.mockListaStakeManager(); - - proposers = [PROPOSER_1, PROPOSER_2]; - executors = [EXECUTOR_1, EXECUTOR_2]; - - DeployVaultFactory factoryDeployer = new DeployVaultFactory(); - factory = VaultFactory(factoryDeployer.deployVaultFactory(actors, 0)); - } - - function testCreateSingleVault() public { - asset.approve(address(factory), 1 ether); - asset.transfer(address(factory), 1 ether); - address vault = factory.createSingleVault(asset, VAULT_NAME, VAULT_SYMBOL, ADMIN, 0, proposers, executors); - (,, string memory symbol,) = factory.vaults(vault); - assertEq(symbol, VAULT_SYMBOL, "Vault timelock should match the expected address"); - } - - function testVaultFactoryAdmin() public view { - assertTrue(factory.hasRole(factory.DEFAULT_ADMIN_ROLE(), ADMIN)); - } - - function skip_testCreateSingleVaultRevertsIfNotAdmin() public { - vm.expectRevert(abi.encodeWithSelector(bytes4(keccak256("AccessControl: must have admin role")))); - factory.createSingleVault(asset, VAULT_NAME, VAULT_SYMBOL, ADMIN, 0, proposers, executors); - } -} diff --git a/test/helpers/Assets.sol b/test/helpers/Assets.sol deleted file mode 100644 index 0ada505..0000000 --- a/test/helpers/Assets.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.24; - -import {IERC20} from "src/Common.sol"; -import {BscContracts} from "script/Contracts.sol"; - -import "forge-std/Test.sol"; - -contract AssetHelper is Test, BscContracts { - address slisBNB_WHALE = 0x6F28FeC449dbd2056b76ac666350Af8773E03873; - - function get_slisBNB(address user, uint256 amount) public { - IERC20 slisBNB = IERC20(slisBNB); - vm.startPrank(slisBNB_WHALE); - slisBNB.approve(user, amount); - slisBNB.transfer(user, amount); - } -} diff --git a/test/helpers/Setup.sol b/test/helpers/Setup.sol deleted file mode 100644 index 5f35d1e..0000000 --- a/test/helpers/Setup.sol +++ /dev/null @@ -1,42 +0,0 @@ -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; -import {IActors, LocalActors} from "script/Actors.sol"; -import {DeployVaultFactory} from "script/Deploy.s.sol"; -import {SingleVault} from "src/SingleVault.sol"; -import {IVaultFactory} from "src/IVaultFactory.sol"; -import {IERC20} from "src/Common.sol"; -import {TestConstants} from "test/helpers/Constants.sol"; - -contract SetupHelper is Test, LocalActors, TestConstants { - IVaultFactory public factory; - - constructor() { - IActors actors = new LocalActors(); - DeployVaultFactory factoryDeployer = new DeployVaultFactory(); - factory = IVaultFactory(factoryDeployer.deployVaultFactory(actors, 0)); - } - - function createVault(IERC20 asset) public returns (SingleVault vault) { - vm.startPrank(ADMIN); - address[] memory proposers = new address[](2); - proposers[0] = PROPOSER_1; - proposers[1] = PROPOSER_2; - address[] memory executors = new address[](2); - executors[0] = EXECUTOR_1; - executors[1] = EXECUTOR_2; - - asset.approve(address(factory), 1 ether); - asset.transfer(address(factory), 1 ether); - address vaultAddress = factory.createSingleVault( - asset, - VAULT_NAME, - VAULT_SYMBOL, - ADMIN, - 0, // time delay - proposers, - executors - ); - vault = SingleVault(payable(vaultAddress)); - } -} diff --git a/test/mocks/MockSingleVault.sol b/test/mocks/MockSingleVault.sol deleted file mode 100644 index 4186af1..0000000 --- a/test/mocks/MockSingleVault.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.24; - -import {SingleVault} from "src/SingleVault.sol"; - -contract MockSingleVault is SingleVault { - constructor() { - _disableInitializers(); - } - - uint256 public constant R_TWO_D = 2; -} diff --git a/test/mocks/MockStETH.sol b/test/mocks/MockStETH.sol new file mode 100644 index 0000000..90e147a --- /dev/null +++ b/test/mocks/MockStETH.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockStETH is ERC20 { + uint256 private _pooledEthPerShare = 1e18; // Start with 1:1 ratio + + constructor() ERC20("Mock Staked Ether", "mstETH") { + _mint(msg.sender, 1000000 * 10 ** 18); // Mint 1 million tokens + } + + function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256) { + return (_sharesAmount * _pooledEthPerShare) / 1e18; + } + + // Function to simulate stETH price changes + function setPooledEthPerShare(uint256 newRatio) external { + _pooledEthPerShare = newRatio; + } +} diff --git a/test/mocks/MockWETH.sol b/test/mocks/MockWETH.sol new file mode 100644 index 0000000..4b47fc9 --- /dev/null +++ b/test/mocks/MockWETH.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.24; + +import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; + +contract MockWETH is ERC20 { + constructor() ERC20("Wrapped Ether", "WETH") { + _mint(msg.sender, 1000000 * 10 ** 18); // Mint 1 million WETH + } +} diff --git a/test/unit/deposit.t.sol b/test/unit/deposit.t.sol new file mode 100644 index 0000000..37d75b0 --- /dev/null +++ b/test/unit/deposit.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: BSD Clause-3 +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {Vault} from "src/Vault.sol"; +import {ETHRateProvider, ProxyAdmin, TransparentUpgradeableProxy, IVault} from "src/Common.sol"; +import {MockWETH} from "test/mocks/MockWETH.sol"; + +contract Vault_Deposit_Unit_Test is Test { + Vault public vaultImplementation; + TransparentUpgradeableProxy public vaultProxy; + + Vault public vault; + ProxyAdmin public proxyAdmin; + MockWETH public mockWETH; + ETHRateProvider public rateProvider; + + address public alice = address(0x1); + uint256 public constant INITIAL_BALANCE = 1000 * 10 ** 18; + + function setUp() public { + mockWETH = new MockWETH(); + vaultImplementation = new Vault(); + proxyAdmin = new ProxyAdmin(); + rateProvider = new ETHRateProvider(); + + // Deploy the proxy + bytes memory initData = abi.encodeWithSelector(Vault.initialize.selector, address(this), "Vault Token", "VTK"); + + vaultProxy = new TransparentUpgradeableProxy(address(vaultImplementation), address(proxyAdmin), initData); + + // Create a Vault interface pointing to the proxy + vault = Vault(address(vaultProxy)); + + // Set up the rate provider + vault.setRateProvider(address(rateProvider)); + + // Add WETH as an asset + vault.addAsset(address(mockWETH), 18); + + // Give Alice some tokens + mockWETH.transfer(alice, INITIAL_BALANCE); + + // Approve vault to spend Alice's tokens + vm.prank(alice); + mockWETH.approve(address(vault), type(uint256).max); + + // Unpause the vault + vault.pause(false); + } + + function testDeposit() public { + uint256 depositAmount = 100 * 10 ** 18; + + vm.prank(alice); + uint256 sharesMinted = vault.deposit(depositAmount, alice); + + // Check that shares were minted + assertGt(sharesMinted, 0, "No shares were minted"); + + // Check that the vault received the tokens + assertEq(mockWETH.balanceOf(address(vault)), depositAmount, "Vault did not receive tokens"); + + // Check that Alice's token balance decreased + assertEq( + mockWETH.balanceOf(alice), INITIAL_BALANCE - depositAmount, "Alice's balance did not decrease correctly" + ); + + // Check that Alice received the correct amount of shares + assertEq(vault.balanceOf(alice), sharesMinted, "Alice did not receive the correct amount of shares"); + + // Check that total assets increased + assertEq(vault.totalAssets(), depositAmount, "Total assets did not increase correctly"); + } + + function testDepositZeroAmount() public { + vm.prank(alice); + uint256 sharesMinted = vault.deposit(0, alice); + + assertEq(sharesMinted, 0, "Shares were minted for zero deposit"); + } + + function testDepositExceedsBalance() public { + uint256 excessiveAmount = INITIAL_BALANCE + 1; + + vm.prank(alice); + vm.expectRevert(); // Expect the transaction to revert + vault.deposit(excessiveAmount, alice); + } + + function testUpgrade() public { + // Deploy a new implementation + Vault newImplementation = new Vault(); + + // Upgrade the proxy to the new implementation + proxyAdmin.upgrade(TransparentUpgradeableProxy(payable(address(vaultProxy))), address(newImplementation)); + + // Verify the upgrade + assertEq( + proxyAdmin.getProxyImplementation(TransparentUpgradeableProxy(payable(address(vaultProxy)))), + address(newImplementation), + "Upgrade failed" + ); + } +}