DepositRedemptionVault

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

Tags:
ERC20, ERC721, ERC165, Multisig, Mintable, Burnable, Non-Fungible, Swap, Yield, Voting, Timelock, Upgradeable, Multi-Signature, Factory, Oracle|addr:0x20a3d8510f2e1176e8db4cea9883a8287a9029db|verified:true|block:23747531|tx:0xb41c36e1a4b58694bd09ceea40bc5225ddc0ba46779e0e0c188d05dd2fa30917|first_check:1762528440

Submitted on: 2025-11-07 16:14:02

Comments

Log in to comment.

No comments yet.