From 414acebe19f1f63d9eab6e88037e0f1888384859 Mon Sep 17 00:00:00 2001 From: Ian Lucas Date: Sun, 26 Jan 2025 09:17:56 -0500 Subject: [PATCH 1/6] feat: Enhance LiquidStone: add Investor and Asset Receiver roles (#187). Add named ERC1155 tokens (#179) --- .../script/DeployLiquidMultiTokenVault.s.sol | 3 +- .../yield/LiquidContinuousMultiTokenVault.sol | 79 +++++++++++--- .../LiquidContinuousMultiTokenVaultTest.t.sol | 75 ++++++++++++- ...uidContinuousMultiTokenVaultUtilTest.t.sol | 103 ++++++++++-------- ...uidContinuousMultiTokenVaultTestBase.t.sol | 3 +- 5 files changed, 196 insertions(+), 67 deletions(-) diff --git a/packages/contracts/script/DeployLiquidMultiTokenVault.s.sol b/packages/contracts/script/DeployLiquidMultiTokenVault.s.sol index 7ddb0abf..cacb2c02 100644 --- a/packages/contracts/script/DeployLiquidMultiTokenVault.s.sol +++ b/packages/contracts/script/DeployLiquidMultiTokenVault.s.sol @@ -40,7 +40,8 @@ contract DeployLiquidMultiTokenVault is TomlConfig { owner: _tomlConfig.readAddress(".evm.address.owner"), operator: _tomlConfig.readAddress(".evm.address.operator"), upgrader: _tomlConfig.readAddress(".evm.address.upgrader"), - assetManager: _tomlConfig.readAddress(".evm.address.asset_manager") + assetManager: _tomlConfig.readAddress(".evm.address.asset_manager"), + assetReceiver: _tomlConfig.readAddress(".evm.address.custodian") }); } diff --git a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol index b799097d..fbacf6e7 100644 --- a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol +++ b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol @@ -49,6 +49,7 @@ contract LiquidContinuousMultiTokenVault is address operator; address upgrader; address assetManager; + address assetReceiver; } struct VaultParams { @@ -65,11 +66,16 @@ contract LiquidContinuousMultiTokenVault is IRedeemOptimizer public _redeemOptimizer; uint256 public _vaultStartTimestamp; + // [Jan-2025] added - must be after previous fields + bool public _shouldCheckInvestorRole = false; + uint256 private constant ZERO_REQUEST_ID = 0; bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); bytes32 public constant ASSET_MANAGER_ROLE = keccak256("ASSET_MANAGER_ROLE"); + bytes32 public constant ASSET_RECEIVER_ROLE = keccak256("ASSET_RECEIVER_ROLE"); + bytes32 public constant INVESTOR_ROLE = keccak256("INVESTOR_ROLE"); // deposits and redeems error LiquidContinuousMultiTokenVault__InvalidFrequency(uint256 frequency); error LiquidContinuousMultiTokenVault__InvalidAuthAddress(string authName, address authAddress); @@ -77,9 +83,9 @@ contract LiquidContinuousMultiTokenVault is error LiquidContinuousMultiTokenVault__UnAuthorized(address sender, address authorizedOwner); error LiquidContinuousMultiTokenVault__AmountMismatch(uint256 amount1, uint256 amount2); error LiquidContinuousMultiTokenVault__UnlockPeriodMismatch(uint256 unlockPeriod1, uint256 unlockPeriod2); - error LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount( - uint256 componentTokenAmount, uint256 unlockRequestedAmount - ); + error LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount( // TODO - fix naming + uint256 componentTokenAmount, uint256 unlockRequestedAmount); + error LiquidContinuousMultiTokenVault__InvestorOnly(address sender, address account); constructor() { _disableInitializers(); @@ -96,6 +102,7 @@ contract LiquidContinuousMultiTokenVault is _initRole("operator", OPERATOR_ROLE, vaultParams.vaultAuth.operator); _initRole("upgrader", UPGRADER_ROLE, vaultParams.vaultAuth.upgrader); _initRole("assetManager", ASSET_MANAGER_ROLE, vaultParams.vaultAuth.assetManager); + _initRole("assetReceiver", ASSET_RECEIVER_ROLE, vaultParams.vaultAuth.assetReceiver); _yieldStrategy = vaultParams.yieldStrategy; _redeemOptimizer = vaultParams.redeemOptimizer; @@ -119,6 +126,17 @@ contract LiquidContinuousMultiTokenVault is // ===================== MultiTokenVault ===================== + /// @inheritdoc MultiTokenVault + function deposit(uint256 assets, address receiver) + public + virtual + override + onlyInvestor(receiver) + returns (uint256 shares) + { + return super.deposit(assets, receiver); + } + /// @inheritdoc MultiTokenVault function convertToSharesForDepositPeriod(uint256 assets, uint256 /* depositPeriod */ ) public @@ -131,6 +149,18 @@ contract LiquidContinuousMultiTokenVault is return assets; // 1 asset = 1 share } + /// @inheritdoc MultiTokenVault + function redeemForDepositPeriod(uint256 shares, address receiver, address owner, uint256 depositPeriod) + public + virtual + override + onlyInvestor(owner) + onlyAuthorized(owner) + returns (uint256 assets) + { + return super.redeemForDepositPeriod(shares, receiver, owner, depositPeriod); + } + /// @inheritdoc MultiTokenVault function redeemForDepositPeriod( uint256 shares, @@ -138,7 +168,7 @@ contract LiquidContinuousMultiTokenVault is address owner, uint256 depositPeriod, uint256 redeemPeriod - ) public virtual override returns (uint256 assets) { + ) public virtual override onlyInvestor(owner) onlyAuthorized(owner) returns (uint256 assets) { _unlock(owner, depositPeriod, redeemPeriod, shares); return _redeemForDepositPeriodAfterUnlock(shares, receiver, owner, depositPeriod, redeemPeriod); @@ -182,6 +212,7 @@ contract LiquidContinuousMultiTokenVault is */ function requestDeposit(uint256 assets, address controller, address owner) public + onlyInvestor(owner) onlyAuthorized(owner) onlyController(controller) returns (uint256 requestId_) @@ -200,6 +231,7 @@ contract LiquidContinuousMultiTokenVault is */ function deposit(uint256 assets, address receiver, address controller) public + onlyInvestor(receiver) onlyController(controller) returns (uint256 shares_) { @@ -216,6 +248,7 @@ contract LiquidContinuousMultiTokenVault is */ function requestRedeem(uint256 shares, address controller, address owner) public + onlyInvestor(owner) onlyAuthorized(owner) onlyController(controller) returns (uint256 requestId_) @@ -237,6 +270,7 @@ contract LiquidContinuousMultiTokenVault is */ function redeem(uint256 shares, address receiver, address controller) public + onlyInvestor(controller) onlyController(controller) returns (uint256 assets) { @@ -249,15 +283,14 @@ contract LiquidContinuousMultiTokenVault is (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) = unlock(controller, requestId); // unlockPeriod = redeemPeriod - uint256 totalAssetsRedeemed = 0; - + assets = 0; for (uint256 i = 0; i < depositPeriods.length; ++i) { - totalAssetsRedeemed += _redeemForDepositPeriodAfterUnlock( + assets += _redeemForDepositPeriodAfterUnlock( sharesAtPeriods[i], receiver, controller, depositPeriods[i], requestId ); } - emit Withdraw(_msgSender(), receiver, controller, totalAssetsRedeemed, shares); - return totalAssetsRedeemed; + emit Withdraw(_msgSender(), receiver, controller, assets, shares); + return assets; } // Getter View Functions @@ -456,11 +489,13 @@ contract LiquidContinuousMultiTokenVault is * @dev Withdraws the assets from out of vault for investment, i.e. in RWA. * Only the Asset Manager can call this function. * - * @param to The trusted address that will receive the assets, e.g. custodian + * @param assetReceiver The trusted address that will receive the assets, e.g. custodian * @param amount The amount of the ERC-20 underlying assets to be withdrawn from the vault. */ - function withdrawAsset(address to, uint256 amount) public onlyRole(ASSET_MANAGER_ROLE) { - _withdrawAssest(to, amount); + function withdrawAsset(address assetReceiver, uint256 amount) public onlyRole(ASSET_MANAGER_ROLE) { + _checkRole(ASSET_RECEIVER_ROLE, assetReceiver); + + _withdrawAssest(assetReceiver, amount); } /** @@ -506,6 +541,16 @@ contract LiquidContinuousMultiTokenVault is // ===================== Utility ===================== + // @dev enable/disable investor role checking + function setShouldCheckInvestorRole(bool shouldCheckInvestorRole_) public virtual onlyRole(OPERATOR_ROLE) { + _shouldCheckInvestorRole = shouldCheckInvestorRole_; + } + + // @@inheritdoc ERC1155Upgradeable + function setURI(string memory newUri) public virtual onlyRole(OPERATOR_ROLE) { + _setURI(newUri); + } + /// minimum shares required to convert to assets and vice-versa. function _minConversionThreshold() internal view returns (uint256 minConversionThreshold) { return SCALE < 10 ? SCALE : 10; @@ -517,6 +562,14 @@ contract LiquidContinuousMultiTokenVault is _; } + // @dev ensure that the account has the INVESTOR_ROLE + modifier onlyInvestor(address account) { + if (_shouldCheckInvestorRole && !hasRole(INVESTOR_ROLE, account)) { + revert LiquidContinuousMultiTokenVault__InvestorOnly(_msgSender(), account); + } + _; + } + // @dev ensure the controller is the caller modifier onlyController(address controller) { address caller = _msgSender(); @@ -545,6 +598,6 @@ contract LiquidContinuousMultiTokenVault is } function getVersion() public pure returns (uint256 version) { - return 2; + return 3; } } diff --git a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol index 71ba72cb..3e9d3b92 100644 --- a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol +++ b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol @@ -164,6 +164,7 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT TestParamSet.TestParam memory testParams = TestParamSet.TestParam({ principal: 2_000 * _scale, depositPeriod: 10, redeemPeriod: 70 }); address assetManager = getAssetManager(); + address assetReceiver = _vaultAuth.assetReceiver; // ---------------- deposit ---------------- _warpToPeriod(liquidVault, testParams.depositPeriod); @@ -173,15 +174,15 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT liquidVault.requestDeposit(testParams.principal, alice, alice); vm.stopPrank(); - uint256 assetManagerStartBalance = _asset.balanceOf(assetManager); + uint256 receiverStartBalance = _asset.balanceOf(assetReceiver); uint256 vaultStartBalance = _asset.balanceOf(address(liquidVault)); - // ---------------- WithdrawAssetFrom Vault ---------------- + // ---------------- WithdrawAssetFrom Vault succeeds ---------------- vm.prank(assetManager); - liquidVault.withdrawAsset(assetManager, vaultStartBalance); + liquidVault.withdrawAsset(assetReceiver, vaultStartBalance); //assert balance - assertEq(assetManagerStartBalance + vaultStartBalance, _asset.balanceOf(assetManager)); + assertEq(receiverStartBalance + vaultStartBalance, _asset.balanceOf(assetReceiver)); } function test__LiquidContinuousMultiTokenVault__RedeemMultiPeriodsAllShares() public { @@ -269,14 +270,78 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT function test__LiquidContinuousMultiTokenVault__WithdrawAssetNotOwnerReverts() public { LiquidContinuousMultiTokenVault liquidVault = _liquidVault; address randomWallet = makeAddr("randomWallet"); + + // fail - wallet not the asset manager vm.startPrank(randomWallet); vm.expectRevert( abi.encodeWithSelector( IAccessControl.AccessControlUnauthorizedAccount.selector, randomWallet, liquidVault.ASSET_MANAGER_ROLE() ) ); - liquidVault.withdrawAsset(randomWallet, 1000); + liquidVault.withdrawAsset(_vaultAuth.assetReceiver, 1000); vm.stopPrank(); + + // fail - wallet not the asset receiver + vm.startPrank(_vaultAuth.assetManager); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + randomWallet, + liquidVault.ASSET_RECEIVER_ROLE() + ) + ); + liquidVault.withdrawAsset(randomWallet, 1000); + } + + function test__LiquidContinuousMultiTokenVault__AccountNotInvestorReverts() public { + TestParamSet.TestParam memory anyParam = TestParamSet.TestParam(100, 0, 10); + LiquidContinuousMultiTokenVault liquidVault = _liquidVault; + address investorWallet = makeAddr("investor"); + address nonInvestorWallet = makeAddr("nonInvestorWallet"); + address anyWallet = address(0); // any wallet is fine - not involved in validation + + // enable investor checking + vm.prank(_vaultAuth.operator); + _liquidVault.setShouldCheckInvestorRole(true); + + bytes memory investorOnlyError = abi.encodeWithSelector( + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__InvestorOnly.selector, + investorWallet, + nonInvestorWallet + ); + + // ======================= deposits ======================= + // function deposit(uint256 assets, address receiver) external returns (uint256 shares); + vm.prank(investorWallet); + vm.expectRevert(investorOnlyError); + liquidVault.deposit(anyParam.principal, nonInvestorWallet); // IMultiToken + + vm.prank(investorWallet); + vm.expectRevert(investorOnlyError); + liquidVault.requestDeposit(anyParam.principal, anyWallet, nonInvestorWallet); // IComponent + + vm.prank(investorWallet); + vm.expectRevert(investorOnlyError); + liquidVault.deposit(anyParam.principal, nonInvestorWallet, anyWallet); // IComponent + + // ======================= redeems ======================= + vm.prank(investorWallet); + vm.expectRevert(investorOnlyError); + liquidVault.redeemForDepositPeriod(anyParam.principal, anyWallet, nonInvestorWallet, anyParam.depositPeriod); + + vm.prank(investorWallet); + vm.expectRevert(investorOnlyError); + liquidVault.redeemForDepositPeriod( + anyParam.principal, anyWallet, nonInvestorWallet, anyParam.depositPeriod, anyParam.redeemPeriod + ); + + vm.prank(investorWallet); + vm.expectRevert(investorOnlyError); + liquidVault.requestRedeem(anyParam.principal, anyWallet, nonInvestorWallet); // IComponent + + vm.prank(investorWallet); + vm.expectRevert(investorOnlyError); + liquidVault.redeem(anyParam.principal, anyWallet, nonInvestorWallet); // IComponent } // Scenario: Calculating returns for a standard investment diff --git a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultUtilTest.t.sol b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultUtilTest.t.sol index 8699b703..303d1a6e 100644 --- a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultUtilTest.t.sol +++ b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultUtilTest.t.sol @@ -163,6 +163,12 @@ contract LiquidContinuousMultiTokenVaultUtilTest is LiquidContinuousMultiTokenVa vm.expectRevert(expectedError); _liquidVault.setReducedRateAtCurrent(newReducedRate); + vm.expectRevert(expectedError); + _liquidVault.setShouldCheckInvestorRole(true); + + vm.expectRevert(expectedError); + _liquidVault.setURI(""); + vm.stopPrank(); // ------------ operator can set ------------ @@ -181,6 +187,16 @@ contract LiquidContinuousMultiTokenVaultUtilTest is LiquidContinuousMultiTokenVa assertEq(newStartTimestamp, _liquidVault.currentPeriodRate().effectiveFromPeriod, "effective period not set"); assertEq(newReducedRate, _liquidVault.currentPeriodRate().interestRate, "rate not set"); + bool newShouldCheckInvestorRole = !_liquidVault._shouldCheckInvestorRole(); + _liquidVault.setShouldCheckInvestorRole(newShouldCheckInvestorRole); + assertEq( + newShouldCheckInvestorRole, _liquidVault._shouldCheckInvestorRole(), "shouldCheckInvestor role not set" + ); + + string memory newUri = "newurl.io/liquid-stone"; + _liquidVault.setURI(newUri); + assertEq(newUri, _liquidVault.uri(0), "uri not set"); + vm.stopPrank(); } @@ -236,64 +252,35 @@ contract LiquidContinuousMultiTokenVaultUtilTest is LiquidContinuousMultiTokenVa address zeroAddress = address(0); LiquidContinuousMultiTokenVault liquidVault = new LiquidContinuousMultiTokenVault(); - LiquidContinuousMultiTokenVault.VaultParams memory paramsZeroOperator = _createVaultParams( - LiquidContinuousMultiTokenVault.VaultAuth({ - owner: makeAddr("owner"), - operator: zeroAddress, - upgrader: makeAddr("upgrader"), - assetManager: makeAddr("assetManager") - }) - ); - - vm.expectRevert( - abi.encodeWithSelector( - LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__InvalidAuthAddress.selector, - "operator", - zeroAddress - ) - ); - + LiquidContinuousMultiTokenVault.VaultParams memory paramsZeroOperator = _createVaultParams(_createVaultAuth()); + paramsZeroOperator.vaultAuth.operator = zeroAddress; + vm.expectRevert(_createInvalidAuthError("operator", zeroAddress)); new ERC1967Proxy( address(liquidVault), abi.encodeWithSelector(liquidVault.initialize.selector, paramsZeroOperator) ); - LiquidContinuousMultiTokenVault.VaultParams memory paramsZeroUpgrader = _createVaultParams( - LiquidContinuousMultiTokenVault.VaultAuth({ - owner: makeAddr("owner"), - operator: makeAddr("operator"), - upgrader: zeroAddress, - assetManager: makeAddr("assetManager") - }) - ); - vm.expectRevert( - abi.encodeWithSelector( - LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__InvalidAuthAddress.selector, - "upgrader", - zeroAddress - ) - ); + LiquidContinuousMultiTokenVault.VaultParams memory paramsZeroUpgrader = _createVaultParams(_createVaultAuth()); + paramsZeroUpgrader.vaultAuth.upgrader = zeroAddress; + vm.expectRevert(_createInvalidAuthError("upgrader", zeroAddress)); new ERC1967Proxy( address(liquidVault), abi.encodeWithSelector(liquidVault.initialize.selector, paramsZeroUpgrader) ); - LiquidContinuousMultiTokenVault.VaultParams memory paramsZeroAssetManager = _createVaultParams( - LiquidContinuousMultiTokenVault.VaultAuth({ - owner: makeAddr("owner"), - operator: makeAddr("operator"), - upgrader: makeAddr("upgrader"), - assetManager: zeroAddress - }) - ); - vm.expectRevert( - abi.encodeWithSelector( - LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__InvalidAuthAddress.selector, - "assetManager", - zeroAddress - ) - ); + LiquidContinuousMultiTokenVault.VaultParams memory paramsZeroAssetManager = + _createVaultParams(_createVaultAuth()); + paramsZeroAssetManager.vaultAuth.assetManager = zeroAddress; + vm.expectRevert(_createInvalidAuthError("assetManager", zeroAddress)); new ERC1967Proxy( address(liquidVault), abi.encodeWithSelector(liquidVault.initialize.selector, paramsZeroAssetManager) ); + + LiquidContinuousMultiTokenVault.VaultParams memory paramsZeroAssetReceiver = + _createVaultParams(_createVaultAuth()); + paramsZeroAssetReceiver.vaultAuth.assetReceiver = zeroAddress; + vm.expectRevert(_createInvalidAuthError("assetReceiver", zeroAddress)); + new ERC1967Proxy( + address(liquidVault), abi.encodeWithSelector(liquidVault.initialize.selector, paramsZeroAssetReceiver) + ); } function test__LiquidContinuousMultiTokenVaultUtil__Metadata() public { @@ -309,6 +296,28 @@ contract LiquidContinuousMultiTokenVaultUtilTest is LiquidContinuousMultiTokenVa assertTrue(vault.getVersion() > 0, "version should be nonzero"); assertTrue(vault.supportsInterface(type(IMultiTokenVault).interfaceId), "should support interface"); } + + function _createInvalidAuthError(string memory authName, address authAddress) + internal + pure + returns (bytes memory authError) + { + return abi.encodeWithSelector( + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__InvalidAuthAddress.selector, + authName, + authAddress + ); + } + + function _createVaultAuth() internal returns (LiquidContinuousMultiTokenVault.VaultAuth memory vaultAuth) { + return LiquidContinuousMultiTokenVault.VaultAuth({ + owner: makeAddr("owner"), + operator: makeAddr("operator"), + upgrader: makeAddr("upgrader"), + assetManager: makeAddr("assetManager"), + assetReceiver: makeAddr("assetManager") + }); + } } contract LiquidContinuousMultiTokenVaultMock is LiquidContinuousMultiTokenVault { diff --git a/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol b/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol index 4a525b71..5ac0dbd2 100644 --- a/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol +++ b/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol @@ -25,7 +25,8 @@ abstract contract LiquidContinuousMultiTokenVaultTestBase is IMultiTokenVaultTes owner: makeAddr("owner"), operator: makeAddr("operator"), upgrader: makeAddr("upgrader"), - assetManager: makeAddr("assetManager") + assetManager: makeAddr("assetManager"), + assetReceiver: makeAddr("assetReceiver") }); IERC20Metadata internal _asset; From 83e4f25681ce93091911aa201001d2a4dec2985e Mon Sep 17 00:00:00 2001 From: Ian Lucas Date: Sun, 26 Jan 2025 09:43:06 -0500 Subject: [PATCH 2/6] chore: LiquidStone minor Error renaming for clarity --- .../src/yield/LiquidContinuousMultiTokenVault.sol | 7 +++---- .../src/yield/LiquidContinuousMultiTokenVaultTest.t.sol | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol index fbacf6e7..ee8234ab 100644 --- a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol +++ b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol @@ -66,7 +66,7 @@ contract LiquidContinuousMultiTokenVault is IRedeemOptimizer public _redeemOptimizer; uint256 public _vaultStartTimestamp; - // [Jan-2025] added - must be after previous fields + // [Jan-2025] added - must be after previous fields due to upgrading bool public _shouldCheckInvestorRole = false; uint256 private constant ZERO_REQUEST_ID = 0; @@ -83,8 +83,7 @@ contract LiquidContinuousMultiTokenVault is error LiquidContinuousMultiTokenVault__UnAuthorized(address sender, address authorizedOwner); error LiquidContinuousMultiTokenVault__AmountMismatch(uint256 amount1, uint256 amount2); error LiquidContinuousMultiTokenVault__UnlockPeriodMismatch(uint256 unlockPeriod1, uint256 unlockPeriod2); - error LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount( // TODO - fix naming - uint256 componentTokenAmount, uint256 unlockRequestedAmount); + error LiquidContinuousMultiTokenVault__RedeemSharesMismatch(uint256 redeemShares, uint256 requestRedeemShares); error LiquidContinuousMultiTokenVault__InvestorOnly(address sender, address account); constructor() { @@ -278,7 +277,7 @@ contract LiquidContinuousMultiTokenVault is uint256 unlockRequestedAmount = unlockRequestAmount(controller, requestId); if (shares != unlockRequestedAmount) { - revert LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount(shares, unlockRequestedAmount); + revert LiquidContinuousMultiTokenVault__RedeemSharesMismatch(shares, unlockRequestedAmount); } (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) = unlock(controller, requestId); // unlockPeriod = redeemPeriod diff --git a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol index 3e9d3b92..90385e67 100644 --- a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol +++ b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol @@ -574,7 +574,7 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT vm.prank(alice); vm.expectRevert( abi.encodeWithSelector( - LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount.selector, + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__RedeemSharesMismatch.selector, invalidRedeemShareAmount, sharesToRedeem ) @@ -586,7 +586,7 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT vm.prank(invalidCaller); vm.expectRevert( abi.encodeWithSelector( - LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount.selector, + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__RedeemSharesMismatch.selector, sharesToRedeem, 0 ) @@ -852,7 +852,7 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT // We expect revert in Alice's redeem because shares and ruquest unlocked amount for Alice are different vm.expectRevert( abi.encodeWithSelector( - LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount.selector, + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__RedeemSharesMismatch.selector, remainingShare_Alice, liquidVault.unlockRequestAmountByDepositPeriod(alice, testParams.depositPeriod) ) From c9a69d2d9faedb6249ce062d5f996b6dea9acecb Mon Sep 17 00:00:00 2001 From: Ian Lucas Date: Wed, 22 Jan 2025 06:23:11 -0500 Subject: [PATCH 3/6] chore: Use stable foundry version in github actions --- .github/workflows/ci-dev-contracts.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-dev-contracts.yml b/.github/workflows/ci-dev-contracts.yml index 2e42f614..ca84232a 100644 --- a/.github/workflows/ci-dev-contracts.yml +++ b/.github/workflows/ci-dev-contracts.yml @@ -27,7 +27,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: stable - name: Run tests run: forge test -vvv @@ -52,7 +52,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: stable # re-run the tests here on purpose to ensure quality. - name: Run tests From 80bfff9c6b288b2521e7eccbc8d4e0ef61fe8ebc Mon Sep 17 00:00:00 2001 From: Ian Lucas Date: Wed, 22 Jan 2025 07:14:34 -0500 Subject: [PATCH 4/6] Use stable foundry version in github actions --- .github/workflows/ci-dev-api.yml | 2 +- .github/workflows/ci-dev-app.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-dev-api.yml b/.github/workflows/ci-dev-api.yml index 27cffd46..eda5ffd7 100644 --- a/.github/workflows/ci-dev-api.yml +++ b/.github/workflows/ci-dev-api.yml @@ -38,7 +38,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: stable - name: Install dependencies run: yarn install diff --git a/.github/workflows/ci-dev-app.yml b/.github/workflows/ci-dev-app.yml index 80002fae..5873a575 100644 --- a/.github/workflows/ci-dev-app.yml +++ b/.github/workflows/ci-dev-app.yml @@ -40,7 +40,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: stable - name: Install Project dependencies run: yarn install From fdaf6855fe06cea86c759ac663f87e080994cfdb Mon Sep 17 00:00:00 2001 From: Ian Lucas Date: Mon, 27 Jan 2025 08:52:04 -0500 Subject: [PATCH 5/6] chore: LiquidStone pendingRedeemRequest to use controller --- .../contracts/src/yield/LiquidContinuousMultiTokenVault.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol index ee8234ab..ee72408a 100644 --- a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol +++ b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol @@ -391,8 +391,8 @@ contract LiquidContinuousMultiTokenVault is * @param requestId Discriminator between non-fungible requests * @return shares Amount of pending redeem shares for the given requestId and controller */ - function pendingRedeemRequest(uint256 requestId, address /* controller */ ) public view returns (uint256 shares) { - return unlockRequestAmount(_msgSender(), requestId); + function pendingRedeemRequest(uint256 requestId, address controller) public view returns (uint256 shares) { + return unlockRequestAmount(controller, requestId); } /** From 00e47062f26ce61fdfa1581e631c352cd0189bb2 Mon Sep 17 00:00:00 2001 From: Ian Lucas Date: Mon, 27 Jan 2025 10:03:02 -0500 Subject: [PATCH 6/6] chore: LiquidStone enables investor role checking by default --- .../script/DeployLiquidMultiTokenVault.s.sol | 3 ++- .../yield/LiquidContinuousMultiTokenVault.sol | 4 +++- .../test/src/LiquidStoneNinetyDayTest.t.sol | 1 + .../LiquidContinuousMultiTokenVaultTest.t.sol | 17 +++++++++++++---- ...iquidContinuousMultiTokenVaultUtilTest.t.sol | 5 +++++ .../DeployAndLoadLiquidMultiTokenVault.s.sol | 3 ++- ...iquidContinuousMultiTokenVaultTestBase.t.sol | 5 +++++ 7 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/contracts/script/DeployLiquidMultiTokenVault.s.sol b/packages/contracts/script/DeployLiquidMultiTokenVault.s.sol index cacb2c02..83be7d79 100644 --- a/packages/contracts/script/DeployLiquidMultiTokenVault.s.sol +++ b/packages/contracts/script/DeployLiquidMultiTokenVault.s.sol @@ -130,7 +130,8 @@ contract DeployLiquidMultiTokenVault is TomlConfig { redeemOptimizer: redeemOptimizer, vaultStartTimestamp: startTimestamp, redeemNoticePeriod: 1, - contextParams: contextParams + contextParams: contextParams, + shouldCheckInvestorRole: true }); return vaultParams; diff --git a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol index ee72408a..cd25abf8 100644 --- a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol +++ b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol @@ -60,6 +60,7 @@ contract LiquidContinuousMultiTokenVault is uint256 vaultStartTimestamp; uint256 redeemNoticePeriod; TripleRateContext.ContextParams contextParams; + bool shouldCheckInvestorRole; } IYieldStrategy public _yieldStrategy; @@ -67,7 +68,7 @@ contract LiquidContinuousMultiTokenVault is uint256 public _vaultStartTimestamp; // [Jan-2025] added - must be after previous fields due to upgrading - bool public _shouldCheckInvestorRole = false; + bool public _shouldCheckInvestorRole = true; uint256 private constant ZERO_REQUEST_ID = 0; @@ -106,6 +107,7 @@ contract LiquidContinuousMultiTokenVault is _yieldStrategy = vaultParams.yieldStrategy; _redeemOptimizer = vaultParams.redeemOptimizer; _vaultStartTimestamp = vaultParams.vaultStartTimestamp; + _shouldCheckInvestorRole = vaultParams.shouldCheckInvestorRole; if (vaultParams.contextParams.frequency != 360 && vaultParams.contextParams.frequency != 365) { revert LiquidContinuousMultiTokenVault__InvalidFrequency(vaultParams.contextParams.frequency); diff --git a/packages/contracts/test/src/LiquidStoneNinetyDayTest.t.sol b/packages/contracts/test/src/LiquidStoneNinetyDayTest.t.sol index f03e9fd5..32f5ada6 100644 --- a/packages/contracts/test/src/LiquidStoneNinetyDayTest.t.sol +++ b/packages/contracts/test/src/LiquidStoneNinetyDayTest.t.sol @@ -29,6 +29,7 @@ contract DeployLiquidStoneNinetyDay is DeployLiquidMultiTokenVault { vaultParams.redeemNoticePeriod = 0; vaultParams.contextParams.fullRateScaled = 10 * scale; vaultParams.contextParams.initialReducedRate.interestRate = 0; // zero for less than tenor + vaultParams.shouldCheckInvestorRole = false; return vaultParams; } diff --git a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol index 90385e67..4d654af6 100644 --- a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol +++ b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol @@ -300,7 +300,7 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT address nonInvestorWallet = makeAddr("nonInvestorWallet"); address anyWallet = address(0); // any wallet is fine - not involved in validation - // enable investor checking + // ensure the investor check is enabled vm.prank(_vaultAuth.operator); _liquidVault.setShouldCheckInvestorRole(true); @@ -310,8 +310,7 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT nonInvestorWallet ); - // ======================= deposits ======================= - // function deposit(uint256 assets, address receiver) external returns (uint256 shares); + // ======================= deposits fail ======================= vm.prank(investorWallet); vm.expectRevert(investorOnlyError); liquidVault.deposit(anyParam.principal, nonInvestorWallet); // IMultiToken @@ -324,7 +323,7 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT vm.expectRevert(investorOnlyError); liquidVault.deposit(anyParam.principal, nonInvestorWallet, anyWallet); // IComponent - // ======================= redeems ======================= + // ======================= redeems fail ======================= vm.prank(investorWallet); vm.expectRevert(investorOnlyError); liquidVault.redeemForDepositPeriod(anyParam.principal, anyWallet, nonInvestorWallet, anyParam.depositPeriod); @@ -342,6 +341,16 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT vm.prank(investorWallet); vm.expectRevert(investorOnlyError); liquidVault.redeem(anyParam.principal, anyWallet, nonInvestorWallet); // IComponent + + // ======================= grant access - succeeds ======================= + // grant investor role + vm.startPrank(_vaultAuth.owner); + _liquidVault.grantRole(_liquidVault.INVESTOR_ROLE(), nonInvestorWallet); + vm.stopPrank(); + + // deposit should succeed (no revert) + vm.prank(investorWallet); + liquidVault.deposit(0, nonInvestorWallet); // IMultiToken } // Scenario: Calculating returns for a standard investment diff --git a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultUtilTest.t.sol b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultUtilTest.t.sol index 303d1a6e..f4272d4e 100644 --- a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultUtilTest.t.sol +++ b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultUtilTest.t.sol @@ -36,6 +36,11 @@ contract LiquidContinuousMultiTokenVaultUtilTest is LiquidContinuousMultiTokenVa TestParamSet.TestParam memory testParams = TestParamSet.TestParam({ principal: 2_000 * scale, depositPeriod: 11, redeemPeriod: 71 }); + // whitelist alice as an investor + vm.startPrank(_vaultAuth.owner); + vaultProxy.grantRole(vaultProxy.INVESTOR_ROLE(), alice); + vm.stopPrank(); + _warpToPeriod(vaultProxy, testParams.depositPeriod); vm.startPrank(alice); diff --git a/packages/contracts/test/test/script/DeployAndLoadLiquidMultiTokenVault.s.sol b/packages/contracts/test/test/script/DeployAndLoadLiquidMultiTokenVault.s.sol index a8ea06e1..ae31e665 100644 --- a/packages/contracts/test/test/script/DeployAndLoadLiquidMultiTokenVault.s.sol +++ b/packages/contracts/test/test/script/DeployAndLoadLiquidMultiTokenVault.s.sol @@ -91,10 +91,11 @@ contract DeployAndLoadLiquidMultiTokenVault is DeployLiquidMultiTokenVault { IERC20 asset = IERC20(vault.asset()); uint256 scale = 10 ** IERC20Metadata(vault.asset()).decimals(); - // --------------------- gift user funds --------------------- + // --------------------- gift user funds & whitelist --------------------- vm.startBroadcast(_owner.key()); asset.transfer(userWallet.addr(), 1_000_000 * scale); + vault.grantRole(vault.INVESTOR_ROLE(), userWallet.addr()); vm.stopBroadcast(); // --------------------- load deposits --------------------- diff --git a/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol b/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol index 5ac0dbd2..2c090e8b 100644 --- a/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol +++ b/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol @@ -39,6 +39,11 @@ abstract contract LiquidContinuousMultiTokenVaultTestBase is IMultiTokenVaultTes DeployLiquidMultiTokenVault _deployVault = new DeployLiquidMultiTokenVault(); _liquidVault = _deployVault.run(_vaultAuth); + // disable the investor whitelist check - test users are already complex + // tested instead specifically in test__LiquidContinuousMultiTokenVault__AccountNotInvestorReverts + vm.prank(_vaultAuth.operator); + _liquidVault.setShouldCheckInvestorRole(false); + // warp to a "real time" time rather than block.timestamp=1 vm.warp(_liquidVault._vaultStartTimestamp() + 1);