Skip to content

Commit

Permalink
Adding MultiTokenVault batch testing
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasia committed Oct 5, 2024
1 parent c92081a commit eb49981
Show file tree
Hide file tree
Showing 10 changed files with 424 additions and 328 deletions.
14 changes: 14 additions & 0 deletions packages/contracts/src/token/ERC1155/IRedeemOptimizer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,25 @@ import { IMultiTokenVault } from "@credbull/token/ERC1155/IMultiTokenVault.sol";
* @notice Interface for optimizing redemptions and withdrawals across deposit periods.
*/
interface IRedeemOptimizer {
struct OptimizerParams {
address owner;
uint256 amountToFind;
uint256 fromDepositPeriod;
uint256 toDepositPeriod;
uint256 redeemPeriod;
AmountType amountType;
}

enum AmountType {
Shares,
AssetsWithReturns
}
/**
* @notice Finds optimal deposit periods and shares to redeem for a given share amount and redeemPeriod
* @return depositPeriods Array of deposit periods to redeem from.
* @return sharesAtPeriods Array of share amounts to redeem for each deposit period.
*/

function optimizeRedeemShares(IMultiTokenVault vault, address owner, uint256 shares, uint256 redeemPeriod)
external
view
Expand Down
92 changes: 51 additions & 41 deletions packages/contracts/src/token/ERC1155/RedeemOptimizerFIFO.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ contract RedeemOptimizerFIFO is IRedeemOptimizer {
error RedeemOptimizer__FutureToDepositPeriod(uint256 toPeriod, uint256 currentPeriod);
error RedeemOptimizer__OptimizerFailed(uint256 amountFound, uint256 amountToFind);

enum AmountType {
Shares,
AssetsWithReturns
}

uint256 public immutable START_DEPOSIT_PERIOD;

constructor(uint256 startDepositPeriod) {
Expand All @@ -31,7 +26,15 @@ contract RedeemOptimizerFIFO is IRedeemOptimizer {
returns (uint256[] memory depositPeriods_, uint256[] memory sharesAtPeriods_)
{
return _findAmount(
vault, owner, shares, START_DEPOSIT_PERIOD, vault.currentPeriodsElapsed(), redeemPeriod, AmountType.Shares
vault,
OptimizerParams({
owner: owner,
amountToFind: shares,
fromDepositPeriod: START_DEPOSIT_PERIOD,
toDepositPeriod: vault.currentPeriodsElapsed(),
redeemPeriod: redeemPeriod,
amountType: AmountType.Shares
})
);
}

Expand All @@ -40,73 +43,80 @@ contract RedeemOptimizerFIFO is IRedeemOptimizer {
function optimizeWithdrawAssets(IMultiTokenVault vault, address owner, uint256 assets, uint256 redeemPeriod)
public
view
returns (uint256[] memory depositPeriods_, uint256[] memory sharesAtPeriods_)
returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods)
{
return _findAmount(
vault,
owner,
assets,
START_DEPOSIT_PERIOD,
vault.currentPeriodsElapsed(),
redeemPeriod,
AmountType.AssetsWithReturns
OptimizerParams({
owner: owner,
amountToFind: assets,
fromDepositPeriod: START_DEPOSIT_PERIOD,
toDepositPeriod: vault.currentPeriodsElapsed(),
redeemPeriod: redeemPeriod,
amountType: AmountType.AssetsWithReturns
})
);
}

/// @notice Returns deposit periods and corresponding amounts (shares or assets) within the specified range.
function _findAmount(
IMultiTokenVault vault,
address owner,
uint256 amountToFind,
uint256 fromDepositPeriod,
uint256 toDepositPeriod,
uint256 redeemPeriod,
AmountType amountType
) internal view returns (uint256[] memory depositPeriods, uint256[] memory amountAtPeriods) {
if (fromDepositPeriod > toDepositPeriod) {
revert RedeemOptimizer__InvalidDepositPeriodRange(fromDepositPeriod, toDepositPeriod);
function _findAmount(IMultiTokenVault vault, OptimizerParams memory params)
internal
view
returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods)
{
if (params.fromDepositPeriod > params.toDepositPeriod) {
revert RedeemOptimizer__InvalidDepositPeriodRange(params.fromDepositPeriod, params.toDepositPeriod);
}

if (toDepositPeriod > vault.currentPeriodsElapsed()) {
revert RedeemOptimizer__FutureToDepositPeriod(toDepositPeriod, vault.currentPeriodsElapsed());
if (params.toDepositPeriod > vault.currentPeriodsElapsed()) {
revert RedeemOptimizer__FutureToDepositPeriod(params.toDepositPeriod, vault.currentPeriodsElapsed());
}

// Create local caching arrays that can contain the maximum number of results.
uint256[] memory cacheDepositPeriods = new uint256[]((toDepositPeriod - fromDepositPeriod) + 1);
uint256[] memory cacheAmountAtPeriods = new uint256[]((toDepositPeriod - fromDepositPeriod) + 1);
uint256[] memory cacheDepositPeriods = new uint256[]((params.toDepositPeriod - params.fromDepositPeriod) + 1);
uint256[] memory cacheSharesAtPeriods = new uint256[]((params.toDepositPeriod - params.fromDepositPeriod) + 1);

uint256 arrayIndex = 0;
uint256 amountFound = 0;

// Iterate over the from/to period range, inclusive of from and to.
for (uint256 depositPeriod = fromDepositPeriod; depositPeriod <= toDepositPeriod; ++depositPeriod) {
uint256 sharesAtPeriod = vault.sharesAtPeriod(owner, depositPeriod);
uint256 amountAtPeriod = amountType == AmountType.Shares
for (uint256 depositPeriod = params.fromDepositPeriod; depositPeriod <= params.toDepositPeriod; ++depositPeriod)
{
uint256 sharesAtPeriod = vault.sharesAtPeriod(params.owner, depositPeriod);

uint256 amountAtPeriod = params.amountType == AmountType.Shares
? sharesAtPeriod
: vault.convertToAssetsForDepositPeriod(sharesAtPeriod, depositPeriod, redeemPeriod);
: vault.convertToAssetsForDepositPeriod(sharesAtPeriod, depositPeriod, params.redeemPeriod);

// If there is an Amount, store the value.
if (amountAtPeriod > 0) {
cacheDepositPeriods[arrayIndex] = depositPeriod;

// check if we will go "over" the Amount To Find.
if (amountFound + amountAtPeriod > amountToFind) {
cacheAmountAtPeriods[arrayIndex] = amountToFind - amountFound; // include only the partial amount
amountFound += cacheAmountAtPeriods[arrayIndex++];
break;
if (amountFound + amountAtPeriod > params.amountToFind) {
uint256 amountToInclude = params.amountToFind - amountFound; // we only need the amount that brings us to amountToFind

// only include equivalent amount of shares for the amountToInclude assets
cacheSharesAtPeriods[arrayIndex] = params.amountType == AmountType.Shares
? amountToInclude
: vault.convertToAssetsForDepositPeriod(amountToInclude, depositPeriod, params.redeemPeriod);

// optimization succeeded - return here to be explicit we exit the function at this point
return _trimToSize(arrayIndex, cacheDepositPeriods, cacheSharesAtPeriods);
} else {
cacheAmountAtPeriods[arrayIndex] = amountAtPeriod;
cacheSharesAtPeriods[arrayIndex] = sharesAtPeriod;
}

amountFound += cacheAmountAtPeriods[arrayIndex];
amountFound += amountAtPeriod;
arrayIndex++;
}
}

if (amountFound < amountToFind) {
revert RedeemOptimizer__OptimizerFailed(amountFound, amountToFind);
if (amountFound < params.amountToFind) {
revert RedeemOptimizer__OptimizerFailed(amountFound, params.amountToFind);
}

return _trimToSize(arrayIndex, cacheDepositPeriods, cacheAmountAtPeriods);
return _trimToSize(arrayIndex, cacheDepositPeriods, cacheSharesAtPeriods);
}

/**
Expand Down
27 changes: 25 additions & 2 deletions packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,17 @@ contract LiquidContinuousMultiTokenVault is

/// @inheritdoc IComponentToken
function requestSell(uint256 componentTokenAmount) public virtual override returns (uint256 requestId) {
// TODO - do we *always* want to optimizingRedeemShares? perhaps we can configure to optimizeByAShares or optimizeByAssets?
(uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) =
_redeemOptimizer.optimizeRedeemShares(this, _msgSender(), componentTokenAmount, minUnlockPeriod());
optimizeRedeemShares(_msgSender(), componentTokenAmount, minUnlockPeriod());
return requestSell(depositPeriods, sharesAtPeriods);
}

function requestSell(uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods)
public
virtual
returns (uint256 requestId)
{
uint256 unlockPeriod = 0;
uint256[] memory unlockPeriods = new uint256[](depositPeriods.length);
for (uint256 i = 0; i < depositPeriods.length; ++i) {
Expand Down Expand Up @@ -204,8 +212,14 @@ contract LiquidContinuousMultiTokenVault is
) public override {
// TODO - we should go through the locks rather than having to figure out the periods again
(uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) =
_redeemOptimizer.optimizeRedeemShares(this, _msgSender(), componentTokenAmount, currentPeriod());
optimizeRedeemShares(requestor, componentTokenAmount, currentPeriod());
return executeSell(requestor, depositPeriods, sharesAtPeriods);
}

function executeSell(address requestor, uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods)
public
virtual
{
for (uint256 i = 0; i < depositPeriods.length; ++i) {
redeemForDepositPeriod(sharesAtPeriods[i], requestor, requestor, depositPeriods[i], currentPeriod());
}
Expand All @@ -216,6 +230,15 @@ contract LiquidContinuousMultiTokenVault is
_redeemOptimizer = redeemOptimizer;
}

// TODO - do we *always* want to optimizingRedeemShares? perhaps we can configure to optimizeByAShares or optimizeByAssets?
function optimizeRedeemShares(address requestor, uint256 shares, uint256 redeemPeriod)
public
virtual
returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods)
{
return _redeemOptimizer.optimizeRedeemShares(this, requestor, shares, redeemPeriod);
}

// ===================== Yield / YieldStrategy =====================

/// @dev set the YieldStrategy
Expand Down
Loading

0 comments on commit eb49981

Please sign in to comment.