PLP Liquidation Instructions
Last updated
Last updated
Basic Liquidation Instructions
Fringe Finance
Primary Lending Platform
Table of contents
Introduction 2
Example of a liquidation bot 2
Reference information 4
Project tokens 4
Lending token 4
Environments 4
Liquidation Operation 5
Liquidation query endpoint 5
How to trigger the liquidation transaction 6
Appendix: Contract interfaces 11
IPrimaryIndexToken.sol: 11
EIP20Interface.sol: 17
The purpose of this document is to provide basic instructions on how to liquidate loan positions on the Primary Lending Platform that are below the minimum collateralization levels.
This document provides liquidation instructions for both the Rinkeby test network (Fringe’s Stage environment) and the Ethereum mainnet.
You can use these basic instructions as a basis for constructing your own liquidation bot.
For an example of a liquidation bot, please refer to the neat open-source code provided by Corey from Dolomite Exchange here.
Note: Corey’s liquidation bot disposes of collateral won in a liquidation in a proprietary manner and hence he does not open-source his disposal logic. Therefore you will need to write your own collateral disposal smart contract.
The liquidation functionality is to keep the platform healthy, overcollateralized and solvent.
A position becomes subject to liquidation when its health factor (HF) falls below 1.
HF = (pit balance) / (total outstanding)
Where PIT balance is the total borrowing capacity of the collateral. This is equal to the collateral’s prevailing market price multiplied by its loan-to-value ratio.
The position is liquidated in full. i.e. currently partial liquidations are not supported.
Conceptually, a liquidation involves the following:
Liquidator pays back the outstanding loan position.
Currently, loans are issued in USDC.
The liquidator receives collateral to the value of the amount paid back PLUS liquidator reward percentage.
The liquidator reward percentage can be set by the Fringe Finance platform as a specific percentage for each collateral asset and therefore differs for each collateral asset.
Any remaining excess collateral is retained by the borrower.
Operationally, a liquidation entails the following steps:
Identifying a position subject to liquidation - using the liquidation query end point.
Approving spend of liquidator’s stablecoin assets to repay/liquidate the loan - using the EIP20INTERFACE.approve() smart contract method.
Invoking the IPRIMARYINDEXTOKEN.liquidate() method to perform the liquidation.
The liquidator pays back the stablecoin for the loan position of an amount equivalent to the loan position’s Total Outstanding Amount (which includes any loan amount advanced plus accrued interest.)
The stablecoin provided by the liquidator goes directly to the platform’s operational wallet for the affected capital pool.
For testing purposes, these tokens are used as collateral on the Rinkeby test network for loan positions. The liquidator will receive these tokens in return for liquidating a loan position.
prj1Address = 0x40EA2e5c5b2104124944282d8db39C5D13ac6770
prj2Address = 0x69648Ef43B7496B1582E900569cd9dDEc49C045e
prj3Address = 0xfA91A86700508806AD2A49Bebce34a08c6ad7a65
prj4Address = 0xc6636b088AB0f794DDfc1204e7C58D8148f62203
prj5Address = 0x37a7D483d2dfe97d0C00cEf6F257e25d321e6D4e
prj6Address = 0x16E2f279A9BabD4CE133745DdA69C910CBe2e490
On mainnet, use the official address for the collateral tokens. e.g. Chainlink = 0x514910771AF9Ca656af840dff83E8264EcF986CA
The following is the USDC test token used on the Rinkeby test network as the capital asset issued as loans to borrowers.
Rinkeby: USDCtest (lending token) = 0x5236aAB9f4b49Bfd93a9500E427B042f65005E6A
On the Ethereum mainnet, the first supported capital asset is the USDC token:
Mainnet: USDC (lending token) = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
Fringe Finance Primary Lending app:
Stage (Rinkeby) : https://stage.fringe.fi
Prod (mainnet) : https://app.fringe.fi
Fringe Finance’s STAGE testing environment operates on the Rinkeby test network.
See section Contract Interfaces for the Solidity code of the contracts that are relevant to liquidations. This section makes reference to those contracts.
The main requirement of liquidation is the health factor of the position is smaller than 1. This means the position’s collateral value in relation to the total outstanding loan position is smaller than the ratio value dictated by the asset’s loan to value ratio (LVR).
The liquidation query endpoints below return a list of positions where Health Factor is below 1.2.
API - Liquidation query endpoint
For STAGE (Rinkeby) environment: https://api-stage.fringe.fi/api/v1/liquidations
For PROD (Mainnet) environment: https://api.fringe.fi/api/v1/liquidations
The endpoint returns the set of relevant positions in the following format:
…
{
"address": "205d2cb6a93acbe59f69efeed82f2f6c0c885a81",
"lendingTokenAddress": "5236aab9f4b49bfd93a9500e427b042f65005e6a",
"loanBalance": "6001.220824",
"totalOutstanding": "6003.995197",
"collateralTokenAddress": "fa91a86700508806ad2a49bebce34a08c6ad7a65",
"collateralBallance": "100.000000000000000000",
"healthFactor": "0.9995379121886396139300575792915645132219115597670255764530052804437644855764197574190697674537130380"
},
…
The key items are:
address (borrower),
lendingTokenAddress (USDCtest in our test case above)
collateralTokenAddress (one of the project tokens in our test case above)
healthFactor (HF < 1 is liquidatable. HF >=1 is not liquidatable.)
To view the heath factor of a position using the smart contracts, call the read-only method of PrimaryIndexToken.getPosition
function getPosition(address account, address projectToken, address lendingToken) external view returns (uint256 depositedProjectTokenAmount, uint256 loanBody, uint256 accrual, uint256 healthFactorNumerator, uint256 healthFactorDenominator);
If healthFactorNumerator < healthFactorDenominator (i.e. Health Factor < 1), the position is liquidatable.
The liquidation process contains two steps.
Call approve() function on EIP20Interface contract, where spender is bLendingToken and amount is how much the liquidator wants to spend scaled by 10**decimals of Lending Token (noting USDC in our example has 6 decimals). The amount must be at least the amount of the outstanding loan for the position being liquidated.
To note: *if* you plan to use the same wallet as a regular user on the Primary Lending Platform (as a lender or a borrower), we recommend you specify an amount for the approval() to be the maximum integer value (i.e. 2^256-1). The reason for this is you will be limited to USDC spending on the PLP platform up to the amount you approve(). If you intend to never use this wallet address as a regular user on the Primary Lending Platform (as a lender or a borrower), you can safely ignore this recommendation.
The EIP20Interface is just an interface to the USDC contract. This action approves the primary lending platform (bLendingToken) to spend your USDC to perform the liquidation.
Call liquidate() function on IPrimaryIndexToken contract
The following describes how to achieve this using Remix. (You may assemble your own liquidation bot that employs these methods - which is outside the scope of this document, but see here for an example.)
1.Create a file IPrimaryIndexToken.sol and paste code (See section Contract Interfaces for the Solidity code):
2. Compile it
3.Go to deploy page. Choose Injected Web3. Choose IPrimaryIndexToken.sol
4.Enter the address of the Primary Index Token (PIT) into the input and click "By Address".
PIT contract addresses:
STAGE (Rinkeby) environment: 0x2D9346C4E84f2eC4F41881BaaCc83E85F11D6519
PROD (mainnet) environment: 0x46558DA82Be1ae1955DE6d6146F8D2c1FE2f9C5E
5.Create a file EIP20Interface.sol and paste code (See section Contract Interfaces for the Solidity code) compile it and deploy it using the following address in "By Address":
Stage (Rinkeby) = 0x5236aAB9f4b49Bfd93a9500E427B042f65005E6A
Prod (mainnet) = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
6. Find the “approve” method in EIP20Interface.so and open it.
Paste the following data:
Spender: address of fUSDC (Fringe Finance’s interest-bearing token issued to lenders in return for their deposits into the capital pool):
Stage (Rinkeby): 0xCE3156761EF59D1543495B3172FE0e0946206Eb7
Prod (mainnet): 0x9fD0928A09E8661945767E75576C912023bA384D
Amount - amount of USDC to safely repay the loan * 10**6
We recommend the maximum integer value (i.e. 2^256-1) as described above.
Click on the [Transact] button and wait for the response.
You have just approved the spend of stablecoins from your wallet.
7.Go to the IPrimaryIndexToken contract and open dropdown menu
8.find the “liquidate” method and click on the expand arrow to reveal the parameters.
account - the user you want to liquidate
projectToken - the address of collateral token
lendingToken - the address of lendingToken (USDC in our example)
Press transact and wait for completion.
Liquidation should now be complete.
###
Smart contract code referenced in this document.
Contents
IPrimaryIndexToken.sol
EIP20Interface.sol
In respect of liquidations, the method IPrimaryIndexToken.liquidate() is called to perform a liquidation.
For convenience of viewing the code, below is a copy of the code, but we recommend using the latest from the Github repo linked above.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
interface IPrimaryIndexToken {
/**
* @dev return keccak("MODERATOR_ROLE")
*/
function MODERATOR_ROLE() external view returns(bytes32);
function name() external view returns(string memory);
function symbol() external view returns(string memory);
function priceOracle() external view returns(address); // address of price oracle with interface of PriceProviderAggregator
function projectTokens(uint256 projectTokenId) external view returns(address);
function projectTokenInfo(address projectToken) external view returns(ProjectTokenInfo memory);
function lendingTokens(uint256 lendingTokenId) external view returns(address);
function lendingTokenInfo(address lendingToken) external view returns(LendingTokenInfo memory);
function totalDepositedProjectToken(address projectToken) external view returns(uint256);
function depositPosition(address account, address projectToken, address lendingToken) external view returns(DepositPosition memory);
function borrowPosition(address account, address projectToken, address lendingToken) external view returns(BorrowPosition memory);
function totalBorrow(address projectToken, address lendingToken) external view returns(uint256);
function borrowLimit(address projectToken, address lendingToken) external view returns(uint256);
struct Ratio {
uint8 numerator;
uint8 denominator;
}
struct ProjectTokenInfo {
bool isListed;
bool isPaused;
Ratio loanToValueRatio;
Ratio liquidationThresholdFactor;
Ratio liquidationIncentive;
}
struct LendingTokenInfo {
bool isListed;
bool isPaused;
address bLendingToken;
}
struct DepositPosition {
uint256 depositedProjectTokenAmount;
}
struct BorrowPosition {
uint256 loanBody; // [loanBody] = lendingToken
uint256 accrual; // [accrual] = lendingToken
}
event AddPrjToken(address indexed tokenPrj);
event LoanToValueRatioSet(address indexed tokenPrj, uint8 lvrNumerator, uint8 lvrDenominator);
event LiquidationThresholdFactorSet(address indexed tokenPrj, uint8 ltfNumerator, uint8 ltfDenominator);
event Deposit(address indexed who, address indexed tokenPrj, uint256 prjDepositAmount, address indexed beneficiar);
event Withdraw(address indexed who, address indexed tokenPrj, uint256 prjWithdrawAmount, address indexed beneficiar);
event Supply(address indexed who, address indexed supplyToken, uint256 supplyAmount, address indexed supplyBToken, uint256 amountSupplyBTokenReceived);
event Redeem(address indexed who, address indexed redeemToken, address indexed redeemBToken, uint256 redeemAmount);
event RedeemUnderlying(address indexed who, address indexed redeemToken, address indexed redeemBToken, uint256 redeemAmountUnderlying);
event Borrow(address indexed who, address indexed borrowToken, uint256 borrowAmount, address indexed prjAddress, uint256 prjAmount);
event RepayBorrow(address indexed who, address indexed borrowToken, uint256 borrowAmount, address indexed prjAddress, bool isPositionFullyRepaid);
event Liquidate(address indexed liquidator, address indexed borrower, address lendingToken, address indexed prjAddress, uint256 amountPrjLiquidated);
function initialize() external;
//************* ADMIN FUNCTIONS ********************************
function addProjectToken(
address _projectToken,
bool _isPaused,
uint8 _loanToValueRatioNumerator,
uint8 _loanToValueRatioDenominator,
uint8 _liquidationTresholdFactorNumerator,
uint8 _liquidationTresholdFactorDenominator,
uint8 _liquidationIncentiveNumerator,
uint8 _liquidationIncentiveDenominator
) external;
function removeProjectToken(
uint256 _projectTokenId
) external;
function addLendingToken(
address _lendingToken,
address _bLendingToken,
bool _isPaused
) external;
function removeLendingToken(
uint256 _lendingTokenId
) external;
function setPriceOracle(
address _priceOracle
) external;
function grandModerator(
address newModerator
) external;
function revokeModerator(
address moderator
) external;
//************* MODERATOR FUNCTIONS ********************************
function setBorrowLimit(
address projectToken,
address lendingToken,
uint256 _borrowLimit
) external;
function setProjectTokenInfo(
address _projectToken,
bool _isPaused,
uint8 _loanToValueRatioNumerator,
uint8 _loanToValueRatioDenominator,
uint8 _liquidationTresholdFactorNumerator,
uint8 _liquidationTresholdFactorDenominator,
uint8 _liquidationIncentiveNumerator,
uint8 _liquidationIncentiveDenominator
) external;
function setPausedProjectToken(address _projectToken, bool _isPaused) external;
function setLendingTokenInfo(
address _lendingToken,
address _bLendingToken,
bool _isPaused
) external;
function setPausedLendingToken(address _lendingToken, bool _isPaused) external;
//************* PUBLIC FUNCTIONS ********************************
function deposit(address projectToken, address lendingToken, uint256 projectTokenAmount) external;
function withdraw(address projectToken, address lendingToken, uint256 projectTokenAmount) external;
function supply(address lendingToken, uint256 lendingTokenAmount) external;
function redeem(address lendingToken, uint256 bLendingTokenAmount) external;
function redeemUnderlying(address lendingToken, uint256 lendingTokenAmount) external;
function borrow(address projectToken, address lendingToken, uint256 lendingTokenAmount) external;
function repay(address projectToken, address lendingToken, uint256 lendingTokenAmount) external;
function liquidate(address account, address projectToken, address lendingToken) external;
function updateInterestInBorrowPosition(address account, address projectToken, address lendingToken) external;
//************* VIEW FUNCTIONS ********************************
function pit(address account, address projectToken, address lendingToken) external view returns (uint256);
function pitRemaining(address account, address projectToken, address lendingToken) external view returns (uint256);
function liquidationThreshold(address account, address projectToken, address lendingToken) external view returns (uint256);
function totalOutstanding(address account, address projectToken, address lendingToken) external view returns (uint256);
function healthFactor(address account, address projectToken, address lendingToken) external view returns (uint256 numerator, uint256 denominator);
function getProjectTokenEvaluation(address projectToken, uint256 projectTokenAmount) external view returns (uint256);
function lendingTokensLength() external view returns (uint256);
function projectTokensLength() external view returns (uint256);
function getPosition(address account, address projectToken, address lendingToken) external view returns (uint256 depositedProjectTokenAmount, uint256 loanBody, uint256 accrual, uint256 healthFactorNumerator, uint256 healthFactorDenominator);
function decimals() external view returns (uint8);
}
The ERC20 token interface:
This code is an interface to the ERC-20 standard contract. In respect of liquidations, it is used to pre-approve the Primary Lending Platform to spend your stablecoin (USDC in our example) to repay the borrower’s loan - via the EIP20Interface.approve() method.
For convenience of viewing the code, below is a copy of the code, but we recommend using the latest from the Github repo linked above.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
/**
* @title ERC 20 Token Standard Interface
* https://eips.ethereum.org/EIPS/eip-20
*/
interface EIP20Interface {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
/**
* @notice Get the total number of tokens in circulation
* @return The supply of tokens
*/
function totalSupply() external view returns (uint256);
/**
* @notice Gets the balance of the specified address
* @param owner The address from which the balance will be retrieved
* return The `balance`
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @notice Transfer `amount` tokens from `msg.sender` to `dst`
* @param dst The address of the destination account
* @param amount The number of tokens to transfer
* return Whether or not the transfer succeeded
*/
function transfer(address dst, uint256 amount) external returns (bool success);
/**
* @notice Transfer `amount` tokens from `src` to `dst`
* @param src The address of the source account
* @param dst The address of the destination account
* @param amount The number of tokens to transfer
* return Whether or not the transfer succeeded
*/
function transferFrom(address src, address dst, uint256 amount) external returns (bool success);
/**
* @notice Approve `spender` to transfer up to `amount` from `src`
* @dev This will overwrite the approval amount for `spender`
* and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve)
* @param spender The address of the account which may transfer tokens
* @param amount The number of tokens that are approved (-1 means infinite)
* return Whether or not the approval succeeded
*/
function approve(address spender, uint256 amount) external returns (bool success);
/**
* @notice Get the current allowance from `owner` for `spender`
* @param owner The address of the account which owns the tokens to be spent
* @param spender The address of the account which may transfer tokens
* return The number of tokens allowed to be spent (-1 means infinite)
*/
function allowance(address owner, address spender) external view returns (uint256 remaining);
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);
}