Fee-on-transfer tokens can cause accounting discrepancies in asset recovery functions
AssetRecovererLib.recoverERC20() emits the requested amount and does not measure the post-transfer balance delta, leading to misleading event data for fee-on-transfer tokens.
Description
The AssetRecovererLib library, which is used by CSAccounting, CSFeeDistributor, and potentially other contracts inheriting from AssetRecoverer, contains a recoverERC20() function that is vulnerable to fee-on-transfer token accounting issues.
This function assumes that the full amount specified will be transferred, which may not be the case for tokens that implement a fee-on-transfer mechanism.
function recoverERC20(address token, uint256 amount) external {IERC20(token).safeTransfer(msg.sender, amount);emit IAssetRecovererLib.ERC20Recovered(token, msg.sender, amount);}
Impact
If a fee-on-transfer token is recovered using this function, the actual amount transferred will be less than the amount specified. This discrepancy could lead to:
- Incorrect accounting of recovered assets.
- Potential loss of funds if the contract's balance is used for future calculations or operations.
- Inconsistencies between on-chain state and off-chain records.
- Misleading event emissions, as the emitted amount may not reflect the actual transferred amount.
While the impact is generally low due to the restricted access of these functions (only callable by a recoverer role), it still presents a risk, especially if these contracts interact with or recover a wide range of tokens in the future.
Proof of Concept:
- Assume a fee-on-transfer token that takes a 1% fee on each transfer.
- The contract has a balance of 1000 of these tokens.
- A recoverer calls
recoverERC20with an amount of 1000. - Only 990 tokens are actually transferred due to the fee.
- The
ERC20Recoveredevent is emitted with an amount of 1000, which is incorrect.
Recommendation
To mitigate this issue, implement a balance check before and after the transfer to determine the actual amount transferred:
function recoverERC20(address token, uint256 amount) external {uint256 balanceBefore = IERC20(token).balanceOf(address(this));IERC20(token).safeTransfer(msg.sender, amount);uint256 balanceAfter = IERC20(token).balanceOf(address(this));uint256 actualAmountTransferred = balanceBefore - balanceAfter;emit IAssetRecovererLib.ERC20Recovered(token, msg.sender, actualAmountTransferred);}

