Skip to content

Commit

Permalink
Add RedeemOptimizer to LiquidContinuousMultiTokenVault
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasia committed Oct 3, 2024
1 parent 0212560 commit 7208f90
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 97 deletions.
48 changes: 25 additions & 23 deletions packages/contracts/script/DeployLiquidMultiTokenVault.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ pragma solidity ^0.8.20;
import { LiquidContinuousMultiTokenVault } from "@credbull/yield/LiquidContinuousMultiTokenVault.sol";
import { IYieldStrategy } from "@credbull/yield/strategy/IYieldStrategy.sol";
import { TripleRateYieldStrategy } from "@credbull/yield/strategy/TripleRateYieldStrategy.sol";
import { TripleRateContext } from "@credbull/yield/context/TripleRateContext.sol";
import { ITripleRateContext } from "@credbull/yield/context/ITripleRateContext.sol";
import { TripleRateContext } from "@credbull/yield/context/TripleRateContext.sol";
import { IRedeemOptimizer } from "@credbull/token/ERC1155/IRedeemOptimizer.sol";
import { RedeemOptimizerFIFO } from "@credbull/token/ERC1155/RedeemOptimizerFIFO.sol";

import { TomlConfig } from "@script/TomlConfig.s.sol";

import { TomlConfig } from "./TomlConfig.s.sol";
import { stdToml } from "forge-std/StdToml.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SimpleUSDC } from "@test/test/token/SimpleUSDC.t.sol";

import { stdToml } from "forge-std/StdToml.sol";
import { console2 } from "forge-std/console2.sol";

contract DeployLiquidMultiTokenVault is TomlConfig {
Expand All @@ -39,10 +42,20 @@ contract DeployLiquidMultiTokenVault is TomlConfig {
function run(address contractOwner) public returns (LiquidContinuousMultiTokenVault vault) {
IERC20Metadata usdc = _usdcOrDeployMock(contractOwner);

IYieldStrategy yieldStrategy = _deployYieldStrategy(contractOwner);
vm.startBroadcast(contractOwner);

IYieldStrategy yieldStrategy = new TripleRateYieldStrategy();
console2.log(string.concat("!!!!! Deploying IYieldStrategy [", vm.toString(address(yieldStrategy)), "] !!!!!"));

IRedeemOptimizer redeemOptimizer = new RedeemOptimizerFIFO();
console2.log(
string.concat("!!!!! Deploying IRedeemOptimizer [", vm.toString(address(redeemOptimizer)), "] !!!!!")
);

vm.stopBroadcast();

LiquidContinuousMultiTokenVault.VaultParams memory vaultParams =
_createVaultParams(contractOwner, usdc, yieldStrategy);
_createVaultParams(contractOwner, usdc, yieldStrategy, redeemOptimizer);

vm.startBroadcast(contractOwner);

Expand Down Expand Up @@ -71,11 +84,12 @@ contract DeployLiquidMultiTokenVault is TomlConfig {
return LiquidContinuousMultiTokenVault(address(liquidVault));
}

function _createVaultParams(address contractOwner, IERC20Metadata asset, IYieldStrategy yieldStrategy)
public
view
returns (LiquidContinuousMultiTokenVault.VaultParams memory vaultParams_)
{
function _createVaultParams(
address contractOwner,
IERC20Metadata asset,
IYieldStrategy yieldStrategy,
IRedeemOptimizer redeemOptimizer
) public view returns (LiquidContinuousMultiTokenVault.VaultParams memory vaultParams_) {
string memory contractKey = ".evm.contracts.liquid_continuous_multi_token_vault";
uint256 fullRateBasisPoints = tomlConfig.readUint(string.concat(contractKey, ".full_rate_bps"));
uint256 reducedRateBasisPoints = tomlConfig.readUint(string.concat(contractKey, ".reduced_rate_bps"));
Expand All @@ -100,6 +114,7 @@ contract DeployLiquidMultiTokenVault is TomlConfig {
contractOperator: operator,
asset: asset,
yieldStrategy: yieldStrategy,
redeemOptimizer: redeemOptimizer,
vaultStartTimestamp: startTimestamp,
redeemNoticePeriod: 1,
contextParams: contextParams
Expand All @@ -108,19 +123,6 @@ contract DeployLiquidMultiTokenVault is TomlConfig {
return vaultParams;
}

function _deployYieldStrategy(address contractOwner) internal returns (IYieldStrategy) {
vm.startBroadcast(contractOwner);

IYieldStrategy yieldStrategy = new TripleRateYieldStrategy();
console2.log(
string.concat("!!!!! Deploying TripleRateYieldStrategy [", vm.toString(address(yieldStrategy)), "] !!!!!")
);

vm.stopBroadcast();

return yieldStrategy;
}

function _usdcOrDeployMock(address contractOwner) internal returns (IERC20Metadata asset) {
bool shouldDeployMocks = _readBoolWithDefault(tomlConfig, ".evm.deploy_mocks", false);

Expand Down
12 changes: 11 additions & 1 deletion packages/contracts/src/token/component/IComponentToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ pragma solidity ^0.8.20;

/**
* @title IComponentToken
* @dev Interface for buying and selling tokens. Selling requires a two-step request and sell process.
* @dev Interface for buying and selling tokens.
*
* Similar to ERC-4626 Tokenized Vault standard, here:
* - currencyToken is the "asset" ERC20 token. e.g. USDC or another stablecoin
* - componentToken is the "share" ERC20 token. i.e. the Vault token
* // TODO - add proposed "deposit-aware" interface
* //
*/
interface IComponentToken {
// --------------------- Plume invoked ---------------------

/**
* @notice Submit a request to send currencyTokenAmount of CurrencyToken to buy ComponentToken
* @param currencyTokenAmount Amount of CurrencyToken to send
Expand All @@ -20,6 +28,8 @@ interface IComponentToken {
*/
function requestSell(uint256 componentTokenAmount) external returns (uint256 requestId);

// --------------------- Credbull invoked ---------------------

/**
* @notice Executes a request to buy ComponentToken with CurrencyToken
* @param requestor Address of the user or smart contract that requested the buy
Expand Down
15 changes: 0 additions & 15 deletions packages/contracts/src/token/component/IComponentTokenYield.sol

This file was deleted.

76 changes: 38 additions & 38 deletions packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ contract LiquidContinuousMultiTokenVault is
address contractOperator;
IERC20Metadata asset;
IYieldStrategy yieldStrategy;
// IRedeemOptimizer redeemOptimizer; // TODO - add in the redeemOptimizer
IRedeemOptimizer redeemOptimizer;
uint256 vaultStartTimestamp;
uint256 redeemNoticePeriod;
TripleRateContext.ContextParams contextParams;
Expand All @@ -56,13 +56,16 @@ contract LiquidContinuousMultiTokenVault is
IYieldStrategy public yieldStrategy;
IRedeemOptimizer public redeemOptimizer;

uint256 private constant ZERO_REQUEST_ID = 0;

bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

error LiquidContinuousMultiTokenVault__InvalidFrequency(uint256 frequency);
error LiquidContinuousMultiTokenVault__InvalidOwnerAddress(address ownerAddress);
error LiquidContinuousMultiTokenVault__InvalidOperatorAddress(address ownerAddress);
error LiquidContinuousMultiTokenVault__AmountMismatch(uint256 amount1, uint256 amount2);
error LiquidContinuousMultiTokenVault__UnlockPeriodMismatch(uint256 unlockPeriod1, uint256 unlockPeriod2);

function initialize(VaultParams memory vaultParams) public initializer {
__UUPSUpgradeable_init();
Expand All @@ -85,6 +88,7 @@ contract LiquidContinuousMultiTokenVault is
_grantRole(UPGRADER_ROLE, vaultParams.contractOperator);

yieldStrategy = vaultParams.yieldStrategy;
redeemOptimizer = vaultParams.redeemOptimizer;

if (vaultParams.contextParams.frequency != 360 && vaultParams.contextParams.frequency != 365) {
revert LiquidContinuousMultiTokenVault__InvalidFrequency(vaultParams.contextParams.frequency);
Expand Down Expand Up @@ -140,31 +144,35 @@ contract LiquidContinuousMultiTokenVault is
// ===================== Buyable/Sellable =====================

/// @inheritdoc IComponentToken
/// @dev - requesting to buy is not required, Users can directly executeBuy
function requestBuy(uint256 /* currencyTokenAmount */ )
public
view
virtual
override
onlyRole(OPERATOR_ROLE)
returns (uint256 requestId)
{
return 0;
}
/// @dev - buys can be directly executed.
function requestBuy(uint256 currencyTokenAmount) public virtual override returns (uint256 requestId) {
uint256 componentTokenAmount = currencyTokenAmount; // 1 asset = 1 share

/// @inheritdoc IComponentToken
function requestSell(uint256 componentTokenAmount) public virtual override returns (uint256 requestId) {
// TODO - need helper to find which depositPeriods we want to sell from...
executeBuy(_msgSender(), ZERO_REQUEST_ID, currencyTokenAmount, componentTokenAmount);

return _requestSell(componentTokenAmount, currentPeriod());
return ZERO_REQUEST_ID;
}

/// @notice Request to sell (redeem) `amount` of tokens at the `depositPeriod`
/// @param amount The amount a User wants to sell (redeem). This could be yield only, or include principal + yield.
function _requestSell(uint256 amount, uint256 depositPeriod) internal virtual returns (uint256 requestId) {
requestUnlock(_msgSender(), depositPeriod, amount);
/// @inheritdoc IComponentToken
function requestSell(uint256 componentTokenAmount) public virtual override returns (uint256 requestId) {
(uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) =
redeemOptimizer.optimizeRedeemShares(this, _msgSender(), componentTokenAmount, minUnlockPeriod());

uint256 unlockPeriod = 0;
uint256[] memory unlockPeriods = new uint256[](depositPeriods.length);
for (uint256 i = 0; i < depositPeriods.length; ++i) {
unlockPeriods[i] = requestUnlock(_msgSender(), depositPeriods[i], sharesAtPeriods[i]);

if (i == 0) {
// initialize
unlockPeriod = unlockPeriods[i];
} else if (unlockPeriod != unlockPeriods[i]) {
// validate for other periods
revert LiquidContinuousMultiTokenVault__UnlockPeriodMismatch(unlockPeriod, unlockPeriods[i]);
}
}

return 0; // TODO - need to add requestId to requestUnlock()
return unlockPeriods[0];
}

/// @inheritdoc IComponentToken
Expand All @@ -184,25 +192,17 @@ contract LiquidContinuousMultiTokenVault is
/// @inheritdoc IComponentToken
function executeSell(
address requestor,
uint256 requestId,
uint256 currencyTokenAmount,
uint256 componentTokenAmount
) public override {
// TODO - verify currencyTokenAmount convertToAssets(componentTokenAmount) = currencyTokenAmount
// TODO - verifying currencyTokeAmount figuring out the componentTokenAmount here is non-trivial. it will span multiple periods.
_executeSell(requestor, currentPeriod(), requestId, currencyTokenAmount, componentTokenAmount);
}

function _executeSell(
address requestor,
uint256 depositPeriod,
uint256, /* requestId */
uint256, /* currencyTokenAmount */
uint256 componentTokenAmount
) internal {
// TODO - verify currencyTokenAmount convertToAssets(componentTokenAmount) = currencyTokenAmount
// TODO - need helper to find which depositPeriods we want to sell from...
redeemForDepositPeriod(componentTokenAmount, requestor, requestor, depositPeriod, currentPeriod());
) 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());

for (uint256 i = 0; i < depositPeriods.length; ++i) {
redeemForDepositPeriod(sharesAtPeriods[i], requestor, requestor, depositPeriods[i], currentPeriod());
}
}

// ===================== Yield / YieldStrategy =====================
Expand Down Expand Up @@ -287,7 +287,7 @@ contract LiquidContinuousMultiTokenVault is
return MultiTokenVault.supportsInterface(interfaceId);
}

function getVersion() external view returns (uint256 version) {
function getVersion() public pure returns (uint256 version) {
return 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.20;
import { LiquidContinuousMultiTokenVault } from "@credbull/yield/LiquidContinuousMultiTokenVault.sol";
import { TripleRateYieldStrategy } from "@credbull/yield/strategy/TripleRateYieldStrategy.sol";
import { IMultiTokenVault } from "@credbull/token/ERC1155/IMultiTokenVault.sol";
import { RedeemOptimizerFIFO } from "@credbull/token/ERC1155/RedeemOptimizerFIFO.sol";
import { Timer } from "@credbull/timelock/Timer.sol";
import { DeployLiquidMultiTokenVault } from "@script/DeployLiquidMultiTokenVault.s.sol";

Expand All @@ -25,10 +26,10 @@ contract LiquidContinuousMultiTokenVaultTest is IMultiTokenVaultTestBase {
_liquidVault = _deployVault.run(owner);

_asset = IERC20Metadata(_liquidVault.asset());
_vaultParams =
_deployVault._createVaultParams(owner, _asset, new TripleRateYieldStrategy(), new RedeemOptimizerFIFO());
_scale = 10 ** _asset.decimals();

_vaultParams = _deployVault._createVaultParams(owner, _asset, new TripleRateYieldStrategy());

_transferAndAssert(_asset, owner, alice, 100_000 * _scale);
}

Expand Down Expand Up @@ -76,7 +77,7 @@ contract LiquidContinuousMultiTokenVaultTest is IMultiTokenVaultTestBase {

// requestSell
vm.prank(alice);
uint256 requestId = liquidVault.requestSellForDepositPeriod(sharesAmount, testParams.depositPeriod); // TODO - test should not pass in depositPeriod here
uint256 requestId = liquidVault.requestSell(sharesAmount);
assertEq(
sharesAmount,
liquidVault.unlockRequested(alice, testParams.depositPeriod),
Expand All @@ -92,9 +93,7 @@ contract LiquidContinuousMultiTokenVaultTest is IMultiTokenVaultTestBase {
_warpToPeriod(liquidVault, testParams.redeemPeriod);

vm.prank(alice);
liquidVault.executeSellForDepositPeriod(
alice, testParams.depositPeriod, requestId, testParams.principal + expectedYield, sharesAmount
);
liquidVault.executeSell(alice, requestId, testParams.principal + expectedYield, sharesAmount);

assertEq(0, liquidVault.balanceOf(alice, testParams.depositPeriod), "user should have no shares remaining");
assertEq(
Expand Down Expand Up @@ -251,20 +250,6 @@ contract LiquidContinuousMultiTokenVaultMock is LiquidContinuousMultiTokenVault
function mockInitialize(VaultParams memory params) public initializer {
super.initialize(params);
}

function requestSellForDepositPeriod(uint256 amount, uint256 depositPeriod) public returns (uint256 requestId) {
return super._requestSell(amount, depositPeriod);
}

function executeSellForDepositPeriod(
address requestor,
uint256 depositPeriod,
uint256 requestId,
uint256 currencyTokenAmount,
uint256 componentTokenAmount
) public {
super._executeSell(requestor, depositPeriod, requestId, currencyTokenAmount, componentTokenAmount);
}
}

contract LiquidContinuousMultiTokenVaultMockV2 is LiquidContinuousMultiTokenVaultMock {
Expand Down

0 comments on commit 7208f90

Please sign in to comment.