From d860230ad3f867373400865182a8171f5a9b3f50 Mon Sep 17 00:00:00 2001 From: Ian Lucas Date: Tue, 29 Oct 2024 13:49:37 -0400 Subject: [PATCH] LiquidStone Product Audit Remediation (#154) LiquidStone audit remediation fixes --------- Co-authored-by: Ian Lucas Co-authored-by: Krishna kumar S Co-authored-by: Jonathan Lodge Co-authored-by: Nawar Hisso <101182851+nawar-hisso@users.noreply.github.com> Co-authored-by: Chai Somsri <60402458+ChaiSomsri96@users.noreply.github.com> --- .github/workflows/ci-dev-ops.yml | 6 +- .github/workflows/ci-dev-sdk.yml | 6 +- packages/contracts/package.json | 2 +- packages/contracts/resource/local.toml | 2 + packages/contracts/resource/mainnet.toml | 37 + .../script/DeployLiquidMultiTokenVault.s.sol | 17 +- .../src/timelock/ITimelockAsyncUnlock.sol | 7 + .../src/timelock/TimelockAsyncUnlock.sol | 25 +- .../src/yield/CalcInterestMetadata.sol | 7 + .../yield/LiquidContinuousMultiTokenVault.sol | 96 +- .../strategy/TripleRateYieldStrategy.sol | 2 +- .../timelock/TimelockAsyncUnlockTest.t.sol | 2 +- .../token/ERC1155/MultiTokenVaultTest.t.sol | 319 +- .../token/ERC1155/RedeemOptimizerTest.t.sol | 85 +- .../LiquidContinuousMultiTokenVaultTest.t.sol | 690 +- ...uidContinuousMultiTokenVaultUtilTest.t.sol | 37 +- ...ultipleRateYieldStrategyScenarioTest.t.sol | 2 - .../DeployAndLoadLiquidMultiTokenVault.s.sol | 77 +- ...ployAndLoadLiquidMultiTokenVaultTest.t.sol | 27 + .../timelock/SimpleTimelockAsyncUnlock.t.sol | 4 + .../ERC1155/IMultiTokenVaultTestBase.t.sol | 262 +- .../ERC1155/MultiTokenVaultDailyPeriods.t.sol | 14 +- .../test/token/ERC1155/TestParamSet.t.sol | 92 +- ...uidContinuousMultiTokenVaultTestBase.t.sol | 201 +- .../strategy/DualRateYieldStrategy.t.sol | 2 +- .../strategy/MultipleRateYieldStrategy.t.sol | 2 +- packages/sdk/test/src/liquid-vault.spec.ts | 103 + .../sdk/test/src/utils/liquid-vault.spec.ts | 61 - packages/sdk/test/src/utils/test-signer.ts | 29 +- .../sdk/test/src/utils/warp-anvil.spec.ts | 39 + spikes/spike-liquid-stone/package.json | 3 + .../app/async/_components/ViewSection.tsx | 63 +- .../packages/nextjs/app/async/page.tsx | 27 +- .../app/helpers/_components/ViewSection.tsx | 103 +- .../app/vault/_components/DepositPoolCard.tsx | 9 +- .../app/vault/_components/ViewSection.tsx | 63 +- .../nextjs/components/general/Button.tsx | 12 +- .../components/general/ContractValueBadge.tsx | 16 +- .../nextjs/contracts/deployedContracts.ts | 10042 ++++++++-------- .../hooks/async/useFetchRequestDetails.ts | 2 +- .../hooks/async/useFetchUnlockRequests.ts | 2 +- .../hooks/custom/useFetchContractData.ts | 41 +- .../hooks/custom/useFetchDepositPools.ts | 27 +- .../hooks/custom/useFetchRedeemRequests.ts | 37 +- .../packages/nextjs/types/vault.ts | 3 +- .../packages/nextjs/utils/vault/general.ts | 5 + .../packages/nextjs/utils/vault/web3.ts | 2 +- spikes/spike-liquid-stone/yarn.lock | 227 +- 48 files changed, 7495 insertions(+), 5444 deletions(-) create mode 100644 packages/contracts/resource/mainnet.toml create mode 100644 packages/contracts/test/test/script/DeployAndLoadLiquidMultiTokenVaultTest.t.sol create mode 100644 packages/sdk/test/src/liquid-vault.spec.ts delete mode 100644 packages/sdk/test/src/utils/liquid-vault.spec.ts create mode 100644 packages/sdk/test/src/utils/warp-anvil.spec.ts diff --git a/.github/workflows/ci-dev-ops.yml b/.github/workflows/ci-dev-ops.yml index 43d16827a..8058c6e1c 100644 --- a/.github/workflows/ci-dev-ops.yml +++ b/.github/workflows/ci-dev-ops.yml @@ -39,7 +39,11 @@ jobs: - name: Install foundry-toolchain uses: foundry-rs/foundry-toolchain@v1.2.0 with: - version: nightly + # using 2024-10-19 Nightly https://github.com/foundry-rs/foundry/tree/nightly-a8c3e9c1376122e7030dbe5c695b2f1f2a6f389b + # latest gives "deserialization error: duplicate field `status` at line 1 column 1085" + # see failure: https://github.com/credbull/credbull-defi/actions/runs/11576938626/job/32227114227 + version: nightly-a8c3e9c1376122e7030dbe5c695b2f1f2a6f389b + - name: Install Project Dependencies run: yarn install diff --git a/.github/workflows/ci-dev-sdk.yml b/.github/workflows/ci-dev-sdk.yml index 06f931e8e..4c5907c43 100644 --- a/.github/workflows/ci-dev-sdk.yml +++ b/.github/workflows/ci-dev-sdk.yml @@ -39,7 +39,11 @@ jobs: - name: Install foundry-toolchain uses: foundry-rs/foundry-toolchain@v1.2.0 with: - version: nightly + # using 2024-10-19 Nightly https://github.com/foundry-rs/foundry/tree/nightly-a8c3e9c1376122e7030dbe5c695b2f1f2a6f389b + # latest gives "deserialization error: duplicate field `status` at line 1 column 1085" + # see failure: https://github.com/credbull/credbull-defi/actions/runs/11576938626/job/32227114227 + version: nightly-a8c3e9c1376122e7030dbe5c695b2f1f2a6f389b + - name: Install Project Dependencies run: yarn install diff --git a/packages/contracts/package.json b/packages/contracts/package.json index fea1af27d..781b7f566 100755 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -10,7 +10,7 @@ "dev": "yarn rm-dbdata && anvil --config-out localhost.json & make deploy-local", "build": "forge build && yarn gen-types", "test": "forge test", - "coverage": "forge coverage --report lcov && genhtml lcov.info -o out/test-reports/coverage --ignore-errors inconsistent", + "coverage": "forge coverage --report lcov && genhtml lcov.info --branch-coverage -o out/test-reports/coverage --ignore-errors inconsistent", "format": "forge fmt && prettier './script/**/*.js' --write", "lint": "forge fmt && eslint --fix --ignore-path .gitignore && yarn solhint './*(test|src)/**/*.sol'", "db-check": "tsc && node ./script/utils/checkDb.js", diff --git a/packages/contracts/resource/local.toml b/packages/contracts/resource/local.toml index 624c10d88..8a8db4703 100644 --- a/packages/contracts/resource/local.toml +++ b/packages/contracts/resource/local.toml @@ -24,6 +24,8 @@ asset_manager = "0x976EA74026E726554dB657fA54763abd0C3a0aa9" full_rate_bps = 10_00 # rate in basis points, e.g. 5.5% = 550 bps reduced_rate_bps = 5_50 +# January 1, 2024 2:00:00 PM UTC = 1704117600 +vault_start_timestamp = 1704117600 [evm.contracts.upside_vault] # 2 decimal place percentage (meaining value divided by 100) as integer. diff --git a/packages/contracts/resource/mainnet.toml b/packages/contracts/resource/mainnet.toml new file mode 100644 index 000000000..8d39edd79 --- /dev/null +++ b/packages/contracts/resource/mainnet.toml @@ -0,0 +1,37 @@ +## +# The Application Configuration for the TestNet Environment. +## + +[evm] +# blockchain id, e.g. arbitrumOne=42161 +chain_id = 42161 +deploy_mocks = false + +[evm.address] +# PrimeVault wallets available ONLY on specific EVM chains, e.g.: Ethereum, Arbitrum, Optimism, Polygon +# primevault Wallet [0xFa..0519] "Credbull Eng - Owner Role v1.0" +owner = "0xFa0C9EB3fd284a87c82b9809162DefEa36070519" +# primevault Wallet [0x5F..bBf1] "Credbull Eng - Operator Role v1.0" +operator = "0x5FAbE5420116E061D0711D849DAA4788F0d4bBf1" +# primevault Wallet [0x1E..81b1] "Credbull Eng - Upgrader Role v1.0" +upgrader = "0x1E2D099F4681394B0c130e7cCcc3F5275eCa81b1" +# primevault Wallet [0x1D..7BFB] "Credbull Eng - Asset Manager Role v1.0" +asset_manager = "0x1DA51e4Ab5D029034Db2caB258EC4e22Be647BFB" +# primevault Wallet [0xce..3FB2] "Credbull DeFi Vault - Treasury v1.0" +custodian = "0xce694E94e1Ddb734f2bD32B2511D193fF2783FB2" + +[evm.contracts.liquid_continuous_multi_token_vault] +# rate in basis points, e.g. 10% = 1000 bps +full_rate_bps = 10_00 +# rate in basis points, e.g. 5.5% = 550 bps +reduced_rate_bps = 5_50 + +[evm.contracts.upside_vault] +# upside rate in basis points, e.g. 2% = 200 bps +collateral_percentage = 200 + +[services.supabase] +url = "" + +# Save the contract deployment details to the database. +update_contract_addresses = false diff --git a/packages/contracts/script/DeployLiquidMultiTokenVault.s.sol b/packages/contracts/script/DeployLiquidMultiTokenVault.s.sol index a4f57a92a..7f9e647b4 100644 --- a/packages/contracts/script/DeployLiquidMultiTokenVault.s.sol +++ b/packages/contracts/script/DeployLiquidMultiTokenVault.s.sol @@ -28,9 +28,10 @@ contract DeployLiquidMultiTokenVault is TomlConfig { using stdToml for string; string private _tomlConfig; - LiquidContinuousMultiTokenVault.VaultAuth public _vaultAuth; + LiquidContinuousMultiTokenVault.VaultAuth internal _vaultAuth; uint256 public constant NOTICE_PERIOD = 1; + string public constant CONTRACT_TOML_KEY = ".evm.contracts.liquid_continuous_multi_token_vault"; constructor() { _tomlConfig = loadTomlConfiguration(); @@ -103,11 +104,9 @@ contract DeployLiquidMultiTokenVault is TomlConfig { IYieldStrategy yieldStrategy, IRedeemOptimizer redeemOptimizer ) public view returns (LiquidContinuousMultiTokenVault.VaultParams memory vaultParams_) { - string memory contractKey = ".evm.contracts.liquid_continuous_multi_token_vault"; - uint256 fullRateBasisPoints = _tomlConfig.readUint(string.concat(contractKey, ".full_rate_bps")); - uint256 reducedRateBasisPoints = _tomlConfig.readUint(string.concat(contractKey, ".reduced_rate_bps")); - uint256 startTimestamp = - _readUintWithDefault(_tomlConfig, string.concat(contractKey, ".vault_start_timestamp"), block.timestamp); + uint256 fullRateBasisPoints = _tomlConfig.readUint(string.concat(CONTRACT_TOML_KEY, ".full_rate_bps")); + uint256 reducedRateBasisPoints = _tomlConfig.readUint(string.concat(CONTRACT_TOML_KEY, ".reduced_rate_bps")); + uint256 startTimestamp = _startTimestamp(); uint256 scale = 10 ** asset.decimals(); @@ -135,6 +134,12 @@ contract DeployLiquidMultiTokenVault is TomlConfig { return vaultParams; } + function _startTimestamp() internal view virtual returns (uint256 startTimestamp_) { + return _readUintWithDefault( + _tomlConfig, string.concat(CONTRACT_TOML_KEY, ".vault_start_timestamp"), block.timestamp + ); + } + function _usdcOrDeployMock(address contractOwner) internal returns (IERC20Metadata asset) { bool shouldDeployMocks = _readBoolWithDefault(_tomlConfig, ".evm.deploy_mocks", false); diff --git a/packages/contracts/src/timelock/ITimelockAsyncUnlock.sol b/packages/contracts/src/timelock/ITimelockAsyncUnlock.sol index 820510846..31832d67c 100644 --- a/packages/contracts/src/timelock/ITimelockAsyncUnlock.sol +++ b/packages/contracts/src/timelock/ITimelockAsyncUnlock.sol @@ -31,6 +31,13 @@ interface ITimelockAsyncUnlock { external returns (uint256[] memory depositPeriods, uint256[] memory amounts); + /** + * @notice Cancel a pending request to unlock + * @param owner Owner of the request + * @param requestId Discriminator between non-fungible requests + */ + function cancelRequestUnlock(address owner, uint256 requestId) external; + /** * @dev Return notice period */ diff --git a/packages/contracts/src/timelock/TimelockAsyncUnlock.sol b/packages/contracts/src/timelock/TimelockAsyncUnlock.sol index bda3e5bd9..0f08769bf 100644 --- a/packages/contracts/src/timelock/TimelockAsyncUnlock.sol +++ b/packages/contracts/src/timelock/TimelockAsyncUnlock.sol @@ -22,6 +22,8 @@ abstract contract TimelockAsyncUnlock is Initializable, ITimelockAsyncUnlock, Co // cache of user requested unlocks by depositPeriod across ALL requests. maps account => map(depositPeriod -> unlockAmount) mapping(address account => EnumerableMap.UintToUintMap) private _depositPeriodAmountCache; + event CancelRedeemRequest(address indexed owner, uint256 indexed requestId, address indexed sender); + error TimelockAsyncUnlock__AuthorizeCallerFailed(address caller, address owner); error TimelockAsyncUnlock__InvalidArrayLength(uint256 depositPeriodsLength, uint256 amountsLength); error TimelockAsyncUnlock__ExceededMaxRequestUnlock( @@ -33,7 +35,7 @@ abstract contract TimelockAsyncUnlock is Initializable, ITimelockAsyncUnlock, Co error TimelockAsyncUnlock__UnlockBeforeDepositPeriod( address caller, address owner, uint256 depositPeriod, uint256 unlockPeriod ); - error TimelockAsyncUnlock__UnlockBeforeUnlockPeriod( + error TimelockAsyncUnlock__UnlockBeforeCurrentPeriod( address caller, address owner, uint256 currentPeriod, uint256 unlockPeriod ); @@ -181,6 +183,10 @@ abstract contract TimelockAsyncUnlock is Initializable, ITimelockAsyncUnlock, Co { _authorizeCaller(_msgSender(), owner); + if (requestId > currentPeriod()) { + revert TimelockAsyncUnlock__UnlockBeforeCurrentPeriod(_msgSender(), owner, currentPeriod(), requestId); + } + // use copy of the depositPeriods and amounts. we will be altering the storage in _unlock() (depositPeriods, amounts) = unlockRequests(owner, requestId); @@ -200,7 +206,9 @@ abstract contract TimelockAsyncUnlock is Initializable, ITimelockAsyncUnlock, Co internal virtual { - _handleUnlockValidation(owner, depositPeriod, requestId); + if (requestId < depositPeriod) { + revert TimelockAsyncUnlock__UnlockBeforeDepositPeriod(_msgSender(), owner, depositPeriod, requestId); + } EnumerableMap.UintToUintMap storage unlockRequestsForRequestId = _unlockRequests[owner][requestId]; @@ -269,19 +277,6 @@ abstract contract TimelockAsyncUnlock is Initializable, ITimelockAsyncUnlock, Co } } - /** - * @dev An internal function to check if unlock can be performed - */ - function _handleUnlockValidation(address owner, uint256 depositPeriod, uint256 unlockPeriod) internal virtual { - if (unlockPeriod > currentPeriod()) { - revert TimelockAsyncUnlock__UnlockBeforeUnlockPeriod(_msgSender(), owner, currentPeriod(), unlockPeriod); - } - - if (unlockPeriod < depositPeriod) { - revert TimelockAsyncUnlock__UnlockBeforeDepositPeriod(_msgSender(), owner, depositPeriod, unlockPeriod); - } - } - function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { return interfaceId == type(ITimelockAsyncUnlock).interfaceId; } diff --git a/packages/contracts/src/yield/CalcInterestMetadata.sol b/packages/contracts/src/yield/CalcInterestMetadata.sol index 0188a7b11..afe161781 100644 --- a/packages/contracts/src/yield/CalcInterestMetadata.sol +++ b/packages/contracts/src/yield/CalcInterestMetadata.sol @@ -40,4 +40,11 @@ abstract contract CalcInterestMetadata is Initializable, ICalcInterestMetadata { function scale() public view virtual returns (uint256 scale_) { return SCALE; } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; } diff --git a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol index 6c6c37d69..61c2986f3 100644 --- a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol +++ b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol @@ -73,7 +73,8 @@ contract LiquidContinuousMultiTokenVault is error LiquidContinuousMultiTokenVault__InvalidFrequency(uint256 frequency); error LiquidContinuousMultiTokenVault__InvalidAuthAddress(string authName, address authAddress); - error LiquidContinuousMultiTokenVault__ControllerMismatch(address sender, address controller); + error LiquidContinuousMultiTokenVault__ControllerNotSender(address sender, address controller); + error LiquidContinuousMultiTokenVault__UnAuthorized(address sender, address authorizedOwner); error LiquidContinuousMultiTokenVault__AmountMismatch(uint256 amount1, uint256 amount2); error LiquidContinuousMultiTokenVault__UnlockPeriodMismatch(uint256 unlockPeriod1, uint256 unlockPeriod2); error LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount( @@ -125,7 +126,7 @@ contract LiquidContinuousMultiTokenVault is override returns (uint256 shares) { - if (assets < SCALE) return 0; // no shares for fractional principal + if (assets < _minConversionThreshold()) return 0; // no shares for small fractional assets return assets; // 1 asset = 1 share } @@ -161,7 +162,7 @@ contract LiquidContinuousMultiTokenVault is override returns (uint256 assets) { - if (shares < SCALE) return 0; // no assets for fractional shares + if (shares < _minConversionThreshold()) return 0; // no assets for small fractional shares if (redeemPeriod < depositPeriod) return 0; // trying to redeem before depositPeriod @@ -179,11 +180,12 @@ contract LiquidContinuousMultiTokenVault is * @param owner Source of the assets to deposit * @return requestId_ Discriminator between non-fungible requests */ - function requestDeposit(uint256 assets, address controller, address owner) public returns (uint256 requestId_) { - if (controller != _msgSender()) { - revert LiquidContinuousMultiTokenVault__ControllerMismatch(_msgSender(), controller); - } - + function requestDeposit(uint256 assets, address controller, address owner) + public + onlyAuthorized(owner) + onlyController(controller) + returns (uint256 requestId_) + { uint256 requestId = ZERO_REQUEST_ID; // requests and requestIds not used in buys. deposit(assets, owner, controller); @@ -196,11 +198,11 @@ contract LiquidContinuousMultiTokenVault is * @param assets Amount of `asset` that was deposited by `requestDeposit` * @param receiver Address to receive the shares */ - function deposit(uint256 assets, address receiver, address controller) public returns (uint256 shares_) { - if (controller != _msgSender()) { - revert LiquidContinuousMultiTokenVault__ControllerMismatch(_msgSender(), controller); - } - + function deposit(uint256 assets, address receiver, address controller) + public + onlyController(controller) + returns (uint256 shares_) + { uint256 shares = deposit(assets, receiver); emit Deposit(controller, receiver, assets, shares); return shares; @@ -212,16 +214,18 @@ contract LiquidContinuousMultiTokenVault is * @param owner Source of the shares to redeem * @return requestId_ Discriminator between non-fungible requests */ - function requestRedeem(uint256 shares, address, /* controller */ address owner) + function requestRedeem(uint256 shares, address controller, address owner) public + onlyAuthorized(owner) + onlyController(controller) returns (uint256 requestId_) { // using optimize() variant in case "shares" represents the IComponent "principal + yield" which is our "assets". (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) = _redeemOptimizer.optimize(this, owner, shares, shares, minUnlockPeriod()); - uint256 requestId = requestUnlock(_msgSender(), depositPeriods, sharesAtPeriods); - emit RedeemRequest(_msgSender(), owner, requestId, _msgSender(), shares); + uint256 requestId = requestUnlock(owner, depositPeriods, sharesAtPeriods); + emit RedeemRequest(controller, owner, requestId, _msgSender(), shares); return requestId; } @@ -229,25 +233,30 @@ contract LiquidContinuousMultiTokenVault is * @notice Fulfill a request to redeem assets by transferring assets to the receiver * @param shares Amount of shares that was redeemed by `requestRedeem` * @param receiver Address to receive the assets + * @dev controller will only have tokens to redeem if they are also the owner */ - function redeem(uint256 shares, address receiver, address /* controller */ ) public returns (uint256 assets) { + function redeem(uint256 shares, address receiver, address controller) + public + onlyController(controller) + returns (uint256 assets) + { uint256 requestId = currentPeriod(); // requestId = redeemPeriod, and redeem can only be called where redeemPeriod = currentPeriod() - uint256 unlockRequestedAmount = unlockRequestAmount(receiver, requestId); + uint256 unlockRequestedAmount = unlockRequestAmount(controller, requestId); if (shares != unlockRequestedAmount) { revert LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount(shares, unlockRequestedAmount); } - (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) = unlock(receiver, requestId); // unlockPeriod = redeemPeriod + (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) = unlock(controller, requestId); // unlockPeriod = redeemPeriod uint256 totalAssetsRedeemed = 0; for (uint256 i = 0; i < depositPeriods.length; ++i) { totalAssetsRedeemed += _redeemForDepositPeriodAfterUnlock( - sharesAtPeriods[i], receiver, _msgSender(), depositPeriods[i], requestId + sharesAtPeriods[i], receiver, controller, depositPeriods[i], requestId ); } - emit Withdraw(_msgSender(), receiver, _msgSender(), totalAssetsRedeemed, shares); + emit Withdraw(_msgSender(), receiver, controller, totalAssetsRedeemed, shares); return totalAssetsRedeemed; } @@ -367,6 +376,12 @@ contract LiquidContinuousMultiTokenVault is /// @dev yield based on the associated yieldStrategy function calcYield(uint256 principal, uint256 fromPeriod, uint256 toPeriod) public view returns (uint256 yield) { + // no yield earned when depositing and requesting redeem within the notice period. + // e.g. deposit day 1, immediately request redeem on day 1. should give 0 returns. + if (toPeriod <= fromPeriod + noticePeriod()) { + return 0; + } + return _yieldStrategy.calcYield(address(this), principal, fromPeriod, toPeriod); } @@ -383,6 +398,17 @@ contract LiquidContinuousMultiTokenVault is _depositForDepositPeriod(amount, account, depositPeriod); } + /// @dev Cancel a pending request to unlock + function cancelRequestUnlock(address owner, uint256 requestId) public onlyAuthorized(owner) { + (uint256[] memory depositPeriods, uint256[] memory amounts) = unlockRequests(owner, requestId); + + for (uint256 i = 0; i < depositPeriods.length; ++i) { + _unlock(owner, depositPeriods[i], requestId, amounts[i]); + } + + emit CancelRedeemRequest(owner, requestId, _msgSender()); + } + /// @inheritdoc TimelockAsyncUnlock function lockedAmount(address account, uint256 depositPeriod) public @@ -393,6 +419,13 @@ contract LiquidContinuousMultiTokenVault is return balanceOf(account, depositPeriod); } + /// @inheritdoc TimelockAsyncUnlock + function _authorizeCaller(address caller, address owner) internal virtual override { + if (caller != owner && !isApprovedForAll(owner, caller)) { + revert LiquidContinuousMultiTokenVault__UnAuthorized(caller, owner); + } + } + // ===================== TripleRateContext ===================== /// @inheritdoc TripleRateContext @@ -458,6 +491,27 @@ contract LiquidContinuousMultiTokenVault is // ===================== Utility ===================== + /// minimum shares required to convert to assets and vice-versa. + function _minConversionThreshold() internal view returns (uint256 minConversionThreshold) { + return SCALE < 10 ? SCALE : 10; + } + + // @dev ensure caller is permitted to act on the owner's tokens + modifier onlyAuthorized(address owner) { + _authorizeCaller(_msgSender(), owner); + _; + } + + // @dev ensure the controller is the caller + modifier onlyController(address controller) { + address caller = _msgSender(); + + if (caller != controller) { + revert LiquidContinuousMultiTokenVault__ControllerNotSender(caller, controller); + } + _; + } + function pause() public onlyRole(OPERATOR_ROLE) { _pause(); } diff --git a/packages/contracts/src/yield/strategy/TripleRateYieldStrategy.sol b/packages/contracts/src/yield/strategy/TripleRateYieldStrategy.sol index 73ea667e9..38c38c1fa 100644 --- a/packages/contracts/src/yield/strategy/TripleRateYieldStrategy.sol +++ b/packages/contracts/src/yield/strategy/TripleRateYieldStrategy.sol @@ -102,7 +102,7 @@ contract TripleRateYieldStrategy is AbstractYieldStrategy { ITripleRateContext context = ITripleRateContext(contextContract); return CalcSimpleInterest.calcPriceFromInterest( - numPeriodsElapsed, context.rateScaled(), context.frequency(), context.scale() + context.rateScaled(), numPeriodsElapsed, context.frequency(), context.scale() ); } diff --git a/packages/contracts/test/src/timelock/TimelockAsyncUnlockTest.t.sol b/packages/contracts/test/src/timelock/TimelockAsyncUnlockTest.t.sol index b293b4c87..60e72a68c 100644 --- a/packages/contracts/test/src/timelock/TimelockAsyncUnlockTest.t.sol +++ b/packages/contracts/test/src/timelock/TimelockAsyncUnlockTest.t.sol @@ -185,7 +185,7 @@ contract TimelockAsyncUnlockTest is Test { vm.expectRevert( abi.encodeWithSelector( - TimelockAsyncUnlock.TimelockAsyncUnlock__UnlockBeforeUnlockPeriod.selector, + TimelockAsyncUnlock.TimelockAsyncUnlock__UnlockBeforeCurrentPeriod.selector, alice, alice, asyncUnlock.currentPeriod(), diff --git a/packages/contracts/test/src/token/ERC1155/MultiTokenVaultTest.t.sol b/packages/contracts/test/src/token/ERC1155/MultiTokenVaultTest.t.sol index cbf83ec32..b4bc0b10e 100644 --- a/packages/contracts/test/src/token/ERC1155/MultiTokenVaultTest.t.sol +++ b/packages/contracts/test/src/token/ERC1155/MultiTokenVaultTest.t.sol @@ -9,7 +9,6 @@ import { MultiTokenVaultDailyPeriods } from "@test/test/token/ERC1155/MultiToken import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { SimpleUSDC } from "@test/test/token/SimpleUSDC.t.sol"; - import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; contract MultiTokenVaultTest is IMultiTokenVaultTestBase { @@ -39,8 +38,8 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { _testParams3 = TestParamSet.TestParam({ principal: 700 * _scale, depositPeriod: 30, redeemPeriod: 55 }); } - function test__MultiTokenVaulTest__SimpleDeposit() public { - uint256 assetToSharesRatio = 1; + function test__MultiTokenVaultTest__SimpleDeposit() public { + uint256 assetToSharesRatio = 2; IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); @@ -65,8 +64,33 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { testVaultAtOffsets(_alice, vault, _testParams1); } - function test__MultiTokenVaulTest__DepositAndRedeem() public { - uint256 assetToSharesRatio = 1; + function test__MultiTokenVaultTest__RevertWhen_DepositExceedsMax() public { + uint256 assetToSharesRatio = 2; + + MultiTokenVaultDailyPeriods vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + uint256 _maxDeposit = 250 * _scale; + vault.setMaxDeposit(_maxDeposit); + + address vaultAddress = address(vault); + vm.startPrank(_alice); + _asset.approve(vaultAddress, _testParams1.principal); + + // deposit amount > max deposit amount should fail. + vm.expectRevert( + abi.encodeWithSelector( + MultiTokenVault.MultiTokenVault__ExceededMaxDeposit.selector, + _alice, + vault.currentPeriodsElapsed(), + _testParams1.principal, + _maxDeposit + ) + ); + vault.deposit(_testParams1.principal, _alice); + vm.stopPrank(); + } + + function test__MultiTokenVaultTest__DepositAndRedeem() public { + uint256 assetToSharesRatio = 3; _transferAndAssert(_asset, _owner, _charlie, 100_000 * _scale); @@ -75,7 +99,7 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { testVaultAtOffsets(_charlie, vault, _testParams1); } - function test__MultiTokenVaulTest__RedeemBeforeDepositPeriodReverts() public { + function test__MultiTokenVaultTest__RedeemBeforeDepositPeriodReverts() public { MultiTokenVault vault = _createMultiTokenVault(_asset, 1, 10); TestParamSet.TestParam memory testParam = @@ -93,7 +117,7 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { vault.redeemForDepositPeriod(1, _alice, _alice, testParam.depositPeriod, testParam.redeemPeriod); } - function test__MultiTokenVaulTest__CurrentBeforeRedeemPeriodReverts() public { + function test__MultiTokenVaultTest__CurrentBeforeRedeemPeriodReverts() public { MultiTokenVault vault = _createMultiTokenVault(_asset, 1, 10); TestParamSet.TestParam memory testParam = @@ -114,7 +138,7 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { vault.redeemForDepositPeriod(1, _alice, _alice, testParam.depositPeriod, testParam.redeemPeriod); } - function test__MultiTokenVaulTest__RedeemOverMaxSharesReverts() public { + function test__MultiTokenVaultTest__RedeemOverMaxSharesReverts() public { MultiTokenVault vault = _createMultiTokenVault(_asset, 1, 10); TestParamSet.TestParam memory testParam = @@ -136,14 +160,16 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { vault.redeemForDepositPeriod(sharesToRedeem, _alice, _alice, testParam.depositPeriod, testParam.redeemPeriod); } - function test__MultiTokenVaulTest__MultipleDepositsAndRedeem() public { - uint256 assetToSharesRatio = 2; + function test__MultiTokenVaultTest__MultipleDepositsAndRedeem() public { + uint256 assetToSharesRatio = 4; // setup IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + TestParamSet.TestUsers memory testUsers = TestParamSet.toSingletonUsers(_alice); + // verify deposit - period 1 - uint256 deposit1Shares = _testDepositOnly(_alice, vault, _testParams1); + uint256 deposit1Shares = _testDepositOnly(testUsers, vault, _testParams1); assertEq(_testParams1.principal / assetToSharesRatio, deposit1Shares, "deposit shares incorrect at period 1"); assertEq( deposit1Shares, @@ -154,7 +180,7 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { assertEq(deposit1Shares, vault.balanceOf(_alice, _testParams1.depositPeriod), "balance incorrect at period 1"); // verify deposit - period 2 - uint256 deposit2Shares = _testDepositOnly(_alice, vault, _testParams2); + uint256 deposit2Shares = _testDepositOnly(testUsers, vault, _testParams2); assertEq(_testParams2.principal / assetToSharesRatio, deposit2Shares, "deposit shares incorrect at period 2"); assertEq( deposit2Shares, @@ -168,7 +194,7 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { // verify redeem - period 1 uint256 deposit1ExpectedYield = _expectedReturns(deposit1Shares, vault, _testParams1); - uint256 deposit1Assets = _testRedeemOnly(_alice, vault, _testParams1, deposit1Shares); + uint256 deposit1Assets = _testRedeemOnly(testUsers, vault, _testParams1, deposit1Shares); assertApproxEqAbs( _testParams1.principal + deposit1ExpectedYield, deposit1Assets, @@ -177,7 +203,7 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { ); // verify redeem - period 2 - uint256 deposit2Assets = _testRedeemOnly(_alice, vault, _testParams2, deposit2Shares); + uint256 deposit2Assets = _testRedeemOnly(testUsers, vault, _testParams2, deposit2Shares); assertApproxEqAbs( _testParams2.principal + _expectedReturns(deposit1Shares, vault, _testParams2), deposit2Assets, @@ -189,11 +215,55 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { testVaultAtOffsets(_alice, vault, _testParams2); } - function test__MultiTokenVaulTest__BatchFunctions() public { + function test__LiquidContinuousMultiTokenVaultUtil__RedeemWithAllowance() public { + uint256 assetToSharesRatio = 2; + + IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + + TestParamSet.TestParam memory testParams = + TestParamSet.TestParam({ principal: 100 * _scale, depositPeriod: 0, redeemPeriod: 10 }); + + vm.prank(_alice); + _asset.approve(address(vault), testParams.principal); // grant vault allowance on alice's principal + _transferFromTokenOwner(_asset, address(vault), testParams.principal); // transfer funds to cover redeem + + vm.prank(_alice); + uint256 shares = vault.deposit(testParams.principal, _alice); + + // ------------ redeem - without allowance ------------ + _warpToPeriod(vault, testParams.redeemPeriod); + + address allowanceAccount = makeAddr("allowanceAccount"); + + // should fail, no allowance given yet + vm.prank(allowanceAccount); + vm.expectRevert( + abi.encodeWithSelector( + MultiTokenVault.MultiTokenVault__CallerMissingApprovalForAll.selector, allowanceAccount, _alice + ) + ); + vault.redeemForDepositPeriod(shares, _alice, _alice, testParams.depositPeriod, testParams.redeemPeriod); + + // ------------ redeem - with allowance ------------ + vm.prank(_alice); + vault.setApprovalForAll(allowanceAccount, true); // grant allowance + + // should succeed - allowance granted + address receiverAccount = makeAddr("receiver"); + vm.prank(allowanceAccount); + uint256 assets = vault.redeemForDepositPeriod( + shares, receiverAccount, _alice, testParams.depositPeriod, testParams.redeemPeriod + ); + + assertEq(assets, _asset.balanceOf(receiverAccount), "receiver did not receive assets"); + } + + function test__MultiTokenVaultTest__BatchFunctions() public { uint256 assetToSharesRatio = 2; uint256 redeemPeriod = 2001; TestParamSet.TestParam[] memory _batchTestParams = new TestParamSet.TestParam[](3); + TestParamSet.TestParam[] memory _batchTestParamsToRevert = new TestParamSet.TestParam[](2); _batchTestParams[0] = TestParamSet.TestParam({ principal: 1001 * _scale, depositPeriod: 1, redeemPeriod: redeemPeriod }); @@ -202,14 +272,30 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { _batchTestParams[2] = TestParamSet.TestParam({ principal: 3003 * _scale, depositPeriod: 303, redeemPeriod: redeemPeriod }); + _batchTestParamsToRevert[0] = + TestParamSet.TestParam({ principal: 1001 * _scale, depositPeriod: 1, redeemPeriod: redeemPeriod }); + _batchTestParamsToRevert[1] = + TestParamSet.TestParam({ principal: 2002 * _scale, depositPeriod: 202, redeemPeriod: redeemPeriod }); + IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + TestParamSet.TestUsers memory testUsers = TestParamSet.toSingletonUsers(_alice); - uint256[] memory shares = _testDepositOnly(_alice, vault, _batchTestParams); + uint256[] memory shares = _testDepositOnly(testUsers, vault, _batchTestParams); uint256[] memory depositPeriods = _batchTestParams.depositPeriods(); + uint256[] memory depositPeriodsToRevert = _batchTestParamsToRevert.depositPeriods(); // ------------------------ batch convert to assets ------------------------ uint256[] memory assets = vault.convertToAssetsForDepositPeriodBatch(shares, depositPeriods, redeemPeriod); + vm.expectRevert( + abi.encodeWithSelector( + MultiTokenVault.MultiTokenVault__InvalidArrayLength.selector, + depositPeriodsToRevert.length, + shares.length + ) + ); + vault.convertToAssetsForDepositPeriodBatch(shares, depositPeriodsToRevert, redeemPeriod); + assertEq(3, assets.length, "assets are wrong length"); assertEq( assets[0], @@ -241,6 +327,205 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { _testBalanceOfBatch(_charlie, vault, _batchTestParams, assetToSharesRatio); // verify bob } + function test__MultiTokenVaultTest__ZeroOrOneAssetsShouldGiveZeroShares() public { + uint256 assetToSharesRatio = 2; + + TestParamSet.TestParam memory zeroPrincipal = + TestParamSet.TestParam({ principal: 0, depositPeriod: 10, redeemPeriod: 21 }); + TestParamSet.TestParam memory onePrincipal = + TestParamSet.TestParam({ principal: 1, depositPeriod: 10, redeemPeriod: 21 }); + + IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + + address vaultAddress = address(vault); + + vm.startPrank(_alice); + _asset.approve(vaultAddress, onePrincipal.principal); + + // ------------- test for deposit of 0 ------------- + vm.startPrank(_alice); + uint256 sharesFromZeroPrincipal = vault.deposit(zeroPrincipal.principal, _alice); + vm.stopPrank(); + + assertEq(zeroPrincipal.principal, _asset.balanceOf(vaultAddress), "vault should have the asset"); + assertEq(0, sharesFromZeroPrincipal, "deposit of zero assets should give zero shares"); + + // ------------- test for deposit of 1 ------------- + vm.startPrank(_alice); + uint256 sharesFromOnePrincipal = vault.deposit(onePrincipal.principal, _alice); + vm.stopPrank(); + + // 1 asset converts to 0 shares with any assetToShare ratio > 0. e.g.: 1 asset / 2 = 0 shares rounded down. + assertEq(onePrincipal.principal, _asset.balanceOf(vaultAddress), "vault should have the asset"); + assertEq(0, sharesFromOnePrincipal, "deposit of fractional assets should give zero shares"); + } + + function test__MultiTokenVaultTest__SafeTransferFrom() public { + uint256 assetToSharesRatio = 2; + + TestParamSet.TestParam memory testParams = + TestParamSet.TestParam({ principal: 100 * _scale, depositPeriod: 0, redeemPeriod: 10 }); + + // Step 1: Create and set up the vault + IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + + // Step 2: Deposit some assets for _alice to receive shares + vm.startPrank(_alice); + _asset.approve(address(vault), testParams.principal); + uint256 shares = vault.deposit(testParams.principal, _alice); + vm.stopPrank(); + + // Verify _alice has received the shares + assertEq( + vault.balanceOf(_alice, testParams.depositPeriod), shares, "_alice should have the shares after deposit" + ); + + // Step 3: Perform the safe transfer from _alice to _bob + vm.startPrank(_alice); + vault.safeTransferFrom(_alice, _bob, testParams.depositPeriod, shares, ""); + vm.stopPrank(); + + // Step 4: Verify the transfer was successful + assertEq(vault.balanceOf(_alice, testParams.depositPeriod), 0, "_alice should have no shares after transfer"); + assertEq(vault.balanceOf(_bob, testParams.depositPeriod), shares, "_bob should have the transferred shares"); + } + + function test__MultiTokenVaultTest__ConvertToSharesForDepositPeriod() public { + // Assuming the asset to shares ratio is set to a fixed value for testing. + uint256 assetToSharesRatio = 2; + uint256 depositPeriod = 1; + + // Step 1: Create and initialize the vault with a dummy asset + IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + + // Typical Case: Convert a positive asset amount to shares + uint256 assets = 500 * _scale; + uint256 expectedShares = assets / assetToSharesRatio; + uint256 shares = vault.convertToSharesForDepositPeriod(assets, depositPeriod); + assertEq(shares, expectedShares, "Conversion to shares did not match expected value"); + + // Edge Case: Convert zero assets to shares + assets = 0; + expectedShares = 0; + shares = vault.convertToSharesForDepositPeriod(assets, depositPeriod); + assertEq(shares, expectedShares, "Conversion of zero assets to shares failed"); + + // Edge Case: Convert maximum assets to shares + assets = type(uint256).max; + expectedShares = assets / assetToSharesRatio; + shares = vault.convertToSharesForDepositPeriod(assets, depositPeriod); + assertEq(shares, expectedShares, "Conversion of max assets to shares failed"); + } + + function test__MultiTokenVaultTest__ConvertToAssetsForDepositPeriod() public { + uint256 assetToSharesRatio = 2; + + TestParamSet.TestParam memory testParams = + TestParamSet.TestParam({ principal: 500 * _scale, depositPeriod: 0, redeemPeriod: 30 }); + + // Step 1: Create and initialize the vault with a dummy asset + IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + + // Typical Case: Convert a positive share amount to assets + uint256 shares = testParams.principal / assetToSharesRatio; + uint256 expectedAssets = shares * assetToSharesRatio + _expectedReturns(shares, vault, testParams); + uint256 assets = + vault.convertToAssetsForDepositPeriod(shares, testParams.depositPeriod, testParams.redeemPeriod); + assertEq(assets, expectedAssets, "Conversion to assets did not match expected value"); + + // Edge Case: Convert zero shares to assets + shares = 0; + expectedAssets = 0; + assets = vault.convertToAssetsForDepositPeriod(shares, testParams.depositPeriod, testParams.redeemPeriod); + assertEq(assets, expectedAssets, "Conversion of zero shares to assets failed"); + + // Edge Case: Convert maximum shares to assets + testParams = + TestParamSet.TestParam({ principal: type(uint128).max * _scale, depositPeriod: 0, redeemPeriod: 30 }); + shares = testParams.principal / assetToSharesRatio; + expectedAssets = shares * assetToSharesRatio + _expectedReturns(shares, vault, testParams); + assets = vault.convertToAssetsForDepositPeriod(shares, testParams.depositPeriod, testParams.redeemPeriod); + assertEq(assets, expectedAssets, "Conversion of max shares to assets failed"); + } + + function test__MultiTokenVaultTest__PreviewDeposit() public { + uint256 assetToSharesRatio = 2; + + TestParamSet.TestParam memory testParams = + TestParamSet.TestParam({ principal: 500 * _scale, depositPeriod: 0, redeemPeriod: 30 }); + + uint256 expectedShares = testParams.principal / assetToSharesRatio; + + IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + + uint256 shares = vault.previewDeposit(testParams.principal); + assertEq(shares, expectedShares, "Preview deposit conversion did not match expected value"); + } + + function test__MultiTokenVaultTest__PreviewRedeemForDepositPeriod() public { + uint256 assetToSharesRatio = 2; + + TestParamSet.TestParam memory testParams = + TestParamSet.TestParam({ principal: 500 * _scale, depositPeriod: 0, redeemPeriod: 30 }); + + IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + + uint256 shares = testParams.principal / assetToSharesRatio; + uint256 expectedAssets = shares * assetToSharesRatio + _expectedReturns(shares, vault, testParams); + + uint256 assets = vault.previewRedeemForDepositPeriod(shares, testParams.depositPeriod); + assertEq(assets, expectedAssets, "Preview redeem conversion did not match expected value"); + } + + function test__MultiTokenVaultTest__IsApprovedForAll() public { + address operator = makeAddr("operator"); + uint256 assetToSharesRatio = 2; + + IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + + // Set approval + vm.prank(_alice); + vault.setApprovalForAll(operator, true); + + // Check if the operator is approved + bool isApproved = vault.isApprovedForAll(_alice, operator); + assertEq(isApproved, true, "Operator should be approved"); + + // Revoke approval and check + vm.prank(_alice); + vault.setApprovalForAll(operator, false); + isApproved = vault.isApprovedForAll(_alice, operator); + assertEq(isApproved, false, "Operator should not be approved"); + } + + function test__MultiTokenVaultTest__MaxDeposit() public { + uint256 assetToSharesRatio = 2; + + IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + + uint256 maxDepositValue = vault.maxDeposit(_alice); + assertEq(maxDepositValue, type(uint256).max, "Max deposit should be uint256 max"); + } + + function test__MultiTokenVaultTest__MaxRedeemAtPeriod() public { + uint256 assetToSharesRatio = 2; + + TestParamSet.TestParam memory testParams = + TestParamSet.TestParam({ principal: 500 * _scale, depositPeriod: 0, redeemPeriod: 30 }); + + IMultiTokenVault vault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + + // Alice deposits assets and receives shares + vm.startPrank(_alice); + _asset.approve(address(vault), testParams.principal); + uint256 shares = vault.deposit(testParams.principal, _alice); + vm.stopPrank(); + + // Check max redeemable shares for the deposit period + uint256 maxShares = vault.maxRedeemAtPeriod(_alice, testParams.depositPeriod); // Assuming deposit period = 1 + assertEq(maxShares, shares, "Max redeemable shares did not match the deposited shares"); + } + function _testBalanceOfBatch( address account, IMultiTokenVault vault, @@ -261,6 +546,7 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { function _expectedReturns(uint256, /* shares */ IMultiTokenVault vault, TestParamSet.TestParam memory testParam) internal view + virtual override returns (uint256 expectedReturns_) { @@ -275,6 +561,7 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase { function _createMultiTokenVault(IERC20Metadata asset_, uint256 assetToSharesRatio, uint256 yieldPercentage) internal + virtual returns (MultiTokenVaultDailyPeriods) { MultiTokenVaultDailyPeriods _vault = new MultiTokenVaultDailyPeriods(); diff --git a/packages/contracts/test/src/token/ERC1155/RedeemOptimizerTest.t.sol b/packages/contracts/test/src/token/ERC1155/RedeemOptimizerTest.t.sol index 05bdfb07e..55ac3845c 100644 --- a/packages/contracts/test/src/token/ERC1155/RedeemOptimizerTest.t.sol +++ b/packages/contracts/test/src/token/ERC1155/RedeemOptimizerTest.t.sol @@ -4,24 +4,36 @@ pragma solidity ^0.8.20; import { IRedeemOptimizer } from "@credbull/token/ERC1155/IRedeemOptimizer.sol"; import { RedeemOptimizerFIFO } from "@credbull/token/ERC1155/RedeemOptimizerFIFO.sol"; import { IMultiTokenVault } from "@credbull/token/ERC1155/IMultiTokenVault.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import { MultiTokenVaultTest } from "@test/src/token/ERC1155/MultiTokenVaultTest.t.sol"; +import { IMultiTokenVaultTestBase } from "@test/test/token/ERC1155/IMultiTokenVaultTestBase.t.sol"; +import { MultiTokenVaultDailyPeriods } from "@test/test/token/ERC1155/MultiTokenVaultDailyPeriods.t.sol"; import { TestParamSet } from "@test/test/token/ERC1155/TestParamSet.t.sol"; +import { SimpleUSDC } from "@test/test/token/SimpleUSDC.t.sol"; -contract RedeemOptimizerTest is MultiTokenVaultTest { +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +contract RedeemOptimizerTest is IMultiTokenVaultTestBase { using TestParamSet for TestParamSet.TestParam[]; + IERC20Metadata internal _asset; + uint256 internal _scale; + address private _owner = makeAddr("owner"); address private _alice = makeAddr("alice"); TestParamSet.TestParam[] private testParams; - function setUp() public override { - super.setUp(); + function setUp() public { + vm.prank(_owner); + _asset = new SimpleUSDC(_owner, 1_000_000 ether); - testParams.push(_testParams1); - testParams.push(_testParams2); - testParams.push(_testParams3); + _scale = 10 ** _asset.decimals(); + _transferAndAssert(_asset, _owner, _alice, 100_000 * _scale); + + testParams.push(TestParamSet.TestParam({ principal: 500 * _scale, depositPeriod: 10, redeemPeriod: 21 })); + testParams.push(TestParamSet.TestParam({ principal: 300 * _scale, depositPeriod: 15, redeemPeriod: 17 })); + testParams.push(TestParamSet.TestParam({ principal: 700 * _scale, depositPeriod: 30, redeemPeriod: 55 })); } function test__RedeemOptimizerTest__RedeemAllShares() public { @@ -32,11 +44,12 @@ contract RedeemOptimizerTest is MultiTokenVaultTest { IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFO(IRedeemOptimizer.OptimizerBasis.Shares, multiTokenVault.currentPeriodsElapsed()); - uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParams); + uint256[] memory depositShares = + _testDepositOnly(TestParamSet.toSingletonUsers(_alice), multiTokenVault, testParams); uint256 totalDepositShares = depositShares[0] + depositShares[1] + depositShares[2]; // warp vault ahead to redeemPeriod - uint256 redeemPeriod = _testParams3.redeemPeriod; + uint256 redeemPeriod = testParams[2].redeemPeriod; _warpToPeriod(multiTokenVault, redeemPeriod); // check full redeem @@ -49,7 +62,7 @@ contract RedeemOptimizerTest is MultiTokenVaultTest { function test__RedeemOptimizerTest__WithdrawAllShares() public { uint256 assetToSharesRatio = 2; - uint256 redeemPeriod = _testParams3.redeemPeriod; + uint256 redeemPeriod = testParams[2].redeemPeriod; // setup IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); @@ -57,7 +70,8 @@ contract RedeemOptimizerTest is MultiTokenVaultTest { IRedeemOptimizer.OptimizerBasis.AssetsWithReturns, multiTokenVault.currentPeriodsElapsed() ); - uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParams); + uint256[] memory depositShares = + _testDepositOnly(TestParamSet.toSingletonUsers(_alice), multiTokenVault, testParams); uint256[] memory depositAssets = multiTokenVault.convertToAssetsForDepositPeriodBatch( depositShares, testParams.depositPeriods(), redeemPeriod ); @@ -77,14 +91,15 @@ contract RedeemOptimizerTest is MultiTokenVaultTest { function test__RedeemOptimizerTest__PartialRedeem() public { uint256 residualShareAmount = 1 * _scale; // leave 1 share after redeem - uint256 redeemPeriod = _testParams3.redeemPeriod; + uint256 redeemPeriod = testParams[2].redeemPeriod; // ---------------------- setup ---------------------- IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, 2, 10); IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFO( IRedeemOptimizer.OptimizerBasis.AssetsWithReturns, multiTokenVault.currentPeriodsElapsed() ); - uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParams); + uint256[] memory depositShares = + _testDepositOnly(TestParamSet.toSingletonUsers(_alice), multiTokenVault, testParams); uint256 sharesToWithdraw = depositShares[0] + depositShares[1] + depositShares[2] - residualShareAmount; @@ -107,7 +122,7 @@ contract RedeemOptimizerTest is MultiTokenVaultTest { function test__RedeemOptimizerTest__PartialWithdraw() public { uint256 residualShareAmount = 1 * _scale; // leave 1 share after redeem - uint256 redeemPeriod = _testParams3.redeemPeriod; + uint256 redeemPeriod = testParams[2].redeemPeriod; // ---------------------- setup ---------------------- IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, 2, 10); @@ -115,13 +130,14 @@ contract RedeemOptimizerTest is MultiTokenVaultTest { IRedeemOptimizer.OptimizerBasis.AssetsWithReturns, multiTokenVault.currentPeriodsElapsed() ); - uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParams); + uint256[] memory depositShares = + _testDepositOnly(TestParamSet.toSingletonUsers(_alice), multiTokenVault, testParams); uint256[] memory depositAssets = multiTokenVault.convertToAssetsForDepositPeriodBatch( depositShares, testParams.depositPeriods(), redeemPeriod ); uint256 residualAssetAmount = multiTokenVault.convertToAssetsForDepositPeriod( - residualShareAmount, _testParams3.depositPeriod, redeemPeriod + residualShareAmount, testParams[2].depositPeriod, redeemPeriod ); uint256 assetsToWithdraw = depositAssets[0] + depositAssets[1] + depositAssets[2] - residualAssetAmount; @@ -170,9 +186,11 @@ contract RedeemOptimizerTest is MultiTokenVaultTest { ); redeemOptimizer.optimizeRedeemShares(multiTokenVault, _alice, oneShare, vaultCurrentPeriod); + (TestParamSet.TestUsers memory depositUsers,) = _createTestUsers(_alice); + // shares to find greater than the deposits - uint256 deposit1Shares = _testDepositOnly(_alice, multiTokenVault, _testParams1); - uint256 deposit2Shares = _testDepositOnly(_alice, multiTokenVault, _testParams2); + uint256 deposit1Shares = _testDepositOnly(depositUsers, multiTokenVault, testParams[0]); + uint256 deposit2Shares = _testDepositOnly(depositUsers, multiTokenVault, testParams[1]); uint256 totalDepositShares = deposit1Shares + deposit2Shares; uint256 sharesGreaterThanDeposits = totalDepositShares + 1; @@ -230,6 +248,37 @@ contract RedeemOptimizerTest is MultiTokenVaultTest { }) ); } + + function _expectedReturns(uint256, /* shares */ IMultiTokenVault vault, TestParamSet.TestParam memory testParam) + internal + view + override + returns (uint256 expectedReturns_) + { + return MultiTokenVaultDailyPeriods(address(vault)).calcYield( + testParam.principal, testParam.depositPeriod, testParam.redeemPeriod + ); + } + + function _warpToPeriod(IMultiTokenVault vault, uint256 timePeriod) internal override { + MultiTokenVaultDailyPeriods(address(vault)).setCurrentPeriodsElapsed(timePeriod); + } + + function _createMultiTokenVault(IERC20Metadata asset_, uint256 assetToSharesRatio, uint256 yieldPercentage) + internal + returns (MultiTokenVaultDailyPeriods) + { + MultiTokenVaultDailyPeriods _vault = new MultiTokenVaultDailyPeriods(); + + return MultiTokenVaultDailyPeriods( + address( + new ERC1967Proxy( + address(_vault), + abi.encodeWithSelector(_vault.initialize.selector, asset_, assetToSharesRatio, yieldPercentage) + ) + ) + ); + } } contract RedeemOptimizerFIFOMock is RedeemOptimizerFIFO { diff --git a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol index b0e1525ce..56cd27d25 100644 --- a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol +++ b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.20; import { LiquidContinuousMultiTokenVault } from "@credbull/yield/LiquidContinuousMultiTokenVault.sol"; +import { TimelockAsyncUnlock } from "@credbull/timelock/TimelockAsyncUnlock.sol"; import { LiquidContinuousMultiTokenVaultTestBase } from "@test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol"; import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; @@ -10,31 +11,55 @@ import { TestParamSet } from "@test/test/token/ERC1155/TestParamSet.t.sol"; contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultTestBase { using TestParamSet for TestParamSet.TestParam[]; - function test__RequestRedeemTest__RedeemAtTenor() public { + function test__LiquidContinuousMultiTokenVault__SimpleDepositAndRedeem() public { + (TestParamSet.TestUsers memory depositUsers, TestParamSet.TestUsers memory redeemUsers) = + _createTestUsers(alice); + TestParamSet.TestParam[] memory testParams = new TestParamSet.TestParam[](1); + testParams[0] = TestParamSet.TestParam({ principal: 100 * _scale, depositPeriod: 0, redeemPeriod: 5 }); + + uint256[] memory sharesAtPeriods = _testDepositOnly(depositUsers, _liquidVault, testParams); + _testRedeemOnly(redeemUsers, _liquidVault, testParams, sharesAtPeriods); + } + + function test__LiquidContinuousMultiTokenVault__RedeemAtTenor() public { testVaultAtOffsets( alice, _liquidVault, - TestParamSet.TestParam({ principal: 100 * _scale, depositPeriod: 0, redeemPeriod: _liquidVault.TENOR() }) + TestParamSet.TestParam({ principal: 250 * _scale, depositPeriod: 0, redeemPeriod: _liquidVault.TENOR() }) ); } - function test__LiquidContinuousVaultTest__RedeemBeforeTenor() public { + function test__LiquidContinuousMultiTokenVault__RedeemBeforeTenor() public { testVaultAtOffsets( bob, _liquidVault, - TestParamSet.TestParam({ principal: 100 * _scale, depositPeriod: 0, redeemPeriod: _liquidVault.TENOR() }) + TestParamSet.TestParam({ + principal: 401 * _scale, + depositPeriod: 1, + redeemPeriod: (_liquidVault.TENOR() - 1) + }) ); } - function test__LiquidContinuousVaultTest__Load() public { + // @dev [Oct 25, 2024] Succeeds with: from=1 and to=600 ; Fails with: from=1 and to=650 + function test__LiquidContinuousMultiTokenVault__LoadTest() public { vm.skip(true); // load test - should only be run during perf testing + TestParamSet.TestParam[] memory loadTestParams = TestParamSet.toLoadSet(100_000 * _scale, 1, 600); - uint256 principal = 100_000 * _scale; + address carol = makeAddr("carol"); + _transferFromTokenOwner(_asset, carol, 1_000_000_000 * _scale); + (TestParamSet.TestUsers memory depositUsers1, TestParamSet.TestUsers memory redeemUsers1) = + _createTestUsers(carol); - _loadTestVault(_liquidVault, principal, 1, 1_000); // 1,000 works, 1800 too much for the vm + // ------------------- deposits w/ redeems per deposit ------------------- + // NB - test all of the deposits BEFORE redeems. verifies no side-effects from deposits when redeeming. + uint256[] memory sharesAtPeriods = _testDepositOnly(depositUsers1, _liquidVault, loadTestParams); + + // NB - test all of the redeems AFTER deposits. verifies no side-effects from deposits when redeeming. + _testRedeemOnly(redeemUsers1, _liquidVault, loadTestParams, sharesAtPeriods); } - function test__LiquidContinuousVaultTest__DepositRedeem() public { + function test__LiquidContinuousMultiTokenVault__DepositRedeem() public { LiquidContinuousMultiTokenVault liquidVault = _liquidVault; // _createLiquidContinueMultiTokenVault(_vaultParams); TestParamSet.TestParam memory testParams = @@ -63,12 +88,45 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT "user should have principal worth of vault shares" ); + // we process deposits immediately - therefore we don't have pending or claimable deposits + vm.startPrank(alice); + assertEq( + 0, + _liquidVault.pendingDepositRequest(testParams.depositPeriod, alice), + "deposits are processed immediately, not pending" + ); + assertEq( + 0, + _liquidVault.claimableDepositRequest(testParams.depositPeriod, alice), + "deposits are processed immediately, not claimable" + ); + assertEq( + 0, + _liquidVault.pendingRedeemRequest(testParams.redeemPeriod, alice), + "there shouldn't be any pending requestRedeems" + ); + assertEq( + 0, + _liquidVault.claimableRedeemRequest(testParams.redeemPeriod, alice), + "there shouldn't be any claimable redeems" + ); + vm.stopPrank(); + // ---------------- requestRedeem ---------------- _warpToPeriod(liquidVault, testParams.redeemPeriod - liquidVault.noticePeriod()); - // requestSell + // requestRedeem + vm.prank(alice); + uint256 requestId = liquidVault.requestRedeem(sharesAmount, alice, alice); + assertEq(requestId, testParams.redeemPeriod, "requestId should be the redeemPeriod"); + vm.prank(alice); - liquidVault.requestRedeem(sharesAmount, alice, alice); + assertEq( + sharesAmount, + _liquidVault.pendingRedeemRequest(testParams.redeemPeriod, alice), + "pending request redeem amount not correct" + ); + assertEq( sharesAmount, liquidVault.unlockRequestAmountByDepositPeriod(alice, testParams.depositPeriod), @@ -82,6 +140,13 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT _warpToPeriod(liquidVault, testParams.redeemPeriod); + vm.prank(alice); + assertEq( + sharesAmount, + _liquidVault.claimableRedeemRequest(testParams.redeemPeriod, alice), + "claimable redeem amount not correct" + ); + vm.prank(alice); liquidVault.redeem(testParams.principal, alice, alice); @@ -93,7 +158,7 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT ); } - function test__LiquidContinuousVaultTest__WithdrawAssetFromVault() public { + function test__LiquidContinuousMultiTokenVault__WithdrawAssetFromVault() public { LiquidContinuousMultiTokenVault liquidVault = _liquidVault; TestParamSet.TestParam memory testParams = @@ -119,7 +184,7 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT assertEq(assetManagerStartBalance + vaultStartBalance, _asset.balanceOf(assetManager)); } - function test__LiquidContinuousVaultTest__RedeemMultiPeriodsAllShares() public { + function test__LiquidContinuousMultiTokenVault__RedeemMultiPeriodsAllShares() public { uint256 depositPeriods = 5; TestParamSet.TestParam[] memory depositTestParams = new TestParamSet.TestParam[](depositPeriods); @@ -133,30 +198,33 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT }); } - _testDepositOnly(alice, _liquidVault, depositTestParams); + (TestParamSet.TestUsers memory depositUsers, TestParamSet.TestUsers memory redeemUsers) = + _createTestUsers(alice); - // ------------ requestRedeem #1 ----------- - uint256 redeemPeriod1 = 31; - TestParamSet.TestParam[] memory redeemParams1 = depositTestParams._split(0, 2); + _testDepositOnly(depositUsers, _liquidVault, depositTestParams); + + // split our deposits into two "batches" of redeems + (TestParamSet.TestParam[] memory redeemParams1, TestParamSet.TestParam[] memory redeemParams2) = + depositTestParams._splitBefore(3); assertEq(3, redeemParams1.length, "array not split 1"); + assertEq(2, redeemParams2.length, "array not split 2"); - _testRequestRedeemMultiDeposit(alice, _liquidVault, redeemParams1, 31); + // ------------ requestRedeem #1 ----------- + uint256 redeemPeriod1 = 31; + _testRequestRedeemMultiDeposit(redeemUsers, _liquidVault, redeemParams1, 31); // ------------ requestRedeem #2 ------------ uint256 redeemPeriod2 = 41; - TestParamSet.TestParam[] memory redeemParams2 = depositTestParams._split(3, 4); - assertEq(2, redeemParams2.length, "array not split 2"); - - _testRequestRedeemMultiDeposit(alice, _liquidVault, redeemParams2, 41); + _testRequestRedeemMultiDeposit(redeemUsers, _liquidVault, redeemParams2, 41); // ------------ redeems ------------ // NB - call the redeem AFTER the multiple requestRedeems. verify multiple requestRedeems work. - _testRedeemMultiDeposit(alice, _liquidVault, redeemParams1, redeemPeriod1); + _testRedeemAfterRequestRedeemMultiDeposit(redeemUsers, _liquidVault, redeemParams1, redeemPeriod1); - _testRedeemMultiDeposit(alice, _liquidVault, redeemParams2, redeemPeriod2); + _testRedeemAfterRequestRedeemMultiDeposit(redeemUsers, _liquidVault, redeemParams2, redeemPeriod2); } - function test__LiquidContinuousVaultTest__RedeemMultiPeriodsPartialShares() public { + function test__LiquidContinuousMultiTokenVault__RedeemMultiPeriodsPartialShares() public { uint256 depositPeriods = 5; TestParamSet.TestParam[] memory depositTestParams = new TestParamSet.TestParam[](depositPeriods); @@ -170,36 +238,35 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT }); } - _testDepositOnly(alice, _liquidVault, depositTestParams); + (TestParamSet.TestUsers memory depositUsers, TestParamSet.TestUsers memory redeemUsers) = _createTestUsers(bob); + + _testDepositOnly(depositUsers, _liquidVault, depositTestParams); uint256 partialShares = 1 * _scale; // ------------ requestRedeem #1 ------------ uint256 redeemPeriod1 = 30; - - TestParamSet.TestParam[] memory redeemParams1 = depositTestParams._split(0, 2); + TestParamSet.TestParam[] memory redeemParams1 = depositTestParams._subset(0, 2); redeemParams1[2].principal = partialShares; - _testRequestRedeemMultiDeposit(alice, _liquidVault, redeemParams1, redeemPeriod1); + _testRequestRedeemMultiDeposit(redeemUsers, _liquidVault, redeemParams1, redeemPeriod1); // ------------ requestRedeem #2 ------------ uint256 redeemPeriod2 = 50; - - TestParamSet.TestParam[] memory redeemParams2 = depositTestParams._split(2, 4); + TestParamSet.TestParam[] memory redeemParams2 = depositTestParams._subset(2, 4); redeemParams2[0].principal = (depositTestParams[2].principal - partialShares); redeemParams2[2].principal = partialShares; - _testRequestRedeemMultiDeposit(alice, _liquidVault, redeemParams2, redeemPeriod2); + _testRequestRedeemMultiDeposit(redeemUsers, _liquidVault, redeemParams2, redeemPeriod2); // ------------ redeems ------------ // NB - call the redeem AFTER the multiple requestRedeems. verify multiple requestRedeems work. + _testRedeemAfterRequestRedeemMultiDeposit(redeemUsers, _liquidVault, redeemParams1, redeemPeriod1); - _testRedeemMultiDeposit(alice, _liquidVault, redeemParams1, redeemPeriod1); - - _testRedeemMultiDeposit(alice, _liquidVault, redeemParams2, redeemPeriod2); + _testRedeemAfterRequestRedeemMultiDeposit(redeemUsers, _liquidVault, redeemParams2, redeemPeriod2); } - function test__LiquidContinuousVaultTest__ShouldRevertWithdrawAssetIfNotOwner() public { + function test__LiquidContinuousMultiTokenVault__WithdrawAssetNotOwnerReverts() public { LiquidContinuousMultiTokenVault liquidVault = _liquidVault; address randomWallet = makeAddr("randomWallet"); vm.startPrank(randomWallet); @@ -213,7 +280,7 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT } // Scenario: Calculating returns for a standard investment - function test__LiquidContinuousVaultTest__50k_Returns() public view { + function test__LiquidContinuousMultiTokenVault__50k_Returns() public view { uint256 deposit = 50_000 * _scale; // verify returns @@ -225,4 +292,555 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT uint256 actualReturns = _liquidVault.convertToAssetsForDepositPeriod(actualShares, 0, _liquidVault.TENOR()); assertEq(50_416_666666, actualReturns, "principal + interest not correct for $50k deposit after 30 days"); } + + function test__LiquidContinuousMultiTokenVault__TotalAssetsAndConvertToAssets() public { + uint256 depositPeriod1 = 5; + uint256 depositPeriod2 = depositPeriod1 + 1; + uint256 redeemPeriod = 10; + TestParamSet.TestParam[] memory testParams = new TestParamSet.TestParam[](2); + testParams[0] = TestParamSet.TestParam({ + principal: 50 * _scale, + depositPeriod: depositPeriod1, + redeemPeriod: redeemPeriod + }); + testParams[1] = TestParamSet.TestParam({ + principal: 80 * _scale, + depositPeriod: depositPeriod2, + redeemPeriod: redeemPeriod + }); + + uint256[] memory shares = _testDepositOnly(TestParamSet.toSingletonUsers(alice), _liquidVault, testParams); + uint256 totalShares = shares[0] + shares[1]; + + // -------------- deposit period1 -------------- + _warpToPeriod(_liquidVault, depositPeriod1); + + uint256 assetsAtDepositPeriod1 = + _liquidVault.convertToAssetsForDepositPeriod(shares[0], testParams[0].depositPeriod, depositPeriod1); + + vm.prank(alice); + assertEq(assetsAtDepositPeriod1, _liquidVault.convertToAssets(shares[0]), "assets wrong at deposit period 1"); + + // -------------- deposit period2 -------------- + _warpToPeriod(_liquidVault, depositPeriod2); + + uint256 assetsAtDepositPeriod2 = + _liquidVault.convertToAssetsForDepositPeriod(shares[1], testParams[1].depositPeriod, depositPeriod2); + + vm.prank(alice); + assertEq(assetsAtDepositPeriod2, _liquidVault.convertToAssets(shares[1]), "assets wrong at deposit period 2"); + assertEq( + assetsAtDepositPeriod1 + assetsAtDepositPeriod2, + _liquidVault.totalAssets(), + "totalAssets wrong at deposit period 2" + ); + + // -------------- requestRedeem period -------------- + uint256 requestRedeemPeriod = redeemPeriod - _liquidVault.noticePeriod(); + _warpToPeriod(_liquidVault, requestRedeemPeriod); + + uint256 assetsAtRequestRedeemPeriod = _liquidVault.convertToAssetsForDepositPeriod( + shares[0], testParams[0].depositPeriod, requestRedeemPeriod + ) + _liquidVault.convertToAssetsForDepositPeriod(shares[1], testParams[1].depositPeriod, requestRedeemPeriod); + + vm.prank(alice); + assertEq( + assetsAtRequestRedeemPeriod, + _liquidVault.convertToAssets(totalShares), + "assets wrong at requestRedeem period" + ); + + assertEq(assetsAtRequestRedeemPeriod, _liquidVault.totalAssets(), "totalAssets wrong at requestRedeem period"); + + // -------------- redeem period -------------- + _warpToPeriod(_liquidVault, redeemPeriod); + + uint256 assetsAtRedeemPeriod = _liquidVault.convertToAssetsForDepositPeriod( + shares[0], testParams[0].depositPeriod, redeemPeriod + ) + _liquidVault.convertToAssetsForDepositPeriod(shares[1], testParams[1].depositPeriod, redeemPeriod); + + vm.prank(alice); + assertEq(assetsAtRedeemPeriod, _liquidVault.convertToAssets(totalShares), "assets wrong at redeem period"); + + assertEq(assetsAtRedeemPeriod, _liquidVault.totalAssets(), "totalAssets wrong at redeem period"); + } + + function test__LiquidContinuousMultiTokenVault__DepositCallerValidation() public { + address randomController = makeAddr("randomController"); + + // ---------------- request deposit ---------------- + vm.prank(bob); + vm.expectRevert( + abi.encodeWithSelector( + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__UnAuthorized.selector, bob, alice + ) + ); + _liquidVault.requestDeposit(1 * _scale, bob, alice); + + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector( + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__ControllerNotSender.selector, + alice, + randomController + ) + ); + _liquidVault.requestDeposit(1 * _scale, randomController, alice); + + // ---------------- deposit ---------------- + + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector( + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__ControllerNotSender.selector, + alice, + randomController + ) + ); + _liquidVault.deposit(1 * _scale, alice, randomController); + } + + function test__LiquidContinuousMultiTokenVault__RedeemCallerValidation() public { + address randomController = makeAddr("randomController"); + + // ---------------- request redeem ---------------- + vm.prank(bob); + vm.expectRevert( + abi.encodeWithSelector( + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__UnAuthorized.selector, bob, alice + ) + ); + _liquidVault.requestRedeem(1 * _scale, bob, alice); + + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector( + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__ControllerNotSender.selector, + alice, + randomController + ) + ); + _liquidVault.requestRedeem(1 * _scale, randomController, alice); + + // ---------------- redeem ---------------- + + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector( + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__ControllerNotSender.selector, + alice, + randomController + ) + ); + _liquidVault.redeem(1 * _scale, makeAddr("receiver"), randomController); + } + + function test__LiquidContinuousMultiTokenVault__FractionalAssetsGivesZeroShares() public view { + uint256 fractionalAssets = 9; // 9 wei - tiny amount + uint256 depositPeriod = 1; + + assertEq( + 0, + _liquidVault.convertToSharesForDepositPeriod(fractionalAssets, depositPeriod), + "zero shares for fractional assets" + ); + } + + function test__LiquidContinuousMultiTokenVault__FractionalSharesGivesZeroAssets() public view { + uint256 fractionalShares = 9; // 9 wei - tiny amount + uint256 depositPeriod = 2; + uint256 redeemPeriod = depositPeriod; + + assertEq( + 0, + _liquidVault.convertToAssetsForDepositPeriod(fractionalShares, depositPeriod, redeemPeriod), + "zero assets for fractional shares" + ); + } + + function test__LiquidContinuousMultiTokenVault__RedeemBeforeDepositPeriodGivesZeroAssets() public view { + uint256 shares = 100 * _scale; + uint256 depositPeriod = 3; + uint256 redeemPeriod = depositPeriod - 1; + + assertEq( + 0, + _liquidVault.convertToAssetsForDepositPeriod(shares, depositPeriod, redeemPeriod), + "zero assets when redeemPeriod < depositPeriod" + ); + } + + function test__LiquidContinuousMultiTokenVault__RedeemMustBeRequestedAmountAndAuthorized() public { + uint256 redeemPeriod = 10; + uint256 depositPeriod = 2; + + TestParamSet.TestParam memory testParam = TestParamSet.TestParam({ + principal: 100 * _scale, + depositPeriod: depositPeriod, + redeemPeriod: redeemPeriod + }); + + // deposit + uint256 shares = _testDepositOnly(TestParamSet.toSingletonUsers(alice), _liquidVault, testParam); + + // request redeem + _warpToPeriod(_liquidVault, redeemPeriod - _liquidVault.noticePeriod()); + + uint256 sharesToRedeem = shares / 2; // redeem say half our shares + + vm.prank(alice); + _liquidVault.requestRedeem(sharesToRedeem, alice, alice); + + // redeem should fail - requestRedeemAmount != redeemAmount + _warpToPeriod(_liquidVault, redeemPeriod); + + uint256 invalidRedeemShareAmount = sharesToRedeem - 1; + + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector( + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount.selector, + invalidRedeemShareAmount, + sharesToRedeem + ) + ); + _liquidVault.redeem(invalidRedeemShareAmount, alice, alice); + + // redeem should fail - caller doesn't have any tokens to redeem + address invalidCaller = makeAddr("randomCaller"); + vm.prank(invalidCaller); + vm.expectRevert( + abi.encodeWithSelector( + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount.selector, + sharesToRedeem, + 0 + ) + ); + _liquidVault.redeem(sharesToRedeem, alice, invalidCaller); + + // redeem should succeed + address receiver = makeAddr("receiver"); + vm.prank(alice); + uint256 assets = _liquidVault.redeem(sharesToRedeem, receiver, alice); + assertEq(assets, _asset.balanceOf(receiver), "redeem should succeed"); + } + + function test__LiquidContinuousMultiTokenVault__RedeemFractional() public { + uint256 redeemPeriod = 10; + uint256 depositPeriod = 2; + + uint256 principalIntegerPart = 5 * _scale; // e.g. 5 ETH + uint256 principalDecimalPart = 10; // e.g. 10 wei + + TestParamSet.TestParam memory testParam = TestParamSet.TestParam({ + principal: principalIntegerPart + principalDecimalPart, + depositPeriod: depositPeriod, + redeemPeriod: redeemPeriod + }); + + TestParamSet.TestUsers memory aliceTestUsers = TestParamSet.toSingletonUsers(alice); + + // deposit + uint256 shares = _testDepositOnly(aliceTestUsers, _liquidVault, testParam); + + assertEq(shares, testParam.principal, "shares should be 1:1 with principal"); // liquid vault should be 1:1 + + // ----------------- redeem principal first ----------------- + TestParamSet.TestParam memory integerTestParam = TestParamSet.TestParam({ + principal: principalIntegerPart, + depositPeriod: depositPeriod, + redeemPeriod: redeemPeriod + }); + + uint256 assetIntegerPart = _testRedeemOnly(aliceTestUsers, _liquidVault, integerTestParam, principalIntegerPart); + assertLe( + principalIntegerPart, assetIntegerPart, "principal + returns should be at least principal integer amount" + ); + + // ----------------- redeem decimal second ----------------- + TestParamSet.TestParam memory decimalTestParam = TestParamSet.TestParam({ + principal: principalDecimalPart, + depositPeriod: depositPeriod, + redeemPeriod: redeemPeriod + }); + + uint256 assetDecimalPart = _testRedeemOnly(aliceTestUsers, _liquidVault, decimalTestParam, principalDecimalPart); + assertLe( + principalDecimalPart, assetDecimalPart, "principal + returns should be at least principal decimal amount" + ); + } + + // ================== F-2024-6700 ================== + /** + * Scenario + * 1. Alice deposits assets at the deposit period. + * 2. Alice requests redeem to withdraw his assets from the vault. + * 3. Alice wants to cancel his redeem request before the redeem period. + */ + function test__LiquidContinuousMultiTokenVault__CancelUnlockRequest__BeforeRedeemPeriod() public { + LiquidContinuousMultiTokenVault liquidVault = _liquidVault; + + TestParamSet.TestParam memory testParams = + TestParamSet.TestParam({ principal: 2_000 * _scale, depositPeriod: 10, redeemPeriod: 70 }); + + uint256 sharesAmount = testParams.principal; + + _warpToPeriod(liquidVault, testParams.depositPeriod); + + vm.startPrank(alice); + _asset.approve(address(liquidVault), testParams.principal); + liquidVault.requestDeposit(testParams.principal, alice, alice); + vm.stopPrank(); + + _warpToPeriod(liquidVault, testParams.redeemPeriod - liquidVault.noticePeriod()); + + // Alice requests redeem for sharesAmount + vm.prank(alice); + uint256 requestId = liquidVault.requestRedeem(sharesAmount, alice, alice); + + assertEq(requestId, testParams.redeemPeriod, "requestId should be the redeemPeriod"); + + // When alice calls unlock before redeem period, it will revert + vm.expectRevert( + abi.encodeWithSelector( + TimelockAsyncUnlock.TimelockAsyncUnlock__UnlockBeforeCurrentPeriod.selector, + alice, + alice, + liquidVault.currentPeriod(), + testParams.redeemPeriod + ) + ); + vm.prank(alice); + liquidVault.unlock(alice, requestId); + + // Alice cancels his redeem request and it works even before the redeem period. + vm.prank(alice); + liquidVault.cancelRequestUnlock(alice, requestId); + + assertEq( + 0, _liquidVault.pendingRedeemRequest(requestId, alice), "there shouldn't be any pending requestRedeems" + ); + + assertEq(0, _liquidVault.claimableRedeemRequest(requestId, alice), "there shouldn't be any claimable redeems"); + + // Alice calls this function again, but nothing happens. + vm.prank(alice); + liquidVault.cancelRequestUnlock(alice, requestId); + } + + /** + * Scenario + * 1. Alice deposits assets at the deposit period. + * 2. Alice requests to redeem for [sharesAmount] + * 3. Alice wants to decrease his redeem request amount to [sharesAmount / 2] before redeem Period + */ + function test__LiquidContinuousMultiTokenVault__ModifyUnlockRequest__BeforeRedeemPeriod() public { + LiquidContinuousMultiTokenVault liquidVault = _liquidVault; + + TestParamSet.TestParam memory testParams = + TestParamSet.TestParam({ principal: 2_000 * _scale, depositPeriod: 10, redeemPeriod: 70 }); + + uint256 sharesAmount = testParams.principal; + + _warpToPeriod(liquidVault, testParams.depositPeriod); + + vm.startPrank(alice); + _asset.approve(address(liquidVault), testParams.principal); + liquidVault.requestDeposit(testParams.principal, alice, alice); + vm.stopPrank(); + + _warpToPeriod(liquidVault, testParams.redeemPeriod - liquidVault.noticePeriod()); + + vm.prank(alice); + uint256 requestId = liquidVault.requestRedeem(sharesAmount, alice, alice); + + // Alice cancels his redeem request first + vm.startPrank(alice); + liquidVault.cancelRequestUnlock(alice, requestId); + + // Alice submits a redeem request again with the amount = [sharesAmount / 2]. + requestId = liquidVault.requestRedeem(sharesAmount / 2, alice, alice); + vm.stopPrank(); + + vm.prank(alice); + assertEq( + sharesAmount / 2, + _liquidVault.pendingRedeemRequest(requestId, alice), + "pending request redeem amount not correct" + ); + + assertEq( + sharesAmount / 2, + liquidVault.unlockRequestAmountByDepositPeriod(alice, testParams.depositPeriod), + "unlockRequest should be created" + ); + } + + /** + * Scenario + * 1. Alice deposits assets at the deposit period. + * 2. Alice requests to redeem for [sharesAmount_1_Alice] + * 3. Alice transfers [sharesAmount_1_David] shares to David + * 4. David requests reedeem for sharesAmount_1_David + * 5. Alice makes another request redeems for [sharesAmount_2_Alice] + * 6. Alice transfers another amount[sharesAmount_2_David] of shares to David + * 7. David makes another redeem request for [sharesAmount_2_David] + * 8. Alice cancels his redeem request (because redeem will fail) + * 9. Alice makes new redeem request at redeem period + * 10.David redeems his shares which already requested at redeem period + */ + function test__LiquidContinuousMultiTokenVault__ModifyUnlockRequest__Sdsed() public { + LiquidContinuousMultiTokenVault liquidVault = _liquidVault; + + TestParamSet.TestParam memory testParams = + TestParamSet.TestParam({ principal: 2_000 * _scale, depositPeriod: 10, redeemPeriod: 70 }); + + uint256 sharesAmount_1_Alice = testParams.principal / 2; + + _warpToPeriod(liquidVault, testParams.depositPeriod); + + vm.startPrank(alice); + _asset.approve(address(liquidVault), testParams.principal); + liquidVault.requestDeposit(testParams.principal, alice, alice); + vm.stopPrank(); + + _warpToPeriod(liquidVault, testParams.redeemPeriod - liquidVault.noticePeriod()); + + // Alice requests redeem for sharesAmount_1_Alice + vm.prank(alice); + uint256 requestId = liquidVault.requestRedeem(sharesAmount_1_Alice, alice, alice); + + address david = makeAddr("david"); + + uint256 sharesAmount_1_David = sharesAmount_1_Alice / 2; + + // Alice transfers [sharesAmount_1_David] shares to David + vm.prank(alice); + liquidVault.safeTransferFrom(alice, david, testParams.depositPeriod, sharesAmount_1_David, ""); + + assertEq(sharesAmount_1_David, liquidVault.balanceOf(david, testParams.depositPeriod)); + assertEq(testParams.principal - sharesAmount_1_David, liquidVault.lockedAmount(alice, testParams.depositPeriod)); + + // David requests reedeem for sharesAmount_1_David + vm.startPrank(david); + liquidVault.requestRedeem(sharesAmount_1_David, david, david); + + assertEq( + sharesAmount_1_David, + _liquidVault.pendingRedeemRequest(requestId, david), + "david pending request redeem amount not correct" + ); + vm.stopPrank(); + + // Alice makes another request redeems + uint256 sharesAmount_2_Alice = testParams.principal - sharesAmount_1_Alice - sharesAmount_1_David; + + vm.startPrank(alice); + liquidVault.requestRedeem(sharesAmount_2_Alice, alice, alice); + + assertEq( + sharesAmount_1_Alice + sharesAmount_2_Alice, + _liquidVault.pendingRedeemRequest(requestId, alice), + "alice pending request redeem amount not correct" + ); + vm.stopPrank(); + + // Alice transfers another amount of shares to David + // amount = (sharesAmount_1_Alice + sharesAmount_2_Alice) / 2 + + uint256 sharesAmount_2_David = (sharesAmount_1_Alice + sharesAmount_2_Alice) / 2; + vm.prank(alice); + liquidVault.safeTransferFrom(alice, david, testParams.depositPeriod, sharesAmount_2_David, ""); + + assertEq(sharesAmount_1_David + sharesAmount_2_David, liquidVault.balanceOf(david, testParams.depositPeriod)); + + uint256 remainingShare_Alice = testParams.principal - sharesAmount_1_David - sharesAmount_2_David; + assertEq(remainingShare_Alice, liquidVault.balanceOf(alice, testParams.depositPeriod)); + assertTrue( + remainingShare_Alice < liquidVault.unlockRequestAmountByDepositPeriod(alice, testParams.depositPeriod), + "Alice requested unlock amount should be bigger than locked amount" + ); + + // David makes another redeem request + vm.startPrank(david); + liquidVault.requestRedeem(sharesAmount_2_David, david, david); + + assertEq( + sharesAmount_1_David + sharesAmount_2_David, + _liquidVault.pendingRedeemRequest(requestId, david), + "david pending request redeem amount not correct" + ); + vm.stopPrank(); + + _warpToPeriod(liquidVault, testParams.redeemPeriod); + + // 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, + remainingShare_Alice, + liquidVault.unlockRequestAmountByDepositPeriod(alice, testParams.depositPeriod) + ) + ); + vm.prank(alice); + liquidVault.redeem(remainingShare_Alice, alice, alice); + + // Alice cancels his redeem request + // Alice can use either cancelRedeemRequest or unlock + vm.prank(alice); + liquidVault.cancelRequestUnlock(alice, requestId); + // Alice makes another unlock request + vm.prank(alice); + liquidVault.requestRedeem(remainingShare_Alice, alice, alice); + + // David redeems his shares + vm.startPrank(david); + liquidVault.redeem(sharesAmount_1_David + sharesAmount_2_David, david, david); + + assertEq( + 0, _liquidVault.pendingRedeemRequest(requestId, david), "david pending request redeem amount should be zero" + ); + + assertEq(0, liquidVault.balanceOf(david, testParams.depositPeriod), "david should have no shares remaining"); + vm.stopPrank(); + } + + // Scenario: User requests redemption before the cutoff time on the same day as deposit + // Given the Redemption Request cutoff time is 2:59:59pm + // And the Redemption Settlement cutoff time is 2:59:59pm the next day + // And Alice deposits 100 USDC on Day 1 at 2:59:58pm + // When Alice requests full redemption on Day 1 at 2:59:58pm + // Then Alice's redemption should be settled on Day 2 at 2:59:59pm + // And Alice should not receive any yield just the pincipal (100 USDC) + function test__LiquidContinuousMultiTokenVault__DepositAndRedeemAtCutOffs() public { + uint256 principal = 10 * _scale; + + deal(address(_asset), address(_liquidVault), 100e6); + + // ----------------- deposit ------------ + uint256 depositAtCutoff = _liquidVault._vaultStartTimestamp() + 1 days - 1 minutes; + vm.warp(depositAtCutoff); // set the time very close to the cut-off + + uint256 depositPeriod = _liquidVault.currentPeriod(); + vm.startPrank(alice); + _asset.approve(address(_liquidVault), principal); + uint256 shares = _liquidVault.deposit(principal, alice); + vm.stopPrank(); + + // ----------------- requestRedeem ------------ + // request redeem on the deposit day + vm.prank(alice); + uint256 redeemPeriod = _liquidVault.requestRedeem(shares, alice, alice); + + // ----------------- redeem ------------ + vm.warp(depositAtCutoff + 2 minutes); // warp to the next day + assertEq(redeemPeriod, _liquidVault.currentPeriod(), "didn't tick over a day"); + + uint256 assetPreview = _liquidVault.previewRedeemForDepositPeriod(shares, depositPeriod, redeemPeriod); + assertEq(principal, assetPreview, "assets should be the same as principal"); + + vm.prank(alice); + uint256 assets = _liquidVault.redeem(shares, alice, alice); + assertEq(principal, assets, "assets should be the same as principal"); + } } diff --git a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultUtilTest.t.sol b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultUtilTest.t.sol index f2a1c0d6b..8699b7038 100644 --- a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultUtilTest.t.sol +++ b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultUtilTest.t.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.20; import { LiquidContinuousMultiTokenVault } from "@credbull/yield/LiquidContinuousMultiTokenVault.sol"; import { TripleRateYieldStrategy } from "@credbull/yield/strategy/TripleRateYieldStrategy.sol"; import { IMultiTokenVault } from "@credbull/token/ERC1155/IMultiTokenVault.sol"; -import { MultiTokenVault } from "@credbull/token/ERC1155/MultiTokenVault.sol"; import { IRedeemOptimizer } from "@credbull/token/ERC1155/IRedeemOptimizer.sol"; import { RedeemOptimizerFIFO } from "@credbull/token/ERC1155/RedeemOptimizerFIFO.sol"; import { Timer } from "@credbull/timelock/Timer.sol"; @@ -17,7 +16,7 @@ import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.so import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -// Tets relation to the Utility / operational aspects of the LiquidContinuousMultiTokenVault +// Tests related to the Utility / operational aspects of the LiquidContinuousMultiTokenVault contract LiquidContinuousMultiTokenVaultUtilTest is LiquidContinuousMultiTokenVaultTestBase { function test__LiquidContinuousMultiTokenVaultUtil__Upgradeability() public { LiquidContinuousMultiTokenVaultMock vaultImpl = new LiquidContinuousMultiTokenVaultMock(); @@ -115,21 +114,11 @@ contract LiquidContinuousMultiTokenVaultUtilTest is LiquidContinuousMultiTokenVa _liquidVault.pause(); vm.prank(alice); - _liquidVault.requestUnlock(alice, _asSingletonArray(testParams.depositPeriod), _asSingletonArray(shares)); _warpToPeriod(_liquidVault, testParams.redeemPeriod); - vm.expectRevert( - abi.encodeWithSelector( - MultiTokenVault.MultiTokenVault__CallerMissingApprovalForAll.selector, address(this), alice - ) - ); - _liquidVault.redeemForDepositPeriod(shares, alice, alice, testParams.depositPeriod, testParams.redeemPeriod); - vm.prank(alice); - _liquidVault.setApprovalForAll(address(this), true); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); _liquidVault.redeemForDepositPeriod(shares, alice, alice, testParams.depositPeriod, testParams.redeemPeriod); @@ -137,7 +126,12 @@ contract LiquidContinuousMultiTokenVaultUtilTest is LiquidContinuousMultiTokenVa vm.prank(_vaultAuth.operator); _liquidVault.unpause(); - _liquidVault.redeemForDepositPeriod(shares, alice, alice, testParams.depositPeriod, testParams.redeemPeriod); + address carol = makeAddr("carol"); // random address to receive redeem + vm.prank(alice); + uint256 assets = + _liquidVault.redeemForDepositPeriod(shares, carol, alice, testParams.depositPeriod, testParams.redeemPeriod); + + assertEq(assets, _asset.balanceOf(carol), "carol did not receive assets"); } function test__LiquidContinuousMultiTokenVaultUtil__UpdateStateVariables() public { @@ -212,19 +206,32 @@ contract LiquidContinuousMultiTokenVaultUtilTest is LiquidContinuousMultiTokenVa _setPeriodAndAssert(_liquidVault, 10); } - // vm.startPrank(_vaultAuth.operator); function _setPeriodAndAssert(LiquidContinuousMultiTokenVault vault, uint256 newPeriod) internal { assertTrue( Timer.timestamp() >= (vault._vaultStartTimestamp() - newPeriod * 24 hours), "trying to set period before block.timestamp" ); - _setPeriod(vault, newPeriod); + _setPeriod(_vaultAuth.operator, vault, newPeriod); assertEq(newPeriod, (block.timestamp - vault._vaultStartTimestamp()) / 24 hours, "timestamp not set correctly"); assertEq(newPeriod, vault.currentPeriod(), "period not set correctly"); } + function test__LiquidContinuousMultiTokenVaultUtil__InvalidFrequencyReverts() public { + LiquidContinuousMultiTokenVault liquidVault = new LiquidContinuousMultiTokenVault(); + LiquidContinuousMultiTokenVault.VaultParams memory invalidParams = _createVaultParams(_vaultAuth); + invalidParams.contextParams.frequency = 12; // Invalid frequency + + vm.expectRevert( + abi.encodeWithSelector( + LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__InvalidFrequency.selector, + invalidParams.contextParams.frequency + ) + ); + new ERC1967Proxy(address(liquidVault), abi.encodeWithSelector(liquidVault.initialize.selector, invalidParams)); + } + function test__LiquidContinuousMultiTokenVaultUtil__ZeroAddressAuthReverts() public { address zeroAddress = address(0); diff --git a/packages/contracts/test/src/yield/strategy/MultipleRateYieldStrategyScenarioTest.t.sol b/packages/contracts/test/src/yield/strategy/MultipleRateYieldStrategyScenarioTest.t.sol index af7b75e36..24c955929 100644 --- a/packages/contracts/test/src/yield/strategy/MultipleRateYieldStrategyScenarioTest.t.sol +++ b/packages/contracts/test/src/yield/strategy/MultipleRateYieldStrategyScenarioTest.t.sol @@ -10,7 +10,6 @@ import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy import { YieldStrategyScenarioTest } from "@test/src/yield/strategy/YieldStrategyScenarioTest.t.sol"; import { Frequencies } from "@test/src/yield/Frequencies.t.sol"; -import { console2 } from "forge-std/console2.sol"; contract MultipleRateYieldStrategyScenarioTest is YieldStrategyScenarioTest { IYieldStrategy internal yieldStrategy; @@ -47,7 +46,6 @@ contract MultipleRateYieldStrategyScenarioTest is YieldStrategyScenarioTest { uint256 tenor, uint256 decimals ) private returns (MultipleRateContext) { - console2.log("maturityperiod", MATURITY_PERIOD); MultipleRateContext _context = new MultipleRateContext(); _context = MultipleRateContext( address( diff --git a/packages/contracts/test/test/script/DeployAndLoadLiquidMultiTokenVault.s.sol b/packages/contracts/test/test/script/DeployAndLoadLiquidMultiTokenVault.s.sol index c80292dc5..a8ea06e1a 100644 --- a/packages/contracts/test/test/script/DeployAndLoadLiquidMultiTokenVault.s.sol +++ b/packages/contracts/test/test/script/DeployAndLoadLiquidMultiTokenVault.s.sol @@ -30,6 +30,8 @@ contract DeployAndLoadLiquidMultiTokenVault is DeployLiquidMultiTokenVault { AnvilWallet private _bob = new AnvilWallet(8); AnvilWallet private _charlie = new AnvilWallet(9); + uint256 private constant TESTING_START_DAY = 30; + error AnvilChainOnly(uint256 actualChainId, uint256 expectedChainId); error OwnerMismatch(address actualOwner, address expectedOwner); @@ -60,9 +62,22 @@ contract DeployAndLoadLiquidMultiTokenVault is DeployLiquidMultiTokenVault { } // --------------------- load deposits --------------------- - _loadDepositsAndRequestSells(vault, _alice, 10); - _loadDepositsAndRequestSells(vault, _bob, 1); - _loadDepositsAndRequestSells(vault, _charlie, 2); + uint256 prevVaultStartTime = vault._vaultStartTimestamp(); // store "original" start time from deploy config + + _loadDepositsAndRequestSells(vault, _alice, 10, 0); + _loadDepositsAndRequestSells(vault, _bob, 1, 1); + _loadDepositsAndRequestSells(vault, _charlie, 2, -1); + + // move the vault time to the testing start time. keep the cut-off "hour:minute:seconds" from deploy config + uint256 elapsedDaysSinceStartTime = + prevVaultStartTime > Timer.timestamp() ? 0 : Timer.elapsed24Hours(prevVaultStartTime); + uint256 vaultTestingTime = + prevVaultStartTime + elapsedDaysSinceStartTime * 24 hours - TESTING_START_DAY * 24 hours; + _setStartTimeStamp(vault, vaultTestingTime); + + console2.log( + "Testing can start! VaultStartTime: %s (Period: %s)", vault._vaultStartTimestamp(), vault.currentPeriod() + ); return vault; } @@ -70,7 +85,8 @@ contract DeployAndLoadLiquidMultiTokenVault is DeployLiquidMultiTokenVault { function _loadDepositsAndRequestSells( LiquidContinuousMultiTokenVault vault, AnvilWallet userWallet, - uint256 userDepositMultiplier + uint256 userDepositMultiplier, + int256 userOffsetInSeconds // exercise deposits/redeems around cut-off times ) internal { IERC20 asset = IERC20(vault.asset()); uint256 scale = 10 ** IERC20Metadata(vault.asset()).decimals(); @@ -92,7 +108,7 @@ contract DeployAndLoadLiquidMultiTokenVault is DeployLiquidMultiTokenVault { for (uint256 depositPeriod = 0; depositPeriod <= vault.TENOR(); ++depositPeriod) { // first set the start time / period as operator - _setPeriod(vault, depositPeriod); + _setPeriod(vault, depositPeriod, userOffsetInSeconds); if (depositPeriod % 7 == 0) { // skip deposits every 7th day @@ -102,7 +118,7 @@ contract DeployAndLoadLiquidMultiTokenVault is DeployLiquidMultiTokenVault { vm.startBroadcast(userWallet.key()); // --------------------- deposits --------------------- - uint256 depositAmount = baseDepositAmount * vault.currentPeriod(); + uint256 depositAmount = baseDepositAmount * (vault.currentPeriod() + 1); asset.approve(address(vault), depositAmount); vault.deposit(depositAmount, userWallet.addr()); totalUserDeposits += depositAmount; @@ -110,7 +126,7 @@ contract DeployAndLoadLiquidMultiTokenVault is DeployLiquidMultiTokenVault { // --------------------- request sell --------------------- if (vault.currentPeriod() == vault.TENOR() - 1) { // queue up one request only - vault.requestRedeem(totalUserDeposits / 10, address(0), userWallet.addr()); // request to sell 10% of deposits so far + vault.requestRedeem(totalUserDeposits / 10, userWallet.addr(), userWallet.addr()); // request to sell 10% of deposits so far } vm.stopBroadcast(); @@ -119,19 +135,54 @@ contract DeployAndLoadLiquidMultiTokenVault is DeployLiquidMultiTokenVault { console2.log("VaultSupply after deposits %s -> %s", prevSupply, vault.totalSupply()); } - function _setPeriod(LiquidContinuousMultiTokenVault vault, uint256 newPeriod) public { - uint256 prevPeriod = vault.currentPeriod(); + function _setPeriod(LiquidContinuousMultiTokenVault vault, uint256 newPeriod, int256 offsetInSeconds) internal { uint256 newPeriodInSeconds = newPeriod * 1 days; + + // add (or subtract) an offset number of seconds from the newPeriod + if (offsetInSeconds >= 0) { + newPeriodInSeconds += uint256(offsetInSeconds); + } else { + // if newPeriod is 0 and offset is negative, just stay at 0 + newPeriodInSeconds -= newPeriod == 0 ? 0 : uint256(int256(-offsetInSeconds)); + } + uint256 currentTime = Timer.timestamp(); uint256 newStartTime = currentTime > newPeriodInSeconds ? (currentTime - newPeriodInSeconds) : (newPeriodInSeconds - currentTime); + _setStartTimeStamp(vault, newStartTime); + } + + function _setStartTimeStamp(LiquidContinuousMultiTokenVault vault, uint256 newStartTimestamp) internal { + uint256 prevPeriod = vault.currentPeriod(); + uint256 prevStartTime = vault._vaultStartTimestamp(); + vm.startBroadcast(_operator.key()); - vault.setVaultStartTimestamp(newStartTime); + vault.setVaultStartTimestamp(newStartTimestamp); vm.stopBroadcast(); - console2.log("VaultCurrentPeriod updated %s -> %s", prevPeriod, vault.currentPeriod()); + console2.log( + string.concat( + "VaultStartTime updated ", + vm.toString(prevStartTime), + " -> ", + vm.toString(vault._vaultStartTimestamp()), + " (Period: ", + vm.toString(prevPeriod), + " -> ", + vm.toString(vault.currentPeriod()), + " )" + ) + ); + } + + function startTimestamp() public view virtual returns (uint256 startTimestamp_) { + return _startTimestamp(); + } + + function auth() public view virtual returns (LiquidContinuousMultiTokenVault.VaultAuth memory auth_) { + return _vaultAuth; } } @@ -140,9 +191,9 @@ contract DeployAndLoadLiquidMultiTokenVault is DeployLiquidMultiTokenVault { //(0) 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH) - owner //(1) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH) - operator //(2) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 ETH) - custody (vault->custody->vault) -//(3) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000 ETH) - treasury +//(3) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000 ETH) - upgrader //(4) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000 ETH) - deployer -//(5) 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc (10000 ETH) - rewards +//(5) 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc (10000 ETH) - treasury //(6) 0x976EA74026E726554dB657fA54763abd0C3a0aa9 (10000 ETH) - asset manager //(7) 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 (10000 ETH) - alice //(8) 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f (10000 ETH) - bob diff --git a/packages/contracts/test/test/script/DeployAndLoadLiquidMultiTokenVaultTest.t.sol b/packages/contracts/test/test/script/DeployAndLoadLiquidMultiTokenVaultTest.t.sol new file mode 100644 index 000000000..4667459d1 --- /dev/null +++ b/packages/contracts/test/test/script/DeployAndLoadLiquidMultiTokenVaultTest.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { LiquidContinuousMultiTokenVault } from "@credbull/yield/LiquidContinuousMultiTokenVault.sol"; +import { LiquidContinuousMultiTokenVaultTestBase } from "@test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol"; +import { DeployAndLoadLiquidMultiTokenVault } from "./DeployAndLoadLiquidMultiTokenVault.s.sol"; + +contract DeployAndLoadLiquidMultiTokenVaultTest is LiquidContinuousMultiTokenVaultTestBase { + DeployAndLoadLiquidMultiTokenVault internal _deployVault; + + function setUp() public override { + _deployVault = new DeployAndLoadLiquidMultiTokenVault(); + + uint256 vaultStartTimestamp = _deployVault.startTimestamp(); + vm.warp(vaultStartTimestamp); // warp to a "real time" time rather than block.timestamp=1 + + _liquidVault = _deployVault.run(); + } + + /// @dev - this SHOULD work, but will have knock-off effects to yield/returns and pending requests + function test__DeployAndLoadLiquidMultiTokenVaultTest__VerifyCutoffs() public { + LiquidContinuousMultiTokenVault.VaultAuth memory vaultAuth = _deployVault.auth(); + + _setPeriod(vaultAuth.operator, _liquidVault, 0); + _setPeriod(vaultAuth.operator, _liquidVault, 30); + } +} diff --git a/packages/contracts/test/test/timelock/SimpleTimelockAsyncUnlock.t.sol b/packages/contracts/test/test/timelock/SimpleTimelockAsyncUnlock.t.sol index d2bd3a13d..205c103a0 100644 --- a/packages/contracts/test/test/timelock/SimpleTimelockAsyncUnlock.t.sol +++ b/packages/contracts/test/test/timelock/SimpleTimelockAsyncUnlock.t.sol @@ -51,4 +51,8 @@ contract SimpleTimelockAsyncUnlock is Initializable, UUPSUpgradeable, TimelockAs function _emptyBytesArray() internal pure returns (bytes[] memory) { return new bytes[](0); } + + // TODO - implement this in test cases... + // solhint-disable-next-line no-empty-blocks + function cancelRequestUnlock(address owner, uint256 requestId) public { } } diff --git a/packages/contracts/test/test/token/ERC1155/IMultiTokenVaultTestBase.t.sol b/packages/contracts/test/test/token/ERC1155/IMultiTokenVaultTestBase.t.sol index e85bec986..bca86d3a5 100644 --- a/packages/contracts/test/test/token/ERC1155/IMultiTokenVaultTestBase.t.sol +++ b/packages/contracts/test/test/token/ERC1155/IMultiTokenVaultTestBase.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.20; import { IMultiTokenVault } from "@credbull/token/ERC1155/IMultiTokenVault.sol"; import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; -import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { Timer } from "@credbull/timelock/Timer.sol"; import { TestParamSet } from "@test/test/token/ERC1155/TestParamSet.t.sol"; @@ -16,10 +15,12 @@ abstract contract IMultiTokenVaultTestBase is Test { uint256 public constant TOLERANCE = 5; // with 6 decimals, diff of 0.000010 /// @dev test the vault at the given test parameters - function testVault(address account, IMultiTokenVault vault, TestParamSet.TestParam[] memory testParams) - internal - returns (uint256[] memory sharesAtPeriods_, uint256[] memory assetsAtPeriods_) - { + function testVault( + TestParamSet.TestUsers memory depositUsers, + TestParamSet.TestUsers memory redeemUsers, + IMultiTokenVault vault, + TestParamSet.TestParam[] memory testParams + ) internal returns (uint256[] memory sharesAtPeriods_, uint256[] memory assetsAtPeriods_) { // previews okay to test individually. don't update vault state. for (uint256 i = 0; i < testParams.length; i++) { TestParamSet.TestParam memory testParam = testParams[i]; @@ -30,26 +31,70 @@ abstract contract IMultiTokenVaultTestBase is Test { // capture vault state before warping around uint256 prevVaultPeriodsElapsed = vault.currentPeriodsElapsed(); - // ------------------- deposits ------------------- + // ------------------- deposits w/ redeems per deposit ------------------- // NB - test all of the deposits BEFORE redeems. verifies no side-effects from deposits when redeeming. - uint256[] memory sharesAtPeriods = _testDepositOnly(account, vault, testParams); + uint256[] memory sharesAtPeriods = _testDepositOnly(depositUsers, vault, testParams); - // ------------------- redeems ------------------- // NB - test all of the redeems AFTER deposits. verifies no side-effects from deposits when redeeming. - uint256[] memory assetsAtPeriods = _testRedeemOnly(account, vault, testParams, sharesAtPeriods); + uint256[] memory assetsAtPeriods = _testRedeemOnly(redeemUsers, vault, testParams, sharesAtPeriods); + + // ------------------- deposits w/ redeems across multiple deposits ------------------- + _testVaultCombineDepositsForRedeem(depositUsers, redeemUsers, vault, testParams, 2); _warpToPeriod(vault, prevVaultPeriodsElapsed); // restore previous period state return (sharesAtPeriods, assetsAtPeriods); } + /// @dev test the vault deposits and redeems across multiple deposit periods + function _testVaultCombineDepositsForRedeem( + TestParamSet.TestUsers memory depositUsers, + TestParamSet.TestUsers memory redeemUsers, + IMultiTokenVault vault, + TestParamSet.TestParam[] memory testParams, + uint256 splitBefore + ) internal returns (uint256[] memory sharesAtPeriods_, uint256 assetsAtPeriods1_, uint256 assetsAtPeriods2_) { + // NB - test all of the deposits BEFORE redeems. verifies no side-effects from deposits when redeeming. + uint256[] memory sharesAtPeriods = _testDepositOnly(depositUsers, vault, testParams); + + // NB - test all of the redeems AFTER deposits. verifies no side-effects from deposits when redeeming. + uint256 finalRedeemPeriod = testParams.latestRedeemPeriod(); + + // split into two batches + (TestParamSet.TestParam[] memory redeemParams1, TestParamSet.TestParam[] memory redeemParams2) = + testParams._splitBefore(splitBefore); + + assertLe(2, redeemParams1.length, "redeem params array 1 should have multiple params"); + assertLe(2, redeemParams2.length, "redeem params array 2 should have multiple params"); + + uint256 partialRedeemPeriod = finalRedeemPeriod - 2; + + uint256 assetsAtPeriods1 = _testRedeemMultiDeposit(redeemUsers, vault, redeemParams1, partialRedeemPeriod); + uint256 assetsAtPeriods2 = _testRedeemMultiDeposit(redeemUsers, vault, redeemParams2, finalRedeemPeriod); + + return (sharesAtPeriods, assetsAtPeriods1, assetsAtPeriods2); + } + /// @dev test Vault at specified redeemPeriod and other "interesting" redeem periods function testVaultAtOffsets(address account, IMultiTokenVault vault, TestParamSet.TestParam memory testParam) internal returns (uint256[] memory sharesAtPeriods_, uint256[] memory assetsAtPeriods_) { + (TestParamSet.TestUsers memory depositUsers, TestParamSet.TestUsers memory redeemUsers) = + _createTestUsers(account); + + return testVaultAtOffsets(depositUsers, redeemUsers, vault, testParam); + } + + /// @dev test Vault at specified redeemPeriod and other "interesting" redeem periods + function testVaultAtOffsets( + TestParamSet.TestUsers memory depositUsers, + TestParamSet.TestUsers memory redeemUsers, + IMultiTokenVault vault, + TestParamSet.TestParam memory testParam + ) internal returns (uint256[] memory sharesAtPeriods_, uint256[] memory assetsAtPeriods_) { TestParamSet.TestParam[] memory testParams = TestParamSet.toOffsetArray(testParam); - return testVault(account, vault, testParams); + return testVault(depositUsers, redeemUsers, vault, testParams); } /// @dev verify convertToAssets and convertToShares. These are a "preview" and do NOT update vault assets or shares. @@ -124,52 +169,54 @@ abstract contract IMultiTokenVaultTestBase is Test { } /// @dev verify deposit. updates vault assets and shares. - function _testDepositOnly(address account, IMultiTokenVault vault, TestParamSet.TestParam[] memory testParams) - internal - virtual - returns (uint256[] memory sharesAtPeriod_) - { + function _testDepositOnly( + TestParamSet.TestUsers memory depositUsers, + IMultiTokenVault vault, + TestParamSet.TestParam[] memory testParams + ) internal virtual returns (uint256[] memory sharesAtPeriod_) { uint256[] memory sharesAtPeriod = new uint256[](testParams.length); for (uint256 i = 0; i < testParams.length; i++) { - sharesAtPeriod[i] = _testDepositOnly(account, vault, testParams[i]); + sharesAtPeriod[i] = _testDepositOnly(depositUsers, vault, testParams[i]); } return sharesAtPeriod; } /// @dev verify deposit. updates vault assets and shares. - function _testDepositOnly(address account, IMultiTokenVault vault, TestParamSet.TestParam memory testParam) - internal - virtual - returns (uint256 actualSharesAtPeriod_) - { + function _testDepositOnly( + TestParamSet.TestUsers memory depositUsers, + IMultiTokenVault vault, + TestParamSet.TestParam memory testParam + ) internal virtual returns (uint256 actualSharesAtPeriod_) { IERC20 asset = IERC20(vault.asset()); // capture state before for validations uint256 prevVaultPeriodsElapsed = vault.currentPeriodsElapsed(); - uint256 prevReceiverVaultBalance = vault.sharesAtPeriod(account, testParam.depositPeriod); + uint256 prevReceiverVaultBalance = vault.sharesAtPeriod(depositUsers.tokenReceiver, testParam.depositPeriod); // ------------------- deposit ------------------- _warpToPeriod(vault, testParam.depositPeriod); // warp to deposit period - vm.startPrank(account); assertGe( - asset.balanceOf(account), + asset.balanceOf(depositUsers.tokenOwner), testParam.principal, _assertMsg("not enough assets for deposit ", vault, testParam.depositPeriod) ); + vm.prank(depositUsers.tokenOwner); // tokenOwner here is the owner of the USDC asset.approve(address(vault), testParam.principal); // grant the vault allowance - uint256 actualSharesAtPeriod = vault.deposit(testParam.principal, account); // now deposit - vm.stopPrank(); + + vm.prank(depositUsers.tokenOwner); // tokenOwner here is the owner of the USDC + uint256 actualSharesAtPeriod = vault.deposit(testParam.principal, depositUsers.tokenReceiver); // now deposit + assertEq( prevReceiverVaultBalance + actualSharesAtPeriod, - vault.sharesAtPeriod(account, testParam.depositPeriod), + vault.sharesAtPeriod(depositUsers.tokenReceiver, testParam.depositPeriod), _assertMsg( "receiver did not receive the correct vault shares - sharesAtPeriod", vault, testParam.depositPeriod ) ); assertEq( prevReceiverVaultBalance + actualSharesAtPeriod, - vault.balanceOf(account, testParam.depositPeriod), + vault.balanceOf(depositUsers.tokenReceiver, testParam.depositPeriod), _assertMsg("receiver did not receive the correct vault shares - balanceOf ", vault, testParam.depositPeriod) ); @@ -180,21 +227,21 @@ abstract contract IMultiTokenVaultTestBase is Test { /// @dev verify redeem. updates vault assets and shares. function _testRedeemOnly( - address account, + TestParamSet.TestUsers memory redeemUsers, IMultiTokenVault vault, TestParamSet.TestParam[] memory testParams, uint256[] memory sharesAtPeriods ) internal virtual returns (uint256[] memory assetsAtPeriods_) { uint256[] memory assetsAtPeriods = new uint256[](testParams.length); for (uint256 i = 0; i < testParams.length; i++) { - assetsAtPeriods[i] = _testRedeemOnly(account, vault, testParams[i], sharesAtPeriods[i]); + assetsAtPeriods[i] = _testRedeemOnly(redeemUsers, vault, testParams[i], sharesAtPeriods[i]); } return assetsAtPeriods; } /// @dev verify redeem. updates vault assets and shares. function _testRedeemOnly( - address account, + TestParamSet.TestUsers memory redeemUsers, IMultiTokenVault vault, TestParamSet.TestParam memory testParam, uint256 sharesToRedeemAtPeriod @@ -204,7 +251,8 @@ abstract contract IMultiTokenVaultTestBase is Test { uint256 prevVaultPeriodsElapsed = vault.currentPeriodsElapsed(); // ------------------- prep redeem ------------------- - uint256 assetBalanceBeforeRedeem = asset.balanceOf(account); + uint256 assetBalanceBeforeRedeem = asset.balanceOf(redeemUsers.tokenReceiver); + uint256 shareBalanceBeforeRedeem = vault.balanceOf(redeemUsers.tokenOwner, testParam.depositPeriod); uint256 expectedReturns = _expectedReturns(sharesToRedeemAtPeriod, vault, testParam); _transferFromTokenOwner(asset, address(vault), expectedReturns); @@ -212,11 +260,20 @@ abstract contract IMultiTokenVaultTestBase is Test { // ------------------- redeem ------------------- _warpToPeriod(vault, testParam.redeemPeriod); // warp the vault to redeem period - vm.startPrank(account); - uint256 actualAssetsAtPeriod = - vault.redeemForDepositPeriod(sharesToRedeemAtPeriod, account, account, testParam.depositPeriod); + // authorize the tokenOperator + vm.prank(redeemUsers.tokenOwner); + vault.setApprovalForAll(redeemUsers.tokenOperator, true); + + vm.startPrank(redeemUsers.tokenOperator); + uint256 actualAssetsAtPeriod = vault.redeemForDepositPeriod( + sharesToRedeemAtPeriod, redeemUsers.tokenReceiver, redeemUsers.tokenOwner, testParam.depositPeriod + ); vm.stopPrank(); + // de-authorize the tokenOperator + vm.prank(redeemUsers.tokenOwner); + vault.setApprovalForAll(redeemUsers.tokenOperator, false); + assertApproxEqAbs( testParam.principal + expectedReturns, actualAssetsAtPeriod, @@ -224,10 +281,17 @@ abstract contract IMultiTokenVaultTestBase is Test { _assertMsg("assets does not equal principal + yield", vault, testParam.depositPeriod) ); + // verify the token owner shares reduced + assertEq( + shareBalanceBeforeRedeem - sharesToRedeemAtPeriod, + vault.balanceOf(redeemUsers.tokenOwner, testParam.depositPeriod), + _assertMsg("shares not reduced by redeem amount", vault, testParam.depositPeriod) + ); + // verify the receiver has the USDC back assertApproxEqAbs( assetBalanceBeforeRedeem + testParam.principal + expectedReturns, - asset.balanceOf(account), + asset.balanceOf(redeemUsers.tokenReceiver), TOLERANCE, _assertMsg("receiver did not receive the correct yield", vault, testParam.depositPeriod) ); @@ -237,40 +301,82 @@ abstract contract IMultiTokenVaultTestBase is Test { return actualAssetsAtPeriod; } - /// @dev performance / load test harness to execute a number of deposits first, and then redeem after - function _loadTestVault(IMultiTokenVault vault, uint256 principal, uint256 fromPeriod, uint256 toPeriod) internal { - address charlie = makeAddr("charlie"); - address david = makeAddr("david"); - - IERC20Metadata _asset = IERC20Metadata(vault.asset()); - uint256 _scale = 10 ** _asset.decimals(); - - _transferFromTokenOwner(_asset, charlie, 1_000_000_000 * _scale); - _transferFromTokenOwner(_asset, david, 1_000_000_000 * _scale); - - // "wastes" storage from 0 -> fromPeriod. but fine in test, and makes the depositPeriod clear - uint256[] memory charlieShares = new uint256[](toPeriod + 1); - uint256[] memory davidShares = new uint256[](toPeriod + 1); - - // ----------------------- deposits ----------------------- - for (uint256 i = fromPeriod; i < toPeriod; ++i) { - TestParamSet.TestParam memory depositTestParam = TestParamSet.TestParam({ - principal: principal, - depositPeriod: i, - redeemPeriod: 0 // not used in deposit flow - }); - charlieShares[i] = _testDepositOnly(charlie, vault, depositTestParam); - davidShares[i] = _testDepositOnly(david, vault, depositTestParam); + /// @dev - requestRedeem over multiple deposit and principals into one requestRedeemPeriod + function _testRedeemMultiDeposit( + TestParamSet.TestUsers memory redeemUsers, + IMultiTokenVault vault, + TestParamSet.TestParam[] memory depositTestParams, + uint256 redeemPeriod // we are testing multiple deposits into one redeemPeriod + ) internal virtual returns (uint256 assets_) { + _warpToPeriod(vault, redeemPeriod); + + IERC20 asset = IERC20(vault.asset()); + + uint256 prevAssetBalance = asset.balanceOf(redeemUsers.tokenReceiver); + uint256[] memory prevSharesBalance = vault.balanceOfBatch( + depositTestParams.accountArray(redeemUsers.tokenOwner), depositTestParams.depositPeriods() + ); + + // get the vault enough to cover redeems + _transferFromTokenOwner(asset, address(vault), depositTestParams.totalPrincipal()); // this will give the vault 2x principal + + uint256 assets = _vaultRedeemBatch(redeemUsers, vault, depositTestParams, redeemPeriod); + + assertEq( + prevAssetBalance + assets, + asset.balanceOf(redeemUsers.tokenReceiver), + "receiver did not receive assets - redeem on multi deposit" + ); + + // check share balances reduced on the owner + uint256[] memory sharesBalance = vault.balanceOfBatch( + depositTestParams.accountArray(redeemUsers.tokenOwner), depositTestParams.depositPeriods() + ); + + assertEq(prevSharesBalance.length, sharesBalance.length, "mismatch on share balance"); + + for (uint256 i = 0; i < prevSharesBalance.length; ++i) { + uint256 sharesAtPeriod = vault.convertToSharesForDepositPeriod( + depositTestParams[i].principal, depositTestParams[i].depositPeriod + ); + assertEq(prevSharesBalance[i] - sharesAtPeriod, sharesBalance[i], "token owner shares balance incorrect"); } - // ----------------------- redeems ----------------------- - for (uint256 i = fromPeriod; i < toPeriod; ++i) { - TestParamSet.TestParam memory redeemTestParam = - TestParamSet.TestParam({ principal: principal, depositPeriod: i, redeemPeriod: toPeriod }); + return assets; + } - _testRedeemOnly(charlie, vault, redeemTestParam, charlieShares[i]); - _testRedeemOnly(david, vault, redeemTestParam, davidShares[i]); + /// @dev vault redeem across multiple deposit periods. (if supported) + function _vaultRedeemBatch( + TestParamSet.TestUsers memory redeemUsers, + IMultiTokenVault vault, + TestParamSet.TestParam[] memory depositTestParams, + uint256 redeemPeriod + ) internal virtual returns (uint256 assets_) { + _warpToPeriod(vault, redeemPeriod); // warp the vault to redeem period + + // authorize the tokenOperator + vm.prank(redeemUsers.tokenOwner); + vault.setApprovalForAll(redeemUsers.tokenOperator, true); + + uint256 assets = 0; + vm.startPrank(redeemUsers.tokenOperator); + // IMultiTokenVault doesn't support redeeming across deposit periods. redeem period by period instead. + for (uint256 i = 0; i < depositTestParams.length; ++i) { + uint256 depositPeriod = depositTestParams[i].depositPeriod; + uint256 sharesAtPeriod = + vault.convertToSharesForDepositPeriod(depositTestParams[i].principal, depositPeriod); + + assets += vault.redeemForDepositPeriod( + sharesAtPeriod, redeemUsers.tokenReceiver, redeemUsers.tokenOwner, depositTestParams[i].depositPeriod + ); } + vm.stopPrank(); + + // de-authorize the tokenOperator + vm.prank(redeemUsers.tokenOwner); + vault.setApprovalForAll(redeemUsers.tokenOperator, false); + + return assets; } /// @dev expected returns. returns is the difference between the assets deposited (i.e. the principal) and the assets redeemed. @@ -296,6 +402,30 @@ abstract contract IMultiTokenVaultTestBase is Test { TestParamSet.TestParam({ principal: principal, depositPeriod: depositPeriod, redeemPeriod: redeemPeriod }); } + // simple scenario with only one user + function _createTestUsers(address account) + internal + virtual + returns (TestParamSet.TestUsers memory depositUsers_, TestParamSet.TestUsers memory redeemUsers_) + { + // Convert the address to a string and then to bytes + string memory accountStr = vm.toString(account); + + TestParamSet.TestUsers memory depositUsers = TestParamSet.TestUsers({ + tokenOwner: account, // owns tokens, can specify who can receive tokens + tokenReceiver: makeAddr(string.concat("depositTokenReceiver-", accountStr)), // receiver of tokens from the tokenOwner + tokenOperator: makeAddr(string.concat("depositTokenOperator-", accountStr)) // granted allowance by tokenOwner to act on their behalf + }); + + TestParamSet.TestUsers memory redeemUsers = TestParamSet.TestUsers({ + tokenOwner: depositUsers.tokenReceiver, // on deposit, the tokenReceiver receives (owns) the tokens + tokenReceiver: account, // virtuous cycle, the account receives the returns in the end + tokenOperator: makeAddr(string.concat("redeemTokenOperator-", accountStr)) // granted allowance by tokenOwner to act on their behalf + }); + + return (depositUsers, redeemUsers); + } + /// @dev - creates a message string for assertions function _assertMsg(string memory prefix, IMultiTokenVault vault, uint256 numPeriods) internal diff --git a/packages/contracts/test/test/token/ERC1155/MultiTokenVaultDailyPeriods.t.sol b/packages/contracts/test/test/token/ERC1155/MultiTokenVaultDailyPeriods.t.sol index a36205402..895599750 100644 --- a/packages/contracts/test/test/token/ERC1155/MultiTokenVaultDailyPeriods.t.sol +++ b/packages/contracts/test/test/token/ERC1155/MultiTokenVaultDailyPeriods.t.sol @@ -9,7 +9,8 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils contract MultiTokenVaultDailyPeriods is Initializable, UUPSUpgradeable, MultiTokenVault { uint256 internal ASSET_TO_SHARES_RATIO; uint256 internal YIELD_PERCENTAGE; - uint256 public _currentPeriod; + uint256 private _currentPeriod; + uint256 private _maxDeposit; function initialize(IERC20Metadata asset, uint256 assetToSharesRatio, uint256 yieldPercentage) public initializer { __MultiTokenVault_init(asset); @@ -56,4 +57,15 @@ contract MultiTokenVaultDailyPeriods is Initializable, UUPSUpgradeable, MultiTok function setCurrentPeriodsElapsed(uint256 currentTimePeriodsElapsed_) public { _currentPeriod = currentTimePeriodsElapsed_; } + + function maxDeposit(address forWhom_) public view virtual override returns (uint256) { + if (_maxDeposit != 0) { + return _maxDeposit; + } + return super.maxDeposit(forWhom_); + } + + function setMaxDeposit(uint256 maxDeposit_) public virtual returns (uint256) { + return _maxDeposit = maxDeposit_; + } } diff --git a/packages/contracts/test/test/token/ERC1155/TestParamSet.t.sol b/packages/contracts/test/test/token/ERC1155/TestParamSet.t.sol index 6e9b0b4ea..b6f4054df 100644 --- a/packages/contracts/test/test/token/ERC1155/TestParamSet.t.sol +++ b/packages/contracts/test/test/token/ERC1155/TestParamSet.t.sol @@ -2,13 +2,21 @@ pragma solidity ^0.8.20; library TestParamSet { + using TestParamSet for TestParam[]; + + // params for testing deposits and redeems struct TestParam { uint256 principal; uint256 depositPeriod; uint256 redeemPeriod; } - using TestParamSet for TestParam[]; + // users involved in deposit and redeems. using "tokenOwner" to distinguish vs. contract "owner" + struct TestUsers { + address tokenOwner; // owns tokens, can specify who can receive tokens + address tokenReceiver; // token owner or granted tokens by the token owner + address tokenOperator; // for txn to succeed MUST be tokenOwner or granted allowance by tokenOwner + } // Generate and add multiple testParams with offsets function toOffsetArray(TestParam memory testParam) @@ -16,18 +24,18 @@ library TestParamSet { pure returns (TestParam[] memory testParamsWithOffsets_) { - uint256[6] memory offsetNumPeriodsArr = + uint256[6] memory offsetAmounts = [0, 1, 2, testParam.redeemPeriod - 1, testParam.redeemPeriod, testParam.redeemPeriod + 1]; - TestParam[] memory testParamsWithOffsets = new TestParam[](offsetNumPeriodsArr.length); + TestParam[] memory testParamsWithOffsets = new TestParam[](offsetAmounts.length); - for (uint256 i = 0; i < offsetNumPeriodsArr.length; i++) { - uint256 offsetNumPeriods = offsetNumPeriodsArr[i]; + for (uint256 i = 0; i < offsetAmounts.length; i++) { + uint256 offsetAmount = offsetAmounts[i]; TestParam memory testParamsWithOffset = TestParam({ - principal: testParam.principal, - depositPeriod: testParam.depositPeriod + offsetNumPeriods, - redeemPeriod: testParam.redeemPeriod + offsetNumPeriods + principal: testParam.principal * (1 + offsetAmount), + depositPeriod: testParam.depositPeriod + offsetAmount, + redeemPeriod: testParam.redeemPeriod + offsetAmount }); testParamsWithOffsets[i] = testParamsWithOffset; @@ -36,6 +44,47 @@ library TestParamSet { return testParamsWithOffsets; } + // Generate and add multiple testParams with offsets + function toLoadSet(uint256 principal, uint256 fromPeriod, uint256 toPeriod) + internal + pure + returns (TestParam[] memory loadTestParams_) + { + TestParam[] memory loadTestParams = new TestParam[](toPeriod - fromPeriod); + + uint256 arrayIndex = 0; + for (uint256 i = fromPeriod; i < toPeriod; ++i) { + loadTestParams[arrayIndex] = + TestParamSet.TestParam({ principal: principal, depositPeriod: i, redeemPeriod: toPeriod }); + arrayIndex++; + } + + return loadTestParams; + } + + // simple scenario with only one user + function toSingletonUsers(address account) internal pure returns (TestUsers memory testUsers_) { + TestUsers memory testUsers = TestUsers({ + tokenOwner: account, // owns tokens, can specify who can receive tokens + tokenReceiver: account, // receiver of tokens from the tokenOwner + tokenOperator: account // granted allowance by tokenOwner to act on their behalf + }); + + return testUsers; + } + + // Calculate the total principal across all TestParams + function latestRedeemPeriod(TestParam[] memory self) internal pure returns (uint256 latestRedeemPeriod_) { + uint256 _latestRedeemPeriod = 0; + for (uint256 i = 0; i < self.length; i++) { + uint256 redeemPeriod = self[i].redeemPeriod; + if (_latestRedeemPeriod == 0 || redeemPeriod > _latestRedeemPeriod) { + _latestRedeemPeriod = redeemPeriod; + } + } + return _latestRedeemPeriod; + } + // Calculate the total principal across all TestParams function totalPrincipal(TestParam[] memory self) internal pure returns (uint256 totalPrincipal_) { uint256 principal = 0; @@ -90,7 +139,7 @@ library TestParamSet { return accounts; } - function _split(TestParam[] memory origTestParams, uint256 from, uint256 to) + function _subset(TestParam[] memory origTestParams, uint256 from, uint256 to) public pure returns (TestParam[] memory newTestParams_) @@ -104,4 +153,29 @@ library TestParamSet { } return newTestParams_; } + + function _splitBefore(TestParam[] memory origTestParams, uint256 splitBefore) + public + pure + returns (TestParam[] memory leftSet_, TestParam[] memory rightSet_) + { + // assert we can actually split in two at the splitBefore + assert(splitBefore < (origTestParams.length - 1)); + + // Initialize leftSet and rightSet arrays with their respective sizes + TestParam[] memory leftSet = new TestParam[](splitBefore); // Elements before splitBefore + TestParam[] memory rightSet = new TestParam[](origTestParams.length - splitBefore); // Elements from splitBefore onwards + + // Copy elements to leftSet (up to splitBefore, exclusive) + for (uint256 i = 0; i < splitBefore; i++) { + leftSet[i] = origTestParams[i]; + } + + // Copy elements to rightSet (starting from splitBefore) + for (uint256 i = splitBefore; i < origTestParams.length; i++) { + rightSet[i - splitBefore] = origTestParams[i]; + } + + return (leftSet, rightSet); + } } diff --git a/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol b/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol index b24cf07f5..6181b41f6 100644 --- a/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol +++ b/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol @@ -15,7 +15,6 @@ import { SimpleUSDC } from "@test/test/token/SimpleUSDC.t.sol"; import { TestParamSet } from "@test/test/token/ERC1155/TestParamSet.t.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; abstract contract LiquidContinuousMultiTokenVaultTestBase is IMultiTokenVaultTestBase { using TestParamSet for TestParamSet.TestParam[]; @@ -35,10 +34,13 @@ abstract contract LiquidContinuousMultiTokenVaultTestBase is IMultiTokenVaultTes address internal alice = makeAddr("alice"); address internal bob = makeAddr("bob"); - function setUp() public { + function setUp() public virtual { DeployLiquidMultiTokenVault _deployVault = new DeployLiquidMultiTokenVault(); _liquidVault = _deployVault.run(_vaultAuth); + // warp to a "real time" time rather than block.timestamp=1 + vm.warp(_liquidVault._vaultStartTimestamp() + 1); + _asset = IERC20Metadata(_liquidVault.asset()); _scale = 10 ** _asset.decimals(); @@ -47,27 +49,47 @@ abstract contract LiquidContinuousMultiTokenVaultTestBase is IMultiTokenVaultTes } // verify deposit. updates vault assets and shares. - function _testDepositOnly(address receiver, IMultiTokenVault vault, TestParamSet.TestParam memory testParam) - internal - virtual - override - returns (uint256 actualSharesAtPeriod_) - { - uint256 actualSharesAtPeriod = super._testDepositOnly(receiver, vault, testParam); + function _testDepositOnly( + TestParamSet.TestUsers memory testUsers, + IMultiTokenVault vault, + TestParamSet.TestParam memory testParam + ) internal virtual override returns (uint256 actualSharesAtPeriod_) { + LiquidContinuousMultiTokenVault liquidVault = LiquidContinuousMultiTokenVault(address(vault)); + + uint256 prevVaultBalanceOf = _asset.balanceOf(address(_liquidVault)); + uint256 prevVaultPeriodsElapsed = vault.currentPeriodsElapsed(); + + _warpToPeriod(vault, testParam.depositPeriod); // warp to deposit period to calc totalAssets correctly + uint256 prevVaultTotalAssets = liquidVault.totalAssets(); + _warpToPeriod(vault, prevVaultPeriodsElapsed); // restore previous period state + + uint256 actualSharesAtPeriod = super._testDepositOnly(testUsers, vault, testParam); assertEq( actualSharesAtPeriod, - vault.balanceOf(receiver, testParam.depositPeriod), + vault.balanceOf(testUsers.tokenReceiver, testParam.depositPeriod), _assertMsg( "!!! receiver did not receive the correct vault shares - balanceOf ", vault, testParam.depositPeriod ) ); - LiquidContinuousMultiTokenVault liquidVault = LiquidContinuousMultiTokenVault(address(vault)); + assertEq( + prevVaultBalanceOf + testParam.principal, + _asset.balanceOf(address(_liquidVault)), + "vault didn't receive the assets" + ); + + assertEq( + testParam.principal, + liquidVault.lockedAmount(testUsers.tokenReceiver, testParam.depositPeriod), + "principal not locked" + ); + _warpToPeriod(vault, testParam.depositPeriod); // warp to deposit period to calc totalAssets correctly assertEq( - testParam.principal, liquidVault.lockedAmount(receiver, testParam.depositPeriod), "principal not locked" + prevVaultTotalAssets + testParam.principal, liquidVault.totalAssets(), "vault total assets not updated" ); + _warpToPeriod(vault, prevVaultPeriodsElapsed); // restore previous period state return actualSharesAtPeriod; } @@ -88,38 +110,52 @@ abstract contract LiquidContinuousMultiTokenVaultTestBase is IMultiTokenVaultTes return vaultParams; } - // this vault requires an unlock request prior to redeeming function _testRedeemOnly( - address receiver, + TestParamSet.TestUsers memory redeemUsers, IMultiTokenVault vault, TestParamSet.TestParam memory testParam, uint256 sharesToRedeemAtPeriod ) internal virtual override returns (uint256 actualAssetsAtPeriod_) { LiquidContinuousMultiTokenVault liquidVault = LiquidContinuousMultiTokenVault(address(vault)); + uint256 prevLockedAmount = liquidVault.lockedAmount(redeemUsers.tokenOwner, testParam.depositPeriod); + uint256 prevBalanceOf = liquidVault.balanceOf(redeemUsers.tokenOwner, testParam.depositPeriod); + // request unlock _warpToPeriod(liquidVault, testParam.redeemPeriod - liquidVault.noticePeriod()); - vm.prank(receiver); + vm.prank(redeemUsers.tokenOwner); + vault.setApprovalForAll(redeemUsers.tokenOperator, true); - liquidVault.requestUnlock( - receiver, _asSingletonArray(testParam.depositPeriod), _asSingletonArray(testParam.principal) - ); + // this vault requires an unlock/redeem request prior to redeeming + vm.prank(redeemUsers.tokenOperator); + liquidVault.requestRedeem(sharesToRedeemAtPeriod, redeemUsers.tokenOperator, redeemUsers.tokenOwner); + + vm.prank(redeemUsers.tokenOwner); + vault.setApprovalForAll(redeemUsers.tokenOperator, false); assertEq( - testParam.principal, - liquidVault.unlockRequestAmountByDepositPeriod(receiver, testParam.depositPeriod), + sharesToRedeemAtPeriod, + liquidVault.unlockRequestAmountByDepositPeriod(redeemUsers.tokenOwner, testParam.depositPeriod), "unlockRequest should be created" ); - uint256 actualAssetsAtPeriod = super._testRedeemOnly(receiver, vault, testParam, sharesToRedeemAtPeriod); + uint256 actualAssetsAtPeriod = super._testRedeemOnly(redeemUsers, vault, testParam, sharesToRedeemAtPeriod); // verify locks and request locks released - assertEq(0, liquidVault.lockedAmount(receiver, testParam.depositPeriod), "deposit lock not released"); - assertEq(0, liquidVault.balanceOf(receiver, testParam.depositPeriod), "deposits should be redeemed"); + assertEq( + prevLockedAmount - sharesToRedeemAtPeriod, + liquidVault.lockedAmount(redeemUsers.tokenOwner, testParam.depositPeriod), + "deposit lock not released" + ); + assertEq( + prevBalanceOf - sharesToRedeemAtPeriod, + liquidVault.balanceOf(redeemUsers.tokenOwner, testParam.depositPeriod), + "deposits should be redeemed" + ); assertEq( 0, - liquidVault.unlockRequestAmountByDepositPeriod(receiver, testParam.depositPeriod), + liquidVault.unlockRequestAmountByDepositPeriod(redeemUsers.tokenOwner, testParam.depositPeriod), "unlockRequest should be released" ); @@ -128,7 +164,7 @@ abstract contract LiquidContinuousMultiTokenVaultTestBase is IMultiTokenVaultTes /// @dev - requestRedeem over multiple deposit and principals into one requestRedeemPeriod function _testRequestRedeemMultiDeposit( - address account, + TestParamSet.TestUsers memory redeemUsers, LiquidContinuousMultiTokenVault liquidVault, TestParamSet.TestParam[] memory depositTestParams, uint256 redeemPeriod // we are testing multiple deposits into one redeemPeriod @@ -137,10 +173,17 @@ abstract contract LiquidContinuousMultiTokenVaultTestBase is IMultiTokenVaultTes uint256 sharesToRedeem = depositTestParams.totalPrincipal(); - vm.prank(account); - uint256 requestId = liquidVault.requestRedeem(sharesToRedeem, account, account); + vm.prank(redeemUsers.tokenOwner); + liquidVault.setApprovalForAll(redeemUsers.tokenOperator, true); + + vm.prank(redeemUsers.tokenOperator); + uint256 requestId = liquidVault.requestRedeem(sharesToRedeem, redeemUsers.tokenOperator, redeemUsers.tokenOwner); + + vm.prank(redeemUsers.tokenOwner); + liquidVault.setApprovalForAll(redeemUsers.tokenOperator, false); + (uint256[] memory unlockDepositPeriods, uint256[] memory unlockShares) = - liquidVault.unlockRequests(account, requestId); + liquidVault.unlockRequests(redeemUsers.tokenOwner, requestId); (uint256[] memory expectedDepositPeriods, uint256[] memory expectedShares) = depositTestParams.deposits(); @@ -148,45 +191,66 @@ abstract contract LiquidContinuousMultiTokenVaultTestBase is IMultiTokenVaultTes assertEq(expectedShares, unlockShares, "shares mismatch for requestRedeem"); } - /// @dev - requestRedeem over multiple deposit and principals into one requestRedeemPeriod + /// @dev - redeem ONLY (no requestRedeem) over multiple deposit and principals into one requestRedeemPeriod + function _testRedeemAfterRequestRedeemMultiDeposit( + TestParamSet.TestUsers memory redeemUsers, + IMultiTokenVault vault, + TestParamSet.TestParam[] memory depositTestParams, + uint256 redeemPeriod // we are testing multiple deposits into one redeemPeriod + ) internal virtual returns (uint256 assets_) { + LiquidContinuousMultiTokenVault liquidVault = LiquidContinuousMultiTokenVault(address(vault)); + + uint256 assets = super._testRedeemMultiDeposit(redeemUsers, vault, depositTestParams, redeemPeriod); + + // verify the requestRedeems are released + (uint256[] memory unlockDepositPeriods, uint256[] memory unlockAmounts) = + liquidVault.unlockRequests(redeemUsers.tokenOwner, redeemPeriod); + assertEq(0, unlockDepositPeriods.length, "unlock should be released"); + assertEq(0, unlockAmounts.length, "unlock should be released"); + + return assets; + } + + /// @dev - requestRedeem AND redeem over multiple deposit and principals into one requestRedeemPeriod function _testRedeemMultiDeposit( - address account, - LiquidContinuousMultiTokenVault liquidVault, + TestParamSet.TestUsers memory redeemUsers, + IMultiTokenVault vault, TestParamSet.TestParam[] memory depositTestParams, uint256 redeemPeriod // we are testing multiple deposits into one redeemPeriod - ) internal virtual { - _warpToPeriod(_liquidVault, redeemPeriod); + ) internal virtual override returns (uint256 assets_) { + LiquidContinuousMultiTokenVault liquidVault = LiquidContinuousMultiTokenVault(address(vault)); - uint256 sharesToRedeem = depositTestParams.totalPrincipal(); + // first requestRedeem + _testRequestRedeemMultiDeposit(redeemUsers, liquidVault, depositTestParams, redeemPeriod); - IERC20 asset = IERC20(liquidVault.asset()); + // now redeem + return _testRedeemAfterRequestRedeemMultiDeposit(redeemUsers, vault, depositTestParams, redeemPeriod); + } - uint256 prevAssetBalance = asset.balanceOf(account); - uint256[] memory prevSharesBalance = - _liquidVault.balanceOfBatch(depositTestParams.accountArray(account), depositTestParams.depositPeriods()); + /// @dev redeem across multiple deposit periods + function _vaultRedeemBatch( + TestParamSet.TestUsers memory redeemUsers, + IMultiTokenVault vault, + TestParamSet.TestParam[] memory depositTestParams, + uint256 redeemPeriod + ) internal virtual override returns (uint256 assets_) { + LiquidContinuousMultiTokenVault liquidVault = LiquidContinuousMultiTokenVault(address(vault)); - // get the vault enough to cover redeems - _transferFromTokenOwner(asset, address(liquidVault), sharesToRedeem); // this will give the vault 2x principal + _warpToPeriod(vault, redeemPeriod); // warp the vault to redeem period - vm.prank(account); - uint256 assets = liquidVault.redeem(sharesToRedeem, account, account); + // authorize the tokenOperator + vm.prank(redeemUsers.tokenOwner); + vault.setApprovalForAll(redeemUsers.tokenOperator, true); - assertEq(prevAssetBalance + assets, asset.balanceOf(account), "did not receive assets"); + vm.prank(redeemUsers.tokenOperator); + uint256 assets = + liquidVault.redeem(depositTestParams.totalPrincipal(), redeemUsers.tokenReceiver, redeemUsers.tokenOperator); - // check unlocks are released - (uint256[] memory unlockDepositPeriods, uint256[] memory unlockAmounts) = - _liquidVault.unlockRequests(account, redeemPeriod); - assertEq(0, unlockDepositPeriods.length, "unlock should be released"); - assertEq(0, unlockAmounts.length, "unlock should be released"); + // de-authorize the tokenOperator + vm.prank(redeemUsers.tokenOwner); + vault.setApprovalForAll(redeemUsers.tokenOperator, false); - // check share balances reduced - uint256[] memory sharesBalance = - _liquidVault.balanceOfBatch(depositTestParams.accountArray(account), depositTestParams.depositPeriods()); - for (uint256 i = 0; i < prevSharesBalance.length; ++i) { - assertEq( - prevSharesBalance[i] - depositTestParams[i].principal, sharesBalance[i], "shares balance incorrect" - ); - } + return assets; } function _expectedReturns(uint256, /* shares */ IMultiTokenVault vault, TestParamSet.TestParam memory testParam) @@ -211,14 +275,35 @@ abstract contract LiquidContinuousMultiTokenVaultTestBase is IMultiTokenVaultTes vm.warp(warpToTimeInSeconds); } - function _setPeriod(LiquidContinuousMultiTokenVault vault, uint256 newPeriod) public { + // simple scenario with only one user + function _createTestUsers(address account) + internal + virtual + override + returns (TestParamSet.TestUsers memory depositUsers_, TestParamSet.TestUsers memory redeemUsers_) + { + (TestParamSet.TestUsers memory depositUsers, TestParamSet.TestUsers memory redeemUsers) = + super._createTestUsers(account); + + // in LiquidContinuousMultiTokenVault - tokenOwner and tokenOperator (aka controller) must be the same + // because IComponentToken.redeem() does not have an `owner` parameter. // TODO - we should add this in with Plume ! + TestParamSet.TestUsers memory redeemUsersOperatorIsOwner = TestParamSet.TestUsers({ + tokenOwner: redeemUsers.tokenOwner, + tokenReceiver: redeemUsers.tokenReceiver, + tokenOperator: redeemUsers.tokenOwner + }); + + return (depositUsers, redeemUsersOperatorIsOwner); + } + + function _setPeriod(address operator, LiquidContinuousMultiTokenVault vault, uint256 newPeriod) public { uint256 newPeriodInSeconds = newPeriod * 1 days; uint256 currentTime = Timer.timestamp(); uint256 newStartTime = currentTime > newPeriodInSeconds ? (currentTime - newPeriodInSeconds) : (newPeriodInSeconds - currentTime); - vm.prank(_vaultAuth.operator); + vm.prank(operator); vault.setVaultStartTimestamp(newStartTime); } diff --git a/packages/contracts/test/test/yield/strategy/DualRateYieldStrategy.t.sol b/packages/contracts/test/test/yield/strategy/DualRateYieldStrategy.t.sol index b7ddee0db..dee024126 100644 --- a/packages/contracts/test/test/yield/strategy/DualRateYieldStrategy.t.sol +++ b/packages/contracts/test/test/yield/strategy/DualRateYieldStrategy.t.sol @@ -60,7 +60,7 @@ contract DualRateYieldStrategy is IYieldStrategy { IDualRateContext context = IDualRateContext(contextContract); return CalcSimpleInterest.calcPriceFromInterest( - numPeriodsElapsed, context.rateScaled(), context.frequency(), context.scale() + context.rateScaled(), numPeriodsElapsed, context.frequency(), context.scale() ); } diff --git a/packages/contracts/test/test/yield/strategy/MultipleRateYieldStrategy.t.sol b/packages/contracts/test/test/yield/strategy/MultipleRateYieldStrategy.t.sol index 288d96e27..db3a64393 100644 --- a/packages/contracts/test/test/yield/strategy/MultipleRateYieldStrategy.t.sol +++ b/packages/contracts/test/test/yield/strategy/MultipleRateYieldStrategy.t.sol @@ -61,7 +61,7 @@ contract MultipleRateYieldStrategy is AbstractYieldStrategy { IMultipleRateContext context = IMultipleRateContext(contextContract); return CalcSimpleInterest.calcPriceFromInterest( - numPeriodsElapsed, context.rateScaled(), context.frequency(), context.scale() + context.rateScaled(), numPeriodsElapsed, context.frequency(), context.scale() ); } diff --git a/packages/sdk/test/src/liquid-vault.spec.ts b/packages/sdk/test/src/liquid-vault.spec.ts new file mode 100644 index 000000000..6fac28025 --- /dev/null +++ b/packages/sdk/test/src/liquid-vault.spec.ts @@ -0,0 +1,103 @@ +import { ERC20__factory, LiquidContinuousMultiTokenVault__factory } from '@credbull/contracts'; +import { expect, test } from '@playwright/test'; +import {BigNumber, type BigNumberish, type CallOverrides, ethers} from 'ethers'; + +import { TestSigners } from './utils/test-signer'; + +let provider: ethers.providers.JsonRpcProvider; +let testSigners: TestSigners; + +test.beforeAll(async () => { + provider = new ethers.providers.JsonRpcProvider(); // no url, defaults to 'http://localhost:8545' + testSigners = new TestSigners(provider); +}); + +const VAULT_PROXY_CONTRACT_ADDRESS = '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9'; + +test.describe.skip('Test LiquidContinuousMultiTokenVault ethers operations', () => { + test('Test read operations', async () => { + const user = testSigners.alice; + + const liquidVault = LiquidContinuousMultiTokenVault__factory.connect( + VAULT_PROXY_CONTRACT_ADDRESS, + user.getDelegate(), + ); + + const blockNumber = provider.getBlockNumber(); + const block = await provider.getBlock(blockNumber); + + // check state initialized + expect(liquidVault.asset()).resolves.not.toEqual(ethers.constants.AddressZero); + expect(liquidVault._yieldStrategy()).resolves.not.toEqual(ethers.constants.AddressZero); + expect(liquidVault._redeemOptimizer()).resolves.not.toEqual(ethers.constants.AddressZero); + expect(liquidVault._vaultStartTimestamp()).resolves.not.toEqual(ethers.constants.AddressZero); + + // check the asset + const usdc = ERC20__factory.connect(await liquidVault.asset(), user.getDelegate()); + expect(usdc.symbol()).resolves.toEqual('sUSDC'); + + // check some behavior + const expectedTenor = 30; + expect(liquidVault._vaultStartTimestamp().then((ts) => ts.toNumber())).resolves.toBeLessThanOrEqual( + block.timestamp, + ); + expect(liquidVault.TENOR()).resolves.toEqual(BigNumber.from(expectedTenor)); + + const vaultStartTimestamp = (await liquidVault._vaultStartTimestamp()).toNumber(); + console.log('Vault StarTime:', new Date(vaultStartTimestamp * 1000).toUTCString()); + + expect(liquidVault['totalSupply()']().then((ts) => ts.toNumber())).resolves.toBeGreaterThanOrEqual( + BigNumber.from(0).toNumber(), + ); + + expect(liquidVault.currentPeriod()).resolves.toEqual(BigNumber.from(expectedTenor)); + }); + + test('Redeem a redeemRequest', async () => { + const user = testSigners.alice; + const redeemPeriod = BigNumber.from(30).toNumber(); // redeemPeriod and requestId are equal + + const liquidVault = LiquidContinuousMultiTokenVault__factory.connect( + VAULT_PROXY_CONTRACT_ADDRESS, + user.getDelegate(), + ); + const userAddress = await user.getAddress(); + + // unlock requests + const unlockRequestAmount = (await liquidVault.unlockRequestAmount(userAddress, redeemPeriod)).toNumber(); + + if (unlockRequestAmount > 0) { + console.log('Redeeming for redeemPeriod = %s ...', redeemPeriod); + + await liquidVault.redeem(unlockRequestAmount, userAddress, userAddress); + + // verify unlock succeeded + expect((await liquidVault.unlockRequestAmount(userAddress, redeemPeriod)).toNumber()).toEqual(0); + } + }); + + test('Release a redeemRequest without redeeming', async () => { + const user = testSigners.alice; + const redeemPeriod = BigNumber.from(30).toNumber(); // redeemPeriod and requestId are equal + + const liquidVault = LiquidContinuousMultiTokenVault__factory.connect( + VAULT_PROXY_CONTRACT_ADDRESS, + user.getDelegate(), + ); + const userAddress = await user.getAddress(); + + const unlockRequestAmount = (await liquidVault.unlockRequestAmount(userAddress, redeemPeriod)).toNumber(); + + if (unlockRequestAmount > 0) { + console.log('Releasing requestRedeem without redeeming for redeemPeriod = %s ...', redeemPeriod); + + // unlocks does NOT redeem. it only deletes the request. + await liquidVault.unlock(userAddress, BigNumber.from(redeemPeriod).toNumber()); + + // verify unlock succeeded + expect((await liquidVault.unlockRequestAmount(userAddress, redeemPeriod)).toNumber()).toEqual(0); + } + }); + + +}); diff --git a/packages/sdk/test/src/utils/liquid-vault.spec.ts b/packages/sdk/test/src/utils/liquid-vault.spec.ts deleted file mode 100644 index 62776ed4c..000000000 --- a/packages/sdk/test/src/utils/liquid-vault.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ERC20__factory, LiquidContinuousMultiTokenVault__factory } from '@credbull/contracts'; -import { expect, test } from '@playwright/test'; -import { BigNumber, Wallet, ethers } from 'ethers'; - -import { OWNER_PUBLIC_KEY_LOCAL, TestSigner, TestSigners } from './test-signer'; - -let provider: ethers.providers.JsonRpcProvider; -let testSigners: TestSigners; - -test.beforeAll(async () => { - provider = new ethers.providers.JsonRpcProvider(); // no url, defaults to 'http://localhost:8545' - testSigners = new TestSigners(provider); -}); - -// See: https://github.com/safe-global/safe-core-sdk/tree/main/packages/protocol-kit -test.describe.skip('Test reading contracts', () => { - test('Create a signer from the first account', async () => { - const owner = new TestSigner(0, provider).getDelegate(); - - expect(await owner.getAddress()).toEqual(OWNER_PUBLIC_KEY_LOCAL); - }); - - test('Test read operations', async () => { - const vaultProxyAddress = '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9'; // with data - - const liquidVault = LiquidContinuousMultiTokenVault__factory.connect( - vaultProxyAddress, - testSigners.admin.getDelegate(), - ); - - const blockNumber = provider.getBlockNumber(); - const block = await provider.getBlock(blockNumber); - - // check state initialized - expect(liquidVault.asset()).resolves.not.toEqual(ethers.constants.AddressZero); - expect(liquidVault._yieldStrategy()).resolves.not.toEqual(ethers.constants.AddressZero); - expect(liquidVault._redeemOptimizer()).resolves.not.toEqual(ethers.constants.AddressZero); - expect(liquidVault._vaultStartTimestamp()).resolves.not.toEqual(ethers.constants.AddressZero); - - // check the asset - const usdc = ERC20__factory.connect(await liquidVault.asset(), testSigners.admin.getDelegate()); - expect(usdc.symbol()).resolves.toEqual('sUSDC'); - - // check some behavior - const expectedTenor = 30; - expect(liquidVault._vaultStartTimestamp().then((ts) => ts.toNumber())).resolves.toBeLessThanOrEqual( - block.timestamp, - ); - expect(liquidVault.TENOR()).resolves.toEqual(BigNumber.from(expectedTenor)); - - // output the timestamp - const vaultStartTimestamp = (await liquidVault._vaultStartTimestamp()).toNumber(); - console.log('StarTime:', new Date(vaultStartTimestamp * 1000).toLocaleString()); - - // requires the data loaded version - expect(liquidVault['totalSupply()']().then((ts) => ts.toNumber())).resolves.toBeGreaterThanOrEqual( - BigNumber.from(0).toNumber(), - ); - expect(liquidVault.currentPeriod()).resolves.toEqual(BigNumber.from(expectedTenor)); - }); -}); diff --git a/packages/sdk/test/src/utils/test-signer.ts b/packages/sdk/test/src/utils/test-signer.ts index 0f711e5fa..be931c945 100644 --- a/packages/sdk/test/src/utils/test-signer.ts +++ b/packages/sdk/test/src/utils/test-signer.ts @@ -35,22 +35,25 @@ export class TestSigners { private _admin: TestSigner; private _operator: TestSigner; private _custodian: TestSigner; - private _treasury: TestSigner; + private _upgrader: TestSigner; private _deployer: TestSigner; - private _rewardVault: TestSigner; + private _treasury: TestSigner; + private _assetManager: TestSigner; private _alice: TestSigner; private _bob: TestSigner; + private _charlie: TestSigner; constructor(provider: providers.JsonRpcProvider) { this._admin = new TestSigner(0, provider); this._operator = new TestSigner(1, provider); this._custodian = new TestSigner(2, provider); - this._treasury = new TestSigner(3, provider); + this._upgrader = new TestSigner(3, provider); this._deployer = new TestSigner(4, provider); - this._rewardVault = new TestSigner(5, provider); - + this._treasury = new TestSigner(5, provider); + this._assetManager = new TestSigner(6, provider); this._alice = new TestSigner(7, provider); this._bob = new TestSigner(8, provider); + this._charlie = new TestSigner(9, provider); } get admin(): TestSigner { @@ -65,16 +68,20 @@ export class TestSigners { return this._custodian; } - get treasury(): TestSigner { - return this._treasury; + get upgrader(): TestSigner { + return this._upgrader; } get deployer(): TestSigner { return this._deployer; } - get rewardVault(): TestSigner { - return this._rewardVault; + get treasury(): TestSigner { + return this._treasury; + } + + get assetManager(): TestSigner { + return this._assetManager; } get alice(): TestSigner { @@ -84,4 +91,8 @@ export class TestSigners { get bob(): TestSigner { return this._bob; } + + get charlie(): TestSigner { + return this._charlie; + } } diff --git a/packages/sdk/test/src/utils/warp-anvil.spec.ts b/packages/sdk/test/src/utils/warp-anvil.spec.ts new file mode 100644 index 000000000..3d6a6e391 --- /dev/null +++ b/packages/sdk/test/src/utils/warp-anvil.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test'; +import { ethers } from 'ethers'; + +let provider: ethers.providers.JsonRpcProvider; + +test.beforeAll(async () => { + provider = new ethers.providers.JsonRpcProvider(); // no url, defaults to 'http://localhost:8545' +}); + +// See: https://github.com/safe-global/safe-core-sdk/tree/main/packages/protocol-kit +test.describe.skip('Warp ahead in time on Anvil', () => { + test('Warp time forward by 1 day and check new block timestamp', async () => { + // Fetch the current block number and timestamp + const blockNumber = await provider.getBlockNumber(); + const block = await provider.getBlock(blockNumber); + const currentBlockTime = block.timestamp; + + console.log('Current Block Time:', new Date(currentBlockTime * 1000).toLocaleString()); + + const oneDayInSeconds = 24 * 60 * 60; // 1 day (86400 seconds) + + await provider.send('evm_increaseTime', [oneDayInSeconds]); + await provider.send('evm_mine', []); // Mine a new block to apply the time change + + // Fetch the new block timestamp + const newBlockNumber = await provider.getBlockNumber(); + const newBlock = await provider.getBlock(newBlockNumber); + const newBlockTime = newBlock.timestamp; + + console.log('New Block Time:', new Date(newBlockTime * 1000).toLocaleString()); + + // Assert the new block time is within a reasonable range of the expected time + const toleranceInSeconds = 100; // Allow for a 100-second tolerance + const expectedBlockTime = currentBlockTime + oneDayInSeconds; + + expect(newBlockTime).toBeGreaterThanOrEqual(expectedBlockTime - toleranceInSeconds); + expect(newBlockTime).toBeLessThanOrEqual(expectedBlockTime + toleranceInSeconds); + }); +}); diff --git a/spikes/spike-liquid-stone/package.json b/spikes/spike-liquid-stone/package.json index f5c0cd9a1..9c18e4550 100644 --- a/spikes/spike-liquid-stone/package.json +++ b/spikes/spike-liquid-stone/package.json @@ -46,5 +46,8 @@ "alchemy-sdk": "^3.4.2", "ethers": "^6.13.3", "react-tooltip": "^5.28.0" + }, + "resolutions": { + "@babel/traverse": "^7.25.6" } } diff --git a/spikes/spike-liquid-stone/packages/nextjs/app/async/_components/ViewSection.tsx b/spikes/spike-liquid-stone/packages/nextjs/app/async/_components/ViewSection.tsx index 40ab518d5..a483d8902 100644 --- a/spikes/spike-liquid-stone/packages/nextjs/app/async/_components/ViewSection.tsx +++ b/spikes/spike-liquid-stone/packages/nextjs/app/async/_components/ViewSection.tsx @@ -8,27 +8,30 @@ import RequestUnlockAction from "./RequestUnlockAction"; import SetCurrentPeriod from "./SetCurrentPeriod"; import UnlockAction from "./UnlockAction"; import { useTheme } from "next-themes"; +import { useAccount } from "wagmi"; import { useFetchContractData } from "~~/hooks/async/useFetchContractData"; import { useFetchLocks } from "~~/hooks/async/useFetchLocks"; import { useFetchRequestDetails } from "~~/hooks/async/useFetchRequestDetails"; import { useFetchUnlockRequests } from "~~/hooks/async/useFetchUnlockRequests"; -import { ContractAbi } from "~~/utils/scaffold-eth/contract"; - -const ViewSection = ({ - address, - deployedContractAddress, - deployedContractAbi, - deployedContractLoading, -}: { - address: string | undefined; - deployedContractAddress: string; - deployedContractAbi: ContractAbi; - deployedContractLoading: boolean; -}) => { +import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; +import { ContractAbi, ContractName } from "~~/utils/scaffold-eth/contract"; +import { getAllContracts } from "~~/utils/scaffold-eth/contractsData"; + +const contractsData = getAllContracts(); + +const ViewSection = () => { const { resolvedTheme } = useTheme(); const [mounted, setMounted] = useState(false); const [refetch, setRefetch] = useState(false); + const { address } = useAccount(); + const contractNames = Object.keys(contractsData) as ContractName[]; + + const { data: implementationContractData, isLoading: implementationContractLoading } = useDeployedContractInfo( + contractNames[6], + ); + const { data: proxyContractData, isLoading: proxyContractLoading } = useDeployedContractInfo(contractNames[7]); + // Action Values const [expandedRowId, setExpandedRowId] = useState(null); const [selectedRequestId, setSelectedRequestId] = useState(""); @@ -38,22 +41,22 @@ const ViewSection = ({ }, []); const { noticePeriod, currentPeriod, minUnlockPeriod } = useFetchContractData({ - deployedContractAddress, - deployedContractAbi, + deployedContractAddress: proxyContractData?.address || "", + deployedContractAbi: implementationContractData?.abi as ContractAbi, dependencies: [refetch], }); const { lockDatas } = useFetchLocks({ address: address || "", - deployedContractAddress, - deployedContractAbi, + deployedContractAddress: proxyContractData?.address || "", + deployedContractAbi: implementationContractData?.abi as ContractAbi, refetch, }); const { unlockRequests } = useFetchUnlockRequests({ address: address || "", - deployedContractAddress, - deployedContractAbi, + deployedContractAddress: proxyContractData?.address || "", + deployedContractAbi: implementationContractData?.abi as ContractAbi, currentPeriod, noticePeriod, refetch, @@ -61,8 +64,8 @@ const ViewSection = ({ const { requestDetails } = useFetchRequestDetails({ address: address || "", - deployedContractAddress, - deployedContractAbi, + deployedContractAddress: proxyContractData?.address || "", + deployedContractAbi: implementationContractData?.abi as ContractAbi, requestId: expandedRowId, refetch, }); @@ -80,7 +83,7 @@ const ViewSection = ({ >

Contract Details

- {deployedContractLoading ? ( + {implementationContractLoading || proxyContractLoading ? ( ) : (
@@ -182,23 +185,23 @@ const ViewSection = ({ {/* Lock */} setRefetch(prev => !prev)} /> {/* Request Unlock */} setRefetch(prev => !prev)} /> {/* Unlock */} setRefetch(prev => !prev)} /> @@ -206,8 +209,8 @@ const ViewSection = ({ {/* SetCurrentPeriod */} setRefetch(prev => !prev)} />
diff --git a/spikes/spike-liquid-stone/packages/nextjs/app/async/page.tsx b/spikes/spike-liquid-stone/packages/nextjs/app/async/page.tsx index 4f531b04a..973c2649f 100644 --- a/spikes/spike-liquid-stone/packages/nextjs/app/async/page.tsx +++ b/spikes/spike-liquid-stone/packages/nextjs/app/async/page.tsx @@ -1,32 +1,17 @@ -"use client"; - import ViewSection from "./_components/ViewSection"; import type { NextPage } from "next"; -import { useAccount } from "wagmi"; -import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; -import { ContractAbi, ContractName } from "~~/utils/scaffold-eth/contract"; -import { getAllContracts } from "~~/utils/scaffold-eth/contractsData"; +import { getMetadata } from "~~/utils/scaffold-eth/getMetadata"; -const contractsData = getAllContracts(); +export const metadata = getMetadata({ + title: "Timelock Async Unlock", + description: "", +}); const AsyncInterface: NextPage = () => { - const { address } = useAccount(); - const contractNames = Object.keys(contractsData) as ContractName[]; - - const { data: implementationContractData, isLoading: implementationContractLoading } = useDeployedContractInfo( - contractNames[6], - ); - const { data: proxyContractData, isLoading: proxyContractLoading } = useDeployedContractInfo(contractNames[7]); - return ( <>
- +
); diff --git a/spikes/spike-liquid-stone/packages/nextjs/app/helpers/_components/ViewSection.tsx b/spikes/spike-liquid-stone/packages/nextjs/app/helpers/_components/ViewSection.tsx index fe0e4fcb4..273e0d0e5 100644 --- a/spikes/spike-liquid-stone/packages/nextjs/app/helpers/_components/ViewSection.tsx +++ b/spikes/spike-liquid-stone/packages/nextjs/app/helpers/_components/ViewSection.tsx @@ -19,7 +19,7 @@ import { useDeployedContractInfo, useTransactor } from "~~/hooks/scaffold-eth"; import { notification } from "~~/utils/scaffold-eth"; import { ContractAbi, ContractName } from "~~/utils/scaffold-eth/contract"; import { getAllContracts } from "~~/utils/scaffold-eth/contractsData"; -import { formatAddress } from "~~/utils/vault/general"; +import { formatAddress, formatNumber } from "~~/utils/vault/general"; const contractsData = getAllContracts(); @@ -43,11 +43,9 @@ const ViewSection = () => { const [numOfPeriods, setNumOfPeriods] = useState(""); const [assets, setAssets] = useState(""); const [reducedRate, setReducedRate] = useState(""); - const [effectivePeriod, setEffectivePeriod] = useState(""); const [selectedDate, setSelectedDate] = useState(null); const [selectedTimestamp, setSelectedTimestamp] = useState(null); - const [useCurrentPeriod, setUseCurrentPeriod] = useState(false); const contractNames = Object.keys(contractsData) as ContractName[]; const { data: simpleUsdcContractData } = useDeployedContractInfo(contractNames[0]); @@ -59,7 +57,7 @@ const ViewSection = () => { const custodian = process.env.NEXT_PUBLIC_CUSTODIAN || ""; - const { currentPeriod, startTimeNumber } = useFetchContractData({ + const { currentPeriod, startTimestamp, previousReducedRate } = useFetchContractData({ deployedContractAddress: proxyContractData?.address || "", deployedContractAbi: implementationContractData?.abi as ContractAbi, simpleUsdcContractData, @@ -244,20 +242,27 @@ const ViewSection = () => { const secondsInADay = BigInt(86400); switch (directionIndex) { case 0: // Going backward - updatedTime = startTimeNumber + BigInt(numOfPeriods) * secondsInADay; + if (previousReducedRate) { + notification.warning( + "Sorry! Since you initiated the reduced rate with a new value, we prevented going backward in the UI so it does not fail the Yield calculation.", + ); + setPeriodTrxLoading(false); + return; + } + updatedTime = startTimestamp + BigInt(numOfPeriods) * secondsInADay; const currentTimeStamp = Math.floor(Date.now() / 1000); if (updatedTime > currentTimeStamp) { - notification.error("Cannot move backward beyond the vault's start time."); + notification.warning("You are trying to set the current period as a negative value."); setPeriodTrxLoading(false); return; } break; case 1: // Going forward - updatedTime = startTimeNumber - BigInt(numOfPeriods) * secondsInADay; + updatedTime = startTimestamp - BigInt(numOfPeriods) * secondsInADay; break; default: - updatedTime = startTimeNumber + BigInt(numOfPeriods) * secondsInADay; + updatedTime = startTimestamp + BigInt(numOfPeriods) * secondsInADay; break; } @@ -300,11 +305,21 @@ const ViewSection = () => { const currentTimeStamp = Math.floor(Date.now() / 1000); if (selectedTimestamp > currentTimeStamp) { - notification.error("Cannot move backward beyond the vault's start time."); + notification.warning( + "Not Allowed in the UI. If we set the time to be after the current timestamp this will result in a negative current period value.", + ); setTimestampTrxLoading(false); return; } + if (selectedTimestamp > startTimestamp && previousReducedRate) { + notification.warning( + "Sorry! Since you initiated the reduced rate with a new value, we prevented going backward in the UI so it does not fail the Yield calculation.", + ); + setPeriodTrxLoading(false); + return; + } + if (writeContractAsync) { setTimestampTrxLoading(true); @@ -333,7 +348,7 @@ const ViewSection = () => { }; const handleSetReducedRate = async () => { - if (!reducedRate || !effectivePeriod) { + if (!reducedRate) { notification.error("Missing required fields"); return; } @@ -350,9 +365,9 @@ const ViewSection = () => { const makeSetReducedRateTrx = () => writeContractAsync({ address: proxyContractData?.address || "", - functionName: "setReducedRate", + functionName: "setReducedRateAtCurrent", abi: implementationContractData?.abi || [], - args: [ethers.parseUnits(reducedRate, 6), BigInt(effectivePeriod)], + args: [ethers.parseUnits(reducedRate, 6)], }); const trx = await writeTxn(makeSetReducedRateTrx); @@ -361,7 +376,6 @@ const ViewSection = () => { if (transactionReceipt.status === "success") { setRefetch(prev => !prev); setReducedRate(""); - setEffectivePeriod(""); setReducedRateTrxLoading(false); } } @@ -397,6 +411,9 @@ const ViewSection = () => { } const amountToWithdraw = !withdrawType ? ethers.parseUnits(assets?.toString(), 6) : vaultBalance; + const amountToWithdrawInt = BigInt( + typeof amountToWithdraw === "string" ? Math.floor(Number(amountToWithdraw) * 1e6) : amountToWithdraw, + ); if (vaultBalance < amountToWithdraw) { notification.error("Insufficient assets to withdraw"); @@ -409,7 +426,7 @@ const ViewSection = () => { address: proxyContractData?.address || "", functionName: "withdrawAsset", abi: implementationContractData?.abi || [], - args: [custodian, BigInt(amountToWithdraw)], + args: [custodian, BigInt(amountToWithdrawInt)], }); const trx = await writeTxn(makeWithdrawTrx); @@ -443,16 +460,6 @@ const ViewSection = () => { return ; } - // Toggling between using the current period or enabling input - const handleToggle = () => { - setUseCurrentPeriod(!useCurrentPeriod); - if (!useCurrentPeriod) { - setEffectivePeriod(currentPeriod.toString()); // Set effectivePeriod to currentPeriod - } else { - setEffectivePeriod(""); // Reset effectivePeriod - } - }; - return ( <>
@@ -488,6 +495,8 @@ const ViewSection = () => { bgColor="blue" tooltipData="Grant operator role" flex="flex-1" + disabled={!allDataFetched} + loading={!allDataFetched} onClickHandler={() => handleGrantRole(0)} /> ); }; diff --git a/spikes/spike-liquid-stone/packages/nextjs/components/general/ContractValueBadge.tsx b/spikes/spike-liquid-stone/packages/nextjs/components/general/ContractValueBadge.tsx index 4a81885f1..4c392e7d8 100644 --- a/spikes/spike-liquid-stone/packages/nextjs/components/general/ContractValueBadge.tsx +++ b/spikes/spike-liquid-stone/packages/nextjs/components/general/ContractValueBadge.tsx @@ -1,16 +1,19 @@ "use client"; +import { ReactNode } from "react"; import { useTheme } from "next-themes"; const ContractValueBadge = ({ name, value, theme = "gray", + icon, onClickHandler, }: { name?: string; value: any; theme?: "gray" | "red"; + icon?: ReactNode; onClickHandler?: (params: any) => void; }) => { const { resolvedTheme } = useTheme(); @@ -35,7 +38,18 @@ const ContractValueBadge = ({ }`} onClick={onClickHandler} > - {name ? `${name} :` : ""} {value} + {icon ? ( +
+ {name ? {name} : : ""} + {value} + {icon} +
+ ) : ( + + {name ? `${name} :` : ""} {value} {icon} + + )} +
{ const getRequestDetails = async () => { try { - if (!address || !deployedContractAddress || !requestId) return; + if (!address || !deployedContractAddress || deployedContractAbi || !requestId) return; const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545"); const contract = new ethers.Contract(deployedContractAddress, deployedContractAbi, provider); diff --git a/spikes/spike-liquid-stone/packages/nextjs/hooks/async/useFetchUnlockRequests.ts b/spikes/spike-liquid-stone/packages/nextjs/hooks/async/useFetchUnlockRequests.ts index 3b64ae6ba..985af6c25 100644 --- a/spikes/spike-liquid-stone/packages/nextjs/hooks/async/useFetchUnlockRequests.ts +++ b/spikes/spike-liquid-stone/packages/nextjs/hooks/async/useFetchUnlockRequests.ts @@ -24,7 +24,7 @@ export const useFetchUnlockRequests = ({ useEffect(() => { async function getUnlockRequests() { try { - if (!address || !deployedContractAddress || !noticePeriod) return; + if (!address || !deployedContractAddress || deployedContractAbi || !noticePeriod) return; const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545"); const contract = new ethers.Contract(deployedContractAddress, deployedContractAbi, provider); diff --git a/spikes/spike-liquid-stone/packages/nextjs/hooks/custom/useFetchContractData.ts b/spikes/spike-liquid-stone/packages/nextjs/hooks/custom/useFetchContractData.ts index bcbfb6cb4..983cefdd2 100644 --- a/spikes/spike-liquid-stone/packages/nextjs/hooks/custom/useFetchContractData.ts +++ b/spikes/spike-liquid-stone/packages/nextjs/hooks/custom/useFetchContractData.ts @@ -18,14 +18,16 @@ export const useFetchContractData = ({ }) => { const [currentPeriod, setCurrentPeriod] = useState(0); const [assetAmount, setAssetAmount] = useState(""); - const [startTimeNumber, setStartTimeNumber] = useState(BigInt(0)); + const [startTimestamp, setStartTimestamp] = useState(BigInt(0)); const [startTime, setStartTime] = useState(""); const [noticePeriod, setNoticePeriod] = useState(0); const [frequency, setFrequency] = useState(0); const [tenor, setTenor] = useState(0); const [scale, setScale] = useState(0); const [fullRate, setFullRate] = useState(0); - const [reducedRate, setReducedRate] = useState(0); + const [effectiveReducedRate, setEffectiveReducedRate] = useState("0"); + const [currentReducedRate, setCurrentReducedRate] = useState(0); + const [previousReducedRate, setPreviousReducedRate] = useState(0); const { refetch: refetchCurrentPeriod } = useReadContract({ address: deployedContractAddress, @@ -83,7 +85,14 @@ export const useFetchContractData = ({ args: [], }); - const { refetch: refetchReducedRate } = useReadContract({ + const { refetch: refetchPreviousReducedRate } = useReadContract({ + address: deployedContractAddress, + functionName: "previousPeriodRate", + abi: deployedContractAbi, + args: [], + }); + + const { refetch: refetchCurrentReducedRate } = useReadContract({ address: deployedContractAddress, functionName: "currentPeriodRate", abi: deployedContractAbi, @@ -94,14 +103,15 @@ export const useFetchContractData = ({ useEffect(() => { const fetchData = async () => { const currentPeriodData = await refetchCurrentPeriod(); - setCurrentPeriod(Number(currentPeriodData?.data)); + const _currentPeriod = Number(currentPeriodData?.data); + setCurrentPeriod(_currentPeriod); const assetAmountData = await refetchAssetAmount(); const assetAmountBigInt = BigInt(assetAmountData?.data as bigint); setAssetAmount(ethers.formatUnits(assetAmountBigInt, 6)); const startTimeData = await refetchStartTime(); - setStartTimeNumber(startTimeData?.data as bigint); + setStartTimestamp(startTimeData?.data as bigint); setStartTime(formatTimestamp(Number(startTimeData?.data))); const noticePeriodData = await refetchNoticePeriod(); @@ -121,9 +131,17 @@ export const useFetchContractData = ({ setFullRate(Number(fullRateData?.data) / scale); } - const reducedRateData = await refetchReducedRate(); + const previousReducedRateData = await refetchPreviousReducedRate(); + const currentReducedRateData = await refetchCurrentReducedRate(); + if (scale > 0) { - setReducedRate(Number((reducedRateData?.data as PeriodRate)?.interestRate) / scale); + setPreviousReducedRate(Number((previousReducedRateData?.data as PeriodRate)?.interestRate) / scale); + setCurrentReducedRate(Number((currentReducedRateData?.data as PeriodRate)?.interestRate) / scale); + if (_currentPeriod < Number(currentReducedRateData?.data?.effectiveFromPeriod)) { + setEffectiveReducedRate("0"); + } else { + setEffectiveReducedRate("1"); + } } }; @@ -139,7 +157,8 @@ export const useFetchContractData = ({ refetchTenor, refetchScale, refetchFullRate, - refetchReducedRate, + refetchPreviousReducedRate, + refetchCurrentReducedRate, scale, ...dependencies, ]); @@ -147,13 +166,15 @@ export const useFetchContractData = ({ return { currentPeriod, assetAmount, - startTimeNumber, + startTimestamp, startTime, noticePeriod, frequency, tenor, scale, fullRate, - reducedRate, + previousReducedRate, + currentReducedRate, + effectiveReducedRate, }; }; diff --git a/spikes/spike-liquid-stone/packages/nextjs/hooks/custom/useFetchDepositPools.ts b/spikes/spike-liquid-stone/packages/nextjs/hooks/custom/useFetchDepositPools.ts index 73e8ed389..c13bf81cb 100644 --- a/spikes/spike-liquid-stone/packages/nextjs/hooks/custom/useFetchDepositPools.ts +++ b/spikes/spike-liquid-stone/packages/nextjs/hooks/custom/useFetchDepositPools.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { OwnedNft } from "alchemy-sdk"; import { ethers } from "ethers"; import { DepositPool } from "~~/types/vault"; +import { notification } from "~~/utils/scaffold-eth"; import { getNFTsForOwner } from "~~/utils/vault/web3"; export const useFetchDepositPools = ({ @@ -41,7 +42,7 @@ export const useFetchDepositPools = ({ useEffect(() => { async function fetchBalances() { - if (!deployedContractAddress || !address || userDepositIds.length === 0) return; + if (!deployedContractAddress || !deployedContractAbi || !address || userDepositIds.length === 0) return; const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545"); const contract = new ethers.Contract(deployedContractAddress, deployedContractAbi, provider); @@ -61,8 +62,20 @@ export const useFetchDepositPools = ({ const unlockRequestAmount = ethers.formatUnits(unlockRequestAmountBigInt, 6); if (balanceBigInt > 0 && Number(depositId) <= currentPeriod) { - const yieldAmount = - currentPeriod > Number(depositId) ? await contract.calcYield(balanceBigInt, depositId, currentPeriod) : 0; + let yieldAmount = 0; + + try { + yieldAmount = + currentPeriod > Number(depositId) + ? await contract.calcYield(balanceBigInt, depositId, currentPeriod) + : 0; + } catch (error) { + yieldAmount = 0; + + notification.warning( + "It seems like you are testing a wrong scenario! You may set the reduced rate and get back to a period where it is not effective anymore. So you will see a wrong number regarding the `Yield Amount`.", + ); + } return { depositId, @@ -75,7 +88,13 @@ export const useFetchDepositPools = ({ return null; } catch (error) { - console.error("Error fetching balance for depositId:", depositId.tokenId, error); + console.error( + "Error fetching balance for depositId:", + depositId, + " and currentPeriod: ", + currentPeriod, + error, + ); return null; } }); diff --git a/spikes/spike-liquid-stone/packages/nextjs/hooks/custom/useFetchRedeemRequests.ts b/spikes/spike-liquid-stone/packages/nextjs/hooks/custom/useFetchRedeemRequests.ts index 0c7746946..259215cf5 100644 --- a/spikes/spike-liquid-stone/packages/nextjs/hooks/custom/useFetchRedeemRequests.ts +++ b/spikes/spike-liquid-stone/packages/nextjs/hooks/custom/useFetchRedeemRequests.ts @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { ethers } from "ethers"; import { RedeemRequest } from "~~/types/vault"; +import { notification } from "~~/utils/scaffold-eth"; export const useFetchRedeemRequests = ({ address, @@ -26,12 +27,42 @@ export const useFetchRedeemRequests = ({ const requests: RedeemRequest[] = []; for (let i = 0; i <= currentPeriod + 1; i++) { - const unlockAmount = await contract.unlockRequestAmount(address, i); + // Fetch the deposit periods and amounts (shares) from unlockRequests + const [depositPeriods, shares] = await contract.unlockRequests(address, i); - if (unlockAmount > 0) { + // If there are no shares, skip this request + if (shares.length === 0) continue; + + let totalShareAmount = BigInt(0); + let totalAssetAmount = BigInt(0); + + // Loop through each deposit period and calculate the corresponding asset value + for (let index = 0; index < depositPeriods.length; index++) { + const depositPeriod = depositPeriods[index]; + const share = shares[index]; + + totalShareAmount += ethers.toBigInt(share); + + // only yield if the deposit was in the past + if (currentPeriod > depositPeriod) { + try { + const assetAmount = await contract.convertToAssetsForDepositPeriod(share, depositPeriod); + totalAssetAmount += ethers.toBigInt(assetAmount); + } catch (error) { + notification.warning( + "It seems like you are testing a wrong scenario! You may set the reduced rate and get back to a period where it is not effective anymore. So you will see a wrong number regarding the `Assets Amount` in some of your redeem requests.", + ); + } + } else { + totalAssetAmount += ethers.toBigInt(share); // yield is 0, assets = shares + } + } + + if (totalShareAmount > 0) { const redeemRequest: RedeemRequest = { id: i, - amount: ethers.formatUnits(unlockAmount, 6) as unknown as bigint, + shareAmount: ethers.formatUnits(totalShareAmount, 6) as unknown as bigint, + assetAmount: ethers.formatUnits(totalAssetAmount, 6) as unknown as bigint, }; requests.push(redeemRequest); } diff --git a/spikes/spike-liquid-stone/packages/nextjs/types/vault.ts b/spikes/spike-liquid-stone/packages/nextjs/types/vault.ts index 0c250d9e4..d087085a6 100644 --- a/spikes/spike-liquid-stone/packages/nextjs/types/vault.ts +++ b/spikes/spike-liquid-stone/packages/nextjs/types/vault.ts @@ -15,5 +15,6 @@ export type DepositPool = { export type RedeemRequest = { id: number; - amount: bigint; + shareAmount: bigint; + assetAmount: bigint; }; diff --git a/spikes/spike-liquid-stone/packages/nextjs/utils/vault/general.ts b/spikes/spike-liquid-stone/packages/nextjs/utils/vault/general.ts index f6a96f7c0..2b33c4f57 100644 --- a/spikes/spike-liquid-stone/packages/nextjs/utils/vault/general.ts +++ b/spikes/spike-liquid-stone/packages/nextjs/utils/vault/general.ts @@ -17,3 +17,8 @@ export const formatAddress = (address: string, chars = 6) => { const end = address.slice(-chars); return `${start}...${end}`; }; + +export const formatNumber = (value: string | number | bigint) => + Number(value).toLocaleString("en-US", { + minimumFractionDigits: value.toString().split(".")[1]?.length || 0, + }); diff --git a/spikes/spike-liquid-stone/packages/nextjs/utils/vault/web3.ts b/spikes/spike-liquid-stone/packages/nextjs/utils/vault/web3.ts index 901ef3d27..8331302f1 100644 --- a/spikes/spike-liquid-stone/packages/nextjs/utils/vault/web3.ts +++ b/spikes/spike-liquid-stone/packages/nextjs/utils/vault/web3.ts @@ -2,7 +2,7 @@ import { Alchemy, AlchemySettings, Network } from "alchemy-sdk"; export async function getNFTsForOwner(chain: number, owner: string, contract: string) { if (chain === 31337) { - return Array.from({ length: 101 }, (_, index) => index); + return Array.from({ length: 1001 }, (_, index) => index); } else { const settings: AlchemySettings = { apiKey: process.env.ALCHEMY_API_KEY, diff --git a/spikes/spike-liquid-stone/yarn.lock b/spikes/spike-liquid-stone/yarn.lock index 092059a86..52b6f7e3e 100644 --- a/spikes/spike-liquid-stone/yarn.lock +++ b/spikes/spike-liquid-stone/yarn.lock @@ -26,13 +26,14 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/code-frame@npm:7.24.7" +"@babel/code-frame@npm:^7.25.9": + version: 7.26.0 + resolution: "@babel/code-frame@npm:7.26.0" dependencies: - "@babel/highlight": ^7.24.7 + "@babel/helper-validator-identifier": ^7.25.9 + js-tokens: ^4.0.0 picocolors: ^1.0.0 - checksum: 830e62cd38775fdf84d612544251ce773d544a8e63df667728cc9e0126eeef14c6ebda79be0f0bc307e8318316b7f58c27ce86702e0a1f5c321d842eb38ffda4 + checksum: 2a677369e9b80b956401809485e8c2ae24df5e6076f669cf26a2809fcb88f91c2f6bb1bf3fb799dfe8487b2b7a276b62d14ac230a79d7ac8c7b369090d0a43fc languageName: node linkType: hard @@ -47,52 +48,16 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.17.3": - version: 7.25.0 - resolution: "@babel/generator@npm:7.25.0" +"@babel/generator@npm:^7.25.9": + version: 7.26.0 + resolution: "@babel/generator@npm:7.26.0" dependencies: - "@babel/types": ^7.25.0 + "@babel/parser": ^7.26.0 + "@babel/types": ^7.26.0 "@jridgewell/gen-mapping": ^0.3.5 "@jridgewell/trace-mapping": ^0.3.25 - jsesc: ^2.5.1 - checksum: bf25649dde4068bff8e387319bf820f2cb3b1af7b8c0cfba0bd90880656427c8bad96cd5cb6db7058d20cffe93149ee59da16567018ceaa21ecaefbf780a785c - languageName: node - linkType: hard - -"@babel/helper-environment-visitor@npm:^7.16.7": - version: 7.24.7 - resolution: "@babel/helper-environment-visitor@npm:7.24.7" - dependencies: - "@babel/types": ^7.24.7 - checksum: 079d86e65701b29ebc10baf6ed548d17c19b808a07aa6885cc141b690a78581b180ee92b580d755361dc3b16adf975b2d2058b8ce6c86675fcaf43cf22f2f7c6 - languageName: node - linkType: hard - -"@babel/helper-function-name@npm:^7.16.7": - version: 7.24.7 - resolution: "@babel/helper-function-name@npm:7.24.7" - dependencies: - "@babel/template": ^7.24.7 - "@babel/types": ^7.24.7 - checksum: 142ee08922074dfdc0ff358e09ef9f07adf3671ab6eef4fca74dcf7a551f1a43717e7efa358c9e28d7eea84c28d7f177b7a58c70452fc312ae3b1893c5dab2a4 - languageName: node - linkType: hard - -"@babel/helper-hoist-variables@npm:^7.16.7": - version: 7.24.7 - resolution: "@babel/helper-hoist-variables@npm:7.24.7" - dependencies: - "@babel/types": ^7.24.7 - checksum: 6cfdcf2289cd12185dcdbdf2435fa8d3447b797ac75851166de9fc8503e2fd0021db6baf8dfbecad3753e582c08e6a3f805c8d00cbed756060a877d705bd8d8d - languageName: node - linkType: hard - -"@babel/helper-split-export-declaration@npm:^7.16.7": - version: 7.24.7 - resolution: "@babel/helper-split-export-declaration@npm:7.24.7" - dependencies: - "@babel/types": ^7.24.7 - checksum: e3ddc91273e5da67c6953f4aa34154d005a00791dc7afa6f41894e768748540f6ebcac5d16e72541aea0c89bee4b89b4da6a3d65972a0ea8bfd2352eda5b7e22 + jsesc: ^3.0.2 + checksum: 3b1edb8202f39e1600eb1342a04571b8ba66148b7165ec3cf7a072696fa81301f373648e19492289aa832e60a42f3ed367ae4b1ae6ad92968393f11a35dae70c languageName: node linkType: hard @@ -103,6 +68,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-string-parser@npm:7.25.9" + checksum: 6435ee0849e101681c1849868278b5aee82686ba2c1e27280e5e8aca6233af6810d39f8e4e693d2f2a44a3728a6ccfd66f72d71826a94105b86b731697cdfa99 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.16.7, @babel/helper-validator-identifier@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-validator-identifier@npm:7.24.7" @@ -110,19 +82,14 @@ __metadata: languageName: node linkType: hard -"@babel/highlight@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/highlight@npm:7.24.7" - dependencies: - "@babel/helper-validator-identifier": ^7.24.7 - chalk: ^2.4.2 - js-tokens: ^4.0.0 - picocolors: ^1.0.0 - checksum: 5cd3a89f143671c4ac129960024ba678b669e6fc673ce078030f5175002d1d3d52bc10b22c5b916a6faf644b5028e9a4bd2bb264d053d9b05b6a98690f1d46f1 +"@babel/helper-validator-identifier@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-identifier@npm:7.25.9" + checksum: 5b85918cb1a92a7f3f508ea02699e8d2422fe17ea8e82acd445006c0ef7520fbf48e3dbcdaf7b0a1d571fc3a2715a29719e5226636cb6042e15fe6ed2a590944 languageName: node linkType: hard -"@babel/parser@npm:^7.17.3, @babel/parser@npm:^7.20.5, @babel/parser@npm:^7.25.0": +"@babel/parser@npm:^7.20.5": version: 7.25.3 resolution: "@babel/parser@npm:7.25.3" dependencies: @@ -133,6 +100,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.0": + version: 7.26.1 + resolution: "@babel/parser@npm:7.26.1" + dependencies: + "@babel/types": ^7.26.0 + bin: + parser: ./bin/babel-parser.js + checksum: 354320d1a0a7102b2f25620ceea1bbc809f5225432a73e8a8874009d2f82ed29e2b035fe68fb6d18bb7eafed78df1ec0fa12e8d8226b295d7a020f9b852de653 + languageName: node + linkType: hard + "@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2": version: 7.25.0 resolution: "@babel/runtime@npm:7.25.0" @@ -142,32 +120,29 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.24.7": - version: 7.25.0 - resolution: "@babel/template@npm:7.25.0" +"@babel/template@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/template@npm:7.25.9" dependencies: - "@babel/code-frame": ^7.24.7 - "@babel/parser": ^7.25.0 - "@babel/types": ^7.25.0 - checksum: 3f2db568718756d0daf2a16927b78f00c425046b654cd30b450006f2e84bdccaf0cbe6dc04994aa1f5f6a4398da2f11f3640a4d3ee31722e43539c4c919c817b + "@babel/code-frame": ^7.25.9 + "@babel/parser": ^7.25.9 + "@babel/types": ^7.25.9 + checksum: 103641fea19c7f4e82dc913aa6b6ac157112a96d7c724d513288f538b84bae04fb87b1f1e495ac1736367b1bc30e10f058b30208fb25f66038e1f1eb4e426472 languageName: node linkType: hard -"@babel/traverse@npm:7.17.3": - version: 7.17.3 - resolution: "@babel/traverse@npm:7.17.3" +"@babel/traverse@npm:^7.25.6": + version: 7.25.9 + resolution: "@babel/traverse@npm:7.25.9" dependencies: - "@babel/code-frame": ^7.16.7 - "@babel/generator": ^7.17.3 - "@babel/helper-environment-visitor": ^7.16.7 - "@babel/helper-function-name": ^7.16.7 - "@babel/helper-hoist-variables": ^7.16.7 - "@babel/helper-split-export-declaration": ^7.16.7 - "@babel/parser": ^7.17.3 - "@babel/types": ^7.17.0 - debug: ^4.1.0 + "@babel/code-frame": ^7.25.9 + "@babel/generator": ^7.25.9 + "@babel/parser": ^7.25.9 + "@babel/template": ^7.25.9 + "@babel/types": ^7.25.9 + debug: ^4.3.1 globals: ^11.1.0 - checksum: 780d7ecf711758174989794891af08d378f81febdb8932056c0d9979524bf0298e28f8e7708a872d7781151506c28f56c85c63ea3f1f654662c2fcb8a3eb9fdc + checksum: 901d325662ff1dd9bc51de00862e01055fa6bc374f5297d7e3731f2f0e268bbb1d2141f53fa82860aa308ee44afdcf186a948f16c83153927925804b95a9594d languageName: node linkType: hard @@ -181,7 +156,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.17.0, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.2": +"@babel/types@npm:^7.17.0, @babel/types@npm:^7.25.2": version: 7.25.2 resolution: "@babel/types@npm:7.25.2" dependencies: @@ -192,6 +167,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/types@npm:7.26.0" + dependencies: + "@babel/helper-string-parser": ^7.25.9 + "@babel/helper-validator-identifier": ^7.25.9 + checksum: a3dd37dabac693018872da96edb8c1843a605c1bfacde6c3f504fba79b972426a6f24df70aa646356c0c1b19bdd2c722c623c684a996c002381071680602280d + languageName: node + linkType: hard + "@coinbase/wallet-sdk@npm:4.0.4": version: 4.0.4 resolution: "@coinbase/wallet-sdk@npm:4.0.4" @@ -3169,15 +3154,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^3.2.1": - version: 3.2.1 - resolution: "ansi-styles@npm:3.2.1" - dependencies: - color-convert: ^1.9.0 - checksum: d85ade01c10e5dd77b6c89f34ed7531da5830d2cb5882c645f330079975b716438cd7ebb81d0d6e6b4f9c577f19ae41ab55f07f19786b02f9dfd9e0377395665 - languageName: node - linkType: hard - "ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": version: 4.3.0 resolution: "ansi-styles@npm:4.3.0" @@ -3742,17 +3718,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^2.4.2": - version: 2.4.2 - resolution: "chalk@npm:2.4.2" - dependencies: - ansi-styles: ^3.2.1 - escape-string-regexp: ^1.0.5 - supports-color: ^5.3.0 - checksum: ec3661d38fe77f681200f878edbd9448821924e0f93a9cefc0e26a33b145f1027a2084bf19967160d11e1f03bfe4eaffcabf5493b89098b2782c3fe0b03d80c2 - languageName: node - linkType: hard - "chalk@npm:^4.0.0, chalk@npm:^4.1.1": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -3925,15 +3890,6 @@ __metadata: languageName: node linkType: hard -"color-convert@npm:^1.9.0": - version: 1.9.3 - resolution: "color-convert@npm:1.9.3" - dependencies: - color-name: 1.1.3 - checksum: fd7a64a17cde98fb923b1dd05c5f2e6f7aefda1b60d67e8d449f9328b4e53b228a428fd38bfeaeb2db2ff6b6503a776a996150b80cdf224062af08a5c8a3a203 - languageName: node - linkType: hard - "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -3943,13 +3899,6 @@ __metadata: languageName: node linkType: hard -"color-name@npm:1.1.3": - version: 1.1.3 - resolution: "color-name@npm:1.1.3" - checksum: 09c5d3e33d2105850153b14466501f2bfb30324a2f76568a408763a3b7433b0e50e5b4ab1947868e65cb101bb7cb75029553f2c333b6d4b8138a73fcc133d69d - languageName: node - linkType: hard - "color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" @@ -4229,7 +4178,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.6": +"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.6": version: 4.3.6 resolution: "debug@npm:4.3.6" dependencies: @@ -4268,6 +4217,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.3.1": + version: 4.3.7 + resolution: "debug@npm:4.3.7" + dependencies: + ms: ^2.1.3 + peerDependenciesMeta: + supports-color: + optional: true + checksum: 822d74e209cd910ef0802d261b150314bbcf36c582ccdbb3e70f0894823c17e49a50d3e66d96b633524263975ca16b6a833f3e3b7e030c157169a5fabac63160 + languageName: node + linkType: hard + "decamelize@npm:^1.2.0": version: 1.2.0 resolution: "decamelize@npm:1.2.0" @@ -5084,13 +5045,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^1.0.5": - version: 1.0.5 - resolution: "escape-string-regexp@npm:1.0.5" - checksum: 6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 - languageName: node - linkType: hard - "escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -6224,13 +6178,6 @@ __metadata: languageName: node linkType: hard -"has-flag@npm:^3.0.0": - version: 3.0.0 - resolution: "has-flag@npm:3.0.0" - checksum: 4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b - languageName: node - linkType: hard - "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -7047,6 +6994,15 @@ __metadata: languageName: node linkType: hard +"jsesc@npm:^3.0.2": + version: 3.0.2 + resolution: "jsesc@npm:3.0.2" + bin: + jsesc: bin/jsesc + checksum: a36d3ca40574a974d9c2063bf68c2b6141c20da8f2a36bd3279fc802563f35f0527a6c828801295bdfb2803952cf2cf387786c2c90ed564f88d5782475abfe3c + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -7786,7 +7742,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.1.1": +"ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -10024,15 +9980,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^5.3.0": - version: 5.5.0 - resolution: "supports-color@npm:5.5.0" - dependencies: - has-flag: ^3.0.0 - checksum: 95f6f4ba5afdf92f495b5a912d4abee8dcba766ae719b975c56c084f5004845f6f5a5f7769f52d53f40e21952a6d87411bafe34af4a01e65f9926002e38e1dac - languageName: node - linkType: hard - "supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0"