Skip to content

Commit

Permalink
Allow the Sentinel service account to submit redemption requests on b…
Browse files Browse the repository at this point in the history
…ehalf of the lender

1. Add a flag called `autoRedemptionAfterLockup` to `LPConfig` to indicate whether we'll
   allow the Sentinel service account to add redemption requests on behalf of lenders.
2. If `true`, disable redemption cancellation.
  • Loading branch information
ljiatu committed Oct 30, 2024
1 parent 6a04a5a commit 6f14300
Show file tree
Hide file tree
Showing 13 changed files with 401 additions and 163 deletions.
1 change: 1 addition & 0 deletions contracts/common/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ contract Errors {
error AlreadyALender(); // 0xce4108ca
error NonReinvestYieldLenderCapacityReached(); // 0x39ad903d
error ReinvestYieldOptionAlreadySet(); // 0xb44d305d
error RedemptionCancellationDisabled(); // 0x70c5f64d

// First loss cover
error InsufficientFirstLossCover(); // 0x86fdb63a
Expand Down
4 changes: 4 additions & 0 deletions contracts/common/PoolConfig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ struct LPConfig {
uint16 tranchesRiskAdjustmentInBps;
// How long a lender has to wait after the last deposit before they can withdraw
uint16 withdrawalLockoutPeriodInDays;
// When enabled, lenders' shares are automatically redeemed after the lockup period.
bool autoRedemptionAfterLockup;
}

struct FrontLoadingFeesStructure {
Expand Down Expand Up @@ -197,6 +199,7 @@ contract PoolConfig is Initializable, AccessControlUpgradeable, UUPSUpgradeable
uint16 fixedSeniorYieldInBps,
uint16 tranchesRiskAdjustmentInBps,
uint16 withdrawalLockoutInDays,
bool autoRedemptionAfterLockup,
address by
);
event FrontLoadingFeesChanged(
Expand Down Expand Up @@ -553,6 +556,7 @@ contract PoolConfig is Initializable, AccessControlUpgradeable, UUPSUpgradeable
lpConfig.fixedSeniorYieldInBps,
lpConfig.tranchesRiskAdjustmentInBps,
lpConfig.withdrawalLockoutPeriodInDays,
lpConfig.autoRedemptionAfterLockup,
msg.sender
);
}
Expand Down
6 changes: 4 additions & 2 deletions contracts/factory/PoolFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -464,15 +464,17 @@ contract PoolFactory is Initializable, AccessControlUpgradeable, UUPSUpgradeable
uint8 maxSeniorJuniorRatio,
uint16 fixedSeniorYieldInBps,
uint16 tranchesRiskAdjustmentInBps,
uint16 withdrawalLockoutPeriodInDays
uint16 withdrawalLockoutPeriodInDays,
bool autoRedemptionAfterLockup
) external {
_onlyDeployer(msg.sender);
LPConfig memory lpConfig = LPConfig({
liquidityCap: liquidityCap,
maxSeniorJuniorRatio: maxSeniorJuniorRatio,
fixedSeniorYieldInBps: fixedSeniorYieldInBps,
tranchesRiskAdjustmentInBps: tranchesRiskAdjustmentInBps,
withdrawalLockoutPeriodInDays: withdrawalLockoutPeriodInDays
withdrawalLockoutPeriodInDays: withdrawalLockoutPeriodInDays,
autoRedemptionAfterLockup: autoRedemptionAfterLockup
});
PoolConfig(_pools[poolId_].poolConfigAddress).setLPConfig(lpConfig);
}
Expand Down
56 changes: 41 additions & 15 deletions contracts/liquidity/TrancheVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity 0.8.23;

import {Errors} from "../common/Errors.sol";
import {PoolConfig, PoolSettings} from "../common/PoolConfig.sol";
import {LPConfig, PoolConfig, PoolSettings} from "../common/PoolConfig.sol";
import {PoolConfigCache} from "../common/PoolConfigCache.sol";
import {DEFAULT_DECIMALS_FACTOR, SECONDS_IN_A_DAY} from "../common/SharedDefs.sol";
import {TrancheVaultStorage, IERC20} from "./TrancheVaultStorage.sol";
Expand All @@ -15,6 +15,7 @@ import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/acce
import {IERC20MetadataUpgradeable, ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {HumaConfig} from "../common/HumaConfig.sol";

/**
* @title TrancheVault
Expand Down Expand Up @@ -96,11 +97,17 @@ contract TrancheVault is

/**
* @notice A redemption request has been added.
* @param account The account whose shares to be redeemed.
* @param account The account whose shares are requested for redemption.
* @param requester The account that requested redemption.
* @param shares The number of shares to be redeemed.
* @param epochId The epoch ID.
*/
event RedemptionRequestAdded(address indexed account, uint256 shares, uint256 epochId);
event RedemptionRequestAdded(
address indexed account,
address indexed requester,
uint256 shares,
uint256 epochId
);

/**
* @notice A redemption request has been canceled.
Expand Down Expand Up @@ -313,11 +320,22 @@ contract TrancheVault is

/**
* @notice Records a new redemption request.
* @notice If `autoRedemptionAfterLockup` is true, then allow Sentinel service account to request redemption
* on behalf of the lender as required by the SPV of certain pools.
* @param lender The account whose shares are requested for redemption.
* @param shares The number of shares the lender wants to redeem.
* @custom:access Only the lender can submit for themselves.
* @custom:access Only the lender and the Sentinel service account can request redemption.
*/
function addRedemptionRequest(uint256 shares) external {
function addRedemptionRequest(address lender, uint256 shares) external {
if (shares == 0) revert Errors.ZeroAmountProvided();
LPConfig memory lpConfig = poolConfig.getLPConfig();
if (msg.sender != lender) {
if (lpConfig.autoRedemptionAfterLockup && msg.sender != humaConfig.sentinelServiceAccount()) {
revert Errors.SentinelServiceAccountRequired();
} else {
revert Errors.LenderRequired();
}
}
poolConfig.onlyProtocolAndPoolOn();

PoolSettings memory poolSettings = poolConfig.getPoolSettings();
Expand All @@ -326,22 +344,22 @@ contract TrancheVault is
block.timestamp
);

// Checks against withdrawal lockup period.
DepositRecord memory depositRecord = _getDepositRecord(msg.sender);
// Check against withdrawal lockup period.
DepositRecord memory depositRecord = _getDepositRecord(lender);
if (
nextEpochStartTime <
depositRecord.lastDepositTime +
poolConfig.getLPConfig().withdrawalLockoutPeriodInDays *
lpConfig.withdrawalLockoutPeriodInDays *
SECONDS_IN_A_DAY
) revert Errors.WithdrawTooEarly();

uint256 sharesBalance = ERC20Upgradeable.balanceOf(msg.sender);
uint256 sharesBalance = ERC20Upgradeable.balanceOf(lender);
if (shares > sharesBalance) {
revert Errors.InsufficientSharesForRequest();
}
uint256 assetsAfterRedemption = convertToAssets(sharesBalance - shares);
poolConfig.checkLiquidityRequirementForRedemption(
msg.sender,
lender,
address(this),
assetsAfterRedemption
);
Expand All @@ -362,27 +380,31 @@ contract TrancheVault is
_setEpochRedemptionSummary(currRedemptionSummary);

LenderRedemptionRecord memory lenderRedemptionRecord = _getLatestLenderRedemptionRecord(
msg.sender,
lender,
currentEpochId
);
lenderRedemptionRecord.numSharesRequested += uint96(shares);
uint256 principalRequested = (depositRecord.principal * shares) / sharesBalance;
lenderRedemptionRecord.principalRequested += uint96(principalRequested);
_setLenderRedemptionRecord(msg.sender, lenderRedemptionRecord);
_setLenderRedemptionRecord(lender, lenderRedemptionRecord);
depositRecord.principal -= uint96(principalRequested);
_setDepositRecord(msg.sender, depositRecord);
_setDepositRecord(lender, depositRecord);

ERC20Upgradeable._transfer(msg.sender, address(this), shares);
ERC20Upgradeable._transfer(lender, address(this), shares);

emit RedemptionRequestAdded(msg.sender, shares, currentEpochId);
emit RedemptionRequestAdded(lender, msg.sender, shares, currentEpochId);
}

/**
* @notice Cancels a redemption request submitted before.
* @notice If `autoRedemptionAfterLockup` is true, then cancellation is disabled to enforce
* redemption requests processing.
* @param shares The number of shares in the redemption request to be canceled.
* @custom:access Only the lender can submit for themselves.
*/
function cancelRedemptionRequest(uint256 shares) external {
LPConfig memory lpConfig = poolConfig.getLPConfig();
if (lpConfig.autoRedemptionAfterLockup) revert Errors.RedemptionCancellationDisabled();
if (shares == 0) revert Errors.ZeroAmountProvided();
poolConfig.onlyProtocolAndPoolOn();

Expand Down Expand Up @@ -590,6 +612,10 @@ contract TrancheVault is
underlyingToken = IERC20(addr);
_decimals = IERC20MetadataUpgradeable(addr).decimals();

addr = address(poolConfig_.humaConfig());
assert(addr != address(0));
humaConfig = HumaConfig(addr);

addr = poolConfig_.pool();
assert(addr != address(0));
pool = IPool(addr);
Expand Down
2 changes: 2 additions & 0 deletions contracts/liquidity/TrancheVaultStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {IPool} from "./interfaces/IPool.sol";
import {IPoolSafe} from "./interfaces/IPoolSafe.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ICalendar} from "../common/interfaces/ICalendar.sol";
import {HumaConfig} from "../common/HumaConfig.sol";

contract TrancheVaultStorage {
struct LenderRedemptionRecord {
Expand Down Expand Up @@ -36,6 +37,7 @@ contract TrancheVaultStorage {
/// Senior or junior tranche index.
uint8 public trancheIndex;

HumaConfig public humaConfig;
IPool public pool;
IPoolSafe public poolSafe;
IEpochManager public epochManager;
Expand Down
2 changes: 1 addition & 1 deletion scripts/error-functions.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"TrancheRequired()": "0x56b15134",
"TreasuryFeeHighThanUpperLimit()": "0x743a9631",
"AlreadyAPauser()": "0x2fe391f2",
"AlreadyPoolAdmin()": "0xebe277bd",
"UnderlyingTokenNotApprovedForHumaProtocol()": "0x28918c5b",
"AdminRewardRateTooHigh()": "0xa01087e8",
"PoolOwnerInsufficientLiquidity()": "0xbd8efe51",
Expand All @@ -45,6 +44,7 @@
"AlreadyALender()": "0xce4108ca",
"NonReinvestYieldLenderCapacityReached()": "0x39ad903d",
"ReinvestYieldOptionAlreadySet()": "0xb44d305d",
"RedemptionCancellationDisabled()": "0x70c5f64d",
"InsufficientFirstLossCover()": "0x86fdb63a",
"FirstLossCoverLiquidityCapExceeded()": "0x2025075b",
"TooManyProviders()": "0x71bcfb10",
Expand Down
4 changes: 2 additions & 2 deletions test/integration/liquidity/LenderIntegrationTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ async function testRedemptionRequest(jLenderRequests: BN[], sLenderRequests: BN[
const oldShares = await juniorTrancheVaultContract.balanceOf(jLenders[i].address);
await juniorTrancheVaultContract
.connect(jLenders[i])
.addRedemptionRequest(jLenderRequests[i]);
.addRedemptionRequest(jLenders[i].address, jLenderRequests[i]);
expect(await juniorTrancheVaultContract.balanceOf(jLenders[i].address)).to.equal(
oldShares.sub(jLenderRequests[i]),
);
Expand Down Expand Up @@ -515,7 +515,7 @@ async function testRedemptionRequest(jLenderRequests: BN[], sLenderRequests: BN[
const oldShares = await seniorTrancheVaultContract.balanceOf(sLenders[i].address);
await seniorTrancheVaultContract
.connect(sLenders[i])
.addRedemptionRequest(sLenderRequests[i]);
.addRedemptionRequest(sLenders[i].address, sLenderRequests[i]);
expect(await seniorTrancheVaultContract.balanceOf(sLenders[i].address)).to.equal(
oldShares.sub(sLenderRequests[i]),
);
Expand Down
9 changes: 9 additions & 0 deletions test/unit/common/PoolConfigTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2036,6 +2036,7 @@ describe("PoolConfig Tests", function () {
fixedSeniorYieldInBps: 2000,
tranchesRiskAdjustmentInBps: 8000,
withdrawalLockoutPeriodInDays: 30,
autoRedemptionAfterLockup: true,
};
});

Expand All @@ -2048,6 +2049,7 @@ describe("PoolConfig Tests", function () {
newLPConfig.fixedSeniorYieldInBps,
newLPConfig.tranchesRiskAdjustmentInBps,
newLPConfig.withdrawalLockoutPeriodInDays,
newLPConfig.autoRedemptionAfterLockup,
poolOwner.address,
);
const lpConfig = await poolConfigContract.getLPConfig();
Expand All @@ -2060,6 +2062,9 @@ describe("PoolConfig Tests", function () {
expect(lpConfig.tranchesRiskAdjustmentInBps).to.equal(
newLPConfig.tranchesRiskAdjustmentInBps,
);
expect(lpConfig.autoRedemptionAfterLockup).to.equal(
newLPConfig.autoRedemptionAfterLockup,
);
});

it("Should allow the Huma owner to set the LP config", async function () {
Expand All @@ -2071,6 +2076,7 @@ describe("PoolConfig Tests", function () {
newLPConfig.fixedSeniorYieldInBps,
newLPConfig.tranchesRiskAdjustmentInBps,
newLPConfig.withdrawalLockoutPeriodInDays,
newLPConfig.autoRedemptionAfterLockup,
protocolOwner.address,
);
const lpConfig = await poolConfigContract.getLPConfig();
Expand All @@ -2083,6 +2089,9 @@ describe("PoolConfig Tests", function () {
expect(lpConfig.tranchesRiskAdjustmentInBps).to.equal(
newLPConfig.tranchesRiskAdjustmentInBps,
);
expect(lpConfig.autoRedemptionAfterLockup).to.equal(
newLPConfig.autoRedemptionAfterLockup,
);
});

it("Should reject non-owner or admin to set the LP config", async function () {
Expand Down
Loading

0 comments on commit 6f14300

Please sign in to comment.