The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: medium
Invalid

PAXG Token with a fee-on-transfer mechanism will break a part of the protocol

Summary

Vulnerability Details

  • The function LiquidationPool.distributeAssets() calulated how much portion to reward and that amount of tokens are transferred from manager to LiquidationPool contract.

uint256 _portion = asset.amount * _positionStake / stakeTotal;
code of `LiquidationPool.distributeAssets()`
function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
consolidatePendingStakes();
(,int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
uint256 stakeTotal = getStakeTotal();
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) {
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 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;
if (asset.token.addr == address(0)) {
nativePurchased += _portion;
} else {
IERC20(asset.token.addr).safeTransferFrom(manager, address(this), _portion);
}
}
}
}
positions[holders[j]] = _position;
}
if (burnEuros > 0) IEUROs(EUROs).burn(address(this), burnEuros);
returnUnpurchasedNative(_assets, nativePurchased);
}
  • Now the above line doesnt account the fee, and result in getting more than actually deserving.

  • So at tyhe end, if 1000 PAXG tokens are distributed 2 bps worth of tokens are sent as fees, so now the last users who claim the reward will not be able to get the rewards since the early claimers can claim without accounting for fees o transfer.

  • See this transferFrom line, where 2 bps worth tokens gets to fees, and the reward claimers at last will get rekt.

  • Also The function SmartVaultV3.swap() calculated the minimumAmountOut which doesn't account fee that is charged on transfer.

uint256 minimumAmountOut = calculateMinimumAmountOut(_inToken, _outToken, _amount);
code of SmartVaultV3.swap()
function swap(bytes32 _inToken, bytes32 _outToken, uint256 _amount) external onlyOwner {
uint256 swapFee = _amount * ISmartVaultManagerV3(manager).swapFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC();
address inToken = getSwapAddressFor(_inToken);
uint256 minimumAmountOut = calculateMinimumAmountOut(_inToken, _outToken, _amount);
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: inToken,
tokenOut: getSwapAddressFor(_outToken),
fee: 3000,
recipient: address(this),
deadline: block.timestamp,
amountIn: _amount - swapFee,
amountOutMinimum: minimumAmountOut,
sqrtPriceLimitX96: 0
});
inToken == ISmartVaultManagerV3(manager).weth() ?
executeNativeSwapAndFee(params, swapFee) :
executeERC20SwapAndFee(params, swapFee);
}
  • so, the swap will always fail, since the fee is not considered on swapping, resulting in a failed swap transaction

Impact

Medium

Tools Used

Manual review

Recommendations

  • You should cache the balance before a transfer to the contract and then check it after the transfer and use the difference between them as the newly added balance.

  • Or call PAXG.getFeeFor() or call the PAXG token's feeRate, for how many base points is the fee charged for a transfer.

  • Take inspiration from uniswapV2 handling of Fee on transfer here.

function swap(bytes32 _inToken, bytes32 _outToken, uint256 _amount) external onlyOwner {
uint256 swapFee = _amount * ISmartVaultManagerV3(manager).swapFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC();
address inToken = getSwapAddressFor(_inToken);
+ uint amountInSupportingFee = inToken == PAXG ? _amount - PAXG.getFeeFor(_amount) : _amount ;
- uint256 minimumAmountOut = calculateMinimumAmountOut(_inToken, _outToken, _amount);
+ uint256 minimumAmountOut = calculateMinimumAmountOut(_inToken, _outToken, amountInSupportingFee);
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: inToken,
tokenOut: getSwapAddressFor(_outToken),
fee: 3000,
recipient: address(this),
deadline: block.timestamp,
amountIn: _amount - swapFee,
amountOutMinimum: minimumAmountOut,
sqrtPriceLimitX96: 0
});
inToken == ISmartVaultManagerV3(manager).weth() ?
executeNativeSwapAndFee(params, swapFee) :
executeERC20SwapAndFee(params, swapFee);
}
Updates

Lead Judging Commences

hrishibhat Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

fee-on-transfer

hrishibhat Lead Judge almost 2 years ago
Submission Judgement Published
Invalidated
Reason: Out of scope
Assigned finding tags:

fee-on-transfer

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!