Pieces Protocol

First Flight #32
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Invalid

Unchecked Integer Arithmetic in Token Balance Updates

Summary

The transferErcTokens function in the TokenDivider contract performs arithmetic operations on token balances without proper overflow checks. Despite Solidity 0.8.x's built-in overflow protection, the logic for balance updates could still be vulnerable to certain edge cases and potential arithmetic overflows.

Vulnerability Detail

Current implementation:

function transferErcTokens(address nftAddress, address to, uint256 amount) external {
// ... validation checks ...
// Unchecked balance updates
balances[msg.sender][tokenInfo.erc20Address] -= amount;
balances[to][tokenInfo.erc20Address] += amount; // Potential overflow
IERC20(tokenInfo.erc20Address).transferFrom(msg.sender, to, amount);
}

The vulnerability exists in two areas:

  1. No maximum balance validation

  2. No overflow check during balance addition

  3. No underflow protection for balance subtraction

Impact

This vulnerability could result in:

  1. Token balance corruption

  2. Unlimited token minting through overflow

  3. Loss of user funds

  4. Inconsistent state between actual ERC20 balances and internal balances

  5. Potential economic attacks

Tool Used

Foundry

Proof of Concept

contract BalanceOverflowExploit {
TokenDivider target;
constructor(address _target) {
target = TokenDivider(_target);
}
function exploitOverflow(address nftAddress) external {
// Setup maximum possible balance
uint256 maxBalance = type(uint256).max;
// First transfer to set up large balance
target.transferErcTokens(
nftAddress,
address(this),
maxBalance - 1
);
// Second transfer to trigger overflow
target.transferErcTokens(
nftAddress,
address(this),
2
);
// Balance becomes very small due to overflow
}
}
// Test Contract
contract BalanceTest {
function demonstrateOverflow(
TokenDivider divider,
address nftAddress
) public {
// Get maximum uint256 value
uint256 largeAmount = type(uint256).max;
try divider.transferErcTokens(
nftAddress,
address(this),
largeAmount
) {
revert("Should have failed");
} catch Error(string memory reason) {
require(
keccak256(bytes(reason)) == keccak256(bytes("Overflow")),
"Wrong error"
);
}
}
}

Recommended Mitigation Steps

  1. Implement Safe Math Checks

function _updateBalancesSafely(
address from,
address to,
address tokenAddress,
uint256 amount
) private {
// Check underflow for sender
uint256 senderBalance = balances[from][tokenAddress];
require(senderBalance >= amount, "Insufficient balance");
// Check overflow for receiver
uint256 receiverBalance = balances[to][tokenAddress];
require(
receiverBalance + amount >= receiverBalance,
"Balance overflow"
);
// Update balances
balances[from][tokenAddress] = senderBalance - amount;
balances[to][tokenAddress] = receiverBalance + amount;
}
  1. Add Balance Limits

contract TokenDivider {
uint256 public constant MAX_BALANCE = type(uint128).max;
function _validateBalance(uint256 balance, uint256 amount) private pure {
require(balance + amount <= MAX_BALANCE, "Balance limit exceeded");
}
}
  1. Complete Implementation Example

function transferErcTokens(
address nftAddress,
address to,
uint256 amount
) external nonReentrant {
// Validate inputs
_validateTransferInputs(nftAddress, to, amount);
ERC20Info memory tokenInfo = _validateTokenInfo(nftAddress);
// Safe balance update
_updateBalancesSafely(
msg.sender,
to,
tokenInfo.erc20Address,
amount
);
// Execute transfer
bool success = IERC20(tokenInfo.erc20Address).transferFrom(
msg.sender,
to,
amount
);
require(success, "Transfer failed");
emit TokensTransferred(
msg.sender,
to,
tokenInfo.erc20Address,
amount,
block.timestamp
);
}
function _validateTransferInputs(
address nftAddress,
address to,
uint256 amount
) private view {
require(amount > 0, "Invalid amount");
require(amount <= MAX_BALANCE, "Amount exceeds limit");
require(to != address(0), "Invalid recipient");
require(nftAddress != address(0), "Invalid NFT address");
}
  1. Add Balance Reconciliation

function reconcileBalances(
address tokenAddress
) external onlyOwner {
uint256 contractBalance = IERC20(tokenAddress).balanceOf(address(this));
uint256 recordedBalance = totalRecordedBalance[tokenAddress];
require(
contractBalance >= recordedBalance,
"Balance mismatch"
);
}

Additional Recommendations

  1. Implement Balance Tracking

mapping(address => uint256) public totalRecordedBalance;
function _trackBalance(address token, uint256 amount, bool isAdd) private {
if (isAdd) {
totalRecordedBalance[token] += amount;
} else {
totalRecordedBalance[token] -= amount;
}
}
  1. Add Emergency Pause

modifier whenNotPaused() {
require(!paused, "Transfers paused");
_;
}
function emergencyPause() external onlyOwner {
paused = true;
emit TransfersPaused(block.timestamp);
}

Updates

Lead Judging Commences

fishy Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Overflow / Underflow risks

Appeal created

0xalexsr Auditor
5 months ago
riceee Auditor
5 months ago
fishy Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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