Users can stake Standard Token (TST) and The Standard EURO (EUROs) to get a portion of liquidated assets, and their staked EUROs position is spent to buy their given portion of the liquidated assets.
The function distributeAssets, which distribute liquidated assets for the stakers, calculates the portion to be given to each staker based on the amount of TST or EUR staked, choosing the bigger amount of the two, effectively giving the same weight by unit of either token.
Allowing TST to be considered for the stake position size can result in positions which are given a big portion of the liquidated assets, without having the EUROs to pay for the full portion. In that case, they are given only a smaller portion, that they can pay for with staked EUROs, and the rest is transferred to the protocol wallet, without any EUR backing being burned. It will be a loss for the protocol and the onus of taking EUR out of circulation will be on the protocol admin.
The amount of distributed assets for a user should only consider the amount of EUROs staked, resulting in a better optimized distribution of liquidated assets, and a more resilient protocol with less creation of bad debt.
At this moment for the standard protocol, there is in circulation 65 million TST and 400 thousand EUROs.
There is a possibility that the TST will be worth a lot less than the value of an EUR.
The portion to be sold for each user is calculated by checking TST and EUROs amounts staked and choosing the bigger of the two.
Take this simple example:
Bob has 0 TST and 1.000 EUROs staked.
Alice has 10.000 TST and 10 EUROs staked.
A liquidation occurs with total liquidated collateral cost of 100 EUROs, already with the discount of the defined collateral rate.
stakeTotal = 10.000 + 1.000 = 11.000
Bob will get 1000 / 11.000 = 9,1 EUROs worth of liquidated assets.
9,1 EUROs will be burned and the assets will be available for Bob to claim.
At the end Bob has 0 TST and 990,9 EUROs staked.
Alice will get 10.000 / 11.000 = 90,9 EUROs worth of liquidated assets
10 EUROs will be burned. 80,9 EUROs left will be sent by the LiquidationPoolManager to the protocol
address (supposed to be a gnosis wallet belonging to the project admin).
At the end Alice has 10.000 TST and 0 EUROs staked. The protocol has 80,9 EUROs worth of liquidated assets, without any EUR backing being burned.
If EUR is not burned for liquidated assets, this will mean the EUR token will lose backing and in consequence lose their peg will the Euro.
Liquidations will not be optimized, and on liquidations create partial bad debt for the protocol. In this cases it is possible that there is EUROs in the LiquidationPool that will not be used.
Griefing can also be done, with economic expense to the attacker, by creating sizeable positions of TST without EUROs, removing often the earned EUROs from the fees, resulting in bad debt for the protocol.
In liquidation cascade events, the EUROs liquidity of the liquidation pool will start to dry up, first for the large TST holders, with smaller amount of EUROs, progressively for the rest of the stackers, creating increasingly bigger portions of bad debt for the protocol.
In all the situations mentioned above, stackers with sizeable amounts of EUROs can be given small amounts of debt to purchase, because their stake is diluted by TST stackers. The bad debt will occur while there is liquidity in the LiquidationPool, and it could have been avoided if the distribution of assets only considered the EUROs staked.
Manual Review
Consider only the amount of EUROs staked to determine the portion of liquidated assets to be distributed to each user. Keep the stacking of TST only to earn the protocol fees.
@@ -41,17 +42,7 @@ contract LiquidationPool is ILiquidationPool {
_;
}
- function stake(Position memory _position) private pure returns (uint256) {
- return _position.TST > _position.EUROs ? _position.EUROs : _position.TST;
- }
-
- function getStakeTotal() private view returns (uint256 _stakes) {
- for (uint256 i = 0; i < holders.length; i++) {
- Position memory _position = positions[holders[i]];
- _stakes += stake(_position);
- }
- }
@@ -52,6 +53,14 @@ contract LiquidationPool is ILiquidationPool {
}
}
+ function getEuroTotal() private view returns (uint256 _euro) {
+ for (uint256 i = 0; i < holders.length; i++) {
+ Position memory _position = positions[holders[i]];
+ _euro += _position.EUROs;
+ }
+ }
+
@@ -205,24 +214,36 @@ contract LiquidationPool is ILiquidationPool {
function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
consolidatePendingStakes();
(,int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
- uint256 stakeTotal = getStakeTotal();
+ uint256 stakeTotal = getEuroTotal();
uint256 burnEuros;
uint256 nativePurchased;
for (uint256 j = 0; j < holders.length; j++) {
Position memory _position = positions[holders[j]];
- uint256 _positionStake = stake(_position);
- if (_positionStake > 0) {
+ if (_position.EUROs > 0) {
for (uint256 i = 0; i < _assets.length; i++) {
ILiquidationPoolManager.Asset memory asset = _assets[i];
if (asset.amount > 0) {
(,int256 assetPriceUsd,,,) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData();
- uint256 _portion = asset.amount * _positionStake / stakeTotal;
+ uint256 _portion = asset.amount * _position.EUROs / stakeTotal;
uint256 costInEuros = _portion * 10 ** (18 - asset.token.dec) * uint256(assetPriceUsd) / uint256(priceEurUsd)
* _hundredPC / _collateralRate;
if (costInEuros > _position.EUROs) {
_portion = _portion * _position.EUROs / costInEuros;
costInEuros = _position.EUROs;
}
_position.EUROs -= costInEuros;
rewards[abi.encodePacked(_position.holder, asset.token.symbol)] += _portion;
burnEuros += costInEuros;
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.