Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/policies/deposits/DepositRedemptionVault.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
/// forge-lint: disable-start(asm-keccak256, mixed-case-variable)
pragma solidity >=0.8.15;
// Interfaces
import {IERC20} from "src/interfaces/IERC20.sol";
import {IDepositRedemptionVault} from "src/policies/interfaces/deposits/IDepositRedemptionVault.sol";
import {IDepositManager} from "src/policies/interfaces/deposits/IDepositManager.sol";
import {IReceiptTokenManager} from "src/policies/interfaces/deposits/IReceiptTokenManager.sol";
import {IDepositFacility} from "src/policies/interfaces/deposits/IDepositFacility.sol";
import {IDepositPositionManager} from "src/modules/DEPOS/IDepositPositionManager.sol";
import {IERC165} from "@openzeppelin-5.3.0/interfaces/IERC165.sol";
// Libraries
import {ERC20} from "@solmate-6.2.0/tokens/ERC20.sol";
import {ReentrancyGuard} from "@solmate-6.2.0/utils/ReentrancyGuard.sol";
import {EnumerableSet} from "@openzeppelin-5.3.0/utils/structs/EnumerableSet.sol";
import {FullMath} from "src/libraries/FullMath.sol";
import {TransferHelper} from "src/libraries/TransferHelper.sol";
// Bophades
import {TRSRYv1} from "src/modules/TRSRY/TRSRY.v1.sol";
import {DEPOSv1} from "src/modules/DEPOS/DEPOS.v1.sol";
import {PolicyEnabler} from "src/policies/utils/PolicyEnabler.sol";
import {Kernel, Policy, Keycode, Permissions, toKeycode} from "src/Kernel.sol";
import {ROLESv1} from "src/modules/ROLES/ROLES.v1.sol";
/// @title DepositRedemptionVault
/// @notice A contract that manages the redemption of receipt tokens with facility coordination and borrowing
contract DepositRedemptionVault is Policy, IDepositRedemptionVault, PolicyEnabler, ReentrancyGuard {
using TransferHelper for ERC20;
using FullMath for uint256;
using EnumerableSet for EnumerableSet.AddressSet;
// ========== CONSTANTS ========== //
/// @notice The number representing 100%
uint16 public constant ONE_HUNDRED_PERCENT = 100e2;
/// @notice The number of months in a year
uint8 internal constant _MONTHS_IN_YEAR = 12;
/// @notice Constant for one month
uint48 internal constant _ONE_MONTH = 30 days;
/// @notice Used to denote no position ID
uint256 internal constant _NO_POSITION = type(uint256).max;
// ========== CONFIGURABLE PARAMETERS ========== //
/// @notice Per-asset-facility max borrow percentage (in 100e2, e.g. 8500 = 85%)
mapping(bytes32 => uint16) internal _assetFacilityMaxBorrowPercentages;
/// @notice Per-asset-facility interest rate (annual, in 100e2, e.g. 500 = 5%)
mapping(bytes32 => uint16) internal _assetFacilityAnnualInterestRates;
/// @notice Keeper reward percentage (in 100e2, e.g. 500 = 5%)
uint16 internal _claimDefaultRewardPercentage;
// ========== STATE VARIABLES ========== //
/// @notice The address of the token manager
IDepositManager public immutable DEPOSIT_MANAGER;
/// @notice The TRSRY module.
TRSRYv1 public TRSRY;
/// @notice The DEPOS module.
DEPOSv1 public DEPOS;
/// @notice The number of redemptions per user
mapping(address => uint16) internal _userRedemptionCount;
/// @notice The redemption for each user and redemption ID
/// @dev Use `_getUserRedemptionKey()` to calculate the key for the mapping.
/// A complex key is used to save gas compared to a nested mapping.
mapping(bytes32 => UserRedemption) internal _userRedemptions;
/// @notice Registered facilities
EnumerableSet.AddressSet internal _authorizedFacilities;
/// @notice Loan for each redemption
/// @dev Use `_getUserRedemptionKey()` to calculate the key for the mapping.
/// A complex key is used to save gas compared to a nested mapping.
mapping(bytes32 => Loan) internal _redemptionLoan;
// ========== CONSTRUCTOR ========== //
constructor(address kernel_, address depositManager_) Policy(Kernel(kernel_)) {
// Validate that the DepositManager implements IDepositManager
if (!IERC165(depositManager_).supportsInterface(type(IDepositManager).interfaceId)) {
revert RedemptionVault_InvalidDepositManager(depositManager_);
}
DEPOSIT_MANAGER = IDepositManager(depositManager_);
}
// ========== SETUP ========== //
/// @inheritdoc Policy
function configureDependencies() external override returns (Keycode[] memory dependencies) {
dependencies = new Keycode[](3);
dependencies[0] = toKeycode("TRSRY");
dependencies[1] = toKeycode("ROLES");
dependencies[2] = toKeycode("DEPOS");
TRSRY = TRSRYv1(getModuleAddress(dependencies[0]));
ROLES = ROLESv1(getModuleAddress(dependencies[1]));
DEPOS = DEPOSv1(getModuleAddress(dependencies[2]));
}
/// @inheritdoc Policy
function requestPermissions()
external
pure
override
returns (Permissions[] memory permissions)
{}
// ========== FACILITY MANAGEMENT ========== //
/// @inheritdoc IDepositRedemptionVault
function authorizeFacility(address facility_) external onlyAdminRole {
if (facility_ == address(0)) revert RedemptionVault_InvalidFacility(facility_);
if (_authorizedFacilities.contains(facility_))
revert RedemptionVault_FacilityExists(facility_);
// Validate that the facility implements IDepositFacility (even if it doesn't have the function)
{
(bool success, bytes memory data) = facility_.staticcall(
abi.encodeWithSelector(
IERC165.supportsInterface.selector,
type(IDepositFacility).interfaceId
)
);
if (!success || abi.decode(data, (bool)) == false)
revert RedemptionVault_InvalidFacility(facility_);
}
_authorizedFacilities.add(facility_);
emit FacilityAuthorized(facility_);
}
/// @inheritdoc IDepositRedemptionVault
function deauthorizeFacility(address facility_) external onlyEmergencyOrAdminRole {
if (!_authorizedFacilities.contains(facility_))
revert RedemptionVault_FacilityNotRegistered(facility_);
_authorizedFacilities.remove(facility_);
emit FacilityDeauthorized(facility_);
}
/// @inheritdoc IDepositRedemptionVault
function isAuthorizedFacility(address facility_) external view returns (bool) {
return _authorizedFacilities.contains(facility_);
}
/// @inheritdoc IDepositRedemptionVault
function getAuthorizedFacilities() external view returns (address[] memory) {
return _authorizedFacilities.values();
}
// ========== ASSETS ========== //
/// @notice Pull the receipt tokens from the caller
function _pullReceiptToken(
IERC20 depositToken_,
uint8 depositPeriod_,
address facility_,
uint256 amount_
) internal {
// Transfer the receipt tokens from the caller to this contract
IReceiptTokenManager rtm = DEPOSIT_MANAGER.getReceiptTokenManager();
rtm.transferFrom(
msg.sender,
address(this),
DEPOSIT_MANAGER.getReceiptTokenId(depositToken_, depositPeriod_, facility_),
amount_
);
}
// ========== USER REDEMPTIONS ========== //
function _getUserRedemptionKey(
address user_,
uint16 redemptionId_
) internal pure returns (bytes32) {
return keccak256(abi.encode(user_, redemptionId_));
}
/// @notice Generate a key for the asset-facility parameter mappings
/// @param asset_ The asset address
/// @param facility_ The facility address
/// @return The key for the mapping
function _getAssetFacilityKey(
address asset_,
address facility_
) internal pure returns (bytes32) {
return keccak256(abi.encode(asset_, facility_));
}
/// @inheritdoc IDepositRedemptionVault
function getUserRedemptionCount(address user_) external view returns (uint16 count) {
return _userRedemptionCount[user_];
}
/// @inheritdoc IDepositRedemptionVault
function getUserRedemption(
address user_,
uint16 redemptionId_
) external view returns (UserRedemption memory redemption) {
redemption = _userRedemptions[_getUserRedemptionKey(user_, redemptionId_)];
if (redemption.depositToken == address(0))
revert RedemptionVault_InvalidRedemptionId(user_, redemptionId_);
return redemption;
}
/// @inheritdoc IDepositRedemptionVault
/// @dev Notes:
/// - This function is gas-intensive for users with many redemptions.
/// - The index of an element in the returned array is the redemption ID.
/// - Redemptions with an amount of 0 (fully redeemed) are included in the array.
function getUserRedemptions(address user_) external view returns (UserRedemption[] memory) {
uint16 count = _userRedemptionCount[user_];
UserRedemption[] memory redemptions = new UserRedemption[](count);
for (uint16 i = 0; i < count; i++) {
redemptions[i] = _userRedemptions[_getUserRedemptionKey(user_, i)];
}
return redemptions;
}
// ========== REDEMPTION FLOW ========== //
function _onlyValidRedemptionId(address user_, uint16 redemptionId_) internal view {
// If the deposit token is the zero address, the redemption is invalid
if (
_userRedemptions[_getUserRedemptionKey(user_, redemptionId_)].depositToken == address(0)
) revert RedemptionVault_InvalidRedemptionId(user_, redemptionId_);
}
modifier onlyValidRedemptionId(address user_, uint16 redemptionId_) {
_onlyValidRedemptionId(user_, redemptionId_);
_;
}
function _validateFacility(address facility_) internal view {
if (!_authorizedFacilities.contains(facility_))
revert RedemptionVault_FacilityNotRegistered(facility_);
}
modifier onlyValidFacility(address facility_) {
_validateFacility(facility_);
_;
}
/// @inheritdoc IDepositRedemptionVault
/// @dev This function expects receipt tokens to be unwrapped (i.e. native ERC6909 tokens)
///
/// This function reverts if:
/// - The contract is disabled
/// - The amount is 0
/// - The provided facility is not authorized
function startRedemption(
IERC20 depositToken_,
uint8 depositPeriod_,
uint256 amount_,
address facility_
) external nonReentrant onlyEnabled onlyValidFacility(facility_) returns (uint16 redemptionId) {
// Validate that the amount is not 0
if (amount_ == 0) revert RedemptionVault_ZeroAmount();
// Create a User Redemption
redemptionId = _userRedemptionCount[msg.sender]++;
_userRedemptions[_getUserRedemptionKey(msg.sender, redemptionId)] = UserRedemption({
depositToken: address(depositToken_),
depositPeriod: depositPeriod_,
redeemableAt: uint48(block.timestamp) + uint48(depositPeriod_) * _ONE_MONTH,
amount: amount_,
facility: facility_,
positionId: _NO_POSITION
});
// Mark the funds as committed
IDepositFacility(facility_).handleCommit(depositToken_, depositPeriod_, amount_);
// Pull the receipt tokens from the caller
_pullReceiptToken(depositToken_, depositPeriod_, facility_, amount_);
// Emit events
emit RedemptionStarted(
msg.sender,
redemptionId,
address(depositToken_),
depositPeriod_,
amount_,
facility_
);
return redemptionId;
}
/// @inheritdoc IDepositRedemptionVault
/// @dev This function expects receipt tokens to be unwrapped (i.e. native ERC6909 tokens)
///
/// This function reverts if:
/// - The contract is disabled
/// - The amount is 0
/// - The caller is not the owner of the position
/// - The amount is greater than the remainingDeposit of the position
/// - The facility that created the position is not authorized
function startRedemption(
uint256 positionId_,
uint256 amount_
) external nonReentrant onlyEnabled returns (uint16 redemptionId) {
// Validate that the amount is not 0
if (amount_ == 0) revert RedemptionVault_ZeroAmount();
// Get the position details from DEPOS module
IDepositPositionManager.Position memory position = DEPOS.getPosition(positionId_);
// Validate that the caller owns the position
if (position.owner != msg.sender)
revert IDepositPositionManager.DEPOS_NotOwner(positionId_);
// Validate that the amount is not greater than the remaining deposit
if (amount_ > position.remainingDeposit)
revert IDepositPositionManager.DEPOS_InvalidParams("amount");
// Extract position data
IERC20 depositToken = IERC20(position.asset);
uint8 depositPeriod = position.periodMonths;
address facility = position.operator; // The facility is the operator of the position
// Validate that the facility is authorized
_validateFacility(facility);
// Create a User Redemption
redemptionId = _userRedemptionCount[msg.sender]++;
_userRedemptions[_getUserRedemptionKey(msg.sender, redemptionId)] = UserRedemption({
depositToken: address(depositToken),
depositPeriod: depositPeriod,
redeemableAt: position.expiry, // Use conversion expiry instead of calculated time
amount: amount_,
facility: facility,
positionId: positionId_ // Store the position ID for later use
});
// Mark the funds as committed
IDepositFacility(facility).handleCommit(depositToken, depositPeriod, amount_);
// Immediately update position's remainingDeposit to prevent split/transfer issues
// This change will be reverted if cancelRedemption is called
IDepositFacility(facility).handlePositionRedemption(positionId_, amount_);
// Pull the receipt tokens from the caller
_pullReceiptToken(depositToken, depositPeriod, facility, amount_);
// Emit events
emit RedemptionStarted(
msg.sender,
redemptionId,
address(depositToken),
depositPeriod,
amount_,
facility
);
return redemptionId;
}
/// @inheritdoc IDepositRedemptionVault
/// @dev This function reverts if:
/// - The contract is disabled
/// - The caller is not the owner of the redemption ID
/// - The facility in the redemption record is not authorized
/// - The amount is 0
/// - The amount is greater than the redemption amount
/// - There is an unpaid loan
function cancelRedemption(
uint16 redemptionId_,
uint256 amount_
) external nonReentrant onlyEnabled onlyValidRedemptionId(msg.sender, redemptionId_) {
// Get the redemption
bytes32 redemptionKey = _getUserRedemptionKey(msg.sender, redemptionId_);
UserRedemption storage redemption = _userRedemptions[redemptionKey];
// Check that the facility is authorized
_validateFacility(redemption.facility);
// Check that the amount is not 0
if (amount_ == 0) revert RedemptionVault_ZeroAmount();
// Check that the amount is not greater than the redemption
if (amount_ > redemption.amount)
revert RedemptionVault_InvalidAmount(msg.sender, redemptionId_, amount_);
// Check that there isn't an unpaid loan
if (_redemptionLoan[redemptionKey].principal > 0)
revert RedemptionVault_UnpaidLoan(msg.sender, redemptionId_);
// Update the redemption
redemption.amount -= amount_;
// Reduce the committed funds
IDepositFacility(redemption.facility).handleCommitCancel(
IERC20(redemption.depositToken),
redemption.depositPeriod,
amount_
);
// Handle position-based redemption
if (redemption.positionId != _NO_POSITION) {
// Only increase position.remainingDeposit if the position owner is the same
// The redemption ID has been validated against the caller earlier, so we know that msg.sender is the creator of the original redemption
if (DEPOS.getPosition(redemption.positionId).owner == msg.sender) {
IDepositFacility(redemption.facility).handlePositionCancelRedemption(
redemption.positionId,
amount_
);
}
// If the position ownership has changed, the original (and redemption) owner will receive the receipt tokens,
// but the position will not be modified
}
// Transfer the quantity of receipt tokens to the caller
// Redemptions are only accessible to the owner, so msg.sender is safe here
IReceiptTokenManager rtm = DEPOSIT_MANAGER.getReceiptTokenManager();
rtm.transfer(
msg.sender,
DEPOSIT_MANAGER.getReceiptTokenId(
IERC20(redemption.depositToken),
redemption.depositPeriod,
redemption.facility
),
amount_
);
// Emit the cancelled event
emit RedemptionCancelled(
msg.sender,
redemptionId_,
redemption.depositToken,
redemption.depositPeriod,
amount_,
redemption.amount
);
}
/// @inheritdoc IDepositRedemptionVault
/// @dev This function reverts if:
/// - The contract is disabled
/// - The caller is not the owner of the redemption ID
/// - The facility in the redemption record is not authorized
/// - The redemption amount is 0
/// - It is too early for redemption
/// - There is an unpaid loan
function finishRedemption(
uint16 redemptionId_
)
external
nonReentrant
onlyEnabled
onlyValidRedemptionId(msg.sender, redemptionId_)
returns (uint256 actualAmount)
{
// Get the redemption
bytes32 redemptionKey = _getUserRedemptionKey(msg.sender, redemptionId_);
UserRedemption storage redemption = _userRedemptions[redemptionKey];
// Validate that the facility is authorized
_validateFacility(redemption.facility);
// Check that the redemption is not already redeemed
if (redemption.amount == 0)
revert RedemptionVault_AlreadyRedeemed(msg.sender, redemptionId_);
// Check that the redemption is redeemable
if (block.timestamp < redemption.redeemableAt)
revert RedemptionVault_TooEarly(msg.sender, redemptionId_, redemption.redeemableAt);
// Check that there isn't an unpaid loan
if (_redemptionLoan[redemptionKey].principal > 0)
revert RedemptionVault_UnpaidLoan(msg.sender, redemptionId_);
// Update the redemption
uint256 redemptionAmount = redemption.amount;
redemption.amount = 0;
// Handle the withdrawal
// Redemptions are only accessible to the owner, so msg.sender is safe here
uint256 receiptTokenId = DEPOSIT_MANAGER.getReceiptTokenId(
IERC20(redemption.depositToken),
redemption.depositPeriod,
redemption.facility
);
IReceiptTokenManager rtm = DEPOSIT_MANAGER.getReceiptTokenManager();
rtm.approve(address(DEPOSIT_MANAGER), receiptTokenId, redemptionAmount);
// Withdraw the deposit tokens from the facility to the caller
// The value returned can also be zero
actualAmount = IDepositFacility(redemption.facility).handleCommitWithdraw(
IERC20(redemption.depositToken),
redemption.depositPeriod,
redemptionAmount,
msg.sender
);
// Reset approval, in case not all was used
rtm.approve(address(DEPOSIT_MANAGER), receiptTokenId, 0);
// Emit the redeemed event
emit RedemptionFinished(
msg.sender,
redemptionId_,
redemption.depositToken,
redemption.depositPeriod,
redemptionAmount
);
return actualAmount;
}
// ========== BORROWING FUNCTIONS ========== //
function _calculateInterest(
uint256 principal_,
uint256 interestRate_,
uint256 depositPeriod_
) internal pure returns (uint256) {
// Rounded up, in favour of the protocol
return
principal_.mulDivUp(
interestRate_ * depositPeriod_,
uint256(_MONTHS_IN_YEAR) * uint256(ONE_HUNDRED_PERCENT)
);
}
function _previewBorrowAgainstRedemption(
address user_,
uint16 redemptionId_
) internal view returns (uint256, uint256, uint48) {
// Get the redemption
bytes32 redemptionKey = _getUserRedemptionKey(user_, redemptionId_);
UserRedemption memory redemption = _userRedemptions[redemptionKey];
// Validate that the facility is still authorized
_validateFacility(redemption.facility);
// Determine the amount to borrow
// This deliberately does not revert. That will be handled in the borrowAgainstRedemption() function
bytes32 assetFacilityKey = _getAssetFacilityKey(
redemption.depositToken,
redemption.facility
);
uint256 principal = redemption.amount.mulDiv(
_assetFacilityMaxBorrowPercentages[assetFacilityKey],
ONE_HUNDRED_PERCENT
);
// Interest: annualized, prorated for period
uint256 interest = _calculateInterest(
principal,
_assetFacilityAnnualInterestRates[assetFacilityKey],
redemption.depositPeriod
);
// Due date: now + deposit period
uint48 dueDate = uint48(block.timestamp) + uint48(redemption.depositPeriod) * _ONE_MONTH;
return (principal, interest, dueDate);
}
/// @inheritdoc IDepositRedemptionVault
/// @dev Notes:
/// - The calculated amount may differ from the actual amount borrowed (using `borrowAgainstRedemption()`) by a few wei, due to rounding behaviour in ERC4626 vaults.
function previewBorrowAgainstRedemption(
address user_,
uint16 redemptionId_
) external view returns (uint256, uint256, uint48) {
return _previewBorrowAgainstRedemption(user_, redemptionId_);
}
/// @inheritdoc IDepositRedemptionVault
/// @dev Borrows the maximum possible amount against an existing redemption.
/// The loan will be for a fixed-term. The interest is calculated on the
/// basis of that term, and the full amount will be payable in order to
/// close the loan.
///
/// This function will revert if:
/// - The contract is not enabled
/// - The redemption ID is invalid
/// - The facility is not authorized
/// - The amount is 0
/// - The interest rate is not set
function borrowAgainstRedemption(
uint16 redemptionId_
)
external
nonReentrant
onlyEnabled
onlyValidRedemptionId(msg.sender, redemptionId_)
returns (uint256)
{
// Get the redemption
bytes32 redemptionKey = _getUserRedemptionKey(msg.sender, redemptionId_);
UserRedemption storage redemption = _userRedemptions[redemptionKey];
// Validate that the redemption is not already borrowed against
if (_redemptionLoan[redemptionKey].dueDate != 0)
revert RedemptionVault_LoanIncorrectState(msg.sender, redemptionId_);
// Ensure a non-zero interest rate is configured
if (
_assetFacilityAnnualInterestRates[
_getAssetFacilityKey(redemption.depositToken, redemption.facility)
] == 0
) revert RedemptionVault_InterestRateNotSet(redemption.depositToken, redemption.facility);
// This will also validate the facility
(uint256 principal, uint256 interest, uint48 dueDate) = _previewBorrowAgainstRedemption(
msg.sender,
redemptionId_
);
if (principal == 0) revert RedemptionVault_ZeroAmount();
// Create loan
// Use the calculated amount, independent of any off-by-one rounding errors
Loan memory newLoan = Loan({
initialPrincipal: principal,
principal: principal,
interest: interest,
dueDate: dueDate,
isDefaulted: false
});
_redemptionLoan[redemptionKey] = newLoan;
// Delegate to the facility for borrowing
uint256 principalActual = IDepositFacility(redemption.facility).handleBorrow(
IERC20(redemption.depositToken),
redemption.depositPeriod,
principal,
msg.sender
);
// Validate that the actual loan amount is not 0
// This can happen when calculating the withdrawal amount from a vault
if (principalActual == 0) revert RedemptionVault_ZeroAmount();
// Emit event
emit LoanCreated(msg.sender, redemptionId_, principalActual, redemption.facility);
return principalActual;
}
/// @inheritdoc IDepositRedemptionVault
/// @dev This function will repay the outstanding loan amount.
/// Interest is paid back first, followed by principal.
/// To prevent irrecoverable overpayments, the maximum slippage is used to validate that a repayment is within bounds of the remaining loan principal.
///
/// This function will revert if:
/// - The contract is not enabled
/// - The redemption ID is invalid
/// - The redemption has no loan
/// - The amount is 0
/// - The loan is expired, defaulted or fully repaid
function repayLoan(
uint16 redemptionId_,
uint256 amount_,
uint256 maxSlippage_
) external nonReentrant onlyEnabled onlyValidRedemptionId(msg.sender, redemptionId_) {
// Validate that the amount is not 0
if (amount_ == 0) revert RedemptionVault_ZeroAmount();
// Get the redemption
bytes32 redemptionKey = _getUserRedemptionKey(msg.sender, redemptionId_);
UserRedemption memory redemption = _userRedemptions[redemptionKey];
// Check that the facility is still authorized
_validateFacility(redemption.facility);
// Get the loan
Loan storage loan = _redemptionLoan[redemptionKey];
// Validate that the redemption has a loan
if (loan.dueDate == 0) revert RedemptionVault_InvalidLoan(msg.sender, redemptionId_);
// Validate that the loan is not:
// - expired
// - defaulted
// - fully repaid
if (block.timestamp >= loan.dueDate || loan.isDefaulted || loan.principal == 0)
revert RedemptionVault_LoanIncorrectState(msg.sender, redemptionId_);
// Pull in the deposit tokens from the caller
// This takes place before any state changes to avoid ERC777 re-entrancy
ERC20(redemption.depositToken).safeTransferFrom(msg.sender, address(this), amount_);
// Determine the repayment amounts
// Note that the principal repayment may be different to the calculated amount, due to rounding errors in ERC4626 vaults. The value is updated once the actual value is known.
uint256 interestToRepay;
uint256 principalToRepay;
// Pay interest first, then principal
if (amount_ <= loan.interest) {
interestToRepay = amount_;
} else {
interestToRepay = loan.interest;
principalToRepay = amount_ - loan.interest;
}
// Handle interest
if (interestToRepay > 0) {
// Transfer interest to the TRSRY
ERC20(redemption.depositToken).safeTransfer(address(TRSRY), interestToRepay);
}
// Handle principal
uint256 principalRepaidActual;
if (principalToRepay > 0) {
ERC20(redemption.depositToken).safeApprove(address(DEPOSIT_MANAGER), principalToRepay);
// Delegate to the facility for repayment of principal
principalRepaidActual = IDepositFacility(redemption.facility).handleLoanRepay(
IERC20(redemption.depositToken),
redemption.depositPeriod,
principalToRepay,
loan.principal, // Repayment is capped at the outstanding principal of the loan
address(this)
);
// Validate that the slippage is not too large if this is the final repayment
if (
principalToRepay >= loan.principal &&
principalRepaidActual > loan.principal + maxSlippage_
) {
revert RedemptionVault_MaxSlippageExceeded(
msg.sender,
redemptionId_,
principalRepaidActual,
loan.principal + maxSlippage_
);
}
// The DepositFacility may not use all of the approval, so reset it to 0
ERC20(redemption.depositToken).safeApprove(address(DEPOSIT_MANAGER), 0);
}
// Update loan state
if (interestToRepay > 0) {
loan.interest -= interestToRepay > loan.interest ? loan.interest : interestToRepay;
}
if (principalRepaidActual > 0) {
loan.principal -= principalRepaidActual > loan.principal
? loan.principal
: principalRepaidActual;
}
// Receipt tokens are not returned here.
// They are only returned through cancelRedemption() or finishRedemption().
emit LoanRepaid(msg.sender, redemptionId_, principalRepaidActual, interestToRepay);
}
function _previewExtendLoan(
address asset_,
address facility_,
uint256 principal_,
uint48 dueDate_,
uint8 extensionMonths_
) internal view returns (uint48, uint256) {
// Validate the facility
_validateFacility(facility_);
// Validate interest rate
uint16 interestRate = _assetFacilityAnnualInterestRates[
_getAssetFacilityKey(asset_, facility_)
];
if (interestRate == 0) revert RedemptionVault_InterestRateNotSet(asset_, facility_);
uint256 interestPayable = _calculateInterest(principal_, interestRate, extensionMonths_);
uint48 newDueDate = dueDate_ + uint48(extensionMonths_) * _ONE_MONTH;
return (newDueDate, interestPayable);
}
/// @inheritdoc IDepositRedemptionVault
/// @dev This function will revert if:
/// - The redemption ID is invalid
/// - The loan is invalid
/// - The loan is expired, defaulted or fully repaid
/// - The months is 0
function previewExtendLoan(
address user_,
uint16 redemptionId_,
uint8 months_
) external view onlyValidRedemptionId(user_, redemptionId_) returns (uint48, uint256) {
// Validate that the months is not 0
if (months_ == 0) revert RedemptionVault_ZeroAmount();
// Get the redemption
bytes32 redemptionKey = _getUserRedemptionKey(user_, redemptionId_);
UserRedemption memory redemption = _userRedemptions[redemptionKey];
// Get the loan
Loan memory loan = _redemptionLoan[redemptionKey];
// Validate that the redemption has a loan
if (loan.dueDate == 0) revert RedemptionVault_InvalidLoan(user_, redemptionId_);
// Validate that the loan is not:
// - expired
// - defaulted
// - fully repaid
if (block.timestamp >= loan.dueDate || loan.isDefaulted || loan.principal == 0)
revert RedemptionVault_LoanIncorrectState(user_, redemptionId_);
// Preview the new due date and interest payable
(uint48 newDueDate, uint256 interestPayable) = _previewExtendLoan(
redemption.depositToken,
redemption.facility,
loan.principal,
loan.dueDate,
months_
);
return (newDueDate, interestPayable);
}
/// @inheritdoc IDepositRedemptionVault
/// @dev This function will revert if:
/// - The contract is not enabled
/// - The redemption ID is invalid
/// - The loan is invalid
/// - The loan is expired, defaulted or fully repaid
/// - The months is 0
function extendLoan(
uint16 redemptionId_,
uint8 months_
) external nonReentrant onlyEnabled onlyValidRedemptionId(msg.sender, redemptionId_) {
// Validate that the months is not 0
if (months_ == 0) revert RedemptionVault_ZeroAmount();
// Get the redemption
bytes32 redemptionKey = _getUserRedemptionKey(msg.sender, redemptionId_);
UserRedemption memory redemption = _userRedemptions[redemptionKey];
// Check that the facility is still authorized
_validateFacility(redemption.facility);
// Get the loan
Loan storage loan = _redemptionLoan[redemptionKey];
// Validate that the redemption has a loan
if (loan.dueDate == 0) revert RedemptionVault_InvalidLoan(msg.sender, redemptionId_);
// Validate that the loan is not:
// - expired
// - defaulted
// - fully repaid
if (block.timestamp >= loan.dueDate || loan.isDefaulted || loan.principal == 0)
revert RedemptionVault_LoanIncorrectState(msg.sender, redemptionId_);
(uint48 newDueDate, uint256 interestPayable) = _previewExtendLoan(
redemption.depositToken,
redemption.facility,
loan.principal,
loan.dueDate,
months_
);
// Transfer the interest from the caller to the TRSRY
// This takes place before any state changes to avoid ERC777 re-entrancy
ERC20(redemption.depositToken).safeTransferFrom(
msg.sender,
address(TRSRY),
interestPayable
);
// Update due date by the number of months
loan.dueDate = newDueDate;
// No need to update the interest payable, as it is collected immediately
emit LoanExtended(msg.sender, redemptionId_, loan.dueDate);
}
/// @inheritdoc IDepositRedemptionVault
/// @dev This function will revert if:
/// - The contract is not enabled
/// - The redemption ID is invalid
/// - The loan is invalid
/// - The loan is not expired
/// - The loan is already defaulted
function claimDefaultedLoan(
address user_,
uint16 redemptionId_
) external nonReentrant onlyEnabled onlyValidRedemptionId(user_, redemptionId_) {
// Get the redemption and loan
UserRedemption storage redemption;
Loan storage loan;
{
bytes32 redemptionKey = _getUserRedemptionKey(user_, redemptionId_);
redemption = _userRedemptions[redemptionKey];
loan = _redemptionLoan[redemptionKey];
}
// Validate that the facility is still authorized
_validateFacility(redemption.facility);
// Validate that the redemption has a loan
if (loan.dueDate == 0) revert RedemptionVault_InvalidLoan(user_, redemptionId_);
// Validate that the loan is:
// - expired
// - not defaulted
// - not fully repaid
if (block.timestamp < loan.dueDate || loan.isDefaulted || loan.principal == 0)
revert RedemptionVault_LoanIncorrectState(user_, redemptionId_);
// Determine how much collateral to confiscate
// Any principal that has been paid off will be retained by the borrower
// The remainder, including the buffer, will be confiscated
// e.g. the borrower has a redemption amount of 100, the borrower has borrowed 80, and paid off 20,
// the borrower has an outstanding principal of 60.
// The borrower will retain a redemption amount of 20 (due to the payment).
// The protocol will burn the custodied receipt tokens for the unpaid principal: 80 - 20 = 60.
// The remainder (20) will be sent to the treasury.
uint256 previousPrincipal = loan.principal;
uint256 previousInterest = loan.interest;
uint256 retainedCollateral = redemption.amount - loan.initialPrincipal; // Buffer amount
// Mark loan as defaulted
loan.isDefaulted = true;
loan.principal = 0;
loan.interest = 0;
uint256 receiptTokenId = DEPOSIT_MANAGER.getReceiptTokenId(
IERC20(redemption.depositToken),
redemption.depositPeriod,
redemption.facility
);
uint256 totalToConsume = retainedCollateral + previousPrincipal;
// Handle transfers
uint256 retainedCollateralActual;
{
IReceiptTokenManager rtm = DEPOSIT_MANAGER.getReceiptTokenManager();
rtm.approve(address(DEPOSIT_MANAGER), receiptTokenId, totalToConsume);
// Burn the receipt tokens for the principal
if (previousPrincipal > 0) {
IDepositFacility(redemption.facility).handleLoanDefault(
IERC20(redemption.depositToken),
redemption.depositPeriod,
previousPrincipal,
address(this)
);
}
// Withdraw deposit for retained collateral
if (retainedCollateral > 0) {
// Caution: can be zero
retainedCollateralActual = IDepositFacility(redemption.facility)
.handleCommitWithdraw(
IERC20(redemption.depositToken),
redemption.depositPeriod,
retainedCollateral,
address(this)
);
}
// Reset the approval, in case not all was used
rtm.approve(address(DEPOSIT_MANAGER), receiptTokenId, 0);
}
// Reduce redemption amount by the burned and retained collateral
// Use the calculated amount (retainedCollateral + previousPrincipal) to adjust redemption.
// This leaves redemption.amount equal to (initialPrincipal - previousPrincipal), i.e.,
// any principal already repaid remains redeemable by the borrower. Using calculated amounts
// avoids inconsistencies from ERC4626 rounding in actual transfers.
redemption.amount -= retainedCollateral + previousPrincipal;
// Distribute residual value (keeper reward + treasury)
// Keeper reward is a percentage of the retained collateral, and can be zero
uint256 keeperReward = retainedCollateralActual.mulDiv(
_claimDefaultRewardPercentage,
ONE_HUNDRED_PERCENT
);
// Treasury amount is the remainder of the retained collateral after the keeper reward has been deducted, and can be zero
uint256 treasuryAmount = retainedCollateralActual - keeperReward;
if (keeperReward > 0) {
ERC20(redemption.depositToken).safeTransfer(msg.sender, keeperReward);
}
if (treasuryAmount > 0) {
ERC20(redemption.depositToken).safeTransfer(address(TRSRY), treasuryAmount);
}
emit LoanDefaulted(
user_,
redemptionId_,
previousPrincipal,
previousInterest,
retainedCollateral + previousPrincipal // Calculated amount
);
emit RedemptionCancelled(
user_,
redemptionId_,
address(redemption.depositToken),
redemption.depositPeriod,
retainedCollateral + previousPrincipal, // Calculated amount
redemption.amount
);
}
// ========== BORROWING VIEW FUNCTIONS ========== //
/// @inheritdoc IDepositRedemptionVault
function getRedemptionLoan(
address user_,
uint16 redemptionId_
) external view returns (Loan memory) {
return _redemptionLoan[_getUserRedemptionKey(user_, redemptionId_)];
}
// ========== ADMIN FUNCTIONS ========== //
/// @inheritdoc IDepositRedemptionVault
/// @dev Notes:
/// - When setting the max borrow percentage, keep in mind the annual interest rate and claim default reward percentage, as the three configuration values can create incentives for borrowers to not repay their loans (e.g. claim default on their own loan)
/// - This function allows setting the value even if the asset or facility are not registered
///
/// This function reverts if:
/// - The contract is not enabled
/// - The caller does not have the admin or manager role
/// - asset_ is the zero address
/// - facility_ is the zero address
/// - percent_ is out of range
function setMaxBorrowPercentage(
IERC20 asset_,
address facility_,
uint16 percent_
) external onlyEnabled onlyManagerOrAdminRole {
if (address(asset_) == address(0)) revert RedemptionVault_ZeroAddress();
if (address(facility_) == address(0)) revert RedemptionVault_ZeroAddress();
if (percent_ > ONE_HUNDRED_PERCENT) revert RedemptionVault_OutOfBounds(percent_);
_assetFacilityMaxBorrowPercentages[
_getAssetFacilityKey(address(asset_), facility_)
] = percent_;
emit MaxBorrowPercentageSet(address(asset_), facility_, percent_);
}
/// @inheritdoc IDepositRedemptionVault
function getMaxBorrowPercentage(
IERC20 asset_,
address facility_
) external view returns (uint16) {
return _assetFacilityMaxBorrowPercentages[_getAssetFacilityKey(address(asset_), facility_)];
}
/// @inheritdoc IDepositRedemptionVault
/// @dev Notes:
/// - When setting the annual interest rate, keep in mind the max borrow percentage and claim default reward percentage, as the three configuration values can create incentives for borrowers to not repay their loans (e.g. claim default on their own loan)
/// - This function allows setting the value even if the asset or facility are not registered
///
/// This function reverts if:
/// - The contract is not enabled
/// - The caller does not have the admin or manager role
/// - asset_ is the zero address
/// - facility_ is the zero address
/// - percent_ is out of range
function setAnnualInterestRate(
IERC20 asset_,
address facility_,
uint16 rate_
) external onlyEnabled onlyManagerOrAdminRole {
if (address(asset_) == address(0)) revert RedemptionVault_ZeroAddress();
if (address(facility_) == address(0)) revert RedemptionVault_ZeroAddress();
if (rate_ > ONE_HUNDRED_PERCENT) revert RedemptionVault_OutOfBounds(rate_);
_assetFacilityAnnualInterestRates[_getAssetFacilityKey(address(asset_), facility_)] = rate_;
emit AnnualInterestRateSet(address(asset_), facility_, rate_);
}
/// @inheritdoc IDepositRedemptionVault
function getAnnualInterestRate(
IERC20 asset_,
address facility_
) external view returns (uint16) {
return _assetFacilityAnnualInterestRates[_getAssetFacilityKey(address(asset_), facility_)];
}
/// @inheritdoc IDepositRedemptionVault
/// @dev Notes:
/// - When setting the claim default reward percentage, keep in mind the annual interest rate and max borrow percentage, as the three configuration values can create incentives for borrowers to not repay their loans (e.g. claim default on their own loan)
function setClaimDefaultRewardPercentage(
uint16 percent_
) external onlyEnabled onlyManagerOrAdminRole {
if (percent_ > ONE_HUNDRED_PERCENT) revert RedemptionVault_OutOfBounds(percent_);
_claimDefaultRewardPercentage = percent_;
emit ClaimDefaultRewardPercentageSet(percent_);
}
/// @inheritdoc IDepositRedemptionVault
function getClaimDefaultRewardPercentage() external view returns (uint16) {
return _claimDefaultRewardPercentage;
}
// ========== ERC165 ========== //
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return
interfaceId == type(IERC165).interfaceId ||
interfaceId == type(IDepositRedemptionVault).interfaceId ||
super.supportsInterface(interfaceId);
}
}
/// forge-lint: disable-end(asm-keccak256, mixed-case-variable)
"
},
"src/interfaces/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
// Imported from forge-std
/// @dev Interface of the ERC20 standard as defined in the EIP.
/// @dev This includes the optional name, symbol, and decimals metadata.
interface IERC20 {
/// @dev Emitted when `value` tokens are moved from one account (`from`) to another (`to`).
event Transfer(address indexed from, address indexed to, uint256 value);
/// @dev Emitted when the allowance of a `spender` for an `owner` is set, where `value`
/// is the new allowance.
event Approval(address indexed owner, address indexed spender, uint256 value);
/// @notice Returns the amount of tokens in existence.
function totalSupply() external view returns (uint256);
/// @notice Returns the amount of tokens owned by `account`.
function balanceOf(address account) external view returns (uint256);
/// @notice Moves `amount` tokens from the caller's account to `to`.
function transfer(address to, uint256 amount) external returns (bool);
/// @notice Returns the remaining number of tokens that `spender` is allowed
/// to spend on behalf of `owner`
function allowance(address owner, address spender) external view returns (uint256);
/// @notice Sets `amount` as the allowance of `spender` over the caller's tokens.
/// @dev Be aware of front-running risks: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
function approve(address spender, uint256 amount) external returns (bool);
/// @notice Moves `amount` tokens from `from` to `to` using the allowance mechanism.
/// `amount` is then deducted from the caller's allowance.
function transferFrom(address from, address to, uint256 amount) external returns (bool);
/// @notice Returns the name of the token.
function name() external view returns (string memory);
/// @notice Returns the symbol of the token.
function symbol() external view returns (string memory);
/// @notice Returns the decimals places of the token.
function decimals() external view returns (uint8);
}
"
},
"src/policies/interfaces/deposits/IDepositRedemptionVault.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
// Interfaces
import {IERC20} from "src/interfaces/IERC20.sol";
/// @title IDepositRedemptionVault
/// @notice Interface for a contract that can manage the redemption of receipt tokens for their deposit
interface IDepositRedemptionVault {
// ========== EVENTS ========== //
event RedemptionStarted(
address indexed user,
uint16 indexed redemptionId,
address indexed depositToken,
uint8 depositPeriod,
uint256 amount,
address facility
);
event RedemptionFinished(
address indexed user,
uint16 indexed redemptionId,
address indexed depositToken,
uint8 depositPeriod,
uint256 amount
);
event RedemptionCancelled(
address indexed user,
uint16 indexed redemptionId,
address indexed depositToken,
uint8 depositPeriod,
uint256 amount,
uint256 remainingAmount
);
// Borrowing Events
event LoanCreated(
address indexed user,
uint16 indexed redemptionId,
uint256 amount,
address facility
);
event LoanRepaid(
address indexed user,
uint16 indexed redemptionId,
uint256 principal,
uint256 interest
);
event LoanExtended(address indexed user, uint16 indexed redemptionId, uint256 newDueDate);
event LoanDefaulted(
address indexed user,
uint16 indexed redemptionId,
uint256 principal,
uint256 interest,
uint256 remainingCollateral
);
event FacilityAuthorized(address indexed facility);
event FacilityDeauthorized(address indexed facility);
event AnnualInterestRateSet(address indexed asset, address indexed facility, uint16 rate);
event MaxBorrowPercentageSet(address indexed asset, address indexed facility, uint16 percent);
event ClaimDefaultRewardPercentageSet(uint16 percent);
// ========== ERRORS ========== //
error RedemptionVault_InvalidDepositManager(address depositManager);
error RedemptionVault_ZeroAmount();
error RedemptionVault_InvalidRedemptionId(address user, uint16 redemptionId);
error RedemptionVault_InvalidAmount(address user, uint16 redemptionId, uint256 amount);
error RedemptionVault_TooEarly(address user, uint16 redemptionId, uint48 redeemableAt);
error RedemptionVault_AlreadyRedeemed(address user, uint16 redemptionId);
error RedemptionVault_ZeroAddress();
error RedemptionVault_OutOfBounds(uint16 rate);
error RedemptionVault_UnpaidLoan(address user, uint16 redemptionId);
// Facility Authorization
error RedemptionVault_InvalidFacility(address facility);
error RedemptionVault_FacilityExists(address facility);
error RedemptionVault_FacilityNotRegistered(address facility);
// Borrowing Errors
error RedemptionVault_InterestRateNotSet(address asset, address facility);
error RedemptionVault_LoanAmountExceeded(address user, uint16 redemptionId, uint256 amount);
error RedemptionVault_LoanIncorrectState(address user, uint16 redemptionId);
error RedemptionVault_InvalidLoan(address user, uint16 redemptionId);
error RedemptionVault_MaxSlippageExceeded(
address user,
uint16 redemptionId,
uint256 actualAmount,
uint256 maxAmount
);
// ========== DATA STRUCTURES ========== //
/// @notice Data structure for a redemption of a receipt token
///
/// @param depositToken The address of the deposit token
/// @param depositPeriod The period of the deposit in months
/// @param redeemableAt The timestamp at which the redemption can be finished
/// @param amount The amount of deposit tokens to redeem
/// @param facility The facility that handles this redemption
/// @param positionId The position ID for position-based redemptions (type(uint256).max without a position)
struct UserRedemption {
address depositToken;
uint8 depositPeriod;
uint48 redeemableAt;
uint256 amount;
address facility;
uint256 positionId;
}
/// @notice Data structure for a loan against a redemption
///
/// @param initialPrincipal The initial principal amount borrowed
/// @param principal The principal owed
/// @param interest The interest owed
/// @param dueDate The timestamp when the loan is due
/// @param isDefaulted Whether the loan has defaulted
struct Loan {
uint256 initialPrincipal;
uint256 principal;
uint256 interest;
uint48 dueDate;
bool isDefaulted;
}
// ========== FACILITY MANAGEMENT ========== //
/// @notice Authorize a facility
///
/// @param facility_ The address of the facility to authorize
function authorizeFacility(address facility_) external;
/// @notice Deauthorize a facility
///
/// @param facility_ The address of the facility to deauthorize
function deauthorizeFacility(address facility_) external;
/// @notice Check if a facility is authorized
///
/// @param facility_ The address of the facility to check
/// @return isAuthorized True if the facility is authorized
function isAuthorizedFacility(address facility_) external view returns (bool isAuthorized);
/// @notice Get all authorized facilities
///
/// @return facilities Array of authorized facility addresses
function getAuthorizedFacilities() external view returns (address[] memory facilities);
// ========== REDEMPTION FLOW ========== //
/// @notice Gets the details of a user's redemption
///
/// @param user_ The address of the user
/// @param redemptionId_ The ID of the redemption
/// @return redemption The details of the redemption
function getUserRedemption(
address user_,
uint16 redemptionId_
) external view returns (UserRedemption memory redemption);
/// @notice Gets the number of redemptions a user has started
///
/// @param user_ The address of the user
/// @return count The number of redemptions
function getUserRedemptionCount(address user_) external view returns (uint16 count);
/// @notice Gets all redemptions for a user
///
/// @param user_ The address of the user
/// @return redemptions The array of redemptions
function getUserRedemptions(address user_) external view returns (UserRedemption[] memory);
/// @notice Starts a redemption of a quantity of deposit tokens
///
/// @param depositToken_ The address of the deposit token
/// @param depositPeriod_ The period of the deposit in months
/// @param amount_ The amount of deposit tokens to redeem
/// @param facility_ The facility to handle this redemption
/// @return redemptionId The ID of the user redemption
function startRedemption(
IERC20 depositToken_,
uint8 depositPeriod_,
uint256 amount_,
address facility_
) external returns (uint16 redemptionId);
/// @notice Starts a redemption based on a position ID, using the position's conversion expiry
///
/// @param positionId_ The ID of the position to redeem from
/// @param amount_ The amount of deposit tokens to redeem
/// @return redemptionId The ID of the user redemption
function startRedemption(
uint256 positionId_,
uint256 amount_
) external returns (uint16 redemptionId);
/// @notice Cancels a redemption of a quantity of deposit tokens
///
/// @param redemptionId_ The ID of the user redemption
/// @param amount_ The amount of deposit tokens to cancel
function cancelRedemption(uint16 redemptionId_, uint256 amount_) external;
/// @notice Finishes a redemption of a quantity of deposit tokens
/// @dev This function does not take an amount as an argument, because the amount is determined by the redemption
///
/// @param redemptionId_ The ID of the user redemption
/// @return actualAmount The quantity of deposit tokens transferred to the caller
function finishRedemption(uint16 redemptionId_) external returns (uint256 actualAmount);
// ========== BORROWING FUNCTIONS ========== //
/// @notice Borrow the maximum amount against an active redemption
///
/// @param redemptionId_ The ID of the redemption to borrow against
/// @return actualAmount The quantity of underlying assets transferred to the recipient
function borrowAgainstRedemption(uint16 redemptionId_) external returns (uint256 actualAmount);
/// @notice Preview the maximum amount that can be borrowed against an active redemption
///
/// @param user_ The address of the user
/// @param redemptionId_ The ID of the redemption to borrow against
/// @return principal The principal amount that can be borrowed
/// @return interest The interest amount that will be charged
/// @return dueDate The due date of the loan
function previewBorrowAgainstRedemption(
address user_,
uint16 redemptionId_
) external view returns (uint256 principal, uint256 interest, uint48 dueDate);
/// @notice Repay a loan
///
/// @param redemptionId_ The ID of the redemption
/// @param amount_ The amount to repay
/// @param maxSlippage_ The maximum slippage allowed for the repayment
function repayLoan(uint16 redemptionId_, uint256 amount_, uint256 maxSlippage_) external;
/// @notice Preview the interest payable for extending a loan
///
/// @param user_ The address of the user
/// @param redemptionId_ The ID of the redemption
/// @param months_ The number of months to extend the loan
/// @return newDueDate The new due date
/// @return interestPayable The interest payable upon extension
function previewExtendLoan(
address user_,
uint16 redemptionId_,
uint8 months_
) external view returns (uint48 newDueDate, uint256 interestPayable);
/// @notice Extend a loan's due date
///
/// @param redemptionId_ The ID of the redemption
/// @param months_ The number of months to extend the loan
function extendLoan(uint16 redemptionId_, uint8 months_) external;
/// @notice Claim a defaulted loan and collect the reward
///
/// @param user_ The address of the user
/// @param redemptionId_ The ID of the redemption
function claimDefaultedLoan(address user_, uint16 redemptionId_) external;
// ========== BORROWING VIEW FUNCTIONS ========== //
/// @notice Get all loans for a redemption
///
/// @param user_ The address of the user
/// @param redemptionId_ The ID of the redemption
/// @return loan The loan
function getRedemptionLoan(
address user_,
uint16 redemptionId_
) external view returns (Loan memory loan);
// ========== ADMIN FUNCTIONS ========== //
/// @notice Set the maximum borrow percentage for an asset-facility combination
///
/// @param asset_ The address of the asset
/// @param facility_ The address of the facility
/// @param percent_ The maximum borrow percentage
function setMaxBorrowPercentage(IERC20 asset_, address facility_, uint16 percent_) external;
/// @notice Get the maximum borrow percentage for an asset-facility combination
///
/// @param asset_ The address of the asset
/// @param facility_ The address of the facility
/// @return percent The maximum borrow percentage (100e2 == 100%)
function getMaxBorrowPercentage(
IERC20 asset_,
address facility_
) external view returns (uint16 percent);
/// @notice Set the annual interest rate for an asset-facility combination
///
/// @param asset_ The address of the asset
/// @param facility_ The address of the facility
/// @param rate_ The annual interest rate (100e2 == 100%)
function setAnnualInterestRate(IERC20 asset_, address facility_, uint16 rate_) external;
/// @notice Get the annual interest rate for an asset-facility combination
///
/// @param asset_ The address of the asset
/// @param facility_ The address of the facility
/// @return rate The annual interest rate, in terms of 100e2
function getAnnualInterestRate(
IERC20 asset_,
address facility_
) external view returns (uint16 rate);
/// @notice Set the reward percentage when a claiming a defaulted loan
///
/// @param percent_ The claim default reward percentage
function setClaimDefaultRewardPercentage(uint16 percent_) external;
/// @notice Get the claim default reward percentage
///
/// @return percent The claim default reward percentage, in terms of 100e2
function getClaimDefaultRewardPercentage() external view returns (uint16 percent);
}
"
},
"src/policies/interfaces/deposits/IDepositManager.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {IERC20} from "src/interfaces/IERC20.sol";
import {IERC4626} from "src/interfaces/IERC4626.sol";
import {IAssetManager} from "src/bases/interfaces/IAssetManager.sol";
import {IReceiptTokenManager} from "src/policies/interfaces/deposits/IReceiptTokenManager.sol";
/// @title Deposit Manager
/// @notice Defines an interface for a policy that manages deposits on behalf of other contracts. It is meant to be used by the facilities, and is not an end-user policy.
///
/// Key terms for the contract:
/// - Asset: an ERC20 asset that can be deposited into the contract
/// - Asset vault: an optional ERC4626 vault that assets are deposited into
/// - Asset period: the combination of an asset and deposit period
interface IDepositManager is IAssetManager {
// ========== EVENTS ========== //
event OperatorYieldClaimed(
address indexed asset,
address indexed depositor,
address indexed operator,
uint256 amount
);
// Asset Configuration Events
event OperatorNameSet(address indexed operator, string name);
event AssetPeriodConfigured(
uint256 indexed receiptTokenId,
address indexed asset,
address indexed operator,
uint8 depositPeriod
);
event AssetPeriodEnabled(
uint256 indexed receiptTokenId,
address indexed asset,
address indexed operator,
uint8 depositPeriod
);
event AssetPeriodDisabled(
uint256 indexed receiptTokenId,
address indexed asset,
address indexed operator,
uint8 depositPeriod
);
event TokenRescued(address indexed token, uint256 amount);
// Borrowing Events
event BorrowingWithdrawal(
address indexed asset,
address indexed operator,
address indexed recipient,
uint256 amount
);
event BorrowingRepayment(
address indexed asset,
address indexed operator,
address indexed payer,
uint256 amount
);
event BorrowingDefault(
address indexed asset,
address indexed operator,
address indexed payer,
uint256 amount
);
// ========== ERRORS ========== //
error DepositManager_InvalidParams(string reason);
/// @notice Error if the action would leave the contract insolvent (liabilities > assets + borrowed)
///
/// @param asset The address of the underlying asset
/// @param requiredAssets The quantity of asset liabilities
/// @param depositedSharesInAsset
Submitted on: 2025-11-07 16:14:02
Comments
Log in to comment.
No comments yet.