If a fee distribution during liquidation results in one of the holders having more euros than TST and another holder not having enough euros to cover their portion, it can lead to an incorrect calculation of the burn amount.
The function RunLiquidation calls the distributeFees() function. This function distributes the euros coming from the fees paid by the borrowers.
Furthermore, the stake function, which is used for calculating the collateral portion to be received and the Euro price, takes the minimum of each holder's position.
If a fee distribution during liquidation results in one of the holders having more euros than TST and another holder not having enough euros to cover their portion, it can lead to an incorrect calculation of the burn amount.
If Bob has a position of 100,000 euros and 100,000 TST, and Alice has a position of 5,000 euros and 100,000 TST. Meanwhile, the LiquidationPoolManager
has accumulated 200,000 euros. Subsequently, a vault gets liquidated, and the amount to be paid is 250,000 euros. When the runLiquidation
function is called, it will call the distributeFees
function, resulting in each of them receiving 50,000 euros because 50% is allocated to the protocol's address. Then, they will share the fees based on their TST positions.
This will give Bob a position of 100,000 TST and 150,000 euros, and Alice a position of 100,000 TST and 55,000 euros.
However, only Bob's TST position will be considered. He will pay 75,000 euros and receive half of the collateral. Since Alice cannot pay 75,000 euros, she will only pay 55,000 euros, and the remaining 20,000 euros that could have been burned will not be. In the end, Bob will have 75,000 euros and 100,000 TST, while Alice will have 100,000 TST and 0 euros. It is not Bob's fault since he cannot predict the movements caused by the distributeFees
function.
To run this POC you will need to follow few steps in order to add foundry in the hardhat project:
1-run npm i --save-dev @nomicfoundation/hardhat-foundry
2-Add require("@nomicfoundation/hardhat-foundry");
to the top of your hardhat.config.js
file.
(if the git repository is not initialized run git init
in your terminal before)
3-Run npx hardhat init-foundry
in your terminal. This will generate a foundry.toml
file based on your Hardhat project's existing configuration, and will install the forge-std library
.
After that create a file in the test folder with the extension .sol and copy this code:
pragma solidity ^0.8.17;
import {LiquidationPoolManager} from "../contracts/LiquidationPoolManager.sol";
import {ERC20Mock} from "../contracts/utils/ERC20Mock.sol";
import {EUROsMock} from "../contracts/utils/EUROsMock.sol";
import {ChainlinkMock} from "../contracts/utils/ChainlinkMock.sol";
import {MockSmartVaultManager} from "../contracts/utils/MockSmartVaultManager.sol";
import {LiquidationPool} from "../contracts/LiquidationPool.sol";
import {TokenManagerMock} from "../contracts/utils/TokenManagerMock.sol";
import {Test, console} from "../lib/forge-std/src/Test.sol";
contract POCLiquidation is Test {
bytes32 NATIVE = bytes32(bytes("ETH"));
ChainlinkMock ETHUsd;
MockSmartVaultManager vaultManager;
LiquidationPoolManager poolManager;
EUROsMock euros;
ERC20Mock TST;
ERC20Mock WBTC;
ChainlinkMock EurUsd;
ChainlinkMock BTCUsd;
TokenManagerMock tokenManager;
LiquidationPool pool;
address protocol = makeAddr("protocol");
function setUp() public {
vm.warp(1 days);
TST = new ERC20Mock("The Standard Token", "TST", 18);
ETHUsd = new ChainlinkMock("ETH / USD");
ETHUsd.setPrice(2000e8);
tokenManager = new TokenManagerMock(NATIVE, address(ETHUsd));
euros = new EUROsMock();
EurUsd = new ChainlinkMock("EUR/USD");
EurUsd.setPrice(108e6);
WBTC = new ERC20Mock("wrapped BTC", "WBTC", 8);
BTCUsd = new ChainlinkMock("WBTC/USD");
BTCUsd.setPrice(3500000000000);
tokenManager.addAcceptedToken(address(WBTC), address(BTCUsd));
vaultManager = new MockSmartVaultManager(110000, address(tokenManager));
poolManager = new LiquidationPoolManager(
address(TST), address(euros), address(vaultManager), address(EurUsd), payable(protocol), 50000
);
address poolAddress = poolManager.pool();
pool = LiquidationPool(poolAddress);
euros.grantRole(euros.DEFAULT_ADMIN_ROLE(), address(vaultManager));
euros.grantRole(euros.MINTER_ROLE(), address(this));
euros.grantRole(euros.BURNER_ROLE(), address(this));
euros.grantRole(euros.BURNER_ROLE(), address(pool));
}
function test_incorectBurnAmount() external {
address bob = makeAddr("Bob");
address alice = makeAddr("Alice");
euros.mint(bob, 60_000e18);
TST.mint(bob, 60_000e18);
euros.mint(alice, 60_000e18);
TST.mint(alice, 60_000e18);
vm.startPrank(bob);
euros.approve(address(pool), 60_000e18);
TST.approve(address(pool), 60_000e18);
pool.increasePosition(60_000e18, 60_000e18);
vm.stopPrank();
euros.mint(address(poolManager), 1_800_000e18);
vm.startPrank(alice);
euros.approve(address(pool), 60_000e18);
TST.approve(address(pool), 60_000e18);
pool.increasePosition(60_000e18, 60_000e18);
vm.stopPrank();
vm.warp(block.timestamp + 1 days + 1);
WBTC.mint(address(vaultManager), 28e8);
vm.deal(address(vaultManager), 3e18);
(, int256 WbtcPrice,,,) = BTCUsd.latestRoundData();
(, int256 Ethprice,,,) = ETHUsd.latestRoundData();
(, int256 euroPrice,,,) = EurUsd.latestRoundData();
uint256 costInEurosOfBTC =
28e8 * 10 ** (10) * uint256(WbtcPrice) / uint256(euroPrice) * 100000 / vaultManager.collateralRate();
uint256 costInEurosOfETH =
3e18 * uint256(Ethprice) / uint256(euroPrice) * 100000 / vaultManager.collateralRate();
uint256 totalSupplyBefore = euros.totalSupply();
uint256 euroPoolBalance = euros.balanceOf(address(pool));
poolManager.runLiquidation(0);
uint256 totalSupplyAfter = euros.totalSupply();
uint256 AmountBurnt = totalSupplyBefore - totalSupplyAfter;
(LiquidationPool.Position memory bobPosition, LiquidationPool.Reward[] memory bobRewards) = pool.position(bob);
(LiquidationPool.Position memory alicePosition, LiquidationPool.Reward[] memory aliceRewards) =
pool.position(alice);
console.log(bobRewards[1].amount / 1e7);
console.log(aliceRewards[1].amount / 1e7);
console.log(bobRewards[0].amount / 1e17);
console.log(aliceRewards[0].amount / 1e17);
console.log(WBTC.balanceOf(address(pool)) / 1e8);
console.log(address(pool).balance / 1e18);
console.log(bobPosition.EUROs / 1e18);
console.log(alicePosition.EUROs);
console.log(alicePosition.TST / 1e18);
assertGt(euroPoolBalance/1e18,(costInEurosOfBTC + costInEurosOfETH) / 1e18);
assertLt(AmountBurnt / 1e18, (costInEurosOfBTC + costInEurosOfETH) / 1e18);
}
}
Take into account only Euro position of each holder. by implementing an internal function that sum the euro position of each holder.