Skip to content

Commit

Permalink
feat: Enhance LiquidStone: add Investor and Asset Receiver roles (#187)…
Browse files Browse the repository at this point in the history
…. Add named ERC1155 tokens (#179)
  • Loading branch information
lucasia committed Jan 26, 2025
1 parent fcccbce commit 414aceb
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 67 deletions.
3 changes: 2 additions & 1 deletion packages/contracts/script/DeployLiquidMultiTokenVault.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ contract DeployLiquidMultiTokenVault is TomlConfig {
owner: _tomlConfig.readAddress(".evm.address.owner"),
operator: _tomlConfig.readAddress(".evm.address.operator"),
upgrader: _tomlConfig.readAddress(".evm.address.upgrader"),
assetManager: _tomlConfig.readAddress(".evm.address.asset_manager")
assetManager: _tomlConfig.readAddress(".evm.address.asset_manager"),
assetReceiver: _tomlConfig.readAddress(".evm.address.custodian")
});
}

Expand Down
79 changes: 66 additions & 13 deletions packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ contract LiquidContinuousMultiTokenVault is
address operator;
address upgrader;
address assetManager;
address assetReceiver;
}

struct VaultParams {
Expand All @@ -65,21 +66,26 @@ contract LiquidContinuousMultiTokenVault is
IRedeemOptimizer public _redeemOptimizer;
uint256 public _vaultStartTimestamp;

// [Jan-2025] added - must be after previous fields
bool public _shouldCheckInvestorRole = false;

uint256 private constant ZERO_REQUEST_ID = 0;

bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
bytes32 public constant ASSET_MANAGER_ROLE = keccak256("ASSET_MANAGER_ROLE");
bytes32 public constant ASSET_RECEIVER_ROLE = keccak256("ASSET_RECEIVER_ROLE");
bytes32 public constant INVESTOR_ROLE = keccak256("INVESTOR_ROLE"); // deposits and redeems

error LiquidContinuousMultiTokenVault__InvalidFrequency(uint256 frequency);
error LiquidContinuousMultiTokenVault__InvalidAuthAddress(string authName, address authAddress);
error LiquidContinuousMultiTokenVault__ControllerNotSender(address sender, address controller);
error LiquidContinuousMultiTokenVault__UnAuthorized(address sender, address authorizedOwner);
error LiquidContinuousMultiTokenVault__AmountMismatch(uint256 amount1, uint256 amount2);
error LiquidContinuousMultiTokenVault__UnlockPeriodMismatch(uint256 unlockPeriod1, uint256 unlockPeriod2);
error LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount(
uint256 componentTokenAmount, uint256 unlockRequestedAmount
);
error LiquidContinuousMultiTokenVault__InvalidComponentTokenAmount( // TODO - fix naming
uint256 componentTokenAmount, uint256 unlockRequestedAmount);
error LiquidContinuousMultiTokenVault__InvestorOnly(address sender, address account);

constructor() {
_disableInitializers();
Expand All @@ -96,6 +102,7 @@ contract LiquidContinuousMultiTokenVault is
_initRole("operator", OPERATOR_ROLE, vaultParams.vaultAuth.operator);
_initRole("upgrader", UPGRADER_ROLE, vaultParams.vaultAuth.upgrader);
_initRole("assetManager", ASSET_MANAGER_ROLE, vaultParams.vaultAuth.assetManager);
_initRole("assetReceiver", ASSET_RECEIVER_ROLE, vaultParams.vaultAuth.assetReceiver);

_yieldStrategy = vaultParams.yieldStrategy;
_redeemOptimizer = vaultParams.redeemOptimizer;
Expand All @@ -119,6 +126,17 @@ contract LiquidContinuousMultiTokenVault is

// ===================== MultiTokenVault =====================

/// @inheritdoc MultiTokenVault
function deposit(uint256 assets, address receiver)
public
virtual
override
onlyInvestor(receiver)
returns (uint256 shares)
{
return super.deposit(assets, receiver);
}

/// @inheritdoc MultiTokenVault
function convertToSharesForDepositPeriod(uint256 assets, uint256 /* depositPeriod */ )
public
Expand All @@ -131,14 +149,26 @@ contract LiquidContinuousMultiTokenVault is
return assets; // 1 asset = 1 share
}

/// @inheritdoc MultiTokenVault
function redeemForDepositPeriod(uint256 shares, address receiver, address owner, uint256 depositPeriod)
public
virtual
override
onlyInvestor(owner)
onlyAuthorized(owner)
returns (uint256 assets)
{
return super.redeemForDepositPeriod(shares, receiver, owner, depositPeriod);
}

/// @inheritdoc MultiTokenVault
function redeemForDepositPeriod(
uint256 shares,
address receiver,
address owner,
uint256 depositPeriod,
uint256 redeemPeriod
) public virtual override returns (uint256 assets) {
) public virtual override onlyInvestor(owner) onlyAuthorized(owner) returns (uint256 assets) {
_unlock(owner, depositPeriod, redeemPeriod, shares);

return _redeemForDepositPeriodAfterUnlock(shares, receiver, owner, depositPeriod, redeemPeriod);
Expand Down Expand Up @@ -182,6 +212,7 @@ contract LiquidContinuousMultiTokenVault is
*/
function requestDeposit(uint256 assets, address controller, address owner)
public
onlyInvestor(owner)
onlyAuthorized(owner)
onlyController(controller)
returns (uint256 requestId_)
Expand All @@ -200,6 +231,7 @@ contract LiquidContinuousMultiTokenVault is
*/
function deposit(uint256 assets, address receiver, address controller)
public
onlyInvestor(receiver)
onlyController(controller)
returns (uint256 shares_)
{
Expand All @@ -216,6 +248,7 @@ contract LiquidContinuousMultiTokenVault is
*/
function requestRedeem(uint256 shares, address controller, address owner)
public
onlyInvestor(owner)
onlyAuthorized(owner)
onlyController(controller)
returns (uint256 requestId_)
Expand All @@ -237,6 +270,7 @@ contract LiquidContinuousMultiTokenVault is
*/
function redeem(uint256 shares, address receiver, address controller)
public
onlyInvestor(controller)
onlyController(controller)
returns (uint256 assets)
{
Expand All @@ -249,15 +283,14 @@ contract LiquidContinuousMultiTokenVault is

(uint256[] memory depositPeriods, uint256[] memory sharesAtPeriods) = unlock(controller, requestId); // unlockPeriod = redeemPeriod

uint256 totalAssetsRedeemed = 0;

assets = 0;
for (uint256 i = 0; i < depositPeriods.length; ++i) {
totalAssetsRedeemed += _redeemForDepositPeriodAfterUnlock(
assets += _redeemForDepositPeriodAfterUnlock(
sharesAtPeriods[i], receiver, controller, depositPeriods[i], requestId
);
}
emit Withdraw(_msgSender(), receiver, controller, totalAssetsRedeemed, shares);
return totalAssetsRedeemed;
emit Withdraw(_msgSender(), receiver, controller, assets, shares);
return assets;
}

// Getter View Functions
Expand Down Expand Up @@ -456,11 +489,13 @@ contract LiquidContinuousMultiTokenVault is
* @dev Withdraws the assets from out of vault for investment, i.e. in RWA.
* Only the Asset Manager can call this function.
*
* @param to The trusted address that will receive the assets, e.g. custodian
* @param assetReceiver The trusted address that will receive the assets, e.g. custodian
* @param amount The amount of the ERC-20 underlying assets to be withdrawn from the vault.
*/
function withdrawAsset(address to, uint256 amount) public onlyRole(ASSET_MANAGER_ROLE) {
_withdrawAssest(to, amount);
function withdrawAsset(address assetReceiver, uint256 amount) public onlyRole(ASSET_MANAGER_ROLE) {
_checkRole(ASSET_RECEIVER_ROLE, assetReceiver);

_withdrawAssest(assetReceiver, amount);
}

/**
Expand Down Expand Up @@ -506,6 +541,16 @@ contract LiquidContinuousMultiTokenVault is

// ===================== Utility =====================

// @dev enable/disable investor role checking
function setShouldCheckInvestorRole(bool shouldCheckInvestorRole_) public virtual onlyRole(OPERATOR_ROLE) {
_shouldCheckInvestorRole = shouldCheckInvestorRole_;
}

// @@inheritdoc ERC1155Upgradeable
function setURI(string memory newUri) public virtual onlyRole(OPERATOR_ROLE) {
_setURI(newUri);
}

/// minimum shares required to convert to assets and vice-versa.
function _minConversionThreshold() internal view returns (uint256 minConversionThreshold) {
return SCALE < 10 ? SCALE : 10;
Expand All @@ -517,6 +562,14 @@ contract LiquidContinuousMultiTokenVault is
_;
}

// @dev ensure that the account has the INVESTOR_ROLE
modifier onlyInvestor(address account) {
if (_shouldCheckInvestorRole && !hasRole(INVESTOR_ROLE, account)) {
revert LiquidContinuousMultiTokenVault__InvestorOnly(_msgSender(), account);
}
_;
}

// @dev ensure the controller is the caller
modifier onlyController(address controller) {
address caller = _msgSender();
Expand Down Expand Up @@ -545,6 +598,6 @@ contract LiquidContinuousMultiTokenVault is
}

function getVersion() public pure returns (uint256 version) {
return 2;
return 3;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT
TestParamSet.TestParam memory testParams =
TestParamSet.TestParam({ principal: 2_000 * _scale, depositPeriod: 10, redeemPeriod: 70 });
address assetManager = getAssetManager();
address assetReceiver = _vaultAuth.assetReceiver;

// ---------------- deposit ----------------
_warpToPeriod(liquidVault, testParams.depositPeriod);
Expand All @@ -173,15 +174,15 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT
liquidVault.requestDeposit(testParams.principal, alice, alice);
vm.stopPrank();

uint256 assetManagerStartBalance = _asset.balanceOf(assetManager);
uint256 receiverStartBalance = _asset.balanceOf(assetReceiver);
uint256 vaultStartBalance = _asset.balanceOf(address(liquidVault));

// ---------------- WithdrawAssetFrom Vault ----------------
// ---------------- WithdrawAssetFrom Vault succeeds ----------------
vm.prank(assetManager);
liquidVault.withdrawAsset(assetManager, vaultStartBalance);
liquidVault.withdrawAsset(assetReceiver, vaultStartBalance);

//assert balance
assertEq(assetManagerStartBalance + vaultStartBalance, _asset.balanceOf(assetManager));
assertEq(receiverStartBalance + vaultStartBalance, _asset.balanceOf(assetReceiver));
}

function test__LiquidContinuousMultiTokenVault__RedeemMultiPeriodsAllShares() public {
Expand Down Expand Up @@ -269,14 +270,78 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT
function test__LiquidContinuousMultiTokenVault__WithdrawAssetNotOwnerReverts() public {
LiquidContinuousMultiTokenVault liquidVault = _liquidVault;
address randomWallet = makeAddr("randomWallet");

// fail - wallet not the asset manager
vm.startPrank(randomWallet);
vm.expectRevert(
abi.encodeWithSelector(
IAccessControl.AccessControlUnauthorizedAccount.selector, randomWallet, liquidVault.ASSET_MANAGER_ROLE()
)
);
liquidVault.withdrawAsset(randomWallet, 1000);
liquidVault.withdrawAsset(_vaultAuth.assetReceiver, 1000);
vm.stopPrank();

// fail - wallet not the asset receiver
vm.startPrank(_vaultAuth.assetManager);
vm.expectRevert(
abi.encodeWithSelector(
IAccessControl.AccessControlUnauthorizedAccount.selector,
randomWallet,
liquidVault.ASSET_RECEIVER_ROLE()
)
);
liquidVault.withdrawAsset(randomWallet, 1000);
}

function test__LiquidContinuousMultiTokenVault__AccountNotInvestorReverts() public {
TestParamSet.TestParam memory anyParam = TestParamSet.TestParam(100, 0, 10);
LiquidContinuousMultiTokenVault liquidVault = _liquidVault;
address investorWallet = makeAddr("investor");
address nonInvestorWallet = makeAddr("nonInvestorWallet");
address anyWallet = address(0); // any wallet is fine - not involved in validation

// enable investor checking
vm.prank(_vaultAuth.operator);
_liquidVault.setShouldCheckInvestorRole(true);

bytes memory investorOnlyError = abi.encodeWithSelector(
LiquidContinuousMultiTokenVault.LiquidContinuousMultiTokenVault__InvestorOnly.selector,
investorWallet,
nonInvestorWallet
);

// ======================= deposits =======================
// function deposit(uint256 assets, address receiver) external returns (uint256 shares);
vm.prank(investorWallet);
vm.expectRevert(investorOnlyError);
liquidVault.deposit(anyParam.principal, nonInvestorWallet); // IMultiToken

vm.prank(investorWallet);
vm.expectRevert(investorOnlyError);
liquidVault.requestDeposit(anyParam.principal, anyWallet, nonInvestorWallet); // IComponent

vm.prank(investorWallet);
vm.expectRevert(investorOnlyError);
liquidVault.deposit(anyParam.principal, nonInvestorWallet, anyWallet); // IComponent

// ======================= redeems =======================
vm.prank(investorWallet);
vm.expectRevert(investorOnlyError);
liquidVault.redeemForDepositPeriod(anyParam.principal, anyWallet, nonInvestorWallet, anyParam.depositPeriod);

vm.prank(investorWallet);
vm.expectRevert(investorOnlyError);
liquidVault.redeemForDepositPeriod(
anyParam.principal, anyWallet, nonInvestorWallet, anyParam.depositPeriod, anyParam.redeemPeriod
);

vm.prank(investorWallet);
vm.expectRevert(investorOnlyError);
liquidVault.requestRedeem(anyParam.principal, anyWallet, nonInvestorWallet); // IComponent

vm.prank(investorWallet);
vm.expectRevert(investorOnlyError);
liquidVault.redeem(anyParam.principal, anyWallet, nonInvestorWallet); // IComponent
}

// Scenario: Calculating returns for a standard investment
Expand Down
Loading

0 comments on commit 414aceb

Please sign in to comment.