Summary
The transferErcTokens
function attempts to execute an ERC20 transferFrom
operation without verifying if the contract has sufficient approval to transfer tokens on behalf of the user. This missing validation could lead to failed transactions and inconsistent state.
Vulnerability Detail
Current implementation:
function transferErcTokens(address nftAddress, address to, uint256 amount) external {
ERC20Info memory tokenInfo = nftToErc20Info[nftAddress];
balances[msg.sender][tokenInfo.erc20Address] -= amount;
balances[to][tokenInfo.erc20Address] += amount;
IERC20(tokenInfo.erc20Address).transferFrom(msg.sender, to, amount);
}
Impact
The vulnerability could result in:
Failed transactions with inconsistent state
Permanent loss of internal balance tracking
Gas waste from failed transactions
User funds becoming stuck in the contract
Discrepancy between internal balances and actual token balances
Proof of Concept
contract ApprovalExploitTest {
TokenDivider public divider;
MockERC20 public token;
function demonstrateApprovalIssue() public {
address user = address(this);
address recipient = address(0x123);
uint256 amount = 100;
bool transferSuccess = false;
try divider.transferErcTokens(address(token), recipient, amount) {
transferSuccess = true;
} catch {
}
require(!transferSuccess, "Transfer should fail without approval");
uint256 senderBalance = divider.getBalanceOf(user, address(token));
uint256 recipientBalance = divider.getBalanceOf(recipient, address(token));
}
}
Attack Scenario
User calls transferErcTokens
without first approving the contract
Internal balances are updated
transferFrom
fails due to insufficient allowance
Contract state becomes inconsistent with actual token balances
User funds might become locked
Tool Used
Foundry
Recommended Mitigation Steps
Add Approval Validation
function _validateApproval(
address token,
address owner,
uint256 amount
) private view {
uint256 allowance = IERC20(token).allowance(owner, address(this));
require(allowance >= amount, "Insufficient allowance");
}
Implement Safe Transfer Pattern
function transferErcTokens(
address nftAddress,
address to,
uint256 amount
) external nonReentrant {
ERC20Info memory tokenInfo = _validateTokenInfo(nftAddress);
address tokenAddress = tokenInfo.erc20Address;
_validateApproval(tokenAddress, msg.sender, amount);
_validateBalance(msg.sender, tokenAddress, amount);
bool success = IERC20(tokenAddress).transferFrom(
msg.sender,
to,
amount
);
require(success, "Transfer failed");
_updateBalances(msg.sender, to, tokenAddress, amount);
emit TokensTransferred(msg.sender, to, tokenAddress, amount);
}
Add Recovery Mechanism
function reconcileBalance(
address token,
address account
) external {
uint256 actualBalance = IERC20(token).balanceOf(account);
uint256 recordedBalance = balances[account][token];
if (actualBalance < recordedBalance) {
balances[account][token] = actualBalance;
emit BalanceReconciled(account, token, actualBalance);
}
}
Complete Implementation with Safeguards
contract TokenDivider {
using SafeERC20 for IERC20;
function transferErcTokens(
address nftAddress,
address to,
uint256 amount
) external nonReentrant whenNotPaused {
_validateInputs(nftAddress, to, amount);
ERC20Info memory tokenInfo = _validateTokenInfo(nftAddress);
address tokenAddress = tokenInfo.erc20Address;
uint256 currentAllowance = IERC20(tokenAddress).allowance(
msg.sender,
address(this)
);
require(currentAllowance >= amount, "Insufficient allowance");
require(
balances[msg.sender][tokenAddress] >= amount,
"Insufficient balance"
);
IERC20(tokenAddress).safeTransferFrom(msg.sender, to, amount);
balances[msg.sender][tokenAddress] -= amount;
balances[to][tokenAddress] += amount;
emit TokensTransferred(
msg.sender,
to,
tokenAddress,
amount,
block.timestamp
);
}
}
Additional Recommendations
Implement Approval Tracking
mapping(address => mapping(address => uint256)) public approvalUsage;
function _trackApprovalUsage(
address token,
address owner,
uint256 amount
) private {
approvalUsage[token][owner] += amount;
}
Add Pre-transfer Checks
modifier validTransfer(address token, uint256 amount) {
require(token != address(0), "Invalid token");
require(amount > 0, "Invalid amount");
require(
IERC20(token).allowance(msg.sender, address(this)) >= amount,
"Insufficient allowance"
);
_;
}