Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a FIFO/LIFO IRedeemOptimizer realisation. #128

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/contracts/src/token/ERC1155/IMultiTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
3 changes: 0 additions & 3 deletions packages/contracts/src/token/ERC1155/IRedeemOptimizer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/**
Expand All @@ -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);

/**
Expand All @@ -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);
}
13 changes: 13 additions & 0 deletions packages/contracts/src/token/ERC1155/MultiTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
155 changes: 155 additions & 0 deletions packages/contracts/src/token/ERC1155/RedeemOptimizer.sol
Original file line number Diff line number Diff line change
@@ -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];
}
}
}
137 changes: 22 additions & 115 deletions packages/contracts/src/token/ERC1155/RedeemOptimizerFIFO.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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);
}
}
Loading