Lone Wintergreen Rattlesnake
High
In the liquidation mechanism, liquidators can intentionally perform partial liquidations to force bad debt accumulation and the protocol treats this as clean Liquidation Fee, while liquidators receive no clean liquidation fee benefits for full liquidations, creating misaligned incentives.
In the liquidation logic, when processing full liquidations (100% shares), the protocol intends to give full liquidators clean liquidation fee while this is not factored into the repayment amount calculation, causing liquidators to pay more without receiving the intended fee benefit or rather force liquidators to perform partial liquidation that will force the protocol to have bad debt, this bad debt occur not because there is insufficient collateral coverage.
No response
No response
- Liquidator identifies a liquidatable position
- Instead of full liquidation, liquidator performs partial liquidation taking only profitable portions
- Remaining position becomes bad debt as it's too small to be profitable to liquidate
The protocol faces multiple negative impacts:
- Accumulation of bad debt by encouraging partial liquidations
- Liquidators are disincentivized from performing full liquidations
- Protocol solvency is threatened by growing bad debt positions
- Higher costs for liquidators performing full liquidations
Click to view the full test code
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
// forge
import {Test, console} from "forge-std/Test.sol";
// fraxlend
import {FraxlendPairDeployer, ConstructorParams} from "../src/contracts/FraxlendPairDeployer.sol";
import {FraxlendWhitelist} from "../src/contracts/FraxlendWhitelist.sol";
import {FraxlendPairRegistry} from "../src/contracts/FraxlendPairRegistry.sol";
import {FraxlendPair} from "../src/contracts/FraxlendPair.sol";
import {VariableInterestRate} from "../src/contracts/VariableInterestRate.sol";
import {IERC4626Extended} from "../src/contracts/interfaces/IERC4626Extended.sol";
import {ChainlinkSinglePriceOracle} from "../src/contracts/ChainlinkSinglePriceOracle.sol";
import "../src/contracts/mocks/LendingAssetVault.sol";
// mocks
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {MockERC20} from "../src/contracts/mocks/MockERC20.sol";
contract Fraxlend is Test {
/*///////////////////////////////////////////////////////////////
GLOBAL VARIABLES
///////////////////////////////////////////////////////////////*/
// external actors
address internal user0 = vm.addr(uint256(keccak256("User0")));
address internal user1 = vm.addr(uint256(keccak256("User1")));
address internal user2 = vm.addr(uint256(keccak256("User2")));
address[] internal users = [user0, user1, user2];
uint256[] internal _fraxPercentages = [10000, 2500, 7500, 5000];
// fraxlend protocol actors
address internal comptroller = vm.addr(uint256(keccak256("comptroller")));
address internal circuitBreaker = vm.addr(uint256(keccak256("circuitBreaker")));
address internal timelock = vm.addr(uint256(keccak256("comptroller")));
uint16 internal fee = 100;
uint256 internal PRECISION = 10 ** 27;
/*///////////////////////////////////////////////////////////////
TEST CONTRACTS
///////////////////////////////////////////////////////////////*/
// fraxlend
FraxlendPairDeployer internal _fraxDeployer;
FraxlendWhitelist internal _fraxWhitelist;
FraxlendPairRegistry internal _fraxRegistry;
VariableInterestRate internal _variableInterestRate;
FraxlendPair internal _fraxLPToken1Peas;
MockERC20 internal _mockDai;
MockERC20 internal _collateral;
MockERC20 internal _tokenB;
MockERC20 internal _tokenC;
ChainlinkSinglePriceOracle internal _clOracle;
LendingAssetVault internal _lendingAssetVault;
event Message(string message);
/*///////////////////////////////////////////////////////////////
SETUP FUNCTIONS
///////////////////////////////////////////////////////////////*/
function setUp() public {
// deploy mock tokens
_mockDai = new MockERC20("MockDai", "MDAI");
_collateral = new MockERC20("Collateral", "COL");
_tokenB = new MockERC20("TokenB", "TKB");
_tokenC = new MockERC20("TokenC", "TKC");
_mockDai.mint(address(this), 1000000 ether);
_collateral.mint(address(this), 1000000 ether);
_tokenB.mint(address(this), 1000000e6);
_tokenC.mint(address(this), 1000000 ether);
_deployVariableInterestRate();
_deployFraxWhitelist();
_deployFraxPairRegistry();
_deployFraxPairDeployer();
_deployFraxPairs();
_setupActors();
_deployLendingVault();
vm.startPrank(address(2));
_fraxLPToken1Peas.setExternalAssetVault(IERC4626Extended(address(_lendingAssetVault)));
vm.stopPrank();
}
function _deployVariableInterestRate() internal {
// These values taken from existing Fraxlend Variable Rate Contract
_variableInterestRate = new VariableInterestRate(
"[0.5 [email protected] 5-10k] 2 days (.75-.85)",
87500,
200000000000000000,
75000,
85000,
158247046,
1582470460,
3164940920000,
172800
);
}
function _deployFraxWhitelist() internal {
_fraxWhitelist = new FraxlendWhitelist();
}
function _deployFraxPairRegistry() internal {
address[] memory _initialDeployers = new address[](0);
_fraxRegistry = new FraxlendPairRegistry(address(this), _initialDeployers);
}
function _deployFraxPairDeployer() internal {
ConstructorParams memory _params =
ConstructorParams(address(this), address(1), address(2), address(_fraxWhitelist), address(_fraxRegistry));
_fraxDeployer = new FraxlendPairDeployer(_params);
_fraxDeployer.setCreationCode(type(FraxlendPair).creationCode);
address[] memory _whitelistDeployer = new address[](1);
_whitelistDeployer[0] = address(this);
_fraxWhitelist.setFraxlendDeployerWhitelist(_whitelistDeployer, true);
address[] memory _registryDeployer = new address[](1);
_registryDeployer[0] = address(_fraxDeployer);
_fraxRegistry.setDeployers(_registryDeployer, true);
emit Message("1");
}
function _deployFraxPairs() internal {
// moving time to help out the twap
vm.warp(block.timestamp + 1 days);
_clOracle = new ChainlinkSinglePriceOracle(address(0));
emit Message("1aa");
_fraxLPToken1Peas = FraxlendPair(
_fraxDeployer.deploy(
abi.encode(
address(_mockDai), // asset
address(_collateral), // collateral
address(_clOracle), //oracle
5000, // 5000, // maxOracleDeviation
address(_variableInterestRate), //rateContract
1000, //fullUtilizationRate
75000, // maxLtv
10000, // uint256 _cleanLiquidationFee
// 9000, // uint256 _dirtyLiquidationFee
0 //uint256 _protocolLiquidationFee
)
)
);
emit Message("1b");
// deposit some asset
IERC20(address(_mockDai)).approve(address(_fraxLPToken1Peas), type(uint256).max);
_fraxLPToken1Peas.deposit(100_000 ether, address(this));
}
function _deployLendingVault() internal {
_lendingAssetVault = new LendingAssetVault("LDA", "FLDA", address(_mockDai));
_lendingAssetVault.setVaultWhitelist(address(_fraxLPToken1Peas), true);
address[] memory _vaults = new address[](1);
_vaults[0] = address(_fraxLPToken1Peas);
uint256[] memory _maxAllocations = new uint256[](1);
_maxAllocations[0] = 100_000 ether;
_lendingAssetVault.setVaultMaxAllocation(_vaults, _maxAllocations);
vm.startPrank(users[0]);
IERC20(address(_mockDai)).approve(address(_lendingAssetVault), type(uint256).max);
_lendingAssetVault.deposit(1_000_000 ether, users[0]);
vm.startPrank(users[1]);
IERC20(address(_mockDai)).approve(address(_lendingAssetVault), type(uint256).max);
_lendingAssetVault.deposit(1_000_000 ether, users[0]);
}
/*////////////////////////////////////////////////////////////////
HELPERS
////////////////////////////////////////////////////////////////*/
function _setupActors() internal {
for (uint256 i; i < users.length; i++) {
vm.deal(users[i], 1000000 ether);
vm.startPrank(users[i]);
// _weth.deposit{value: 1000000 ether}();
_collateral.mint(users[i], 100_000 ether);
_tokenB.mint(users[i], 1000000e6);
_tokenC.mint(users[i], 1000000 ether);
_mockDai.mint(users[i], 1000000 ether);
IERC20(address(_collateral)).approve(address(_fraxLPToken1Peas), type(uint256).max);
}
}
}
function testFullLiquidationWithoutCleanLiquidationFeeRemoval() public {
_clOracle.setMinAndMaxPrices(198e18, 200e18);
vm.startPrank(users[1]);
_fraxLPToken1Peas.borrowAsset(370 ether, 100_000 ether, users[1]);
assertEq(_collateral.balanceOf(address(_fraxLPToken1Peas)), 100_000 ether);
assertEq(_collateral.balanceOf(users[1]), 0);
vm.warp(block.timestamp + 1);
uint256 _amountLiquidatorToRepay = 370000000058774236930;
_clOracle.setMinAndMaxPrices(260e18, 262e18);
// Try to preview withdraw more than available
uint256 sharesToLiquidate = _fraxLPToken1Peas.userBorrowShares(users[1]);
assertEq(sharesToLiquidate, 370 ether);
assertEq(_collateral.balanceOf(users[2]), 100_000 ether);
assertEq(_mockDai.balanceOf(users[2]), 1_000_000 ether);
vm.startPrank(users[2]);
IERC20(address(_mockDai)).approve(address(_fraxLPToken1Peas), type(uint256).max);
_fraxLPToken1Peas.liquidate(uint128(sharesToLiquidate), block.timestamp + 10, users[1]);
assertEq(_collateral.balanceOf(users[2]), 200_000 ether);
assertEq(_mockDai.balanceOf(users[2]), 1_000_000 ether - _amountLiquidatorToRepay);
sharesToLiquidate = _fraxLPToken1Peas.userBorrowShares(users[1]);
assertEq(sharesToLiquidate, 0);
}
function testPartialLiquidationWithCleanLiquidationFeeRemoval() public {
_clOracle.setMinAndMaxPrices(198e18, 200e18);
vm.startPrank(users[1]);
_fraxLPToken1Peas.borrowAsset(370 ether, 100_000 ether, users[1]);
assertEq(_collateral.balanceOf(address(_fraxLPToken1Peas)), 100_000 ether);
assertEq(_collateral.balanceOf(users[1]), 0);
vm.warp(block.timestamp + 1);
uint256 _amountLiquidatorToRepay = 350000000055597251150;
_clOracle.setMinAndMaxPrices(260e18, 262e18);
// Try to preview withdraw more than available
uint256 sharesToLiquidate = _fraxLPToken1Peas.userBorrowShares(users[1]);
assertEq(sharesToLiquidate, 370 ether);
assertEq(_collateral.balanceOf(users[2]), 100_000 ether);
assertEq(_mockDai.balanceOf(users[2]), 1_000_000 ether);
vm.startPrank(users[2]);
IERC20(address(_mockDai)).approve(address(_fraxLPToken1Peas), type(uint256).max);
_fraxLPToken1Peas.liquidate(350 ether, block.timestamp + 10, users[1]);
assertEq(_collateral.balanceOf(users[2]), 200_000 ether);
assertEq(_mockDai.balanceOf(users[2]), 1_000_000 ether - _amountLiquidatorToRepay);
sharesToLiquidate = _fraxLPToken1Peas.userBorrowShares(users[1]);
assertEq(sharesToLiquidate, 0);
}
If it is a full liquidation, apply the clean liquidation fee benefit before calculating the repayment amount