F-2024-0003·fee-on-transfer-handling

Incorrect Handling of Fee-On-Transfer Tokens in swapToPaymentCoinAdmin Function

Acknowledgedvaulthealthfipoints
TL;DR

swapToPaymentCoinAdmin assumes the full amount is received from the payment token. Any fee-on-transfer token causes amountReceived to differ from pointToSwap, breaking accounting.

Severity
MEDIUM
Impact
MEDIUM
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
LOW
Exploitability
MEDIUM
02Section · Description

Description

The protocol intends to support all ERC20 tokens but does not currently support fee-on-transfer tokens. These tokens charge a fee on each transfer, meaning the amount received by the recipient is less than the amount sent by the sender. The current implementation of the swapToPaymentCoinAdmin function and similar functions in the Vault contract assumes that the transferred amount is received in full, which may lead to incorrect calculations and potential vulnerabilities when handling fee-on-transfer tokens.

03Section · Impact

Impact

  1. Incorrect Token Amounts: If a token charges a transfer fee, the contract may end up with fewer tokens than expected, leading to incorrect calculations and potential financial discrepancies.
  2. Potential Exploits: Malicious users could exploit this discrepancy to manipulate token balances in their favor, causing financial loss to the contract or other users.
  3. Operational Failures: Functions that rely on the assumption of exact amounts being transferred may fail or behave unpredictably, affecting the contract's functionality and reliability.

Some tokens take a transfer fee (e.g., STA, PAXG), while others do not currently charge a fee but may do so in the future (e.g., USDT, USDC).

solidity
function swapToPaymentCoinAdmin(address user, uint pointToSwap) public {
require(onlyApprovedAdmin[msg.sender] == true, "You are not permitted");
require(_Ipointscoin.balanceOf(user) >= pointToSwap, "Not enough points");
require(pointToSwap >= pointsMin, "Points to swap less than minpoints");
uint balanceBefore = _Ipaymentcoin.balanceOf(address(this));
_Ipointscoin.burn(user, pointToSwap);
uint balanceAfter = _Ipaymentcoin.balanceOf(address(this));
uint amountReceived = balanceAfter - balanceBefore;
claimedPaymentCoin += amountReceived;
uint _fee = (amountReceived * withdrawalFee) / 100;
uint amountAfterFee = amountReceived - _fee;
_Ipaymentcoin.transfer(user, amountAfterFee);
_Ipaymentcoin.transfer(msg.sender, _fee);
emit Swap(user, block.timestamp, pointToSwap, amountAfterFee, _fee);
}

Potential Issue

If the _Ipaymentcoin charges a fee on transfer, amountReceived will be less than pointToSwap, causing the function to behave incorrectly. This issue arises because the implementation verifies that the transfer was successful by checking that the balance of the recipient is greater than or equal to the initial balance plus the amount transferred. This check will fail for fee-on-transfer tokens because the actual received amount will be less than the input amount.

04Section · Recommendation

Recommendation

To mitigate this issue, modify the function to check the contract's balance before and after the transfer to calculate the actual amount received.

solidity
function swapToPaymentCoinAdmin(address user, uint pointToSwap) public {
require(onlyApprovedAdmin[msg.sender] == true, "You are not permitted");
require(_Ipointscoin.balanceOf(user) >= pointToSwap, "Not enough points");
require(pointToSwap >= pointsMin, "Points to swap less than minpoints");
uint balanceBefore = _Ipaymentcoin.balanceOf(address(this));
_Ipointscoin.burn(user, pointToSwap);
uint balanceAfter = _Ipaymentcoin.balanceOf(address(this));
uint amountReceived = balanceAfter - balanceBefore;
claimedPaymentCoin += amountReceived;
uint _fee = (amountReceived * withdrawalFee) / 100;
uint amountAfterFee = amountReceived - _fee;
_Ipaymentcoin.transfer(user, amountAfterFee);
_Ipaymentcoin.transfer(msg.sender, _fee);
emit Swap(user, block.timestamp, pointToSwap, amountAfterFee, _fee);
}

Explanation

  1. Check Balance Before Transfer: Capture the contract's balance of the payment token before the transfer.
  2. Perform the Transfer: Burn the points token from the user and transfer the payment token to the contract.
  3. Check Balance After Transfer: Capture the contract's balance of the payment token after the transfer.
  4. Calculate Amount Received: Calculate the actual amount received by the contract.
  5. Proceed with Fee Calculation and Transfer: Calculate and transfer the fee, then transfer the remaining amount to the user.

Additional recommendations:

  1. Calculate Actual Amount Received: Ensure the function calculates the actual amount received by checking the balance before and after the transfer.
  2. Handle Transfer Fees: Update the function to account for transfer fees and adjust the transferred amounts accordingly.
  3. Documentation: Clearly document the handling of fee-on-transfer tokens to inform users and developers of the expected behavior.
F-2024-0003

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx