Trove Manager

Description:

Decentralized Finance (DeFi) protocol contract providing Swap, Liquidity, Factory functionality.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Vyper",
  "sources": {
    "src/periphery/interfaces/IExchange.vyi": {
      "content": "# @version 0.4.1


# ============================================================================================
# View functions
# ============================================================================================


@external
@view
def BORROW_TOKEN() -> address:
    ...


@external
@view
def COLLATERAL_TOKEN() -> address:
    ...


@external
@view
def price() -> uint256:
    ...


# ============================================================================================
# Mutative functions
# ============================================================================================


@external
def swap(amount: uint256, receiver: address = msg.sender) -> uint256:
    ...",
      "sha256sum": "041e04a8201d46cb1aba81dd5d42ed01edf0d0a10c181f10d9833c2e545ee530"
    },
    "src/interfaces/ISortedTroves.vyi": {
      "content": "# @version 0.4.1


# ============================================================================================
# View functions
# ============================================================================================


@external
@view
def empty() -> bool:
    ...


@external
@view
def size() -> uint256:
    ...


@external
@view
def first() -> uint256:
    ...


@external
@view
def last() -> uint256:
    ...


@external
@view
def next(id: uint256) -> uint256:
    ...


@external
@view
def prev(id: uint256) -> uint256:
    ...


@external
@view
def contains(id: uint256) -> bool:
    ...


@external
@view
def valid_insert_position(annual_interest_rate: uint256, prev_id: uint256, next_id: uint256) -> bool:
    ...


@external
@view
def find_insert_position(annual_interest_rate: uint256, prev_id: uint256, next_id: uint256) -> (uint256, uint256):
    ...


# ============================================================================================
# Mutative functions
# ============================================================================================


@external
def insert(id: uint256, annual_interest_rate: uint256, prev_id: uint256, next_id: uint256):
    ...


@external
def remove(id: uint256):
    ...


@external
def re_insert(id: uint256, new_annual_interest_rate: uint256, prev_id: uint256, next_id: uint256):
    ...",
      "sha256sum": "8ec0bd640e9094bfb58ff20482ca75cae369292472fc7658c24e48dab912ce6e"
    },
    "src/trove_manager.vy": {
      "content": "# @version 0.4.1

"""
@title Trove Manager
@license MIT
@author Flex
@notice Core contract that manages all Troves. Handles opening, closing, liquidating, and updating borrower positions,
        accrues interest, maintains aggregate debt accounting, and coordinates redemptions with the Lender
        and sorted_troves contracts
"""

from ethereum.ercs import IERC20

from periphery.interfaces import IExchange

from interfaces import ISortedTroves


# ============================================================================================
# Events
# ============================================================================================


event OpenTrove:
    trove_id: uint256
    owner: address
    collateral_amount: uint256
    debt_amount: uint256
    upfront_fee: uint256
    annual_interest_rate: uint256

event AddCollateral:
    trove_id: uint256
    owner: address
    collateral_amount: uint256

event RemoveCollateral:
    trove_id: uint256
    owner: address
    collateral_amount: uint256

event Borrow:
    trove_id: uint256
    owner: address
    debt_amount: uint256
    upfront_fee: uint256

event Repay:
    trove_id: uint256
    owner: address
    debt_amount: uint256

event AdjustInterestRate:
    trove_id: uint256
    owner: address
    new_annual_interest_rate: uint256
    upfront_fee: uint256

event CloseTrove:
    trove_id: uint256
    owner: address
    collateral_amount: uint256
    debt_amount: uint256

event CloseZombieTrove:
    trove_id: uint256
    owner: address
    collateral_amount: uint256
    debt_amount: uint256

event LiquidateTrove:
    trove_id: uint256
    liquidator: address
    collateral_amount: uint256
    debt_amount: uint256

event Redeem:
    redeemer: address
    collateral_amount: uint256
    debt_amount: uint256
    debt_amount_out: uint256


# ============================================================================================
# Flags
# ============================================================================================


flag Status:
    ACTIVE
    ZOMBIE
    CLOSED
    LIQUIDATED


# ============================================================================================
# Structs
# ============================================================================================


struct Trove:
    debt: uint256
    collateral: uint256
    annual_interest_rate: uint256
    last_debt_update_time: uint64
    last_interest_rate_adj_time: uint64
    owner: address
    status: Status


# ============================================================================================
# Constants
# ============================================================================================


LENDER: public(immutable(address))

EXCHANGE: public(immutable(IExchange))
SORTED_TROVES: public(immutable(ISortedTroves))

BORROW_TOKEN: public(immutable(IERC20))
COLLATERAL_TOKEN: public(immutable(IERC20))

MINIMUM_COLLATERAL_RATIO: public(immutable(uint256))  # e.g., `110 * _ONE_PCT` for 110%

MIN_DEBT: public(constant(uint256)) = 500 * 10 ** 18
MIN_ANNUAL_INTEREST_RATE: public(constant(uint256)) = _ONE_PCT // 2  # 0.5%
MAX_ANNUAL_INTEREST_RATE: public(constant(uint256)) = 250 * _ONE_PCT  # 250%
UPFRONT_INTEREST_PERIOD: public(constant(uint256)) = 7 * 24 * 60 * 60  # 7 days
INTEREST_RATE_ADJ_COOLDOWN: public(constant(uint256)) = 7 * 24 * 60 * 60  # 7 days

_WAD: constant(uint256) = 10 ** 18
_ONE_PCT: constant(uint256) = _WAD // 100
_MAX_ITERATIONS: constant(uint256) = 1000
_ONE_YEAR: constant(uint256) = 365 * 60 * 60 * 24


# ============================================================================================
# Storage
# ============================================================================================


# ID of a Trove that has been partially redeemed and is now a "zombie".
# We will continue redeeming this Trove first until it's fully redeemed
zombie_trove_id: public(uint256)

# Total outstanding system debt
total_debt: public(uint256)

# Sum of individual trove debts weighted by their annual interest rates
total_weighted_debt: public(uint256)

# Last timestamp when `total_debt` and `total_weighted_debt` were updated
last_debt_update_time: public(uint256)

# Total collateral tokens currently held by the contract
collateral_balance: public(uint256)

# Trove ID --> Trove info
troves: public(HashMap[uint256, Trove])


# ============================================================================================
# Constructor
# ============================================================================================


@deploy
def __init__(
    lender: address,
    exchange: address,
    sorted_troves: address,
    borrow_token: address,
    collateral_token: address,
    minimum_collateral_ratio: uint256
):
    LENDER = lender
    EXCHANGE = IExchange(exchange)
    SORTED_TROVES = ISortedTroves(sorted_troves)
    BORROW_TOKEN = IERC20(borrow_token)
    COLLATERAL_TOKEN = IERC20(collateral_token)
    MINIMUM_COLLATERAL_RATIO = minimum_collateral_ratio

    extcall COLLATERAL_TOKEN.approve(exchange, max_value(uint256), default_return_value=True)


# ============================================================================================
# External view functions
# ============================================================================================


@external
@view
def get_upfront_fee(debt_amount: uint256, annual_interest_rate: uint256) -> uint256:
    """
    @notice Calculate the upfront fee for borrowing a specified amount of debt at a given annual interest rate
    @dev The fee represents prepaid interest over `UPFRONT_INTEREST_PERIOD` using the system's average rate after the new debt
    @param debt_amount The amount of debt to be borrowed
    @param annual_interest_rate The annual interest rate for the debt
    @return upfront_fee The calculated upfront fee
    """
    return self._get_upfront_fee(debt_amount, annual_interest_rate)


@external
@view
def get_trove_debt_after_interest(trove_id: uint256) -> uint256:
    """
    @notice Calculate the Trove's debt after accruing interest
    @param trove_id Unique identifier of the Trove
    @return trove_debt_after_interest The Trove's debt after accruing interest
    """
    return self._get_trove_debt_after_interest(self.troves[trove_id])


# ============================================================================================
# Sync total debt
# ============================================================================================


@external
def sync_total_debt() -> uint256:
    """
    @notice Accrue interest on the total debt and return the updated figure
    @return The updated total debt after accruing interest
    """
    return self._sync_total_debt()


# ============================================================================================
# Ownership
# ============================================================================================
# @todo


@external
def transfer_ownership(new_owner: address):
    """
    @notice Transfer ownership of a Trove to a new owner
    @dev New owner must call `accept_ownership` to finalize the transfer
    @param new_owner The address of the new owner
    """
    pass


@external
def accept_ownership():
    """
    @notice Accept ownership of a Trove
    @dev Can only be called by the new owner
    """
    pass


@external
def force_transfer_ownership(trove_id: uint256, new_owner: address):
    """
    @notice Force transfer ownership of a Trove to a new owner
    @dev Does not require acceptance by the new owner
    @param trove_id Unique identifier of the Trove
    @param new_owner The address of the new owner
    """
    pass


# ============================================================================================
# Open trove
# ============================================================================================


@external
def open_trove(
    index: uint256,
    collateral_amount: uint256,
    debt_amount: uint256,
    prev_id: uint256,
    next_id: uint256,
    annual_interest_rate: uint256,
    max_upfront_fee: uint256,
    min_debt_out: uint256
) -> uint256:
    """
    @notice Open a new Trove with specified collateral, debt, and interest rate
    @dev Caller will become the owner of the Trove
    @param index Unique index to allow multiple Troves per caller
    @param collateral_amount Amount of collateral tokens to deposit
    @param debt_amount Amount of debt to issue before the upfront fee
    @param prev_id ID of previous Trove for the insert position
    @param next_id ID of next Trove for the insert position
    @param annual_interest_rate Fixed annual interest rate to pay on the debt
    @param max_upfront_fee Maximum upfront fee the caller is willing to pay
    @param min_debt_out Minimum amount of borrow tokens the caller is willing to receive
    @return trove_id Unique identifier for the new Trove
    """
    # Make sure collateral and debt amounts are non-zero
    assert collateral_amount > 0, "!collateral_amount"
    assert debt_amount > 0, "!debt_amount"

    # Make sure the annual interest rate is within bounds    
    assert annual_interest_rate >= MIN_ANNUAL_INTEREST_RATE, "!MIN_ANNUAL_INTEREST_RATE"
    assert annual_interest_rate <= MAX_ANNUAL_INTEREST_RATE, "!MAX_ANNUAL_INTEREST_RATE"

    # Generate the Trove ID
    trove_id: uint256 = convert(keccak256(abi_encode(msg.sender, index)), uint256)

    # Make sure the Trove status is empty
    assert self.troves[trove_id].status == empty(Status), "!empty"

    # Calculate the upfront fee and make sure the user is ok with it
    upfront_fee: uint256 = self._get_upfront_fee(debt_amount, annual_interest_rate, max_upfront_fee)

    # Record the debt with the upfront fee
    debt_amount_with_fee: uint256 = debt_amount + upfront_fee

    # Make sure enough debt is being borrowed
    assert debt_amount_with_fee > MIN_DEBT, "!MIN_DEBT"

    # Get the collateral price
    collateral_price: uint256 = staticcall EXCHANGE.price()

    # Calculate the collateral ratio
    trove_collateral_ratio: uint256 = self._calculate_collateral_ratio(collateral_amount, debt_amount_with_fee, collateral_price)

    # Make sure the collateral ratio is above the minimum collateral ratio
    assert trove_collateral_ratio >= MINIMUM_COLLATERAL_RATIO, "!MINIMUM_COLLATERAL_RATIO"

    # Store the Trove info
    self.troves[trove_id] = Trove(
        debt=debt_amount_with_fee,
        collateral=collateral_amount,
        annual_interest_rate=annual_interest_rate,
        last_debt_update_time=convert(block.timestamp, uint64),
        last_interest_rate_adj_time=convert(block.timestamp, uint64),
        owner=msg.sender,
        status=Status.ACTIVE
    )

    # Accrue interest on the total debt and update accounting
    self._accrue_interest_and_account_for_trove_change(
        debt_amount_with_fee, # debt_increase
        0, # debt_decrease
        0, # old_weighted_debt
        debt_amount_with_fee * annual_interest_rate, # new_weighted_debt
    )

    # Record the received collateral
    self.collateral_balance += collateral_amount

    # Add the Trove to the sorted troves list
    extcall SORTED_TROVES.insert(
        trove_id,
        annual_interest_rate,
        prev_id,
        next_id
    )

    # Pull the collateral tokens from caller
    extcall COLLATERAL_TOKEN.transferFrom(msg.sender, self, collateral_amount, default_return_value=True)

    # Deliver borrow tokens to the caller, redeem if liquidity is insufficient
    self._transfer_borrow_tokens(debt_amount, min_debt_out)

    # Emit event
    log OpenTrove(
        trove_id=trove_id,
        owner=msg.sender,
        collateral_amount=collateral_amount,
        debt_amount=debt_amount,
        upfront_fee=upfront_fee,
        annual_interest_rate=annual_interest_rate
    )

    return trove_id


# ============================================================================================
# Adjust trove
# ============================================================================================


@external
def add_collateral(trove_id: uint256, collateral_change: uint256):
    """
    @notice Add collateral to an existing Trove
    @param trove_id Unique identifier of the Trove
    @param collateral_change Amount of collateral tokens to add
    """
    # Make sure collateral amount is non-zero
    assert collateral_change > 0, "!collateral_change"

    # Cache Trove info
    trove: Trove = self.troves[trove_id]

    # Make sure the caller is the owner of the Trove
    assert trove.owner == msg.sender, "!owner"

    # Make sure the Trove is active
    assert trove.status == Status.ACTIVE, "!active"

    # Update the Trove's collateral info
    self.troves[trove_id].collateral += collateral_change

    # Update the contract's recorded collateral balance
    self.collateral_balance += collateral_change

    # Pull the collateral tokens from caller
    extcall COLLATERAL_TOKEN.transferFrom(msg.sender, self, collateral_change, default_return_value=True)

    # Emit event
    log AddCollateral(
        trove_id=trove_id,
        owner=msg.sender,
        collateral_amount=collateral_change
    )


@external
def remove_collateral(trove_id: uint256, collateral_change: uint256):
    """
    @notice Remove collateral from an existing Trove
    @param trove_id Unique identifier of the Trove
    @param collateral_change Amount of collateral tokens to remove
    """
    # Make sure collateral amount is non-zero
    assert collateral_change > 0, "!collateral_change"

    # Cache Trove info
    trove: Trove = self.troves[trove_id]

    # Make sure the caller is the owner of the Trove
    assert trove.owner == msg.sender, "!owner"

    # Make sure the Trove is active
    assert trove.status == Status.ACTIVE, "!active"

    # Make sure the Trove has enough collateral
    assert trove.collateral >= collateral_change, "!trove.collateral"

    # Get the Trove's debt after accruing interest
    trove_debt_after_interest: uint256 = self._get_trove_debt_after_interest(trove)

    # Get the collateral price
    collateral_price: uint256 = staticcall EXCHANGE.price()

    # Calculate the new collateral amount and collateral ratio
    new_collateral: uint256 = trove.collateral - collateral_change
    collateral_ratio: uint256 = self._calculate_collateral_ratio(new_collateral, trove_debt_after_interest, collateral_price)

    # Make sure the new collateral ratio is above the minimum collateral ratio
    assert collateral_ratio >= MINIMUM_COLLATERAL_RATIO, "!MINIMUM_COLLATERAL_RATIO"

    # Update the Trove's collateral info
    self.troves[trove_id].collateral = new_collateral

    # Update the contract's recorded collateral balance
    self.collateral_balance -= collateral_change

    # Transfer the collateral tokens to caller
    extcall COLLATERAL_TOKEN.transfer(msg.sender, collateral_change, default_return_value=True)

    # Emit event
    log RemoveCollateral(
        trove_id=trove_id,
        owner=msg.sender,
        collateral_amount=collateral_change
    )


@external
def borrow(trove_id: uint256, debt_amount: uint256, max_upfront_fee: uint256, min_debt_out: uint256):
    """
    @notice Borrow more tokens from an existing Trove
    @param trove_id Unique identifier of the Trove
    @param debt_amount Amount of additional debt to issue before the upfront fee
    @param max_upfront_fee Maximum upfront fee the caller is willing to pay
    @param min_debt_out Minimum amount of debt the caller is willing to receive
    """
    # Make sure debt amount is non-zero
    assert debt_amount > 0, "!debt_amount"

    # Cache Trove info
    trove: Trove = self.troves[trove_id]

    # Make sure the caller is the owner of the Trove
    assert trove.owner == msg.sender, "!owner"

    # Make sure the Trove is active
    assert trove.status == Status.ACTIVE, "!active"

    # Calculate the upfront fee and make sure the user is ok with it
    upfront_fee: uint256 = self._get_upfront_fee(debt_amount, trove.annual_interest_rate, max_upfront_fee)

    # Record the debt with the upfront fee
    debt_amount_with_fee: uint256 = debt_amount + upfront_fee

    # Get the Trove's debt after accruing interest
    trove_debt_after_interest: uint256 = self._get_trove_debt_after_interest(trove)

    # Calculate the new debt amount
    new_debt: uint256 = trove_debt_after_interest + debt_amount_with_fee

    # Get the collateral price
    collateral_price: uint256 = staticcall EXCHANGE.price()

    # Calculate the collateral ratio
    collateral_ratio: uint256 = self._calculate_collateral_ratio(trove.collateral, new_debt, collateral_price)

    # Make sure the new collateral ratio is above the minimum collateral ratio
    assert collateral_ratio >= MINIMUM_COLLATERAL_RATIO, "!MINIMUM_COLLATERAL_RATIO"

    # Cache the Trove's old debt for global accounting
    old_debt: uint256 = trove.debt

    # Update the Trove's debt info
    trove.debt = new_debt
    trove.last_debt_update_time = convert(block.timestamp, uint64)

    # Save changes to storage
    self.troves[trove_id] = trove

    # Accrue interest on the total debt and update accounting
    self._accrue_interest_and_account_for_trove_change(
        debt_amount_with_fee, # debt_increase
        0, # debt_decrease
        old_debt * trove.annual_interest_rate, # old_weighted
        new_debt * trove.annual_interest_rate, # new_weighted_debt
    )

    # Deliver borrow tokens to the caller, redeem if liquidity is insufficient
    self._transfer_borrow_tokens(debt_amount, min_debt_out)

    # Emit event
    log Borrow(
        trove_id=trove_id,
        owner=msg.sender,
        debt_amount=new_debt,
        upfront_fee=upfront_fee
    )


@external
def repay(trove_id: uint256, debt_amount: uint256):
    """
    @notice Repay part of the debt of an existing Trove
    @param trove_id Unique identifier of the Trove
    @param debt_amount Amount of debt to repay
    """
    # Make sure debt amount is non-zero
    assert debt_amount > 0, "!debt_amount"

    # Cache Trove info
    trove: Trove = self.troves[trove_id]

    # Make sure the caller is the owner of the Trove
    assert trove.owner == msg.sender, "!owner"

    # Make sure the Trove is active
    assert trove.status == Status.ACTIVE, "!active"

    # Get the Trove's debt after accruing interest
    trove_debt_after_interest: uint256 = self._get_trove_debt_after_interest(trove)

    # Calculate the maximum allowable repayment to keep the Trove above the minimum debt
    max_repayment: uint256 = trove_debt_after_interest - MIN_DEBT  # Assumes `trove_debt_after_interest > MIN_DEBT`

    # Scale down the repayment amount if necessary
    debt_to_repay: uint256 = min(debt_amount, max_repayment)

    # Calculate the new debt amount
    new_debt: uint256 = trove_debt_after_interest - debt_to_repay

    # Cache the Trove's old debt for global accounting
    old_debt: uint256 = trove.debt

    # Update the Trove's debt info
    trove.debt = new_debt
    trove.last_debt_update_time = convert(block.timestamp, uint64)

    # Save changes to storage
    self.troves[trove_id] = trove

    # Accrue interest on the total debt and update accounting
    self._accrue_interest_and_account_for_trove_change(
        0, # debt_increase
        debt_to_repay, # debt_decrease
        old_debt * trove.annual_interest_rate, # old_weighted_debt
        new_debt * trove.annual_interest_rate, # new_weighted_debt
    )

    # Pull the borrow tokens from caller and transfer them to the lender
    extcall BORROW_TOKEN.transferFrom(msg.sender, LENDER, debt_to_repay, default_return_value=True)

    # Emit event
    log Repay(
        trove_id=trove_id,
        owner=msg.sender,
        debt_amount=debt_to_repay
    )


@external
def adjust_interest_rate(
    trove_id: uint256,
    new_annual_interest_rate: uint256,
    prev_id: uint256,
    next_id: uint256,
    max_upfront_fee: uint256
):
    """
    @notice Adjust the annual interest rate of an existing Trove
    @param trove_id Unique identifier of the Trove
    @param new_annual_interest_rate New fixed annual interest rate to pay on the debt
    @param prev_id ID of previous Trove for the new insert position
    @param next_id ID of next Trove for the new insert position
    """
    # Make sure the new annual interest rate is within bounds
    assert new_annual_interest_rate >= MIN_ANNUAL_INTEREST_RATE, "!MIN_ANNUAL_INTEREST_RATE"
    assert new_annual_interest_rate <= MAX_ANNUAL_INTEREST_RATE, "!MAX_ANNUAL_INTEREST_RATE"

    # Cache Trove info
    trove: Trove = self.troves[trove_id]

    # Make sure the caller is the owner of the Trove
    assert trove.owner == msg.sender, "!owner"

    # Make sure the Trove is active
    assert trove.status == Status.ACTIVE, "!active"

    # Make sure user is actually changing his rate
    assert new_annual_interest_rate != trove.annual_interest_rate, "!new_annual_interest_rate"

    # Get the Trove's debt after accruing interest
    trove_debt_after_interest: uint256 = self._get_trove_debt_after_interest(trove)

    # Initialize the new debt amount variable. We will charge an upfront fee only if the user is adjusting their rate prematurely
    new_debt: uint256 = trove_debt_after_interest

    # Initialize the upfront fee variable. We will need to increase the global debt by this amount if we charge it
    upfront_fee: uint256 = 0

    # Apply upfront fee on premature adjustments and check collateral ratio
    if block.timestamp < convert(trove.last_interest_rate_adj_time, uint256) + INTEREST_RATE_ADJ_COOLDOWN:
        # Calculate the upfront fee and make sure the user is ok with it
        upfront_fee = self._get_upfront_fee(new_debt, new_annual_interest_rate, max_upfront_fee)

        # Charge the upfront fee
        new_debt += upfront_fee

        # Get the collateral price
        collateral_price: uint256 = staticcall EXCHANGE.price()

        # Calculate the collateral ratio
        collateral_ratio: uint256 = self._calculate_collateral_ratio(trove.collateral, new_debt, collateral_price)

        # Make sure the new collateral ratio is above the minimum collateral ratio
        assert collateral_ratio >= MINIMUM_COLLATERAL_RATIO, "!MINIMUM_COLLATERAL_RATIO"

    # Cache the Trove's old debt and interest rate for global accounting
    old_debt: uint256 = trove.debt
    old_annual_interest_rate: uint256 = trove.annual_interest_rate

    # Update the Trove's interest rate and last adjustment time
    trove.annual_interest_rate = new_annual_interest_rate
    trove.last_interest_rate_adj_time = convert(block.timestamp, uint64)

    # Update the Trove's debt info to reflect accrued interest
    trove.debt = new_debt
    trove.last_debt_update_time = convert(block.timestamp, uint64)

    # Save changes to storage
    self.troves[trove_id] = trove

    # Accrue interest on the total debt and update accounting
    self._accrue_interest_and_account_for_trove_change(
        upfront_fee, # debt_increase
        0, # debt_decrease
        old_debt * old_annual_interest_rate, # old_weighted_debt
        new_debt * new_annual_interest_rate # new_weighted_debt
    )

    # Reinsert the Trove in the sorted list at its new position
    extcall SORTED_TROVES.re_insert(
        trove_id,
        new_annual_interest_rate,
        prev_id,
        next_id
    )

    # Emit event
    log AdjustInterestRate(
        trove_id=trove_id,
        owner=msg.sender,
        new_annual_interest_rate=new_annual_interest_rate,
        upfront_fee=upfront_fee
    )


# ============================================================================================
# Close trove
# ============================================================================================

# @todo -- here
@external
def close_trove(trove_id: uint256):
    """
    @notice Close an existing Trove by repaying all its debt and withdrawing all its collateral
    @param trove_id Unique identifier of the Trove
    """
    # Cache Trove info
    trove: Trove = self.troves[trove_id]

    # Make sure the caller is the owner of the Trove
    assert trove.owner == msg.sender, "!owner"

    # Make sure the Trove is active
    assert trove.status == Status.ACTIVE, "!active"

    # Get the Trove's debt after accruing interest
    trove_debt_after_interest: uint256 = self._get_trove_debt_after_interest(trove)

    # Cache the Trove's old info for global accounting
    old_trove: Trove = trove

    # Delete all Trove info and mark it as closed
    trove = empty(Trove)
    trove.status = Status.CLOSED

    # Save changes to storage
    self.troves[trove_id] = trove

    # Update the contract's recorded collateral balance
    self.collateral_balance -= old_trove.collateral

    # Accrue interest on the total debt and update accounting
    self._accrue_interest_and_account_for_trove_change(
        0, # debt_increase
        trove_debt_after_interest, # debt_decrease
        old_trove.debt * old_trove.annual_interest_rate, # old_weighted_debt
        0 # new_weighted_debt
    )

    # Remove from sorted list
    extcall SORTED_TROVES.remove(trove_id)

    # Pull the borrow tokens from caller and transfer them to the lender
    extcall BORROW_TOKEN.transferFrom(msg.sender, LENDER, trove_debt_after_interest, default_return_value=True)

    # Transfer the collateral tokens to caller
    extcall COLLATERAL_TOKEN.transfer(msg.sender, old_trove.collateral, default_return_value=True)

    # Emit event
    log CloseTrove(
        trove_id=trove_id,
        owner=msg.sender,
        collateral_amount=old_trove.collateral,
        debt_amount=trove_debt_after_interest
    )


@external
def close_zombie_trove(trove_id: uint256):
    """
    @notice Close a zombie Trove by repaying all its debt (if it has any) and withdrawing all its collateral
    @param trove_id Unique identifier of the Trove
    """
    # Cache Trove info
    trove: Trove = self.troves[trove_id]

    # Make sure the caller is the owner of the Trove
    assert trove.owner == msg.sender, "!owner"

    # Make sure the Trove is zombie
    assert trove.status == Status.ZOMBIE, "!zombie"

    # Cache the Trove's old info for global accounting
    old_trove: Trove = trove

    # Delete all Trove info and mark it as closed
    trove = empty(Trove)
    trove.status = Status.CLOSED

    # Save changes to storage
    self.troves[trove_id] = trove

    # If Trove is the current zombie trove, reset the `zombie_trove_id` variable
    if self.zombie_trove_id == trove_id:
        self.zombie_trove_id = 0

    # Update the contract's recorded collateral balance
    self.collateral_balance -= old_trove.collateral

    # Initialize the Trove's debt after interest variable
    trove_debt_after_interest: uint256 = 0

    if old_trove.debt > 0:
        # Get the Trove's debt after accruing interest
        trove_debt_after_interest = self._get_trove_debt_after_interest(old_trove)

        # Accrue interest on the total debt and update accounting
        self._accrue_interest_and_account_for_trove_change(
            0, # debt_increase
            trove_debt_after_interest, # debt_decrease
            old_trove.debt * old_trove.annual_interest_rate, # old_weighted_debt
            0 # new_weighted_debt
        )

        # Pull the borrow tokens from caller and transfer them to the lender
        extcall BORROW_TOKEN.transferFrom(msg.sender, LENDER, trove_debt_after_interest, default_return_value=True)

    # Transfer the collateral tokens to caller
    extcall COLLATERAL_TOKEN.transfer(msg.sender, old_trove.collateral, default_return_value=True)

    # Emit event
    log CloseZombieTrove(
        trove_id=trove_id,
        owner=msg.sender,
        collateral_amount=old_trove.collateral,
        debt_amount=trove_debt_after_interest
    )


# ============================================================================================
# Liquidate trove
# ============================================================================================


@external
def liquidate_trove(trove_id: uint256):
    """
    @notice Liquidate an unhealthy Trove by repaying all its debt and withdrawing all its collateral
    @dev Right now liquidator gets all the collateral. This function needs to be improved!
         Consider using an auction to dump the collateral, maybe with an outside module that
         checks min auction price and re-kicks if necessary. Then take all surplus as fee?
    @param trove_id Unique identifier of the Trove
    """ # @todo -- notice @dev
    # Cache Trove info
    trove: Trove = self.troves[trove_id]

    # Make sure the Trove is active or zombie
    assert trove.status == Status.ACTIVE or trove.status == Status.ZOMBIE, "!active or zombie"

    # Make sure trove has debt
    assert trove.debt > 0, "!debt"

    # Get the Trove's debt after accruing interest
    trove_debt_after_interest: uint256 = self._get_trove_debt_after_interest(trove)

    # Get the collateral price
    collateral_price: uint256 = staticcall EXCHANGE.price()

    # Calculate the collateral ratio
    collateral_ratio: uint256 = self._calculate_collateral_ratio(trove.collateral, trove_debt_after_interest, collateral_price)

    # Make sure the collateral ratio is below the minimum collateral ratio
    assert collateral_ratio < MINIMUM_COLLATERAL_RATIO, ">=MCR"

    # Cache the Trove's old info for global accounting
    old_trove: Trove = trove

    # Delete all Trove info and mark it as liquidated
    trove = empty(Trove)
    trove.status = Status.LIQUIDATED

    # Save changes to storage
    self.troves[trove_id] = trove

    # If Trove is the current zombie trove, reset the `zombie_trove_id` variable
    if self.zombie_trove_id == trove_id:
        self.zombie_trove_id = 0

    # Update the contract's recorded collateral balance
    self.collateral_balance -= old_trove.collateral

    # Accrue interest on the total debt and update accounting
    self._accrue_interest_and_account_for_trove_change(
        0, # debt_increase
        trove_debt_after_interest, # debt_decrease
        old_trove.debt * old_trove.annual_interest_rate, # old_weighted_debt
        0 # new_weighted_debt
    )

    # Remove from sorted list if it was active
    if old_trove.status == Status.ACTIVE:
        extcall SORTED_TROVES.remove(trove_id)

    # Pull the borrow tokens from caller and transfer them to the lender
    extcall BORROW_TOKEN.transferFrom(msg.sender, LENDER, trove_debt_after_interest, default_return_value=True)

    # Transfer the collateral tokens to caller
    extcall COLLATERAL_TOKEN.transfer(msg.sender, old_trove.collateral, default_return_value=True)

    # Emit event
    log LiquidateTrove(
        trove_id=trove_id,
        liquidator=msg.sender,
        collateral_amount=old_trove.collateral,
        debt_amount=trove_debt_after_interest,
    )


# ============================================================================================
# Redeem
# ============================================================================================


@external
def redeem(amount: uint256) -> uint256:
    """
    @notice Attempt to free the specified amount of borrow tokens by selling collateral
    @dev Can only be called by the Lender contract
    @dev Swap sandwich protection is the caller's responsibility
    @param amount Desired amount of borrow tokens to free
    @return amount The actual amount of borrow tokens freed
    """
    # Make sure the caller is the lender
    assert msg.sender == LENDER, "!lender"

    # Attempt to redeem the specified amount and transfer the resulting borrow tokens to the lender
    return self._redeem(amount)


@internal
def _redeem(amount: uint256) -> uint256:
    """
    @notice Internal implementation of `redeem`
    @dev Swap sandwich protection is the caller's responsibility
    @param amount Target amount of borrow tokens to free
    @return amount The actual amount of borrow tokens freed
    """
    # Accrue interest on the total debt and get the updated figure
    self._sync_total_debt()

    # Get the collateral price
    collateral_price: uint256 = staticcall EXCHANGE.price()

    # Initialize the `is_zombie_trove` flag
    is_zombie_trove: bool = False

    # Initialize the Trove to redeem variable
    trove_to_redeem: uint256 = self.zombie_trove_id

    # Use zombie Trove from previous redemption if it exists. Otherwise get the Trove with the lowest interest rate
    if trove_to_redeem != 0:
        is_zombie_trove = True
    else:
        trove_to_redeem = staticcall SORTED_TROVES.last()

    # Cache the amount of debt we need to free
    remaining_debt_to_free: uint256 = amount

    # Cache the total changes we're making so that later we can update the accounting
    total_debt_decrease: uint256 = 0
    total_collateral_decrease: uint256 = 0
    total_new_weighted_debt: uint256 = 0
    total_old_weighted_debt: uint256 = 0

    # Loop through as many Troves as we're allowed or until we redeem all the debt we need
    for _: uint256 in range(_MAX_ITERATIONS):
        # Cache Trove info
        trove: Trove = self.troves[trove_to_redeem]

        # Don't want to redeem a borrower's own Trove
        if msg.sender != trove.owner:
            # Get the Trove's debt after accruing interest
            trove_debt_after_interest: uint256 = self._get_trove_debt_after_interest(trove)

            # Determine the amount to be freed
            debt_to_free: uint256 = min(remaining_debt_to_free, trove_debt_after_interest)

            # Calculate the Trove's new debt amount
            trove_new_debt: uint256 = trove_debt_after_interest - debt_to_free

            # If trove would be left with debt below the minimum, go zombie
            if trove_new_debt < MIN_DEBT:
                # If the trove is not already a zombie trove, we need to mark it as such
                if not is_zombie_trove:
                    # Mark trove as zombie
                    trove.status = Status.ZOMBIE

                    # Remove trove from sorted list
                    extcall SORTED_TROVES.remove(trove_to_redeem)

                    # If it's a partial redemption, record it so we know to continue with it next time
                    if trove_new_debt > 0:
                        self.zombie_trove_id = trove_to_redeem

                # If we fully redeemed a zombie trove, reset the `zombie_trove_id` variable
                elif trove_new_debt == 0:
                    self.zombie_trove_id = 0

            # Get the amount of collateral equal to `debt_to_free`
            collateral_to_redeem: uint256 = debt_to_free * _WAD // collateral_price

            # Calculate the Trove's new collateral amount
            trove_new_collateral: uint256 = trove.collateral - collateral_to_redeem

            # Calculate the Trove's old and new weighted debt
            trove_old_weighted_debt: uint256 = trove.debt * trove.annual_interest_rate
            trove_new_weighted_debt: uint256 = trove_new_debt * trove.annual_interest_rate

            # Update the Trove's info
            trove.debt = trove_new_debt
            trove.collateral = trove_new_collateral
            trove.last_debt_update_time = convert(block.timestamp, uint64)

            # Save changes to storage
            self.troves[trove_to_redeem] = trove

            # Increment the total debt and collateral decrease
            total_debt_decrease += debt_to_free
            total_collateral_decrease += collateral_to_redeem

            # Increment the total old and new weighted debt
            total_old_weighted_debt += trove_old_weighted_debt
            total_new_weighted_debt += trove_new_weighted_debt

            # Update the remaining debt to free
            remaining_debt_to_free -= min(debt_to_free, remaining_debt_to_free)

            # Check if we freed all the debt we wanted
            if remaining_debt_to_free == 0:
                break

        # Get the next trove to redeem
        trove_to_redeem = staticcall SORTED_TROVES.last() if is_zombie_trove else staticcall SORTED_TROVES.prev(trove_to_redeem)

        # If we reached the end of the list, break
        if trove_to_redeem == 0:
            break

        # Reset the `is_zombie_trove` flag
        is_zombie_trove = False

    # Accrue interest on the total debt and update accounting
    self._accrue_interest_and_account_for_trove_change(
        0, # debt_increase
        total_debt_decrease, # debt_decrease
        total_old_weighted_debt, # old_weighted_debt
        total_new_weighted_debt, # new_weighted_debt
    )

    # Update the contract's recorded collateral balance
    self.collateral_balance -= total_collateral_decrease

    # Swap the collateral to borrow token and transfer it to the caller. Does nothing on zero amount
    borrow_token_out: uint256 = extcall EXCHANGE.swap(total_collateral_decrease, msg.sender)

    # Emit event
    log Redeem(
        redeemer=msg.sender,
        collateral_amount=total_collateral_decrease,
        debt_amount=total_debt_decrease,
        debt_amount_out=borrow_token_out
    )

    return borrow_token_out


# ============================================================================================
# Internal pure functions
# ============================================================================================


@internal
@pure
def _calculate_collateral_ratio(collateral: uint256, debt: uint256, collateral_price: uint256) -> uint256:
    """
    @notice Calculate the collateral ratio given collateral, debt, and collateral price
    @param collateral The amount of collateral tokens
    @param debt The amount of debt
    @param collateral_price The price of one collateral token
    @return collateral_ratio The collateral ratio
    """
    return collateral * collateral_price // debt


@internal
@pure
def _calculate_accrued_interest(weighted_debt: uint256, period: uint256) -> uint256:
    """
    @notice Calculate the interest accrued on weighted debt over a given period
    @param weighted_debt The debt weighted by the annual interest rate
    @param period The time period over which interest is calculated (in seconds)
    @return interest The interest accrued over the period
    """
    return weighted_debt * period // _ONE_YEAR // _WAD


# ============================================================================================
# Internal view functions
# ============================================================================================


@internal
@view
def _get_upfront_fee(
    debt_amount: uint256,
    annual_interest_rate: uint256,
    max_upfront_fee: uint256 = max_value(uint256)
) -> uint256:
    """
    @notice Calculate the upfront fee for borrowing a specified amount of debt at a given annual interest rate
    @dev Make sure the calculated fee does not exceed `max_upfront_fee`
    @dev The fee represents prepaid interest over `UPFRONT_INTEREST_PERIOD` using the system's average rate after the new debt
    @param debt_amount The amount of debt to be borrowed
    @param annual_interest_rate The annual interest rate for the debt
    @param max_upfront_fee The maximum upfront fee the caller is willing to pay
    @return upfront_fee The calculated upfront fee
    """
    # Total debt after adding the new debt
    new_total_debt: uint256 = self.total_debt + debt_amount

    # Total weighted debt after adding the new weighted debt
    new_total_weighted_debt: uint256 = self.total_weighted_debt + (debt_amount * annual_interest_rate)

    # Calculate the new average interest rate
    avg_interest_rate: uint256 = new_total_weighted_debt // new_total_debt

    # Calculate the upfront fee using the average interest rate
    upfront_fee: uint256 = self._calculate_accrued_interest(debt_amount * avg_interest_rate, UPFRONT_INTEREST_PERIOD)

    # Make sure the user is ok with the upfront fee
    assert upfront_fee <= max_upfront_fee, "!max_upfront_fee"

    return upfront_fee


@internal
@view
def _get_trove_debt_after_interest(trove: Trove) -> uint256:
    """
    @notice Calculate the Trove's debt after accruing interest
    @param trove The Trove struct
    @return trove_debt_after_interest The Trove's debt after accruing interest
    """
    return trove.debt + self._calculate_accrued_interest(
        trove.debt * trove.annual_interest_rate,  # trove_weighted_debt
        block.timestamp - convert(trove.last_debt_update_time, uint256)  # period since last update
    )


# ============================================================================================
# Internal mutative functions
# ============================================================================================


@internal
def _accrue_interest_and_account_for_trove_change(
    debt_increase: uint256,
    debt_decrease: uint256,
    old_weighted_debt: uint256,
    new_weighted_debt: uint256
):
    """
    @notice Accrue interest on the total debt and update total debt and total weighted debt accounting
    @param debt_increase Amount of debt to add to the total debt
    @param debt_decrease Amount of debt to subtract from the total debt
    @param old_weighted_debt Amount of weighted debt to subtract from the total weighted debt
    @param new_weighted_debt Amount of weighted debt to add to the total weighted debt
    """
    # Update total debt
    new_total_debt: uint256 = self._sync_total_debt()
    new_total_debt += debt_increase
    new_total_debt -= debt_decrease
    self.total_debt = new_total_debt

    # Update total weighted debt
    new_total_weighted_debt: uint256 = self.total_weighted_debt
    new_total_weighted_debt += new_weighted_debt
    new_total_weighted_debt -= old_weighted_debt
    self.total_weighted_debt = new_total_weighted_debt


@internal
def _sync_total_debt() -> uint256:
    """
    @notice Accrue interest on the total debt and return the updated figure
    @return new_total_debt The updated total debt after accruing interest
    """
    # Calculate the pending aggregate interest
    pending_agg_interest: uint256 = (self.total_weighted_debt * (block.timestamp - self.last_debt_update_time)) // (_ONE_YEAR * _WAD)

    # Calculate the new total debt after interest
    new_total_debt: uint256 = self.total_debt + pending_agg_interest

    # Update the total debt
    self.total_debt = new_total_debt

    # Update the last debt update time
    self.last_debt_update_time = block.timestamp

    return new_total_debt


@internal
def _transfer_borrow_tokens(amount: uint256, min_out: uint256):
    """
    @notice Transfer borrow tokens to the caller, redeeming from borrowers if necessary
    @param amount Amount of borrow tokens to transfer
    @param min_out Minimum amount of borrow tokens the caller is willing to receive
    """
    # Check how much borrow token liquidity the lender has
    available_liquidity: uint256 = staticcall BORROW_TOKEN.balanceOf(LENDER)

    # Cache the amount of borrow tokens that we were able to transfer
    amount_out: uint256 = 0

    # If there's not enough liquidity, redeem the difference. Otherwise just transfer the full amount
    if amount > available_liquidity:
        # Transfer whatever we have first
        if available_liquidity > 0:
            extcall BORROW_TOKEN.transferFrom(LENDER, msg.sender, available_liquidity, default_return_value=True)

        # Redeem the difference
        amount_out_of_redeem: uint256 = self._redeem(amount - available_liquidity)

        # Total amount we were able to transfer
        amount_out = amount_out_of_redeem + available_liquidity
    else:
        # We are able to transfer the full amount
        amount_out = amount

        # Transfer the full amount
        extcall BORROW_TOKEN.transferFrom(LENDER, msg.sender, amount, default_return_value=True)

    # Make sure the user is ok with the amount of borrow tokens he got
    assert amount_out >= min_out, "shrekt"",
      "sha256sum": "1da3b5e6324fb185f09242f0aad2e8043578203fd5640d0c661852ce9a23c9e1"
    }
  },
  "settings": {
    "outputSelection": {
      "src/trove_manager.vy": [
        "evm.bytecode",
        "evm.deployedBytecode",
        "abi"
      ]
    },
    "search_paths": [
      "."
    ]
  },
  "compiler_version": "v0.4.1+commit.8a93dd27",
  "integrity": "1bb072863fcb4f1711b7a535507f2e6efe92ca25f8b8abe9fd8594c045db686c"
}}

Tags:
DeFi, Swap, Liquidity, Factory|addr:0x36e9ee7a0ce154cc1e379a7c0231fa19c0b41c1f|verified:true|block:23655983|tx:0xda0a3b40be11e994ab170f246204458e384e107da18e23040fed1bcdb761a59f|first_check:1761417427

Submitted on: 2025-10-25 20:37:09

Comments

Log in to comment.

No comments yet.