Search

LoopFi Contest Review

태그
RedSpider
Blockchain
Property
LoopFiBlog.png

Overview

The following article analyzes the core logic and vulnerabilities discovered in LoopFi during the Code4rena contest held from October 12 to October 19, 2024. Given the complexity of its logic, which is closely linked to multiple services, it is challenging to cover all aspects in detail in a single post. Therefore, this analysis will be presented as a three-part series. In this first article, we will focus on the Vault and Pool, which are central to the carry trade.
LoopFi rewards both ETH suppliers and governance token holders, enabling them to generate additional revenue beyond protocol fees. The second article will delv into these rewards and the profit-generating mechanisms associated with them. Finally, the third part will review the High and Medium severity vulnerabilities identified during the contest.
To better understand the logic discussed in this article, it is recommended to familiarize yourself with the following concepts:
Leveraged lending systems in traditional finance
Fundamental DeFi concepts, such as token staking, lock-up, vesting, and lending protocols
LST (Liquid Staking Token) and LRT (Liquid Restaking Token)

About LoopFi

LoopFi is a lending market for Ethereum carry trade through leveraged collateral deposits. Users can specify a leverage factor and use an LRT derivative as collateral, allowing them to borrow more ETH than they originally hold to generate profit.
Borrowed ETH is not directly transferred to the user; instead it is used to buy additional collateral token, allowing users earn a higher yield than their original equity. For instance, if a user holds 4 st-ETH and sets a leverage factor of 2x, LoopFi lends the user enough ETH to purchase an additional 4 st-ETH, effectively doubling their initial position to 8 st-ETH. LoopFi refers to this series of borrowing - collateral token purchasing - profit generation steps as a Loop, and users who borrow ETH are called Loopers.
LoopFi accepts pendleLP as a collateral token. Pendle is an AMM platform designed for trading LRTs. It allows users to deposit LRTs such as stETH and wraps them into two tokens: PT-stETH and YT-stETH. These tokens can be traded within Pendle's liquidity pools.
In other words, pendleLP represents a user's ownership of the PT and YT tokens supplied to the liquidity pool. For a detailed explanation of Pendle, refer to the official documentation linked below.

Participants

Users can engage with LoopFi in the following three roles (users can assume multiple roles simultaneously):
Lender
Lenders provide ETH to the pool, enabling others to borrow ETH. By depositing ETH, lenders receive lpETH, which earns borrowing interest charged to Loopers. Additionally, lenders can generate extra yields by depositing lpETH into the lpETH/LOOP pool or the lpETH/ETH pool. Notably, the lpETH/LOOP pool emits dLP, LoopFi’s governance token.
Looper
Loopers borrow ETH using pendleLP tokens as collateral, then use the borrowed funds to acquire profit-generating collateral. As borrowing incurs interest on the ETH, a successful carry trade requires that the profit generated by the collateral exceeds the accrued interest.
dLP Lockers
The dLP token is LoopFi’s governance token, representing ownership of the Dynamic Liquidity Pool (dLP), which consists of 80% LOOP and 20% lpETH. This structure is inspired by Radiant.
The Dynamic Liquidity Pool corresponds to the lpETH/LOOP pair mentioned in the Looper section. “Locking dLP” involves supplying LOOP and lpETH in an 8:2 ratio (equivalent to 5% of the total position size) into this pool.
Rewards for dLP Lockers include:
LOOP emissions
A share of platform revenue (in ETH)
Voting power for governance decisions within the LoopFi protocol
Loopers can offset their borrowing costs with these rewards.

The interests

The borrowing interest charged to a Looper consists of two types: "Quota Interest" and "Regular Interest". The concept of a Quota is inspired by GearBox, which simply sets a deposit limit for specific collateral tokens. A separate simple interest is applied to the Quota.

Quota Interest

In LoopFi, the Quota is not used to restric collateral deposits but instead serves as a basis for charging simple interest on the designated Quota. The Quota interest rate is configured individually for each collateral token by the protocol administrator. This mechanism helps minimize the protocol's risk associated with relatively less stable collateral tokens.
// Memebrs except rate & cumulativeIndexLU are not used in LoopFi struct TokenQuotaParams { >> uint16 rate; uint192 cumulativeIndexLU; uint16 quotaIncreaseFee; uint96 totalQuoted; uint96 limit; }
Solidity
복사
2024-10-loopfi/src/interfaces/IPoolQuotaKeeperV3.sol

Normal Intrest

Regular interest functions similarly to traditional interest, being charged on the principal amount of the loan. It is determined by the base rate and an incremental rate set for the pool. This means the regular interest rate can fluctuate based on specific conditions.
What drives these rate changes? The utilization rate of the Lending Pool. A high utilization rate reflects strong borrowing demand, prompting the protocol to increase interest rates and impose higher borrowing costs.
Below is an excerpt from LoopFi’s official documentation, explaining the interest calculation method and the utilization rate brackets of the Lending Pool. It outlines the points where interest rates rise sharply as utilization increases.
0 - 50% Utilization: cheap to borrow
50 - 80% Utilization: high but acceptable rate for borrowing
80 - 100% Utilization: very expensive to borrow

Cumulative Index

Each type of interest is calculated using a combined value index called the CumulativeIndex. This aggregated variable represents the value of the tokens loaned at a specific point in time. The index for regular interest follows the pool's index, whereas quota interest uses a separate quota index.
Since the current interest rate is already factored in when recalculating a new index, using the index for interest calculations does not pose any issues. As the variable name suggests, the index accumulates over time by adding the new value to the previous one.
Below is the formula for calculating the index for regular interest. The increment rate is applied in the calculation to account for variations in the pool's base rate.
// Normal Cumulative Index return (_baseInterestIndexLU * (RAY + baseInterestRate().calcLinearGrowth(timestamp))) / RAY;
Solidity
복사
2024-10-loopfi/src/PoolV3.sol : _calcBaseInterestIndex
Below is the index for quota interest. Unlike regular interest, the quota interest rate is unchangeable.
// Quota Cumulative Index return uint192( uint256(cumulativeIndexLU) + (RAY_DIVIDED_BY_PERCENTAGE * (block.timestamp - lastQuotaRateUpdate) * rate) / SECONDS_PER_YEAR // 86400 );
Solidity
복사
2024-10-loopfi/src/QuotasLogic.sol : cumulativeIndexSince
To provide a clearer understanding, the formulas are presented below(with RAY ommited for simplicity).
CI(t1t_1) : cumulativeIndexNow
CI(t0t_0) : cumulativeIndexLastUpdate
r : Base rate of the pool
g : Increment weight
rqr_q : Quota interest
Normal Cumulative Index
CI(t1)=CI(t0)+CI(t0)rgCI(t_1) = CI(t_0)+CI(t_0)\cdot{r}\cdot{g}
Quota Cumulative Index(below)
CI(t1)=CI(t0)+periodrqCI(t_1) = CI(t_0)+period*r_q

Getting Accrued Interest

Finally, we can calculate the accrued interest on the position! The accrued interest is the sum of the accrued quota interest and the regular interest.
Let’s start by determining the regular interest first.
/// @dev Computes interest accrued since the last update function calcAccruedInterest (uint256 amount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow) internal pure returns (uint256) if (amount == 0) return 0; return (amount * cumulativeIndexNow) / cumulativeIndexLastUpdate - amount; // U:[CL-1] }
Solidity
복사
2024-10-loopfi/src/CDPVault.sol
By substituting and simplifying the formula for cumulativeIndexNow, it becomes clear that the regular cumulative interest reflects the application of the newly increased interest rate to the loan principal.
amount(CI(t0)+CI(t0)rg)CI(t0)amount=amountrg\frac{amount\cdot{(CI(t_0)+CI(t_0)\cdot{r}{g}})}{CI(t_0)} -amount = amount\cdot{rg}
The following calculates the newly accrued quota interest over a specific period. Again, by applying the cumulativeIndexNow formula and simplifying, it becomes clear that quota interest is calculated as simple interest with a fixed rate applied over time.
The total cumulative quota interest is determined by adding the position's previously accrued interest to the newly calculated amount.
return uint128(uint256(quoted) * (cumulativeIndexNow - cumulativeIndexLU) / RAY);
Solidity
복사
2024-10-loopfi/lib/core-v3/contracts/libraries/QuotasLogic.sol : calcAccruedQuotaInterest#L37
quoted(Iold+periodquotaRateIold)=quotedperiodratequoted \cdot (I_{old}+period \cdot quotaRate - I_{old}) = quoted \cdot period \cdot rate

Logic Overview

Protocol Control Flow from Code4rena LoopFi
Users can interact with the Vault or Pool Contract either directly or through the Action Contract. The primary role of the Action Contract is to bundle and execute the preliminary operations required before users interact with the Vault or Pool on their behalf.
When interacting via the Action Contract, users must have a PRBProxy address, which allows multiple contract calls to be bundled into a single transaction. Once the user sets the required parameters and initiates actions such as collateral deposits, withdrawals, borrowing, or debt repayments in the Vault, the proxy executes these functions by calling the Action Contract using delegatecall.
According to the flowchart provided during the Code4rena contest, the LoopFi protocol assumes that general users interact with the Vault or Pool exclusively through the Action Contract. In this case, the position address is always the Proxy Contract, while the actual user address is passed separately in the CreditParams to the Action Contract.
The Action Contract connected to the Vault is named PositionAction. It is extended by token-specific Position Action contracts, which manage token transfers to or withdrawals from the Vault. Currently supported tokens include ERC-20, ERC-4626, and PendleLP.
struct CreditParams { // amount of debt to increase by or the amount of normal debt to decrease by [wad] uint256 amount; // address that will transfer the debt to repay or receive the debt to borrow >> address creditor; // optional swap from underlying token to arbitrary token SwapParams auxSwap; }
Solidity
복사
2024-10-loopfi/src/proxy/PositionAction.sol

Permission

LoopFi employs a permission-based access control system in addition to role-based and address-based access controls. This permission-based mechanism enables specific accounts to perform privileged actions on behalf of the owner.
// User Permissions /// @notice Map specifying whether a `caller` has the permission to perform an action on the `owner`'s behalf mapping(address owner => mapping(address caller => bool permitted)) private _permitted; /// @notice Map specifying whether an `agent` has the permission to modify the permissions of the `owner` mapping(address owner => mapping(address manager => bool permitted)) private _permittedAgents;
Solidity
복사
2024-10-loopfi/src/utils/Permission.sol : _permittedL#26, _permittedAgentsL#29
hasPermission checks whether the msg.sender is authorized to represent an owner and returns true or false. Throughout LoopFi, the protocol ensures that the sender invoking functions related to collateral deposits, collateral withdrawals, borrowing, or repayment for a position possesses the appropriate permissions.

Depositing Collateral

Users who borrow ETH must deposit PendleLP tokens into the CDPVault (hereafter referred to as the Vault). A separate Vault exists for each type of PendleLP, and it provides core functionalities such as collateral deposits, collateral withdrawals, ETH borrowing, and ETH repayment.
Users deposit collateral into the Vault by calling the deposit function in PositionAction through a proxy. This call includes the Vault address, the position address, the type of collateral to be deposited, and swap-related parameters. Since all interactions with the Vault via PositionAction are executed as a delegatecall through the proxy, the position address always refers to the proxy address.
At this stage, users can send either the Vault's underlying token (PendleLP) or WETH. If WETH is sent, it is first swapped into the Vault's underlying token through SwapAction, and the collateral is then deposited into the Vault using the _onDeposit hook.
function _deposit( address vault, address position, CollateralParams calldata collateralParams, PermitParams calldata permitParams ) internal returns (uint256) { uint256 amount = collateralParams.amount; // deposit amount [WAD] // assetIn : Collateral token sent from user >> if (collateralParams.auxSwap.assetIn != address(0)) { if ( collateralParams.auxSwap.assetIn != collateralParams.targetToken || collateralParams.auxSwap.recipient != address(this) // user proxy ) revert PositionAction__deposit_InvalidAuxSwap(); >> amount = _transferAndSwap(collateralParams.collateralizer, collateralParams.auxSwap, permitParams); >> } else if (collateralParams.collateralizer != address(this)) { _transferFrom( collateralParams.targetToken, collateralParams.collateralizer, address(this), amount, permitParams ); } // deposit collateral into Vault return _onDeposit(vault, position, collateralParams.targetToken, amount); }
Solidity
복사
2024-10-loopfi/src/proxy/PositionAction.sol
The _onDeposit hook approves tokens equal to the deposit amount based on the token type and deposits the collateral into the Vault. The vault.deposit function then returns the amount of collateral deposited.
function _onDeposit( address vault, address position, address /*src*/, uint256 amount ) internal override returns (uint256) { address collateralToken = address(ICDPVault(vault).token()); IERC20(collateralToken).forceApprove(vault, amount); >> return ICDPVault(vault).deposit(position, amount); }
Solidity
복사
2024-10-loopfi/src/proxy/PositionAction20.sol
So, the token flows : User(collateral) → proxy → Vault

Deposit collateral into the vault

The deposit function adds collateral to the Vault and increases the user’s collateral balance. The amount is converted to the underlying token scale of the Vault and is represented as a signed integer to differentiate between deposit and withdrawal changes: positive for deposits, negative for withdrawals.
The modifyCollateralAndDebt function updates the modified position information after the collateral is deposited. The collateral belongs to the position owner, while the borrowed ETH belongs to the creditor.
function deposit(address to, uint256 amount) external returns (uint256 tokenAmount) { tokenAmount = wdiv(amount, tokenScale); int256 deltaCollateral = toInt256(tokenAmount); // Update Position Info >> modifyCollateralAndDebt({ owner: to, // position owner collateralizer: msg.sender, // user addr who deposits the collateral creditor: msg.sender, // borrower deltaCollateral: deltaCollateral, // deposit : + deltaDebt: 0 }); }
Solidity
복사
2024-10-loopfi/src/CDPVault.sol

Position Modification Authorization Check

The modifyCollateralAndDebt function verifies authorization to modify a position and updates the current debt state of the position to reflect the latest status. It then executes the user-requested operation, updates the user's position state (e.g., debt principal, interest, and collateral earnings), checks whether the updated position remains safe, and adjusts the ETH/collateral pool state accordingly.
To add collateral, the msg.sender must have the collateralizer permission, as the collateral token is sent from the collateralizer. Additionally, the Vault contract must be in a running state. When the Vault is paused, only repayment operations are allowed.
If the permissions and conditions are met, the _calcDebt function updates the comprehensive debt information (quota interest, regular interest, and indices) for the position from the last update to the current time. The return value represents the total interest accrued to date.
function modifyCollateralAndDebt( address owner, address collateralizer, address creditor, int256 deltaCollateral, int256 deltaDebt ) public { if ( // ... >> (deltaCollateral > 0 && !hasPermission(collateralizer, msg.sender)) || // ... ) revert CDPVault__modifyCollateralAndDebt_noPermission(); >> if (deltaDebt > 0 || deltaCollateral != 0){ _requireNotPaused(); } // ... } Position memory position = positions[owner]; >> DebtData memory debtData = _calcDebt(position); uint256 newDebt; uint256 newCumulativeIndex;
Solidity
복사
2024-10-loopfi/src/CDPVault.sol : modifyCollateralAndDebt#L433

Calculate the position debt

We can observe that the index and the debt principal use different units. RAY represents a 27-digit integer, while WAD represents an 18-digit integer. Loan principals, as token amounts, use the standard WAD unit of wei. In contrast, indices, which involve fractional rates, use RAY to achieve higher precision. RAY is commonly used to represent interest rates or debt positions with nano-level precision.
Information updates are processed sequentially, starting with quota information and followed by general information. The total accrued interest is calculated by summing the two interest amounts and then returned.
The _getQuotedTokensData function calculates and retrieves the quota interest accrued since the last update of the quota interest index, along with the current cumulative quota index. By adding the position's previously accrued quota interest, it determines the total quota interest.
The calcAccruedInterest function calculates the regular interest amount, reflecting the newly adjusted interest rate. As explained in the "Interest" section, regular interest follows the pool's base rate index. For a detailed calculation, refer to .
function _calcDebt(Position memory position) internal view returns (DebtData memory cdd) { uint256 index = pool.baseInterestIndex(); // [RAY] cdd.debt = position.debt; // debt principal of position [WAD] >> cdd.cumulativeIndexNow = index; cdd.cumulativeIndexLastUpdate = position.cumulativeIndexLastUpdate; cdd.cumulativeQuotaIndexLU = position.cumulativeQuotaIndexLU; // update Quota data : newly accrued interest[simple], current QuotaIndex >> (cdd.cumulativeQuotaInterest, cdd.cumulativeQuotaIndexNow) = _getQuotedTokensData(cdd); // Total Quota Interest cdd.cumulativeQuotaInterest += position.cumulativeQuotaInterest; // Update principal interest data [compound] >> cdd.accruedInterest = CreditLogic.calcAccruedInterest(cdd.debt, cdd.cumulativeIndexLastUpdate, index); // The Total interest >> cdd.accruedInterest += cdd.cumulativeQuotaInterest; }
Solidity
복사
2024-10-loopfi/src/CDPVault.sol

Deposit Collateral

The collateral tokens specified by the user are retrieved from the collateralizer and deposited into the CDPVault. The increased collateral is then reflected in the position. Once the collateral deposit and position update are completed in _modifyPosition, the current price of the collateral is fetched to calculate its value. This ensures that the position remains safe after any new loans or collateral withdrawals. If the collateral value decreases and the position becomes unsafe during this process, the entire transaction is reverted.
As deposits do not change the loan amount and only increase the collateral, the position’s safety check is skipped during this operation.
function modifyCollateralAndDebt( address owner, address collateralizer, address creditor, int256 deltaCollateral, int256 deltaDebt ) public { // ... uint256 profit; int256 quotaRevenueChange; if (deltaDebt > 0) {... } else if (deltaDebt < 0) {... } else { newDebt = position.debt; newCumulativeIndex = debtData.cumulativeIndexLastUpdate; } // take collateral token from collateralizer if (deltaCollateral > 0) { uint256 amount = wmul(deltaCollateral.toUint256(), tokenScale); >> token.safeTransferFrom(collateralizer, address(this), amount); } else if (deltaCollateral < 0) {... } >> position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, deltaCollateral, totalDebt); VaultConfig memory config = vaultConfig; uint256 spotPrice_ = spotPrice(); uint256 collateralValue = wmul(position.collateral, spotPrice_); if ( (deltaDebt > 0 || deltaCollateral < 0) && !_isCollateralized(calcTotalDebt(_calcDebt(position)), collateralValue, config.liquidationRatio) ) revert CDPVault__modifyCollateralAndDebt_notSafe(); if (quotaRevenueChange != 0) {... } emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt); }
Solidity
복사
2024-10-loopfi/src/CDPVault.sol : modifyCollateralAndDebt#L449

update position and rewards

_modifyPosition is the only function that is able to modify position data in the entire LoopFi system. It also settles rewards for dLP Lockers and yields accrued from collateral. Within _modifyPosition, the collateral balance of the position is increased by the deposited amount, and all position data, including indices and principal, are updated. However, during a deposit operation, only the collateral balance is affected.
The rewardController handles rewards for dLP Lockers, while the rewardManager manages yields from collateral. Rewards for dLP Lockers are distributed in LOOP tokens, while yields from collateral include PENDLE (the native token of pendleMarket) and other tokens, as yields can consist of multiple token types.
function _modifyPosition( address owner, Position memory position, uint256 newDebt, uint256 newCumulativeIndex, int256 deltaCollateral, uint256 totalDebt_ ) internal returns (Position memory) { uint256 currentDebt = position.debt; uint256 collateralBefore = position.collateral; >> position.collateral = add(position.collateral, deltaCollateral); // add new collateral position.debt = newDebt; // U:[CM-10,11] position.cumulativeIndexLastUpdate = newCumulativeIndex; // U:[CM-10,11] position.lastDebtUpdate = block.timestamp; // U:[CM-10,11] // position either has no debt or more debt than the debt floor if (position.debt != 0 && position.debt < uint256(vaultConfig.debtFloor)) revert CDPVault__modifyPosition_debtFloor(); // store the position's balances >> positions[owner] = position; if (newDebt > currentDebt) {... } else {... } totalDebt = totalDebt_; if (address(rewardController) != address(0)) { >> rewardController.handleActionAfter(owner, position.debt, totalDebt_); // update dLP reward } >> if (address(rewardManager) != address(0)) _handleTokenRewards(owner, collateralBefore, deltaCollateral); // update collateral yield emit ModifyPosition(owner, position.debt, position.collateral, totalDebt_); return position; }
Solidity
복사
2024-10-loopfi/src/CDPVault.sol

Rewards for dLP Locker

The rewardController.handleActionAfter function ensures that only eligible position owners receive rewards as dLP Lockers. The value of dLP owned by the owner and the required dLP lock amount (5% of the owner’s total position loan size) are converted to USD for comparison.
Users receive rewards proportional to the current loan principal in the Vault, multiplied by their dLP share. The rewardDebt represents the rewards already applied to the previous loan principal. It is subtracted from the total rewards, ensuring that only the rewards corresponding to the increase in loan principal are reflected in the pending rewards.
if (amount != 0) { uint256 pending = (amount * accRewardPerShare) / ACC_REWARD_PRECISION - user.rewardDebt; if (pending != 0) { userBaseClaimable[_user] = userBaseClaimable[_user] + pending; } }
Solidity
복사
2024-10-loopfi/src/reward/ChefIncentivesController.sol : _handleActionAfterForToken#L642
The accumulated rewards are stored in userBaseClaimable and can be claimed at any time to be vested in the MultiFeeDistribution contract. After the vesting period ends, the rewards are distributed to the user.

Reward for collateral

The rewardManager._handleTokenRewards function calculates the rewards accrued on deposited collateral tokens. These rewards are settled when the collateral is withdrawn. Essentially, while referred to as rewards, they represent the yield generated by the collateral.
Since the tokens accumulated in the Vault are PendleLP, rewards for these LP tokens are first claimed from the PendleMarket into the Vault. The rewards accumulated in the Vault are then distributed proportionally based on Vault shares.
The Vault receives its reward tokens through the redeemRewards function in the PendleMarket.
function _updateRewardIndex() internal virtual override returns (address[] memory tokens, uint256[] memory indexes) { // ... uint256 totalShares = _rewardSharesTotal(); // The sum of vault position owners's share(amount of collateral == share) // Claim external rewards on Market market.redeemRewards(address(vault)); // receive reward from pendleMarket // .. }
Solidity
복사
2024-10-loopfi/src/pendle-rewards/RewardManager.sol
A user's share of the Vault is not stored in a specific variable. Instead, their potential rewards are determined using the index and rewardAccrued fields in the UserReward object. The index represents the reward amount per user share, while rewardAccrued reflects the total rewards accumulated for the user.
function _distributeRewardsPrivate( address user, uint256 collateralAmountBefore, address[] memory tokens, uint256[] memory indexes ) private { assert(user != address(0) && user != address(this)); // uint256 userShares = _rewardSharesUser(user); uint256 userShares = collateralAmountBefore; for (uint256 i = 0; i < tokens.length; ++i) { address token = tokens[i]; uint256 index = indexes[i]; uint256 userIndex = userReward[token][user].index; if (userIndex == 0) { userIndex = INITIAL_REWARD_INDEX.Uint128(); } if (userIndex == index) continue; uint256 deltaIndex = index - userIndex; // newly accrued user share uint256 rewardDelta = userShares.mulDown(deltaIndex); // reward for increased share >> uint256 rewardAccrued = userReward[token][user].accrued + rewardDelta; >> userReward[token][user] = UserReward({index: index.Uint128(), accrued: rewardAccrued.Uint128()}); } }
Solidity
복사
2024-10-loopfi/src/pendle-rewards/RewardManagerAbstract.sol

Withdrawing Collateral

Withdrawing the desired amount of collateral from the Vault reduces the user’s collateral balance.
When a user calls withdraw via PositionAction, the CDPVault.withdraw function is internally invoked. This function transfers the specified amount of collateral from the Vault to the collateralizer. If collateralParams are provided, the collateral token is swapped for the token the user wants to receive.
if (collateralParams.auxSwap.assetIn != address(0)) { SwapParams memory auxSwap = collateralParams.auxSwap; auxSwap.amount = collateral; _delegateCall( address(swapAction), abi.encodeWithSelector(swapAction.swap.selector, auxSwap) );
Solidity
복사
2024-10-loopfi/src/proxy/PositionAction.sol : _withdraw#L578
Since it is a withdrawal, the deltaCollateral is negative.
function withdraw(address to, uint256 amount) external returns (uint256 tokenAmount) { tokenAmount = wdiv(amount, tokenScale); >> int256 deltaCollateral = -toInt256(tokenAmount); modifyCollateralAndDebt({ owner: to, collateralizer: msg.sender, creditor: msg.sender, >> deltaCollateral: deltaCollateral, deltaDebt: 0 }); }
Solidity
복사
2024-10-loopfi//src/CDPVault.sol

Position Modification Authorization Check

To withdraw collateral, the caller must have the position owner's permission, and the Vault must not be paused. If these conditions are met, the position’s debt data is updated to reflect the current time. The updated data is cached in newDebt and newCumulativeIndex.

Position Health Check

Most lending systems require the collateral value to be sufficient to maintain the loan position. Similarly, in LoopFi, the product of the "liquidation ratio" (the minimum ratio required to avoid liquidation) and the collateral price must not fall below the debt amount.
The _isCollateralized function ensures that the updated position satisfies this condition. If the collateral value decreases or an excessive amount of collateral is withdrawn, making the position unsafe, the transaction is reverted.
function modifyCollateralAndDebt( address owner, address collateralizer, address creditor, int256 deltaCollateral, int256 deltaDebt ) public { if ( // position is either more safe than before or msg.sender has the permission from the owner >> ((deltaDebt > 0 || deltaCollateral < 0) && !hasPermission(owner, msg.sender)) || // ... ) revert CDPVault__modifyCollateralAndDebt_noPermission(); // if the vault is paused allow only debt decreases if (deltaDebt > 0 || deltaCollateral != 0){ _requireNotPaused(); } Position memory position = positions[owner]; >> DebtData memory debtData = _calcDebt(position); uint256 newDebt; uint256 newCumulativeIndex; uint256 profit; int256 quotaRevenueChange; if (deltaDebt > 0) {... } else if (deltaDebt < 0) {... } else { newDebt = position.debt; newCumulativeIndex = debtData.cumulativeIndexLastUpdate; } if (deltaCollateral > 0) {... } else if (deltaCollateral < 0) { uint256 amount = wmul(abs(deltaCollateral), tokenScale); >> token.safeTransfer(collateralizer, amount); } position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, deltaCollateral, totalDebt); VaultConfig memory config = vaultConfig; uint256 spotPrice_ = spotPrice(); uint256 collateralValue = wmul(position.collateral, spotPrice_); if ( (deltaDebt > 0 || deltaCollateral < 0) && >> !_isCollateralized(calcTotalDebt(_calcDebt(position)), collateralValue, config.liquidationRatio) ) revert CDPVault__modifyCollateralAndDebt_notSafe(); if (quotaRevenueChange != 0) { int256 scaledQuotaRevenueChange = wmul(poolUnderlyingScale, quotaRevenueChange); IPoolV3(pool).updateQuotaRevenue(scaledQuotaRevenueChange); } emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt); }
Solidity
복사
2024-10-loopfi//src/CDPVault.sol

Reward Settlement

When a user withdraws collateral, the rewards accumulated on the collateral are settled and paid out to the user. The handleRewardsOnWithdraw hook in the Reward Manager updates the user's rewards up to the current point and transfers the accumulated rewards to the user.
function _handleTokenRewards(address owner, uint256 collateralAmountBefore, int256 deltaCollateral) internal { if (deltaCollateral > 0) {... } else if (deltaCollateral < 0) { (address[] memory tokens, uint256[] memory rewardAmounts, address to) = rewardManager .handleRewardsOnWithdraw(owner, collateralAmountBefore, deltaCollateral); for (uint256 i = 0; i < tokens.length; i++) { if (rewardAmounts[i] != 0) { >> IERC20(tokens[i]).safeTransfer(to, rewardAmounts[i]); } } } } function handleRewardsOnWithdraw( address user, uint collateralAmountBefore, int256 deltaCollateral ) external virtual onlyVault returns (address[] memory tokens, uint256[] memory amounts, address to) { >> _updateAndDistributeRewards(user, collateralAmountBefore, deltaCollateral); return _doTransferOutRewards(user); }
Solidity
복사
2024-10-loopfi/src/pendle-rewards/RewardManager.sol : _handleTokenRewards, handleRewardsOnWithdraw
Before distributing rewards to the user, the Vault first claims rewards from the PendleMarket. It then updates the user's reward information for each token by deducting the withdrawn collateral from the total collateral stored in the Vault. The rewards claimed by the Vault are distributed to the user proportionally based on their Vault share (collateral deposit amount).
if (deltaCollateral > 0) _totalShares = _totalShares + deltaCollateral.toUint256(); else _totalShares = _totalShares - (-deltaCollateral).toUint256();
Solidity
복사
2024-10-loopfi/src/pendle-rewards/RewardManagerAbstract.sol : _updateAndDistributeRewards#L39
While retrieving the user’s reward data, the user's accumulated rewards are reset to zero, and the reward amount to be paid is deducted from the Vault's reward token balance. The return value includes the type and amount of reward tokens distributed, aswell as the receiver information.
When a user withdraws via the ActionContract, the to address is set to the proxy contract's owner to ensure that the reward tokens are sent to the actual user who initiated the withdraw call.
function _doTransferOutRewards( address user ) internal virtual override returns (address[] memory tokens, uint256[] memory rewardAmounts, address to) { tokens = market.getRewardTokens(); rewardAmounts = new uint256[](tokens.length); for (uint256 i = 0; i < tokens.length; i++) { rewardAmounts[i] = userReward[tokens[i]][user].accrued; if (rewardAmounts[i] != 0) { >> userReward[tokens[i]][user].accrued = 0; >> rewardState[tokens[i]].lastBalance -= rewardAmounts[i].Uint128(); //_transferOut(tokens[i], receiver, rewardAmounts[i]); } } if (proxyRegistry.isProxy(user)) { >> to = IPRBProxy(user).owner(); } else { to = user; } return (tokens, rewardAmounts, to); }
Solidity
복사
2024-10-loopfi/src/pendle-rewards/RewardManager.sol : _doTransferOutRewards

Borrowing

The functionality allows users to borrow WETH from the pool using PendleLP tokens as collateral. In LoopFi, a "pool" refers to a contract that aggregates funds in a single location to provide liquidity and related functionality. It is not the typical swap pool with two types of assets but rather a single-asset pool containing only one type of asset. Since this pool lends ETH, the underlying asset is WETH.
Since this function isn’t affected by the token type, the borrow function directly calls modifyCollateralAndDebt in the Vault instead of invoking the Vault’s borrow function through a hook. The Vault's borrow function normally converts the user-specified amount to match the precision of the pool's underlying token. However, this conversion is skipped in the Action Contract’s borrow function. Therefore, the user must ensure the input amount is correctly adjusted for precision.
Both the collateralizer and borrower addresses are set to address(this), which refers to the proxy contract's address since all functions interacting with the Vault from the Action Contract are executed via delegatecall from the proxy.
If the assetIn parameter in the swap configuration specifies an asset other than the underlying token, it indicates the user wants to receive a different asset. In this case, the swapAction converts the borrowed amount into the user-specified asset before transferring it.
function _borrow(address vault, address position, CreditParams calldata creditParams) internal { ICDPVault(vault).modifyCollateralAndDebt( position, // owner >> address(this), // collateralizer >> address(this), // borrower 0, // deltaCollateral toInt256(creditParams.amount) ); if (creditParams.auxSwap.assetIn == address(0)) { underlyingToken.forceApprove(address(this), creditParams.amount); >> underlyingToken.safeTransferFrom(address(this), creditParams.creditor, creditParams.amount); } else { // handle exit swap if (creditParams.auxSwap.assetIn != address(underlyingToken)) { revert PositionAction__borrow_InvalidAuxSwap(); } >> _delegateCall(address(swapAction), abi.encodeWithSelector(swapAction.swap.selector, creditParams.auxSwap)); } }
Solidity
복사
2024-10-loopfi/proxy/PositionAction.sol

Position Modification Authorization Check

The account attempting to execute a loan must have the position owner's permission. If the sender has the appropriate authorization, the new debt is calculated using the current loan principal, the cumulative indices accrued during the period, and the additional loan amount. Since this logic involves three separate debt-related data points, let's clarify their distinctions:
position.debt: The last recorded loan principal. Any changes to the loan principal through the loan process are ultimately written here. At this stage, it does not reflect the new loan amount.
debtData: The loan information, including cumulative indexes and interest accrued since the last update.
newDebt, newCumulativeIndex: The updated loan principal and cumulative indexes after applying the new loan amount. These values have not yet been applied to the position.
From these, the newly calculated quota interest and index in debtData are pre-applied to the position to compute the increased pool earnings. However, the cumulative interest and index applied to the principal are not updated here. These updates occur later in _modifyPosition alongside the final loan data.
As the loan amount increases, the interest accruing to the pool also rises. The additional pool earnings are calculated as increased loan amount * quota interest rate, and the loan is then executed from the pool.
if (deltaDebt > 0) { uint256 debtToIncrease = uint256(deltaDebt); // Internal debt calculation remains in 18-decimal precision (newDebt, newCumulativeIndex) = CreditLogic.calcIncrease( debtToIncrease, position.debt, debtData.cumulativeIndexNow, position.cumulativeIndexLastUpdate ); position.cumulativeQuotaInterest = debtData.cumulativeQuotaInterest; position.cumulativeQuotaIndexLU = debtData.cumulativeQuotaIndexNow; >> quotaRevenueChange = _calcQuotaRevenueChange(deltaDebt); uint256 scaledDebtIncrease = wmul(debtToIncrease, poolUnderlyingScale); >> pool.lendCreditAccount(scaledDebtIncrease, creditor); }
Solidity
복사
2024-10-loopfi/src/CDPVault.sol : modifyCollateralAndDebt#L451

Loan Execution

The pool keeps track of the amount of WETH borrowed by users for each Vault and the total amount of WETH borrowed accross all Vaults.
The WETH borrowed by a user is stored in the _creditManagerDebt mapping under the user's address, while the total WETH borrowed from the pool is recorded in _totalDebt.borrowed. Loan execution is handled by the lendCreditAccount function in the pool, which can only be called by addresses registered as CreditManagers. These registered addresses include Vault contracts and the FlashLender contract that provides FlashLoan services.
The lendCreditAccount function increases the user's debt (_creditManagerDebt[msg.sender]) and the total borrowed amount (_totalDebt.borrowed) by the loan amount. Since cmDebt is declared as a storage variable, modifying it directly affects _creditManagerDebt[msg.sender]. The function then transfers WETH to the creditor.
If the loan is executed through the Action Contract, the creditor is the user's proxy. In this case, WETH is first sent to the proxy, which then forwards it to the user.
function lendCreditAccount( uint256 borrowedAmount, address creditAccount ) external override creditManagerOnly // U:[LP-2C] whenNotPaused // U:[LP-2A] nonReentrant // U:[LP-2B] { uint128 borrowedAmountU128 = borrowedAmount.toUint128(); DebtParams storage cmDebt = _creditManagerDebt[msg.sender]; >> uint128 totalBorrowed_ = _totalDebt.borrowed + borrowedAmountU128; >> uint128 cmBorrowed_ = cmDebt.borrowed + borrowedAmountU128; if (borrowedAmount == 0 || cmBorrowed_ > cmDebt.limit || totalBorrowed_ > _totalDebt.limit) { revert CreditManagerCantBorrowException(); // U:[LP-2C,13A] } _updateBaseInterest({ expectedLiquidityDelta: 0, availableLiquidityDelta: -borrowedAmount.toInt256(), checkOptimalBorrowing: true }); // U:[LP-13B] >> cmDebt.borrowed = cmBorrowed_; // U:[LP-13B] >> _totalDebt.borrowed = totalBorrowed_; // U:[LP-13B] >> IERC20(underlyingToken).safeTransfer({to: creditAccount, value: borrowedAmount}); // U:[LP-13B] emit Borrow(msg.sender, creditAccount, borrowedAmount); // U:[LP-13B] }
Solidity
복사
2024-10-loopfi/src/PoolV3.sol : lendCreditAccount

Minimum Loan Amount

LoopFi ensures that users do not borrow excessively small amounts by requiring the loan principal of a position to be at least equal to the minimum loan amount, debtFloor. If this condition is not met, the transaction is reverted.
function _modifyPosition( address owner, Position memory position, uint256 newDebt, uint256 newCumulativeIndex, int256 deltaCollateral, uint256 totalDebt_ ) internal returns (Position memory) { uint256 currentDebt = position.debt; uint256 collateralBefore = position.collateral; // update collateral and debt amounts by the deltas position.collateral = add(position.collateral, deltaCollateral); position.debt = newDebt; // U:[CM-10,11] position.cumulativeIndexLastUpdate = newCumulativeIndex; // U:[CM-10,11] position.lastDebtUpdate = block.timestamp; // U:[CM-10,11] // position either has no debt or more debt than the debt floor >> if (position.debt != 0 && position.debt < uint256(vaultConfig.debtFloor)) revert CDPVault__modifyPosition_debtFloor(); // store the position's balances positions[owner] = position; // update the global debt balance >> if (newDebt > currentDebt) { totalDebt_ = totalDebt_ + (newDebt - currentDebt); } else {... } totalDebt = totalDebt_; if (address(rewardController) != address(0)) { rewardController.handleActionAfter(owner, position.debt, totalDebt_); } if (address(rewardManager) != address(0)) _handleTokenRewards(owner, collateralBefore, deltaCollateral); emit ModifyPosition(owner, position.debt, position.collateral, totalDebt_); return position; }
Solidity
복사
2024-10-loopfi/src/CDPVault.sol
Now, the total ETH borrowed by all users in the Vault, represented as totalDebt, is increased by the newly borrowed amount.
The detailed process for reward settlement has already been described earlier and will not be repeated here.

Pool Revenue Update

Since all changes, including the loan amount, interest, and rewards, have been reflected in the position, the collateral value is checked one final time before updating the increased pool revenue.
newQuotaRevenue is calculated by adding the newly increased pool revenue, scaledQuotaRevenueChange, to the previous revenue.
function _setQuotaRevenue(uint256 newQuotaRevenue) internal { uint256 timestampLU = lastQuotaRevenueUpdate; if (block.timestamp != timestampLU) { _expectedLiquidityLU += _calcQuotaRevenueAccrued(timestampLU).toUint128(); // U:[LP-20] lastQuotaRevenueUpdate = uint40(block.timestamp); // U:[LP-20] } _quotaRevenue = newQuotaRevenue.toUint96(); // U:[LP-20] }
Solidity
복사
2024-10-loopfi/src/PoolV3.sol : _setQuotaRevenue

Repaying

The final step!
This functionality allows users to repay their debt by the desired amount, updating the position's information (debt and reward details) and the pool's revenue accordingly.
Just as collateral deposits allow both the Vault's underlying token and other tokens, debt repayment does not necessarily require WETH. If the user repays with a token other than WETH, PositionAction uses the SwapAction contract to swap the asset sent by the creditor into WETH before proceeding with the repayment.
Even when WETH is sent, if the creditor is not the user's proxy contract but the user's own address (EOA) or another address, the token is first transferred from that address to the proxy before repayment. Since repayment reduces the debt, the deltaDebt is equal to -amount.
/// @notice Repays debt by redeeming underlying token and optionally swaps an arbitrary token to underlying token /// @param vault The CDP Vault /// @param creditParams The credit parameters /// @param permitParams The permit parameters function _repay( address vault, address position, CreditParams calldata creditParams, PermitParams calldata permitParams ) internal { // transfer arbitrary token and swap to underlying token uint256 amount = creditParams.amount; >> if (creditParams.auxSwap.assetIn != address(0)) { // WETH가 아닌 다른 자산을 보내 swap이 필요할 때 if (creditParams.auxSwap.recipient != address(this)) revert PositionAction__repay_InvalidAuxSwap(); amount = _transferAndSwap(creditParams.creditor, creditParams.auxSwap, permitParams); >> } else {// if creditor is not proxy if (creditParams.creditor != address(this)) { // transfer directly from creditor >> _transferFrom( address(underlyingToken), creditParams.creditor, // receive WETH from the creditor address(this), creditParams.amount, permitParams ); } } underlyingToken.forceApprove(address(vault), amount); ICDPVault(vault).modifyCollateralAndDebt( position, address(this), address(this), 0, -toInt256(amount) ); }
Solidity
복사
2024-10-loopfi/src/proxy/PositionAction.sol : _repay

Position Modification Authorization Check

For repayment, the creditor’s permissions is required, rather than the position owner's or the collateralizer's. This ensures that even if a user is managing the position or depositing collateral, they cannot repay the borrowed ETH without the explicit consent of the account that actually borrowed it. Additionally, repayments are allowed even if the Vault is paused.
if{ // ... (deltaDebt < 0 && !hasPermission(creditor, msg.sender)) } revert CDPVault__modifyCollateralAndDebt_noPermission();
Solidity
복사
2024-10-loopfi/src/CDPVault.sol : modifyCollateralAndDebt#L435

Debt Repayment

If the repayment amount exceeds the total debt (the sum of the loan principal and accrued interest), it is adjusted to match the total debt, ensuring no excess amount is sent beyond the position's total liability. The debt amount, converted to WETH precision, is transferred to the pool from the creditor via safeTransferFrom. If the transfer fails, the transaction reverts.
Once WETH is successfully transferred to the pool, the remaining debt and pool earnings are calculated based on whether the entire debt was repaid or only a portion:
1.
Full Repayment:
Remaining debt and interest amounts are set to 0.
Pool earnings equal the total interest repaid by the creditor.
The cumulative index is not reset to 1 but retains the current cumulative value.
2.
Partial Repayment:
Remaining debt is updated, and interest applicable to the remaining debt is recalculated.
The new cumulative index is computed to reflect the updated debt until the next update.
We will take a closer look at how these values are calculated shortly.
By repaying the debt, the debt amount decreases, leading to a reduction in the pool's revenue. If the debt is fully repaid, debtData.debt - newDebt equals 0, making quotaRevenueChange (calculated as debt principal * interest rate) also 0.
else if (deltaDebt < 0) { uint256 debtToDecrease = abs(deltaDebt); uint256 maxRepayment = calcTotalDebt(debtData); // debt principal + total interest >> if (debtToDecrease >= maxRepayment) { debtToDecrease = maxRepayment; deltaDebt = -toInt256(debtToDecrease); } uint256 scaledDebtDecrease = wmul(debtToDecrease, poolUnderlyingScale); >> poolUnderlying.safeTransferFrom(creditor, address(pool), scaledDebtDecrease); uint128 newCumulativeQuotaInterest; >> if (debtToDecrease == maxRepayment) { // when repay all debt newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedInterest; newCumulativeQuotaInterest = 0; >> } else { // when repay partial of debt (newDebt, newCumulativeIndex, profit, newCumulativeQuotaInterest) = calcDecrease( debtToDecrease, position.debt, debtData.cumulativeIndexNow, position.cumulativeIndexLastUpdate, debtData.cumulativeQuotaInterest ); } >> quotaRevenueChange = _calcQuotaRevenueChange(-int(debtData.debt - newDebt)); uint256 scaledRemainingDebt = wmul(debtData.debt - newDebt, poolUnderlyingScale); // amount of repaied debt >> pool.repayCreditAccount(scaledRemainingDebt, profit, 0); position.cumulativeQuotaInterest = newCumulativeQuotaInterest; // all repaied -> 0 position.cumulativeQuotaIndexLU = debtData.cumulativeQuotaIndexNow; }
Solidity
복사
2024-10-loopfi/src/CDPVault.sol : modifyCollateralAndDebt#L469

Interest and Pool Revenue Calculation

Let’s examine how new interest and pool revenue are calculated as debt decreases. By the time calcDecrease is called, the loan repayment amount from the creditor has already been transferred to the pool.
This function adjusts the debt by applying the repayment amount and calculates the remaining debt and updated indices. It prioritizes repaying accrued quota interest and principal interest before reducing the principal itself.
Quota Interest Repayment
If there is quota interest and the repayment amount (amountToRepay) is sufficient:
The accumulated quota interest is added to the pool's profit (profit), and the quota interest is reset to 0.
If any repayment amount remains after clearing the quota interest, it is applied to repay principal interest.
If the repayment amount is insufficient to cover the quota interest:
The repayment amount is fully applied to reduce the quota interest, and the remaining quota interest is calculated.
Since all the repayment amount has been used, amountToRepay is reset to 0.
If there is no quota interest or the repayment amount is 0:
The newCumulativeQuotaInterest remains unchanged, retaining either its previous value or 0.
Principal Interest Repayment
If quota interest has been fully repaid and repayment amount remains:
The newly accrued interest from the last index update to the current time is calculated.
If the remaining repayment amount is sufficient to cover this accrued interest:
The entire accrued interest is added to the pool's profit, and the cumulative index is updated to the current value.
If the repayment amount is insufficient:
It is fully used to partially repay the accrued interest, and amountToRepay is reset to 0.
The repaid portion is added to the pool’s profit, and a new cumulative index is calculated based on the remaining debt and interest rate changes.
New Cumulative Index Calculation
The updated cumulative index considers the ratio of the repaid principal to the remaining principal and adjusts accordingly. The formula can be summarized as:
CI2=CI1DD(1+rg)ΔDCI_2 =\frac{CI_1\cdot D}{ D(1+rg) - \Delta D}
CI2CI_2 - New Cumulative Index
CI1CI_1 - Current Cumulative Index
DD - Loan principal
ΔD\Delta D - Repaid amount
rgrg - Interest Rate * Interest Increment Weight
This ensures that the new cumulative index accurately reflects the repayment and the updated debt.
Principal Repayment
After fully repaying all the interest, if amountToRepay is still greater than 0, the remaining amount is used to repay the principal. newDebt then represents the remaining loan principal after repayment.
function calcDecrease( uint256 amount, uint256 debt, uint256 cumulativeIndexNow, uint256 cumulativeIndexLastUpdate, uint128 cumulativeQuotaInterest ) internal pure returns (uint256 newDebt, uint256 newCumulativeIndex, uint256 profit, uint128 newCumulativeQuotaInterest) { uint256 amountToRepay = amount; if (cumulativeQuotaInterest != 0 && amountToRepay != 0) { // All interest accrued on the quota interest is taken by the DAO to be distributed to LP stakers, dLP stakers and the DAO // When repaied amount is enough to remove all the accrued Quota Interest, change QuotaInterest to 0 >> if (amountToRepay >= cumulativeQuotaInterest) { amountToRepay -= cumulativeQuotaInterest; // U:[CL-3] profit += cumulativeQuotaInterest; // U:[CL-3] newCumulativeQuotaInterest = 0; // U:[CL-3] >> } else { // If amount is not enough to repay quota interest + DAO fee, then send all to the stakers uint256 quotaInterestPaid = amountToRepay; // U:[CL-3] profit += amountToRepay; // U:[CL-3] >> amountToRepay = 0; // U:[CL-3] >> newCumulativeQuotaInterest = uint128(cumulativeQuotaInterest - quotaInterestPaid); // U:[CL-3] } } else { // when QuotaInterest is 0 or repaied amount is 0 newCumulativeQuotaInterest = cumulativeQuotaInterest; } // When amount is still left after repaying QuotaInterest, repay interest for debt principal if (amountToRepay != 0) { uint256 interestAccrued = CreditLogic.calcAccruedInterest({ amount: debt, cumulativeIndexLastUpdate: cumulativeIndexLastUpdate, cumulativeIndexNow: cumulativeIndexNow }); // All interest accrued on the base interest is taken by the DAO to be distributed to LP stakers, dLP stakers and the DAO if (amountToRepay >= interestAccrued) { >> amountToRepay -= interestAccrued; >> profit += interestAccrued; newCumulativeIndex = cumulativeIndexNow; } else { // If amount is not enough to repay interest, then send all to the stakers and update index >> profit += amountToRepay; // U:[CL-3] >> newCumulativeIndex = (INDEX_PRECISION * cumulativeIndexNow * cumulativeIndexLastUpdate) / (INDEX_PRECISION * cumulativeIndexNow - (INDEX_PRECISION * amountToRepay * cumulativeIndexLastUpdate) / debt); // U:[CL-3] >> amountToRepay = 0; // U:[CL-3] } } else { newCumulativeIndex = cumulativeIndexLastUpdate; } >> newDebt = debt - amountToRepay; // debt principal - remained amount after repaying interests }
Solidity
복사
2024-10-loopfi/src/CDPVault.sol
To summarize of calcDecrease results,
newDebt: The remaining principal after repayment.
If only interest is repaid, it remains the same as before.
If fully repaid, it becomes 0.
If partially repaid, it is calculated as (principal - repaid amount).
newCumulativeIndex: Updated after principal interest repayment.
If fully repaid, it remains unchanged.
If partially repaid, it is recalculated to reflect the repaid amount.
profit: The repaid interest amount.
This can include quota interest or a combination of quota interest and principal interest.
If only a portion of the interest is repaid, it reflects the partial repayment amount.
newCumulativeQuotaInterest: The updated quota interest.
If fully repaid, it is reset to 0.
If partially repaid, it reflects the remaining quota interest.

Pool Profit Settlement

What happens in the pool when debt is repaid? As mentioned earlier, the pool tracks the total WETH borrowed across all Vaults and the individual debts of users within each Vault. When debt is repaid, these values decrease accordingly.
Additionally, the profit (interest) earned by the pool generates new shares for the treasury. The treasury, which inherits from OpenZeppelin's PaymentSplitter, distributes pool profits to a predefined list of accounts based on their share ownership.
If the collateral value drops and the position is liquidated, the pool incurs a loss, represented as loss. In such cases, treasury shares are burned to offset the loss to minimize its impact. The pool's base interest rate is then adjusted based on the estimated liquidity after deducting the loss. However, in a normal repayment scenario, loss is 0, so the base interest rate remains unchanged.
function repayCreditAccount( uint256 repaidAmount, uint256 profit, uint256 loss ) external override creditManagerOnly // U:[LP-2C] whenNotPaused // U:[LP-2A] nonReentrant // U:[LP-2B] { uint128 repaidAmountU128 = repaidAmount.toUint128(); DebtParams storage cmDebt = _creditManagerDebt[msg.sender]; uint128 cmBorrowed = cmDebt.borrowed; if (cmBorrowed == 0) { revert CallerNotCreditManagerException(); // U:[LP-2C,14A] } if (profit > 0) { >> _mint(treasury, _convertToShares(profit)); // U:[LP-14B] } else if (loss > 0) { // when repay, 0 address treasury_ = treasury; uint256 sharesInTreasury = balanceOf(treasury_); uint256 sharesToBurn = _convertToShares(loss); if (sharesToBurn > sharesInTreasury) { unchecked { emit IncurUncoveredLoss({ creditManager: msg.sender, loss: _convertToAssets(sharesToBurn - sharesInTreasury) }); // U:[LP-14D]` } sharesToBurn = sharesInTreasury; } >> _burn(treasury_, sharesToBurn); // U:[LP-14C,14D] } _updateBaseInterest({ >> expectedLiquidityDelta: -loss.toInt256(), // when repay, 0 availableLiquidityDelta: 0, checkOptimalBorrowing: false }); // U:[LP-14B,14C,14D] _totalDebt.borrowed -= repaidAmountU128; // U:[LP-14B,14C,14D] cmDebt.borrowed = cmBorrowed - repaidAmountU128; // U:[LP-14B,14C,14D] emit Repay(msg.sender, repaidAmount, profit, loss); // U:[LP-14B,14C,14D] }
Solidity
복사
2024-10-loopfi/src/PoolV3.sol

Position and Reward Update

At this stage, _modifyPosition updates the new loan principal and cumulative index while reflecting the debt changes in totalDebt. If the new loan principal has decreased compared to the previous amount, the reduced amount is subtracted from the Vault's total loan principal.
if (newDebt > currentDebt) { totalDebt_ = totalDebt_ + (newDebt - currentDebt); } else { totalDebt_ = totalDebt_ - (currentDebt - newDebt); }
Solidity
복사
2024-10-loopfi/src/CDPVault.sol : _modifyPosition#L381
The remaining process involves updating the debt discount rewards and the rewards accumulated on the collateral, as previously discussed.
When the loan amount decreases, the amount required to qualify for discount rewards (5% of the position) also decreases. This means that a user who previously did not meet the reward eligibility criteria might become eligible after repaying part of their debt.
Once the position update is complete, the final step is to reflect the reduced pool revenue (quota interest) in the pool.

Conclusion

In this article, we explored the Vault and Pool, which are most closely related to LoopFi's Leveraged Lending service. In the next article, we will take a closer look at the various rewards that LoopFi participants can receive.