Skip to content

Commit

Permalink
Add withdraw to RedeemOptimizerFIFO
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasia committed Oct 3, 2024
1 parent f75bbde commit 833a59e
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 78 deletions.
13 changes: 13 additions & 0 deletions packages/contracts/src/token/ERC1155/IMultiTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,19 @@ interface IMultiTokenVault is IERC1155Upgradeable {
view
returns (uint256 assets);

/**
* @notice Converts shares to assets for the given deposit periods at the redeem period.
* @param shares The amount of shares to convert.
* @param depositPeriods The periods during which the shares were issued.
* @param redeemPeriod The period during which the shares are redeemed.
* @return assets The equivalent amount of assets.
*/
function convertToAssetsForDepositPeriods(
uint256[] memory shares,
uint256[] memory depositPeriods,
uint256 redeemPeriod
) external view returns (uint256[] memory assets);

/**
* @notice Converts shares to assets for a specific deposit period at the current redeem period.
* @param shares The amount of shares to convert.
Expand Down
10 changes: 4 additions & 6 deletions packages/contracts/src/token/ERC1155/IRedeemOptimizer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,21 @@ import { IMultiTokenVault } from "@credbull/token/ERC1155/IMultiTokenVault.sol";
*/
interface IRedeemOptimizer {
/**
* @notice Finds optimal deposit periods and shares to redeem.
* @param shares The total shares to redeem.
* @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 optimizeRedeem(IMultiTokenVault vault, address owner, uint256 shares)
function optimizeRedeemShares(IMultiTokenVault vault, address owner, uint256 shares, uint256 redeemPeriod)
external
view
returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods);

/**
* @notice Finds optimal deposit periods and shares to redeem for a given asset amount.
* @param assets The total asset amount to withdraw.
* @notice Finds optimal deposit periods and shares to withdraw for a given asset 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 optimizeWithdraw(IMultiTokenVault vault, address owner, uint256 assets)
function optimizeWithdrawAssets(IMultiTokenVault vault, address owner, uint256 assets, uint256 redeemPeriod)
external
view
returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods);
Expand Down
21 changes: 21 additions & 0 deletions packages/contracts/src/token/ERC1155/MultiTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ abstract contract MultiTokenVault is
error MultiTokenVault__RedeemTimePeriodNotSupported(address owner, uint256 period, uint256 redeemPeriod);
error MultiTokenVault__CallerMissingApprovalForAll(address operator, address owner);
error MultiTokenVault__RedeemBeforeDeposit(address owner, uint256 depositPeriod, uint256 redeemPeriod);
error MultiTokenVault__InvalidArrayLength(uint256 depositPeriodsLength, uint256 sharesLength);

/**
* @notice Initializes the vault with the asset, treasury, and token URI for ERC1155 tokens.
Expand Down Expand Up @@ -179,6 +180,26 @@ abstract contract MultiTokenVault is
virtual
returns (uint256 assets);

/**
* @inheritdoc IMultiTokenVault
*/
function convertToAssetsForDepositPeriods(
uint256[] memory shares,
uint256[] memory depositPeriods,
uint256 redeemPeriod
) external view returns (uint256[] memory assets_) {
if (shares.length != depositPeriods.length) {
revert MultiTokenVault__InvalidArrayLength(depositPeriods.length, shares.length);
}

uint256[] memory assets = new uint256[](depositPeriods.length);
for (uint256 i = 0; i < depositPeriods.length; ++i) {
assets[i] = convertToAssetsForDepositPeriod(shares[i], depositPeriods[i], redeemPeriod);
}

return assets;
}

/**
* @inheritdoc IMultiTokenVault
*/
Expand Down
131 changes: 73 additions & 58 deletions packages/contracts/src/token/ERC1155/RedeemOptimizerFIFO.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,94 +12,109 @@ contract RedeemOptimizerFIFO is IRedeemOptimizer {
error RedeemOptimizer__InvalidPeriodRange(uint256 fromPeriod, uint256 toPeriod);
error RedeemOptimizer__FuturePeriodNotAllowed(uint256 toPeriod, uint256 currentPeriod);

enum AmountType {
Shares,
AssetsWithReturns
}

uint256 public _fromDepositPeriod = 0;

/// @inheritdoc IRedeemOptimizer
function optimizeRedeem(IMultiTokenVault vault, address owner, uint256 shares)
function optimizeRedeemShares(IMultiTokenVault vault, address owner, uint256 shares, uint256 redeemPeriod)
public
view
returns (uint256[] memory depositPeriods_, uint256[] memory sharesAtPeriods_)
{
return _sharesAtPeriods(vault, owner, shares, 0, vault.currentPeriodsElapsed());
return _amountsAtPeriods(
vault, owner, shares, _fromDepositPeriod, vault.currentPeriodsElapsed(), redeemPeriod, AmountType.Shares
);
}

// TODO - confirm whether assets here is the "principal" (easier) or the "total desired withdraw" (trickier)
/// @inheritdoc IRedeemOptimizer
function optimizeWithdraw(IMultiTokenVault vault, address owner, uint256 assets)
/// @dev - assets include deposit (principal) and any returns up to the redeem period
// TODO - confirm whether returns are calculated on the requestRedeem period or redeemPeriod ?
function optimizeWithdrawAssets(IMultiTokenVault vault, address owner, uint256 assets, uint256 redeemPeriod)
public
view
returns (uint256[] memory depositPeriods_, uint256[] memory sharesAtPeriods_)
{
return _sharesAtPeriods(vault, owner, assets, 0, vault.currentPeriodsElapsed());
return _amountsAtPeriods(
vault,
owner,
assets,
_fromDepositPeriod,
vault.currentPeriodsElapsed(),
redeemPeriod,
AmountType.AssetsWithReturns
);
}

/// @notice Returns deposit periods and corresponding shares within the specified range.
function _sharesAtPeriods(
/// @notice Returns deposit periods and corresponding amounts (shares or assets) within the specified range.
function _amountsAtPeriods(
IMultiTokenVault vault,
address owner,
uint256 maxShares,
uint256 fromPeriod,
uint256 toPeriod
) internal view returns (uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods_) {
// count periods with a non-zero balance
(uint256 numPeriodsWithBalance,) = _numPeriodsWithBalance(vault, owner, maxShares, fromPeriod, toPeriod);

// TODO - if the sharesCollected is less than the maxShares - should we revert or continue?

depositPeriods = new uint256[](numPeriodsWithBalance);
sharesAtPeriods_ = new uint256[](numPeriodsWithBalance);

// populate arrays
uint256 arrayIndex = 0;
uint256 sharesCollected_ = 0;

for (uint256 i = fromPeriod; i <= toPeriod; i++) {
uint256 sharesAtPeriod = vault.balanceOf(owner, i);
uint256 maxAmount,
uint256 fromDepositPeriod,
uint256 toDepositPeriod,
uint256 redeemPeriod,
AmountType amountType
) internal view returns (uint256[] memory depositPeriods, uint256[] memory amountAtPeriods) {
if (fromDepositPeriod > toDepositPeriod) {
revert RedeemOptimizer__InvalidPeriodRange(fromDepositPeriod, toDepositPeriod);
}

if (sharesAtPeriod > 0) {
depositPeriods[arrayIndex] = i;
uint256 currentPeriod = vault.currentPeriodsElapsed();
if (toDepositPeriod > currentPeriod) {
revert RedeemOptimizer__FuturePeriodNotAllowed(toDepositPeriod, currentPeriod);
}

uint256 shares =
sharesAtPeriod + sharesCollected_ <= maxShares ? sharesAtPeriod : maxShares - sharesCollected_;
// first loop: check for periods with balances. needed to correctly size our array results
uint256 numPeriodsWithBalance = 0;
for (uint256 depositPeriod = fromDepositPeriod; depositPeriod <= toDepositPeriod; ++depositPeriod) {
uint256 sharesAtPeriod = vault.balanceOf(owner, depositPeriod);

sharesAtPeriods_[arrayIndex] = shares;
uint256 amountAtPeriod = amountType == AmountType.Shares
? sharesAtPeriod
: vault.convertToAssetsForDepositPeriod(sharesAtPeriod, depositPeriod, redeemPeriod);

arrayIndex++;
if (amountAtPeriod > 0) {
numPeriodsWithBalance++;
}
}
}

/// @notice Counts periods with non-zero shares and the total collected shares.
function _numPeriodsWithBalance(
IMultiTokenVault vault,
address owner,
uint256 maxShares,
uint256 fromPeriod,
uint256 toPeriod
) internal view returns (uint256 numPeriodsWithBalance, uint256 sharesCollected) {
if (fromPeriod > toPeriod) {
revert RedeemOptimizer__InvalidPeriodRange(fromPeriod, toPeriod);
}
// second loop - collect and return the periods and amounts
depositPeriods = new uint256[](numPeriodsWithBalance);
amountAtPeriods = new uint256[](numPeriodsWithBalance);

uint256 currentPeriod = vault.currentPeriodsElapsed();
if (toPeriod > currentPeriod) {
revert RedeemOptimizer__FuturePeriodNotAllowed(toPeriod, currentPeriod);
}
uint256 arrayIndex = 0;
uint256 amountCollected = 0;

uint256 numPeriodsWithBalance_ = 0;
uint256 sharesCollected_ = 0;
for (uint256 depositPeriod = fromDepositPeriod; depositPeriod <= toDepositPeriod; ++depositPeriod) {
uint256 sharesAtPeriod = vault.balanceOf(owner, depositPeriod);

for (uint256 i = fromPeriod; i <= toPeriod; i++) {
uint256 sharesAtPeriod = vault.balanceOf(owner, i);
uint256 amountAtPeriod = amountType == AmountType.Shares
? sharesAtPeriod
: vault.convertToAssetsForDepositPeriod(sharesAtPeriod, depositPeriod, redeemPeriod);

if (sharesAtPeriod > 0) {
numPeriodsWithBalance_++;
sharesCollected_ += sharesAtPeriod;
}
if (amountAtPeriod > 0) {
depositPeriods[arrayIndex] = depositPeriod;

// check if we will go "over" the max amount
if ((amountCollected + amountAtPeriod) > maxAmount) {
amountAtPeriods[arrayIndex] = maxAmount - amountCollected; // include only the partial amount

if (sharesCollected_ >= maxShares) {
return (numPeriodsWithBalance_, maxShares);
return (depositPeriods, amountAtPeriods); // we're done, no need to keep looping
} else {
amountAtPeriods[arrayIndex] = amountAtPeriod;
}

amountCollected += amountAtPeriods[arrayIndex];
arrayIndex++;
}
}

return (numPeriodsWithBalance_, sharesCollected);
// TODO - if the sharesCollected is less than the maxShares - should we revert or return what we have?

return (depositPeriods, amountAtPeriods);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ contract MultiTokenVaultTest is IMultiTokenVaultTestBase {

deposit1TestParams = IMultiTokenVaultTestParams({ principal: 500 * SCALE, depositPeriod: 10, redeemPeriod: 21 });
deposit2TestParams = IMultiTokenVaultTestParams({ principal: 300 * SCALE, depositPeriod: 15, redeemPeriod: 17 });
deposit3TestParams = IMultiTokenVaultTestParams({ principal: 700 * SCALE, depositPeriod: 30, redeemPeriod: 24 });
deposit3TestParams = IMultiTokenVaultTestParams({ principal: 700 * SCALE, depositPeriod: 30, redeemPeriod: 55 });
}

function test__MultiTokenVaulTest__Period10() public {
Expand Down
57 changes: 44 additions & 13 deletions packages/contracts/test/src/token/ERC1155/RedeemOptimizerTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,55 @@ contract RedeemOptimizerTest is MultiTokenVaultTest {
IMultiTokenVault multiTokenVault = _createMultiTokenVault(asset, assetToSharesRatio, 10);
IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFO();

// verify deposit - period 1
uint256 deposit1Shares = _testDepositOnly(alice, multiTokenVault, deposit1TestParams);
uint256 deposit2Shares = _testDepositOnly(alice, multiTokenVault, deposit2TestParams);
uint256 deposit3Shares = _testDepositOnly(alice, multiTokenVault, deposit3TestParams);
(uint256[] memory depositPeriods, uint256[] memory depositShares) = _testDeposits(alice, multiTokenVault); // make a few deposits
uint256 totalDepositShares = depositShares[0] + depositShares[1] + depositShares[2];

uint256 totalDepositShares = deposit1Shares + deposit2Shares + deposit3Shares;

// warp vault to last depositPeriod
_warpToPeriod(multiTokenVault, deposit3TestParams.depositPeriod);
// warp vault ahead redemPeriod
uint256 redeemPeriod = deposit3TestParams.redeemPeriod;
_warpToPeriod(multiTokenVault, redeemPeriod);

// check full redeem
(uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) =
redeemOptimizer.optimizeRedeem(multiTokenVault, alice, totalDepositShares);
(uint256[] memory redeemDepositPeriods, uint256[] memory sharesAtPeriods) =
redeemOptimizer.optimizeRedeemShares(multiTokenVault, alice, totalDepositShares, redeemPeriod);

assertEq(3, depositPeriods.length, "depositPeriods wrong length - full redeem");
assertEq(3, redeemDepositPeriods.length, "depositPeriods wrong length - full redeem");
assertEq(3, sharesAtPeriods.length, "sharesAtPeriods wrong length - full redeem");

assertEq(deposit1TestParams.depositPeriod, depositPeriods[0]);
assertEq(deposit1Shares, sharesAtPeriods[0]);
assertEq(deposit1TestParams.depositPeriod, redeemDepositPeriods[0], "optimizeRedeem - wrong depositPeriod");
assertEq(depositShares[0], sharesAtPeriods[0], "optimizeRedeem - wrong shares");

// TODO - check the other depositPeriods

uint256[] memory expectedAssetsAtPeriods =
multiTokenVault.convertToAssetsForDepositPeriods(depositShares, depositPeriods, redeemPeriod);

(uint256[] memory withdrawDepositPeriods, uint256[] memory actualAssetsAtPeriods) =
redeemOptimizer.optimizeWithdrawAssets(multiTokenVault, alice, totalDepositShares, redeemPeriod);

assertEq(3, withdrawDepositPeriods.length, "depositPeriods wrong length - full redeem");
assertEq(3, actualAssetsAtPeriods.length, "sharesAtPeriods wrong length - full redeem");

assertEq(deposit1TestParams.depositPeriod, withdrawDepositPeriods[0], "optimizeWithdraw - wrong depositPeriod");
assertEq(expectedAssetsAtPeriods[0], actualAssetsAtPeriods[0], "optimizeWithdraw - wrong assets");

// TODO - check the other withdrawPeriods
}

function _testDeposits(address receiver, IMultiTokenVault vault)
internal
returns (uint256[] memory depositPeriods_, uint256[] memory shares_)
{
uint256[] memory depositPeriods = new uint256[](3);
uint256[] memory shares = new uint256[](3);

depositPeriods[0] = deposit1TestParams.depositPeriod;
depositPeriods[1] = deposit2TestParams.depositPeriod;
depositPeriods[2] = deposit3TestParams.depositPeriod;

shares[0] = _testDepositOnly(receiver, vault, deposit1TestParams);
shares[1] = _testDepositOnly(receiver, vault, deposit2TestParams);
shares[2] = _testDepositOnly(receiver, vault, deposit3TestParams);

return (depositPeriods, shares);
}
}

0 comments on commit 833a59e

Please sign in to comment.