diff --git a/packages/contracts/src/token/ERC1155/IMultiTokenVault.sol b/packages/contracts/src/token/ERC1155/IMultiTokenVault.sol index 2b63fb98f..95268bc85 100644 --- a/packages/contracts/src/token/ERC1155/IMultiTokenVault.sol +++ b/packages/contracts/src/token/ERC1155/IMultiTokenVault.sol @@ -181,4 +181,10 @@ interface IMultiTokenVault is IERC1155 { * @return currentPeriodsElapsed_ The number of elapsed time periods. */ function currentPeriodsElapsed() external view returns (uint256 currentPeriodsElapsed_); + + /** + * @notice Indicates whether any token exist with a given `depositPeriod`, or not. + * @return [true] if there is supply at `depositPeriod`, [false] otherwise. + */ + function exists(uint256 depositPeriod) external view returns (bool); } diff --git a/packages/contracts/src/token/ERC1155/IRedeemOptimizer.sol b/packages/contracts/src/token/ERC1155/IRedeemOptimizer.sol index e088e1a60..4aa53c5d2 100644 --- a/packages/contracts/src/token/ERC1155/IRedeemOptimizer.sol +++ b/packages/contracts/src/token/ERC1155/IRedeemOptimizer.sol @@ -29,7 +29,6 @@ interface IRedeemOptimizer { */ function optimize(IMultiTokenVault vault, address owner, uint256 shares, uint256 assets, uint256 redeemPeriod) external - view returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods); /** @@ -39,7 +38,6 @@ interface IRedeemOptimizer { */ function optimizeRedeemShares(IMultiTokenVault vault, address owner, uint256 shares, uint256 redeemPeriod) external - view returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods); /** @@ -49,6 +47,5 @@ interface IRedeemOptimizer { */ function optimizeWithdrawAssets(IMultiTokenVault vault, address owner, uint256 assets, uint256 redeemPeriod) external - view returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods); } diff --git a/packages/contracts/src/token/ERC1155/MultiTokenVault.sol b/packages/contracts/src/token/ERC1155/MultiTokenVault.sol index 370cdafd3..d849634ab 100644 --- a/packages/contracts/src/token/ERC1155/MultiTokenVault.sol +++ b/packages/contracts/src/token/ERC1155/MultiTokenVault.sol @@ -319,6 +319,19 @@ abstract contract MultiTokenVault is ERC1155SupplyUpgradeable._update(from, to, ids, values); } + /** + * @inheritdoc ERC1155SupplyUpgradeable + */ + function exists(uint256 id) + public + view + virtual + override(IMultiTokenVault, ERC1155SupplyUpgradeable) + returns (bool) + { + return ERC1155SupplyUpgradeable.exists(id); + } + /** * @inheritdoc ERC1155Upgradeable */ diff --git a/packages/contracts/src/token/ERC1155/RedeemOptimizer.sol b/packages/contracts/src/token/ERC1155/RedeemOptimizer.sol new file mode 100644 index 000000000..75daa49be --- /dev/null +++ b/packages/contracts/src/token/ERC1155/RedeemOptimizer.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { IMultiTokenVault } from "@credbull/token/ERC1155/IMultiTokenVault.sol"; +import { IRedeemOptimizer } from "@credbull/token/ERC1155/IRedeemOptimizer.sol"; + +/** + * @title RedeemOptimizer + * @dev Provides Optimizes the redemption of shares using a FIFO strategy. + */ +abstract contract RedeemOptimizer is IRedeemOptimizer { + error RedeemOptimizer__InvalidDepositPeriodRange(uint256 fromPeriod, uint256 toPeriod); + error RedeemOptimizer__FutureToDepositPeriod(uint256 toPeriod, uint256 currentPeriod); + error RedeemOptimizer__OptimizerFailed(uint256 amountFound, uint256 amountToFind); + + OptimizerBasis public immutable DEFAULT_BASIS; + uint256 private _startDepositPeriod; + + constructor(OptimizerBasis defaultBasis, uint256 startDepositPeriod) { + DEFAULT_BASIS = defaultBasis; + _startDepositPeriod = startDepositPeriod; + } + + /// @inheritdoc IRedeemOptimizer + function optimize(IMultiTokenVault vault, address owner, uint256 shares, uint256 assets, uint256 redeemPeriod) + public + virtual + returns (uint256[] memory depositPeriods_, uint256[] memory sharesAtPeriods_) + { + return OptimizerBasis.AssetsWithReturns == DEFAULT_BASIS + ? optimizeWithdrawAssets(vault, owner, assets, redeemPeriod) + : optimizeRedeemShares(vault, owner, shares, redeemPeriod); + } + + /// @inheritdoc IRedeemOptimizer + function optimizeRedeemShares(IMultiTokenVault vault, address owner, uint256 shares, uint256 redeemPeriod) + public + virtual + returns (uint256[] memory depositPeriods_, uint256[] memory sharesAtPeriods_) + { + OptimizerParams memory optimizerParams = OptimizerParams({ + owner: owner, + amountToFind: shares, + fromDepositPeriod: _earliestPeriodWithDeposit(vault), + toDepositPeriod: vault.currentPeriodsElapsed(), + redeemPeriod: redeemPeriod, + optimizerBasis: OptimizerBasis.Shares + }); + _assertOptimization(vault, optimizerParams); + return _findAmount(vault, optimizerParams); + } + + /// @inheritdoc IRedeemOptimizer + /// @dev - assets include deposit (principal) and any returns up to the redeem period + function optimizeWithdrawAssets(IMultiTokenVault vault, address owner, uint256 assets, uint256 redeemPeriod) + public + virtual + returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) + { + OptimizerParams memory optimizerParams = OptimizerParams({ + owner: owner, + amountToFind: assets, + fromDepositPeriod: _earliestPeriodWithDeposit(vault), + toDepositPeriod: vault.currentPeriodsElapsed(), + redeemPeriod: redeemPeriod, + optimizerBasis: OptimizerBasis.AssetsWithReturns + }); + _assertOptimization(vault, optimizerParams); + return _findAmount(vault, optimizerParams); + } + + /** + * @notice Execute checks against the parameters to verify that the optimization is possible. + * + * @param vault The [IMultiTokenVault] to query. + * @param optimizerParams The [OptimizerParams] governing the optimization. + */ + function _assertOptimization(IMultiTokenVault vault, OptimizerParams memory optimizerParams) + internal + view + virtual + { + if (optimizerParams.fromDepositPeriod > optimizerParams.toDepositPeriod) { + revert RedeemOptimizer__InvalidDepositPeriodRange( + optimizerParams.fromDepositPeriod, optimizerParams.toDepositPeriod + ); + } + + if (optimizerParams.toDepositPeriod > vault.currentPeriodsElapsed()) { + revert RedeemOptimizer__FutureToDepositPeriod( + optimizerParams.toDepositPeriod, vault.currentPeriodsElapsed() + ); + } + + // NOTE (JL,2024-10-08): Why no `redeemPeriod` checks? + } + + /** + * @notice Returns deposit periods and corresponding shares amounts according to the `optimizerParams` and the + * realisation strategy. + * @dev Queries the vault and processes the data according to the realised strategy to determine AN optimial + * arrangement of Deposit Period and associated Share Amounts to satisfy the redeem requirement. + * + * @param vault The [IMultiTokenVault] to query. + * @param optimizerParams The [OptimizerParams] governing the optimization. + * @return depositPeriods The result array of Deposit Periods. + * @return sharesAtPeriods The result array of Share Amounts. + */ + function _findAmount(IMultiTokenVault vault, OptimizerParams memory optimizerParams) + internal + view + virtual + returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods); + + /** + * @notice Determines the earliest Deposit Period at which there are deposits. + * @dev Queries the `vault` to find the earliest Deposit Period at which there are deposits. Sets this value as + * `_startDepositPeriod`, the starting point for the optimizer and returns the same value. + * @return _startDepositPeriod The earliest Deposit Period at which there are deposits. + */ + function _earliestPeriodWithDeposit(IMultiTokenVault vault) internal virtual returns (uint256) { + for (uint256 period = _startDepositPeriod; period <= vault.currentPeriodsElapsed(); ++period) { + if (vault.exists(period)) { + _startDepositPeriod = period; + break; + } + } + return _startDepositPeriod; + } + + /** + * @notice Utility function that trims the specified arrays to the specified size. + * @dev Allocates 2 arrays of size `toSize` and copies the `array1` and `array2` elements to their corresponding + * trimmed version. Assumes that the parameter arrays are at least as large as `toSize`. + * + * @param toSize The size to trim the arrays to. + * @param toTrim1 The first array to trim. + * @param toTrim2 The second array to trim. + * @return trimmed1 The trimmed version of `array1`. + * @return trimmed2 The trimmed version of `array2`. + */ + function _trimToSize(uint256 toSize, uint256[] memory toTrim1, uint256[] memory toTrim2) + internal + pure + virtual + returns (uint256[] memory trimmed1, uint256[] memory trimmed2) + { + trimmed1 = new uint256[](toSize); + trimmed2 = new uint256[](toSize); + for (uint256 i = 0; i < toSize; i++) { + trimmed1[i] = toTrim1[i]; + trimmed2[i] = toTrim2[i]; + } + } +} diff --git a/packages/contracts/src/token/ERC1155/RedeemOptimizerFIFO.sol b/packages/contracts/src/token/ERC1155/RedeemOptimizerFIFO.sol index e84a80767..5a3a8930a 100644 --- a/packages/contracts/src/token/ERC1155/RedeemOptimizerFIFO.sol +++ b/packages/contracts/src/token/ERC1155/RedeemOptimizerFIFO.sol @@ -2,102 +2,30 @@ pragma solidity ^0.8.20; import { IMultiTokenVault } from "@credbull/token/ERC1155/IMultiTokenVault.sol"; -import { IRedeemOptimizer } from "@credbull/token/ERC1155/IRedeemOptimizer.sol"; +import { RedeemOptimizer } from "@credbull/token/ERC1155/RedeemOptimizer.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; /** * @title RedeemOptimizerFIFO * @dev Optimizes the redemption of shares using a FIFO strategy. */ -contract RedeemOptimizerFIFO is IRedeemOptimizer { +contract RedeemOptimizerFIFO is RedeemOptimizer { using Math for uint256; - error RedeemOptimizer__InvalidDepositPeriodRange(uint256 fromPeriod, uint256 toPeriod); - error RedeemOptimizer__FutureToDepositPeriod(uint256 toPeriod, uint256 currentPeriod); - error RedeemOptimizer__OptimizerFailed(uint256 amountFound, uint256 amountToFind); - - OptimizerBasis public immutable DEFAULT_BASIS; - uint256 public immutable START_DEPOSIT_PERIOD; - - constructor(OptimizerBasis defaultBasis, uint256 startDepositPeriod) { - DEFAULT_BASIS = defaultBasis; - START_DEPOSIT_PERIOD = startDepositPeriod; - } - - /// @inheritdoc IRedeemOptimizer - function optimize(IMultiTokenVault vault, address owner, uint256 shares, uint256 assets, uint256 redeemPeriod) - public - view - returns (uint256[] memory depositPeriods_, uint256[] memory sharesAtPeriods_) - { - return OptimizerBasis.AssetsWithReturns == DEFAULT_BASIS - ? optimizeWithdrawAssets(vault, owner, assets, redeemPeriod) - : optimizeRedeemShares(vault, owner, shares, redeemPeriod); - } - - /// @inheritdoc IRedeemOptimizer - function optimizeRedeemShares(IMultiTokenVault vault, address owner, uint256 shares, uint256 redeemPeriod) - public - view - returns (uint256[] memory depositPeriods_, uint256[] memory sharesAtPeriods_) - { - return _findAmount( - vault, - OptimizerParams({ - owner: owner, - amountToFind: shares, - fromDepositPeriod: START_DEPOSIT_PERIOD, - toDepositPeriod: vault.currentPeriodsElapsed(), - redeemPeriod: redeemPeriod, - optimizerBasis: OptimizerBasis.Shares - }) - ); - } - - /// @inheritdoc IRedeemOptimizer - /// @dev - assets include deposit (principal) and any returns up to the redeem period - function optimizeWithdrawAssets(IMultiTokenVault vault, address owner, uint256 assets, uint256 redeemPeriod) - public - view - returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) - { - return _findAmount( - vault, - OptimizerParams({ - owner: owner, - amountToFind: assets, - fromDepositPeriod: START_DEPOSIT_PERIOD, - toDepositPeriod: vault.currentPeriodsElapsed(), - redeemPeriod: redeemPeriod, - optimizerBasis: OptimizerBasis.AssetsWithReturns - }) - ); - } + constructor(OptimizerBasis defaultBasis, uint256 startDepositPeriod) + RedeemOptimizer(defaultBasis, startDepositPeriod) + { } /// @notice Returns deposit periods and corresponding amounts (shares or assets) within the specified range. function _findAmount(IMultiTokenVault vault, OptimizerParams memory optimizerParams) internal view + override returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) { - if (optimizerParams.fromDepositPeriod > optimizerParams.toDepositPeriod) { - revert RedeemOptimizer__InvalidDepositPeriodRange( - optimizerParams.fromDepositPeriod, optimizerParams.toDepositPeriod - ); - } - - if (optimizerParams.toDepositPeriod > vault.currentPeriodsElapsed()) { - revert RedeemOptimizer__FutureToDepositPeriod( - optimizerParams.toDepositPeriod, vault.currentPeriodsElapsed() - ); - } - - // Create local caching arrays that can contain the maximum number of results. - uint256[] memory cacheDepositPeriods = - new uint256[]((optimizerParams.toDepositPeriod - optimizerParams.fromDepositPeriod) + 1); - uint256[] memory cacheSharesAtPeriods = - new uint256[]((optimizerParams.toDepositPeriod - optimizerParams.fromDepositPeriod) + 1); - + uint256 noOfPeriods = (optimizerParams.toDepositPeriod - optimizerParams.fromDepositPeriod) + 1; + depositPeriods = new uint256[](noOfPeriods); + sharesAtPeriods = new uint256[](noOfPeriods); uint256 arrayIndex = 0; uint256 amountFound = 0; @@ -108,31 +36,34 @@ contract RedeemOptimizerFIFO is IRedeemOptimizer { ++depositPeriod ) { uint256 sharesAtPeriod = vault.sharesAtPeriod(optimizerParams.owner, depositPeriod); - uint256 amountAtPeriod = optimizerParams.optimizerBasis == OptimizerBasis.Shares ? sharesAtPeriod : vault.convertToAssetsForDepositPeriod(sharesAtPeriod, depositPeriod, optimizerParams.redeemPeriod); // If there is an Amount, store the value. if (amountAtPeriod > 0) { - cacheDepositPeriods[arrayIndex] = depositPeriod; + depositPeriods[arrayIndex] = depositPeriod; // check if we will go "over" the Amount To Find. if (amountFound + amountAtPeriod > optimizerParams.amountToFind) { - uint256 amountToInclude = optimizerParams.amountToFind - amountFound; // we only need the amount that brings us to amountToFind + // we only need the amount that brings us to amountToFind + uint256 amountToInclude = optimizerParams.amountToFind - amountFound; // only include equivalent amount of shares for the amountToInclude assets - // in the assets case, the amounts include principal AND returns. we want the shares on deposit, which is the principal only. + // in the assets case, the amounts include principal AND returns. we want the shares on deposit, + // which is the principal only. // use this ratio: partialShares / totalShares = partialAssets / totalAssets // partialShares = (partialAssets * totalShares) / totalAssets - cacheSharesAtPeriods[arrayIndex] = optimizerParams.optimizerBasis == OptimizerBasis.Shares - ? amountToInclude // amount is shares, amountToInclude already correct - : amountToInclude.mulDiv(sharesAtPeriod, amountAtPeriod); // amount is assets, calc the correct shares + sharesAtPeriods[arrayIndex] = optimizerParams.optimizerBasis == OptimizerBasis.Shares + // amount is shares, amountToInclude already correct + ? amountToInclude + // amount is assets, calc the correct shares + : amountToInclude.mulDiv(sharesAtPeriod, amountAtPeriod); // optimization succeeded - return here to be explicit we exit the function at this point - return _trimToSize(arrayIndex + 1, cacheDepositPeriods, cacheSharesAtPeriods); + return _trimToSize(arrayIndex + 1, depositPeriods, sharesAtPeriods); } else { - cacheSharesAtPeriods[arrayIndex] = sharesAtPeriod; + sharesAtPeriods[arrayIndex] = sharesAtPeriod; } amountFound += amountAtPeriod; @@ -144,30 +75,6 @@ contract RedeemOptimizerFIFO is IRedeemOptimizer { revert RedeemOptimizer__OptimizerFailed(amountFound, optimizerParams.amountToFind); } - return _trimToSize(arrayIndex, cacheDepositPeriods, cacheSharesAtPeriods); - } - - /** - * @notice Utility function that trims the specified arrays to the specified size. - * @dev Allocates 2 arrays of size `toSize` and copies the `array1` and `array2` elements to their corresponding - * trimmed version. Assumes that the parameter arrays are at least as large as `toSize`. - * - * @param toSize The size to trim the arrays to. - * @param toTrim1 The first array to trim. - * @param toTrim2 The second array to trim. - * @return trimmed1 The trimmed version of `array1`. - * @return trimmed2 The trimmed version of `array2`. - */ - function _trimToSize(uint256 toSize, uint256[] memory toTrim1, uint256[] memory toTrim2) - private - pure - returns (uint256[] memory trimmed1, uint256[] memory trimmed2) - { - trimmed1 = new uint256[](toSize); - trimmed2 = new uint256[](toSize); - for (uint256 i = 0; i < toSize; i++) { - trimmed1[i] = toTrim1[i]; - trimmed2[i] = toTrim2[i]; - } + return _trimToSize(arrayIndex, depositPeriods, sharesAtPeriods); } } diff --git a/packages/contracts/src/token/ERC1155/RedeemOptimizerFIFOLIFO.sol b/packages/contracts/src/token/ERC1155/RedeemOptimizerFIFOLIFO.sol new file mode 100644 index 000000000..778e37bab --- /dev/null +++ b/packages/contracts/src/token/ERC1155/RedeemOptimizerFIFOLIFO.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { IMultiTokenVault } from "@credbull/token/ERC1155/IMultiTokenVault.sol"; +import { RedeemOptimizer } from "@credbull/token/ERC1155/RedeemOptimizer.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +/** + * @title The redemption optimizer utilising a combined FIFO/LIFO strategy. + * @notice Optimizes the redemption of shares using a FIFO strategy with a LIFO component. + * @dev The strategy is applied FIFO first and, if needed, LIFO. FIFO selects mature deposits to redeem to maximise + * the value returned while minimising deposits redeemed. If more value is required, then LIFO selects the most recent + * first deposits, to make up the value and reduce the loss of invested time in the older deposits. + */ +contract RedeemOptimizerFIFOLIFO is RedeemOptimizer { + using Math for uint256; + + uint256 public immutable TENOR; + + constructor(OptimizerBasis defaultBasis, uint256 startDepositPeriod, uint256 tenor) + RedeemOptimizer(defaultBasis, startDepositPeriod) + { + TENOR = tenor; + } + + /// @dev Calculates the effective Period Span between a From and a To date. + // TODO (JL,2024-10-08): Could we expose this on the IYieldStrategy? As effective span is a yield concept. + function _inclusivePeriodSpan(OptimizerParams memory optimizerParams) internal pure returns (uint256) { + return (optimizerParams.toDepositPeriod - optimizerParams.fromDepositPeriod) + 1; + } + + /// @dev Calculates the latest Deposit Period that can be mature. Only invoke from a context where it is known that + /// the period span is greater than the tenor. So, the subtraction is safe. + function _lastMatureDepositPeriod(OptimizerParams memory optimizerParams) internal view returns (uint256) { + return (optimizerParams.toDepositPeriod - TENOR) + 1; + } + + /// @dev Calculates the first Deposit Period that cannot be mature. + function _firstImmatureDepositPeriod(OptimizerParams memory optimizerParams) internal view returns (uint256) { + if (_inclusivePeriodSpan(optimizerParams) >= TENOR) { + return _lastMatureDepositPeriod(optimizerParams) + 1; + } else { + return optimizerParams.fromDepositPeriod; + } + } + + /** + * @inheritdoc RedeemOptimizer + * @dev First, tries to satisfy the Amount To Find with mature deposits. If insufficient, reverse iterate from the + * 'to' Deposit Period to find more recent immature deposits that satisfy the requirement. + */ + function _findAmount(IMultiTokenVault vault, OptimizerParams memory optimizerParams) + internal + view + override + returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) + { + IterationData memory i = IterationData({ + vault: vault, + optimizerParams: optimizerParams, + arrayIndex: 0, + depositPeriod: 0, + amountFound: 0, + depositPeriods: new uint256[](_inclusivePeriodSpan(optimizerParams)), + sharesAtPeriods: new uint256[](_inclusivePeriodSpan(optimizerParams)), + isDone: false + }); + + // If there are mature deposits. + if (_inclusivePeriodSpan(optimizerParams) >= TENOR) { + // Iterate over the from -> last mature period range, inclusive of both. + uint256 lastMatureDepositPeriod = _lastMatureDepositPeriod(optimizerParams); + for ( + uint256 depositPeriod = optimizerParams.fromDepositPeriod; + depositPeriod <= lastMatureDepositPeriod && !i.isDone; + ++depositPeriod + ) { + i.depositPeriod = depositPeriod; + i = _iteration(i); + } + } + + // If the Amount Found is not satisfied, search for value in the immature deposits. + if (i.amountFound < optimizerParams.amountToFind && !i.isDone) { + // Search in the range of the first immature deposit period -> to period. + return _findMostRecentFirst(_firstImmatureDepositPeriod(optimizerParams), i); + } + + return _trimToSize(i.arrayIndex, i.depositPeriods, i.sharesAtPeriods); + } + + /** + * @dev Reverse iterates from the `to` period to the `firstImmaturePeriod` finding the most recent deposits that + * can be added to the redeem amount. This gives the older, not yet mature deposits more time to mature. + * + * @param firstImmaturePeriod The initial period in the redeem range that cannot be a mature deposit. + * @param i The [IterationData] that encapsulates the processing to this point. + * @return depositPeriods The result array of Deposit Periods to harvest. + * @return sharesAtPeriods The result array of Share Amounts At Periods to harvest. + */ + function _findMostRecentFirst(uint256 firstImmaturePeriod, IterationData memory i) + internal + view + returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) + { + // Reverse iterate over the to -> first non-mature period range, inclusive of both. + for ( + uint256 depositPeriod = i.optimizerParams.toDepositPeriod; + depositPeriod >= firstImmaturePeriod && !i.isDone; + --depositPeriod + ) { + i.depositPeriod = depositPeriod; + i = _iteration(i); + if (depositPeriod == 0) break; // Prevent underflow by exiting loop before final decrement. + } + + if (i.amountFound < i.optimizerParams.amountToFind) { + revert RedeemOptimizer__OptimizerFailed(i.amountFound, i.optimizerParams.amountToFind); + } + + return _trimToSize(i.arrayIndex, i.depositPeriods, i.sharesAtPeriods); + } + + /// @notice A struct to capture all the data that is iterated over. A means to reduce Stack Depth. + struct IterationData { + /// @dev The [IMultiTokenVault] we are querying against. + IMultiTokenVault vault; + /// @dev The [OptimizerParams] governing how we optimise for the redemption/withdrawal. + OptimizerParams optimizerParams; + /// @dev The current index of results written to the result array pair. + uint256 arrayIndex; + /// @dev The current Deposit Period which we are processing for. + uint256 depositPeriod; + /// @dev The sum of amounts found so far. + uint256 amountFound; + /// @dev The Deposit Period result array. + uint256[] depositPeriods; + /// @dev The Share Amounts At Period result array. + uint256[] sharesAtPeriods; + bool isDone; + } + + /** + * @dev Encapsulates the processing of an iteration of one of the Period Ranges. + * + * @param i The [IterationData] capturing the current state of the processing. + * @return _updated The updated [IterationData]. + */ + function _iteration(IterationData memory i) internal view returns (IterationData memory _updated) { + uint256 sharesAtPeriod = i.vault.sharesAtPeriod(i.optimizerParams.owner, i.depositPeriod); + uint256 amountAtPeriod = i.optimizerParams.optimizerBasis == OptimizerBasis.Shares + ? sharesAtPeriod + : i.vault.convertToAssetsForDepositPeriod(sharesAtPeriod, i.depositPeriod, i.optimizerParams.redeemPeriod); + + // If there is an Amount, store the value. + if (amountAtPeriod > 0) { + i.depositPeriods[i.arrayIndex] = i.depositPeriod; + + // check if we will go "over" the Amount To Find. + if (i.amountFound + amountAtPeriod > i.optimizerParams.amountToFind) { + // we only need the amount that brings us to amountToFind + uint256 amountToInclude = i.optimizerParams.amountToFind - i.amountFound; + + // in the assets case, the amounts include principal AND returns. we want the shares on deposit, + // which is the principal only. + // use this ratio: partialShares / totalShares = partialAssets / totalAssets + // partialShares = (partialAssets * totalShares) / totalAssets + uint256 sharesToInclude = sharesAtPeriod.mulDiv(amountToInclude, amountAtPeriod); + + // only include equivalent amount of shares for the amountToInclude assets + i.sharesAtPeriods[i.arrayIndex] = + i.optimizerParams.optimizerBasis == OptimizerBasis.Shares ? amountToInclude : sharesToInclude; + + i.amountFound += amountToInclude; + i.isDone = true; + } else { + i.sharesAtPeriods[i.arrayIndex] = sharesAtPeriod; + i.amountFound += amountAtPeriod; + } + i.arrayIndex++; + + if (i.amountFound == i.optimizerParams.amountToFind && !i.isDone) { + i.isDone = true; + } + } + return i; + } +} diff --git a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol index 177fd7072..e61725b05 100644 --- a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol +++ b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol @@ -170,7 +170,7 @@ contract LiquidContinuousMultiTokenVault is function requestBuy(uint256 currencyTokenAmount) public virtual override returns (uint256 requestId) { uint256 componentTokenAmount = currencyTokenAmount; // 1 asset = 1 share - uint256 requestId = ZERO_REQUEST_ID; // requests and requestIds not used in buys. + requestId = ZERO_REQUEST_ID; // requests and requestIds not used in buys. executeBuy(_msgSender(), requestId, currencyTokenAmount, componentTokenAmount); diff --git a/packages/contracts/test/src/token/ERC1155/RedeemOptimizerFIFOLIFOTest.t.sol b/packages/contracts/test/src/token/ERC1155/RedeemOptimizerFIFOLIFOTest.t.sol new file mode 100644 index 000000000..61182ca0a --- /dev/null +++ b/packages/contracts/test/src/token/ERC1155/RedeemOptimizerFIFOLIFOTest.t.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { IRedeemOptimizer } from "@credbull/token/ERC1155/IRedeemOptimizer.sol"; +import { RedeemOptimizer } from "@credbull/token/ERC1155/RedeemOptimizer.sol"; +import { RedeemOptimizerFIFOLIFO } from "@credbull/token/ERC1155/RedeemOptimizerFIFOLIFO.sol"; +import { IMultiTokenVault } from "@credbull/token/ERC1155/IMultiTokenVault.sol"; + +import { MultiTokenVaultTest } from "@test/src/token/ERC1155/MultiTokenVaultTest.t.sol"; +import { IMTVTestParamArray } from "@test/test/token/ERC1155/IMTVTestParamArray.t.sol"; + +contract RedeemOptimizerFIFOLIFOTest is MultiTokenVaultTest { + uint256 private constant TENOR = 30; // days + address private _owner = makeAddr("owner"); + address private _alice = makeAddr("alice"); + + IMTVTestParamArray private testParamsArr; + + function setUp() public override { + super.setUp(); + + testParamsArr = new IMTVTestParamArray(); + testParamsArr.addTestParam(_testParams1); + testParamsArr.addTestParam(_testParams2); + testParamsArr.addTestParam(_testParams3); + } + + function test_RedeemOptimizerFIFOLIFOTest_RedeemAllShares() public { + uint256 assetToSharesRatio = 2; + + // setup + IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFOLIFO( + IRedeemOptimizer.OptimizerBasis.Shares, multiTokenVault.currentPeriodsElapsed(), TENOR + ); + + uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParamsArr.all()); + uint256 totalDepositShares = depositShares[0] + depositShares[1] + depositShares[2]; + + // warp vault ahead to redeemPeriod + uint256 redeemPeriod = _testParams3.redeemPeriod; + _warpToPeriod(multiTokenVault, redeemPeriod); + + // check full redeem + (uint256[] memory redeemDepositPeriods, uint256[] memory sharesAtPeriods) = + redeemOptimizer.optimize(multiTokenVault, _alice, totalDepositShares, 0, redeemPeriod); // optimize using share basis. assets not used + + assertEq(testParamsArr.depositPeriods(), redeemDepositPeriods, "optimizeRedeem - depositPeriods not correct"); + assertEq(depositShares, sharesAtPeriods, "optimizeRedeem - shares not correct"); + } + + function test_RedeemOptimizerFIFOLIFOTest_WithdrawAllShares() public { + uint256 assetToSharesRatio = 2; + uint256 redeemPeriod = _testParams3.redeemPeriod; + + // setup + IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFOLIFO( + IRedeemOptimizer.OptimizerBasis.AssetsWithReturns, multiTokenVault.currentPeriodsElapsed(), TENOR + ); + + uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParamsArr.all()); + uint256[] memory depositAssets = multiTokenVault.convertToAssetsForDepositPeriodBatch( + depositShares, testParamsArr.depositPeriods(), redeemPeriod + ); + assertEq(depositShares.length, depositAssets.length, "mismatch in convertToAssets"); + uint256 totalAssets = depositAssets[0] + depositAssets[1] + depositAssets[2]; + + // warp vault ahead to redeemPeriod + _warpToPeriod(multiTokenVault, redeemPeriod); + + // check full withdraw + (uint256[] memory withdrawDepositPeriods, uint256[] memory sharesAtPeriods) = + redeemOptimizer.optimize(multiTokenVault, _alice, 0, totalAssets, redeemPeriod); // optimize using asset basis. shares not used + + assertEq(testParamsArr.depositPeriods(), withdrawDepositPeriods, "optimizeRedeem - depositPeriods not correct"); + assertEq(depositShares, sharesAtPeriods, "optimizeRedeem - shares not correct"); + } + + function test_RedeemOptimizerFIFOLIFOTest_PartialWithdraw() public { + uint256 residualShareAmount = 1 * _scale; // leave 1 share after redeem + uint256 redeemPeriod = _testParams3.redeemPeriod; + + // ---------------------- setup ---------------------- + IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, 2, 10); + IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFOLIFO( + IRedeemOptimizer.OptimizerBasis.AssetsWithReturns, multiTokenVault.currentPeriodsElapsed(), TENOR + ); + + uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParamsArr.all()); + uint256[] memory depositAssets = multiTokenVault.convertToAssetsForDepositPeriodBatch( + depositShares, testParamsArr.depositPeriods(), redeemPeriod + ); + + uint256 residualAssetAmount = multiTokenVault.convertToAssetsForDepositPeriod( + residualShareAmount, _testParams3.depositPeriod, redeemPeriod + ); + uint256 assetsToWithdraw = depositAssets[0] + depositAssets[1] + depositAssets[2] - residualAssetAmount; + + // ---------------------- redeem ---------------------- + _warpToPeriod(multiTokenVault, redeemPeriod); // warp vault ahead to redeemPeriod + + (uint256[] memory actualDepositPeriods, uint256[] memory actualSharesAtPeriods) = + redeemOptimizer.optimizeWithdrawAssets(multiTokenVault, _alice, assetsToWithdraw, redeemPeriod); + + // verify using shares + assertEq(depositShares[0], actualSharesAtPeriods[0], "optimizeWithdraw - wrong shares period 0"); + assertEq(depositShares[1], actualSharesAtPeriods[1], "optimizeWithdraw - wrong shares period 1"); + assertEq( + depositShares[2] - residualShareAmount, actualSharesAtPeriods[2], "optimizeWithdraw - wrong shares period 2" + ); // reduced by 1 share with returns + + // // verify using assets + uint256[] memory actualAssetsAtPeriods = multiTokenVault.convertToAssetsForDepositPeriodBatch( + actualSharesAtPeriods, actualDepositPeriods, redeemPeriod + ); + + assertEq( + testParamsArr.depositPeriods().length, + actualAssetsAtPeriods.length, + "convertToAssetsForDepositPeriods (partial) - length incorrect" + ); + assertEq( + assetsToWithdraw, + actualAssetsAtPeriods[0] + actualAssetsAtPeriods[1] + actualAssetsAtPeriods[2], + "convertToAssetsForDepositPeriods (partial) - total incorrect" + ); + } + + function test_RedeemOptimizerFIFOLIFOTest_InsufficientSharesShouldRevert() public { + IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, 1, 10); + uint256 vaultCurrentPeriod = multiTokenVault.currentPeriodsElapsed(); + + // no deposits - should fail + uint256 oneShare = 1; + IRedeemOptimizer redeemOptimizer = + new RedeemOptimizerFIFOLIFO(IRedeemOptimizer.OptimizerBasis.Shares, vaultCurrentPeriod, TENOR); + vm.expectRevert(abi.encodeWithSelector(RedeemOptimizer.RedeemOptimizer__OptimizerFailed.selector, 0, oneShare)); + redeemOptimizer.optimizeRedeemShares(multiTokenVault, _alice, oneShare, vaultCurrentPeriod); + + // shares to find greater than the deposits + uint256 deposit1Shares = _testDepositOnly(_alice, multiTokenVault, _testParams1); + uint256 deposit2Shares = _testDepositOnly(_alice, multiTokenVault, _testParams2); + uint256 totalDepositShares = deposit1Shares + deposit2Shares; + + uint256 sharesGreaterThanDeposits = totalDepositShares + 1; + vm.expectRevert( + abi.encodeWithSelector( + RedeemOptimizer.RedeemOptimizer__OptimizerFailed.selector, 0, sharesGreaterThanDeposits + ) + ); + redeemOptimizer.optimizeRedeemShares(multiTokenVault, _alice, sharesGreaterThanDeposits, vaultCurrentPeriod); + } +} diff --git a/packages/contracts/test/src/token/ERC1155/RedeemOptimizerFIFOTest.t.sol b/packages/contracts/test/src/token/ERC1155/RedeemOptimizerFIFOTest.t.sol new file mode 100644 index 000000000..967f116b1 --- /dev/null +++ b/packages/contracts/test/src/token/ERC1155/RedeemOptimizerFIFOTest.t.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { IRedeemOptimizer } from "@credbull/token/ERC1155/IRedeemOptimizer.sol"; +import { RedeemOptimizer } from "@credbull/token/ERC1155/RedeemOptimizer.sol"; +import { RedeemOptimizerFIFO } from "@credbull/token/ERC1155/RedeemOptimizerFIFO.sol"; +import { IMultiTokenVault } from "@credbull/token/ERC1155/IMultiTokenVault.sol"; + +import { MultiTokenVaultTest } from "@test/src/token/ERC1155/MultiTokenVaultTest.t.sol"; +import { IMTVTestParamArray } from "@test/test/token/ERC1155/IMTVTestParamArray.t.sol"; + +contract RedeemOptimizerFIFOTest is MultiTokenVaultTest { + address private _owner = makeAddr("owner"); + address private _alice = makeAddr("alice"); + + IMTVTestParamArray private testParamsArr; + + function setUp() public override { + super.setUp(); + + testParamsArr = new IMTVTestParamArray(); + testParamsArr.addTestParam(_testParams1); + testParamsArr.addTestParam(_testParams2); + testParamsArr.addTestParam(_testParams3); + } + + function test__RedeemOptimizerTest__RedeemAllShares() public { + uint256 assetToSharesRatio = 2; + + // setup + IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + IRedeemOptimizer redeemOptimizer = + new RedeemOptimizerFIFO(IRedeemOptimizer.OptimizerBasis.Shares, multiTokenVault.currentPeriodsElapsed()); + + uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParamsArr.all()); + uint256 totalDepositShares = depositShares[0] + depositShares[1] + depositShares[2]; + + // warp vault ahead to redeemPeriod + uint256 redeemPeriod = _testParams3.redeemPeriod; + _warpToPeriod(multiTokenVault, redeemPeriod); + + // check full redeem + (uint256[] memory redeemDepositPeriods, uint256[] memory sharesAtPeriods) = + redeemOptimizer.optimize(multiTokenVault, _alice, totalDepositShares, 0, redeemPeriod); // optimize using share basis. assets not used + + assertEq(testParamsArr.depositPeriods(), redeemDepositPeriods, "optimizeRedeem - depositPeriods not correct"); + assertEq(depositShares, sharesAtPeriods, "optimizeRedeem - shares not correct"); + } + + function test__RedeemOptimizerTest__WithdrawAllShares() public { + uint256 assetToSharesRatio = 2; + uint256 redeemPeriod = _testParams3.redeemPeriod; + + // setup + IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); + IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFO( + IRedeemOptimizer.OptimizerBasis.AssetsWithReturns, multiTokenVault.currentPeriodsElapsed() + ); + + uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParamsArr.all()); + uint256[] memory depositAssets = multiTokenVault.convertToAssetsForDepositPeriodBatch( + depositShares, testParamsArr.depositPeriods(), redeemPeriod + ); + assertEq(depositShares.length, depositAssets.length, "mismatch in convertToAssets"); + uint256 totalAssets = depositAssets[0] + depositAssets[1] + depositAssets[2]; + + // warp vault ahead to redeemPeriod + _warpToPeriod(multiTokenVault, redeemPeriod); + + // check full withdraw + (uint256[] memory withdrawDepositPeriods, uint256[] memory sharesAtPeriods) = + redeemOptimizer.optimize(multiTokenVault, _alice, 0, totalAssets, redeemPeriod); // optimize using asset basis. shares not used + + assertEq(testParamsArr.depositPeriods(), withdrawDepositPeriods, "optimizeRedeem - depositPeriods not correct"); + assertEq(depositShares, sharesAtPeriods, "optimizeRedeem - shares not correct"); + } + + function test__RedeemOptimizerTest__PartialRedeem() public { + uint256 residualShareAmount = 1 * _scale; // leave 1 share after redeem + uint256 redeemPeriod = _testParams3.redeemPeriod; + + // ---------------------- setup ---------------------- + IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, 2, 10); + IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFO( + IRedeemOptimizer.OptimizerBasis.AssetsWithReturns, multiTokenVault.currentPeriodsElapsed() + ); + uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParamsArr.all()); + + uint256 sharesToWithdraw = depositShares[0] + depositShares[1] + depositShares[2] - residualShareAmount; + + // ---------------------- redeem ---------------------- + _warpToPeriod(multiTokenVault, redeemPeriod); // warp vault ahead to redeemPeriod + + (uint256[] memory redeemDepositPeriods, uint256[] memory redeemSharesAtPeriods) = + redeemOptimizer.optimizeRedeemShares(multiTokenVault, _alice, sharesToWithdraw, redeemPeriod); + // verify using shares + assertEq(depositShares[0], redeemSharesAtPeriods[0], "optimizeRedeem partial - wrong shares period 0"); + assertEq(depositShares[1], redeemSharesAtPeriods[1], "optimizeRedeem partial - wrong shares period 1"); + assertEq( + depositShares[2] - residualShareAmount, + redeemSharesAtPeriods[2], + "optimizeRedeem partial - wrong shares period 2" + ); // reduced by 1 share + + assertEq(testParamsArr.depositPeriods(), redeemDepositPeriods, "optimizeRedeem - depositPeriods not correct"); + } + + function test__RedeemOptimizerTest__PartialWithdraw() public { + uint256 residualShareAmount = 1 * _scale; // leave 1 share after redeem + uint256 redeemPeriod = _testParams3.redeemPeriod; + + // ---------------------- setup ---------------------- + IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, 2, 10); + IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFO( + IRedeemOptimizer.OptimizerBasis.AssetsWithReturns, multiTokenVault.currentPeriodsElapsed() + ); + + uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParamsArr.all()); + uint256[] memory depositAssets = multiTokenVault.convertToAssetsForDepositPeriodBatch( + depositShares, testParamsArr.depositPeriods(), redeemPeriod + ); + + uint256 residualAssetAmount = multiTokenVault.convertToAssetsForDepositPeriod( + residualShareAmount, _testParams3.depositPeriod, redeemPeriod + ); + uint256 assetsToWithdraw = depositAssets[0] + depositAssets[1] + depositAssets[2] - residualAssetAmount; + + // ---------------------- redeem ---------------------- + _warpToPeriod(multiTokenVault, redeemPeriod); // warp vault ahead to redeemPeriod + + (uint256[] memory actualDepositPeriods, uint256[] memory actualSharesAtPeriods) = + redeemOptimizer.optimizeWithdrawAssets(multiTokenVault, _alice, assetsToWithdraw, redeemPeriod); + + // verify using shares + assertEq(depositShares[0], actualSharesAtPeriods[0], "optimizeWithdraw partial - wrong shares period 0"); + assertEq(depositShares[1], actualSharesAtPeriods[1], "optimizeWithdraw partial - wrong shares period 1"); + assertEq( + depositShares[2] - residualShareAmount, + actualSharesAtPeriods[2], + "optimizeWithdraw partial - wrong shares period 2" + ); // reduced by 1 share with returns + + // // verify using assets + uint256[] memory actualAssetsAtPeriods = multiTokenVault.convertToAssetsForDepositPeriodBatch( + actualSharesAtPeriods, actualDepositPeriods, redeemPeriod + ); + + assertEq( + testParamsArr.depositPeriods().length, + actualAssetsAtPeriods.length, + "convertToAssetsForDepositPeriods partial - length incorrect" + ); + assertEq( + assetsToWithdraw, + actualAssetsAtPeriods[0] + actualAssetsAtPeriods[1] + actualAssetsAtPeriods[2], + "convertToAssetsForDepositPeriods partial - total incorrect" + ); + } + + function test__RedeemOptimizerTest__InsufficientSharesShouldRevert() public { + IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, 1, 10); + uint256 vaultCurrentPeriod = multiTokenVault.currentPeriodsElapsed(); + + // no deposits - should fail + uint256 oneShare = 1; + IRedeemOptimizer redeemOptimizer = + new RedeemOptimizerFIFO(IRedeemOptimizer.OptimizerBasis.Shares, vaultCurrentPeriod); + vm.expectRevert(abi.encodeWithSelector(RedeemOptimizer.RedeemOptimizer__OptimizerFailed.selector, 0, oneShare)); + redeemOptimizer.optimizeRedeemShares(multiTokenVault, _alice, oneShare, vaultCurrentPeriod); + + // shares to find greater than the deposits + uint256 deposit1Shares = _testDepositOnly(_alice, multiTokenVault, _testParams1); + uint256 deposit2Shares = _testDepositOnly(_alice, multiTokenVault, _testParams2); + uint256 totalDepositShares = deposit1Shares + deposit2Shares; + + uint256 sharesGreaterThanDeposits = totalDepositShares + 1; + vm.expectRevert( + abi.encodeWithSelector( + RedeemOptimizer.RedeemOptimizer__OptimizerFailed.selector, 0, sharesGreaterThanDeposits + ) + ); + redeemOptimizer.optimizeRedeemShares(multiTokenVault, _alice, sharesGreaterThanDeposits, vaultCurrentPeriod); + } +} diff --git a/packages/contracts/test/src/token/ERC1155/RedeemOptimizerTest.t.sol b/packages/contracts/test/src/token/ERC1155/RedeemOptimizerTest.t.sol index 3c18e8eb3..814c01eab 100644 --- a/packages/contracts/test/src/token/ERC1155/RedeemOptimizerTest.t.sol +++ b/packages/contracts/test/src/token/ERC1155/RedeemOptimizerTest.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import { IRedeemOptimizer } from "@credbull/token/ERC1155/IRedeemOptimizer.sol"; -import { RedeemOptimizerFIFO } from "@credbull/token/ERC1155/RedeemOptimizerFIFO.sol"; +import { RedeemOptimizer } from "@credbull/token/ERC1155/RedeemOptimizer.sol"; import { IMultiTokenVault } from "@credbull/token/ERC1155/IMultiTokenVault.sol"; import { MultiTokenVaultTest } from "@test/src/token/ERC1155/MultiTokenVaultTest.t.sol"; @@ -16,171 +16,6 @@ contract RedeemOptimizerTest is MultiTokenVaultTest { function setUp() public override { super.setUp(); - - testParamsArr = new IMTVTestParamArray(); - testParamsArr.addTestParam(_testParams1); - testParamsArr.addTestParam(_testParams2); - testParamsArr.addTestParam(_testParams3); - } - - function test__RedeemOptimizerTest__RedeemAllShares() public { - uint256 assetToSharesRatio = 2; - - // setup - IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); - IRedeemOptimizer redeemOptimizer = - new RedeemOptimizerFIFO(IRedeemOptimizer.OptimizerBasis.Shares, multiTokenVault.currentPeriodsElapsed()); - - uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParamsArr.all()); - uint256 totalDepositShares = depositShares[0] + depositShares[1] + depositShares[2]; - - // warp vault ahead to redeemPeriod - uint256 redeemPeriod = _testParams3.redeemPeriod; - _warpToPeriod(multiTokenVault, redeemPeriod); - - // check full redeem - (uint256[] memory redeemDepositPeriods, uint256[] memory sharesAtPeriods) = - redeemOptimizer.optimize(multiTokenVault, _alice, totalDepositShares, 0, redeemPeriod); // optimize using share basis. assets not used - - assertEq(testParamsArr.depositPeriods(), redeemDepositPeriods, "optimizeRedeem - depositPeriods not correct"); - assertEq(depositShares, sharesAtPeriods, "optimizeRedeem - shares not correct"); - } - - function test__RedeemOptimizerTest__WithdrawAllShares() public { - uint256 assetToSharesRatio = 2; - uint256 redeemPeriod = _testParams3.redeemPeriod; - - // setup - IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, assetToSharesRatio, 10); - IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFO( - IRedeemOptimizer.OptimizerBasis.AssetsWithReturns, multiTokenVault.currentPeriodsElapsed() - ); - - uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParamsArr.all()); - uint256[] memory depositAssets = multiTokenVault.convertToAssetsForDepositPeriodBatch( - depositShares, testParamsArr.depositPeriods(), redeemPeriod - ); - assertEq(depositShares.length, depositAssets.length, "mismatch in convertToAssets"); - uint256 totalAssets = depositAssets[0] + depositAssets[1] + depositAssets[2]; - - // warp vault ahead to redeemPeriod - _warpToPeriod(multiTokenVault, redeemPeriod); - - // check full withdraw - (uint256[] memory withdrawDepositPeriods, uint256[] memory sharesAtPeriods) = - redeemOptimizer.optimize(multiTokenVault, _alice, 0, totalAssets, redeemPeriod); // optimize using asset basis. shares not used - - assertEq(testParamsArr.depositPeriods(), withdrawDepositPeriods, "optimizeRedeem - depositPeriods not correct"); - assertEq(depositShares, sharesAtPeriods, "optimizeRedeem - shares not correct"); - } - - function test__RedeemOptimizerTest__PartialRedeem() public { - uint256 residualShareAmount = 1 * _scale; // leave 1 share after redeem - uint256 redeemPeriod = _testParams3.redeemPeriod; - - // ---------------------- setup ---------------------- - IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, 2, 10); - IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFO( - IRedeemOptimizer.OptimizerBasis.AssetsWithReturns, multiTokenVault.currentPeriodsElapsed() - ); - uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParamsArr.all()); - - uint256 sharesToWithdraw = depositShares[0] + depositShares[1] + depositShares[2] - residualShareAmount; - - // ---------------------- redeem ---------------------- - _warpToPeriod(multiTokenVault, redeemPeriod); // warp vault ahead to redeemPeriod - - (uint256[] memory redeemDepositPeriods, uint256[] memory redeemSharesAtPeriods) = - redeemOptimizer.optimizeRedeemShares(multiTokenVault, _alice, sharesToWithdraw, redeemPeriod); - // verify using shares - assertEq(depositShares[0], redeemSharesAtPeriods[0], "optimizeRedeem partial - wrong shares period 0"); - assertEq(depositShares[1], redeemSharesAtPeriods[1], "optimizeRedeem partial - wrong shares period 1"); - assertEq( - depositShares[2] - residualShareAmount, - redeemSharesAtPeriods[2], - "optimizeRedeem partial - wrong shares period 2" - ); // reduced by 1 share - - assertEq(testParamsArr.depositPeriods(), redeemDepositPeriods, "optimizeRedeem - depositPeriods not correct"); - } - - function test__RedeemOptimizerTest__PartialWithdraw() public { - uint256 residualShareAmount = 1 * _scale; // leave 1 share after redeem - uint256 redeemPeriod = _testParams3.redeemPeriod; - - // ---------------------- setup ---------------------- - IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, 2, 10); - IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFO( - IRedeemOptimizer.OptimizerBasis.AssetsWithReturns, multiTokenVault.currentPeriodsElapsed() - ); - - uint256[] memory depositShares = _testDepositOnly(_alice, multiTokenVault, testParamsArr.all()); - uint256[] memory depositAssets = multiTokenVault.convertToAssetsForDepositPeriodBatch( - depositShares, testParamsArr.depositPeriods(), redeemPeriod - ); - - uint256 residualAssetAmount = multiTokenVault.convertToAssetsForDepositPeriod( - residualShareAmount, _testParams3.depositPeriod, redeemPeriod - ); - uint256 assetsToWithdraw = depositAssets[0] + depositAssets[1] + depositAssets[2] - residualAssetAmount; - - // ---------------------- redeem ---------------------- - _warpToPeriod(multiTokenVault, redeemPeriod); // warp vault ahead to redeemPeriod - - (uint256[] memory actualDepositPeriods, uint256[] memory actualSharesAtPeriods) = - redeemOptimizer.optimizeWithdrawAssets(multiTokenVault, _alice, assetsToWithdraw, redeemPeriod); - - // verify using shares - assertEq(depositShares[0], actualSharesAtPeriods[0], "optimizeWithdraw partial - wrong shares period 0"); - assertEq(depositShares[1], actualSharesAtPeriods[1], "optimizeWithdraw partial - wrong shares period 1"); - assertEq( - depositShares[2] - residualShareAmount, - actualSharesAtPeriods[2], - "optimizeWithdraw partial - wrong shares period 2" - ); // reduced by 1 share with returns - - // // verify using assets - uint256[] memory actualAssetsAtPeriods = multiTokenVault.convertToAssetsForDepositPeriodBatch( - actualSharesAtPeriods, actualDepositPeriods, redeemPeriod - ); - - assertEq( - testParamsArr.depositPeriods().length, - actualAssetsAtPeriods.length, - "convertToAssetsForDepositPeriods partial - length incorrect" - ); - assertEq( - assetsToWithdraw, - actualAssetsAtPeriods[0] + actualAssetsAtPeriods[1] + actualAssetsAtPeriods[2], - "convertToAssetsForDepositPeriods partial - total incorrect" - ); - } - - function test__RedeemOptimizerTest__InsufficientSharesShouldRevert() public { - IMultiTokenVault multiTokenVault = _createMultiTokenVault(_asset, 1, 10); - uint256 vaultCurrentPeriod = multiTokenVault.currentPeriodsElapsed(); - - // no deposits - should fail - uint256 oneShare = 1; - IRedeemOptimizer redeemOptimizer = - new RedeemOptimizerFIFO(IRedeemOptimizer.OptimizerBasis.Shares, vaultCurrentPeriod); - vm.expectRevert( - abi.encodeWithSelector(RedeemOptimizerFIFO.RedeemOptimizer__OptimizerFailed.selector, 0, oneShare) - ); - redeemOptimizer.optimizeRedeemShares(multiTokenVault, _alice, oneShare, vaultCurrentPeriod); - - // shares to find greater than the deposits - uint256 deposit1Shares = _testDepositOnly(_alice, multiTokenVault, _testParams1); - uint256 deposit2Shares = _testDepositOnly(_alice, multiTokenVault, _testParams2); - uint256 totalDepositShares = deposit1Shares + deposit2Shares; - - uint256 sharesGreaterThanDeposits = totalDepositShares + 1; - vm.expectRevert( - abi.encodeWithSelector( - RedeemOptimizerFIFO.RedeemOptimizer__OptimizerFailed.selector, 0, sharesGreaterThanDeposits - ) - ); - redeemOptimizer.optimizeRedeemShares(multiTokenVault, _alice, sharesGreaterThanDeposits, vaultCurrentPeriod); } function test__RedeemOptimizerTest__InvalidPeriodRangeShouldRevert() public { @@ -190,10 +25,10 @@ contract RedeemOptimizerTest is MultiTokenVaultTest { uint256 invalidFromDepositPeriod = vaultCurrentPeriod + 1; // from greater than to period is not allowed IRedeemOptimizer redeemOptimizer = - new RedeemOptimizerFIFO(IRedeemOptimizer.OptimizerBasis.Shares, invalidFromDepositPeriod); + new ExposeAssertionRedeemOptimizer(IRedeemOptimizer.OptimizerBasis.Shares, invalidFromDepositPeriod); vm.expectRevert( abi.encodeWithSelector( - RedeemOptimizerFIFO.RedeemOptimizer__InvalidDepositPeriodRange.selector, + RedeemOptimizer.RedeemOptimizer__InvalidDepositPeriodRange.selector, invalidFromDepositPeriod, vaultCurrentPeriod ) @@ -207,17 +42,17 @@ contract RedeemOptimizerTest is MultiTokenVaultTest { uint256 vaultCurrentPeriod = multiTokenVault.currentPeriodsElapsed(); uint256 invalidToDepositPeriod = vaultCurrentPeriod + 1; // future to period is not allowed - RedeemOptimizerFIFOMock redeemOptimizerMock = - new RedeemOptimizerFIFOMock(IRedeemOptimizer.OptimizerBasis.Shares, vaultCurrentPeriod); + ExposeAssertionRedeemOptimizer redeemOptimizer = + new ExposeAssertionRedeemOptimizer(IRedeemOptimizer.OptimizerBasis.Shares, vaultCurrentPeriod); vm.expectRevert( abi.encodeWithSelector( - RedeemOptimizerFIFO.RedeemOptimizer__FutureToDepositPeriod.selector, + RedeemOptimizer.RedeemOptimizer__FutureToDepositPeriod.selector, invalidToDepositPeriod, vaultCurrentPeriod ) ); - redeemOptimizerMock.findAmount( + redeemOptimizer.assertOptimization( multiTokenVault, IRedeemOptimizer.OptimizerParams({ owner: _owner, @@ -231,16 +66,22 @@ contract RedeemOptimizerTest is MultiTokenVaultTest { } } -contract RedeemOptimizerFIFOMock is RedeemOptimizerFIFO { +contract ExposeAssertionRedeemOptimizer is RedeemOptimizer { constructor(OptimizerBasis preferredOptimizationBasis, uint256 startDepositPeriod) - RedeemOptimizerFIFO(preferredOptimizationBasis, startDepositPeriod) + RedeemOptimizer(preferredOptimizationBasis, startDepositPeriod) { } - function findAmount(IMultiTokenVault vault, OptimizerParams memory params) - public - view + function assertOptimization(IMultiTokenVault vault, OptimizerParams memory optimizerParams) public view { + _assertOptimization(vault, optimizerParams); + } + + function _findAmount(IMultiTokenVault, /*vault*/ OptimizerParams memory /*optimizerParams*/ ) + internal + pure + override returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) { - return _findAmount(vault, params); + // Stubbed. + return (new uint256[](0), new uint256[](0)); } }