DeFiFoundry
50,000 USDC
View results
Submission Details
Severity: medium
Invalid

Token compatibility issues in the PerpetualVault

Summary

If the collateralToken is a fee-on-transfer token, the actual amount received by the contract will be less than the amount sent by the user. This can lead to incorrect accounting, failed transactions, or loss of funds.

Vulnerability Details

  • Some tokens deduct a fee during transfers, meaning the actual amount received by the contract is less than the amount sent.

  • Some tokens automatically adjust balances (e.g., staking rewards or inflationary tokens).

  • Some tokens do not follow the ERC20 standard (e.g., missing return values or non-boolean return values).

contract PerpetualVault is IPerpetualVault, Initializable, Ownable2StepUpgradeable, ReentrancyGuardUpgradeable {
using SafeERC20 for IERC20;

PoC to demonstrate

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract FeeOnTransferToken is IERC20 {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
uint256 private _fee;
constructor(string memory name_, string memory symbol_, uint256 fee_) {
_name = name_;
_symbol = symbol_;
_fee = fee_;
}
function name() public view returns (string memory) {
return _name;
}
function symbol() public view returns (string memory) {
return _symbol;
}
function decimals() public pure returns (uint8) {
return 18;
}
function totalSupply() public view override returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) public view override returns (uint256) {
return _balances[account];
}
function transfer(address recipient, uint256 amount) public override returns (bool) {
uint256 fee = (amount * _fee) / 100;
uint256 amountAfterFee = amount - fee;
_balances[msg.sender] -= amount;
_balances[recipient] += amountAfterFee;
_balances[address(this)] += fee; // Collect fee
emit Transfer(msg.sender, recipient, amountAfterFee);
emit Transfer(msg.sender, address(this), fee);
return true;
}
function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) {
uint256 fee = (amount * _fee) / 100;
uint256 amountAfterFee = amount - fee;
_balances[sender] -= amount;
_balances[recipient] += amountAfterFee;
_balances[address(this)] += fee; // Collect fee
emit Transfer(sender, recipient, amountAfterFee);
emit Transfer(sender, address(this), fee);
return true;
}
function approve(address spender, uint256 amount) public override returns (bool) {
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function allowance(address owner, address spender) public view override returns (uint256) {
return _allowances[owner][spender];
}
}
contract PerpetualVault {
using SafeERC20 for IERC20;
IERC20 public collateralToken;
uint256 public totalDepositAmount;
constructor(address _collateralToken) {
collateralToken = IERC20(_collateralToken);
}
function deposit(uint256 amount) external {
uint256 balanceBefore = collateralToken.balanceOf(address(this));
collateralToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 balanceAfter = collateralToken.balanceOf(address(this));
uint256 actualAmount = balanceAfter - balanceBefore;
totalDepositAmount += actualAmount;
}
function getTotalDepositAmount() external view returns (uint256) {
return totalDepositAmount;
}
}

Steps to Reproduce

  • Deploy the FeeOnTransferToken contract with a fee (e.g., 1%).

  • Deploy the PerpetualVault contract, passing the address of the FeeOnTransferToken contract.

  • Approve the PerpetualVault contract to spend tokens on behalf of the user.

  • Call the deposit function on the PerpetualVault contract with an amount (e.g., 100 ether).

  • Observe that the totalDepositAmount is less than the expected amount due to the fee.

Impact

  • The contract may miscalculate balances, leading to incorrect accounting or failed transactions.

  • The contract may not account for balance changes, leading to incorrect calculations.

  • Users may lose funds due to the fee, and the contract may not account for it correctly.

  • The contract expects a specific amount but receives less due to the fee, transactions may fail.

Tools Used

Manual code review

Recommendations

  • Clearly document the types of tokens supported by the contract and warn users about unsupported tokens.

  • Use slippage protection for swaps to account for unexpected token behavior (e.g., fee-on-transfer tokens).

function _doDexSwap(bytes memory data, bool isCollateralToIndex) internal returns (uint256 outputAmount) {
(address to, uint256 amount, bytes memory callData, uint256 minOutputAmount) = abi.decode(data, (address, uint256, bytes, uint256));
uint256 balBefore = outputToken.balanceOf(address(this));
ParaSwapUtils.swap(to, callData);
outputAmount = outputToken.balanceOf(address(this)) - balBefore;
require(outputAmount >= minOutputAmount, "Slippage too high");
// ...
}
  • Restrict the contract to work only with standard ERC20 tokens that follow the specification strictly.

function setCollateralToken(address token) external onlyOwner {
require(token != address(0), "Invalid token");
// Add checks to ensure the token is standard ERC20
collateralToken = IERC20(token);
}
  • Check token balances before and after transfers to account for fee-on-transfer tokens or rebasing tokens.

function deposit(uint256 amount) external nonReentrant payable {
uint256 balanceBefore = collateralToken.balanceOf(address(this));
collateralToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 balanceAfter = collateralToken.balanceOf(address(this));
uint256 actualAmount = balanceAfter - balanceBefore;
require(actualAmount >= minDepositAmount, "Insufficient deposit");
// ...
}
Updates

Lead Judging Commences

n0kto Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope
n0kto Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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