Because fee-on-transfer tokens are not specifically handled, LiquidationPool::distributeAssets()
incorrectly calculates rewards for holders.
This results in some holders receiving more rewards than they should while others will be unable to claim any rewards for any token if their rewards amount of the fee-on-transfer token is greater than the remaining balance in LiquidationPool
.
For fee-on-transfer tokens, when an amount X
is sent from one account to another, the actual amount received is X - f
, where f
is a fee.
In distributeAssets()
this is not taken into account, so each holder is being allocated X
rewards in reality the contract only receives X-f
.
This is particularly relevant as the protocol intends to allow users deposit the PAXG
fee-on-transfer token as collateral, a token which charges 0.02%
fee per on-chain transaction; see here
The relevant lines from distributeAssets()
are indicated below. The function is looping through all holders
and assigning portions of the assets being distributed from a liquidated position to each eligible holder.
First, a _portion
amount of each token is calculated for an eligible holder, X
in the previous explanation. Second, the rewards
mapping storage variable is updated for that holder by _portion
amount of the token. Finally, _portion
amount of the token is sent from the LiquidationPoolManager
contract to the LiquidationPool
.
When holders subsequently call LiquidationPool::claimRewards()
the function will revert if the amount of the fee-on-transfer token available in it's balances is less than the _rewardAmount
due to the customer.
Some holders will receive more fee-on-transfer rewards than they are entitled to while others will be unable to claim rewards for any tokens; as fee-on-transfer rewards are claimed the amount of people unable to claim will increase as there are less and less funds to withdraw.
As the protocol plans on allowing PAXG
which implements a 0.02%
fee on transfers, this represents a serious risk to the functioning of the protocol.
To test, we need to update the code in 4 places:
Create a contract representing a fee-on-transfer token in contract/utils
called paxgMock.sol
:
Update the mockTokenManager
const in common.js
, where we mock the tokenManager
contract, to configure the fee-on-transfer token:
In liquidationPoolManager.js
create new variable PAXG
and import PAXG
from mockTokenManager()
:
Finally, add the following test to liquidationPoolManager.js
and run:
Manual Review
Hardhat Testing
Update distributeAssets()
to check if the token is PAXG
and if so apply a 0.02%
reduction to the amount sent to rewards. This should be made dynamic if other fee-on-transfer tokens are going to be permitted as collateral.
```diff
// Some Code
_position.EUROs -= costInEuros;
+ bytes32 PAXGSymbol = keccak256(abi.encodePacked("PAXG"));
+ // Check if the token symbol is PAXG
+ if (asset.token.symbol == PAXGSymbol) {
+ uint256 fee = _portion * 2 / 10000; // 0.02% fee
+ _portion -= fee; // Deduct the fee from the _portion
+ }
rewards[abi.encodePacked(_position.holder, asset.token.sym+= _portion;
burnEuros += costInEuros;
if (asset.token.addr == address(0)) {
nativePurchased += _portion;
} else {
// @audit-issue : 0 amount can be sent in this
IERC20(asset.token.addr).safeTransferFrom(manager, a(this), _portion);
}
```
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.