F-2025-0019·unchecked-return

Unsafe ERC20 transfer operations allow silent failures and incompatibility with non-standard tokens

Fixedrafflelotteryvrf
TL;DR

The protocol uses raw ERC20 transfer/transferFrom calls and require-wrapped transfers without SafeERC20, which can silently fail or break against non-standard tokens like USDT.

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

Description

The protocol uses two unsafe patterns for ERC20 token transfers throughout the codebase:

Pattern 1: Unchecked transfers

solidity
// TreasuryBTC.sol:277
function recoverERC20(address tokenAddress, uint256 amount) external onlyOwner {
require(tokenAddress != address(stablecoin), "Cannot recover stablecoin");
require(amount > 0, "Amount must be > 0");
IERC20(tokenAddress).transfer(owner(), amount); // No return value check
}

This pattern appears in:

  • TreasuryBTC.sol:277 - recoverERC20()
  • DonationVault.sol:266 - recoverTokens()
  • NexaloStaking.sol:286 - recoverToken()

These transfers have no return value validation. If the token returns false on failure (instead of reverting), the function continues execution as if the transfer succeeded, causing state updates without actual token movement.

Pattern 2: Require-wrapped transfers (compatibility issue)

solidity
// TreasuryBTC.sol:158
require(stablecoin.transfer(msg.sender, userReward), "Transfer failed");

This pattern assumes all ERC20 tokens return a boolean value. However, some tokens (like certain implementations) may:

  • Return nothing (void) instead of bool
  • Not fully comply with ERC20 standard return values
  • Cause unexpected behavior with strict ABI decoding

Additionally, the protocol doesn't handle tokens that return false on failure instead of reverting. While require() catches explicit reverts, it doesn't protect against tokens that return false to signal failure.

03Section · Recommendation

Recommendation

Adopt OpenZeppelin's SafeERC20 library for all ERC20 token interactions:

solidity
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract TreasuryBTC is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
IERC20 public immutable stablecoin;
IERC20 public immutable nxlToken;
// Replace all transfers:
// OLD: require(stablecoin.transfer(msg.sender, amount), "Transfer failed");
// NEW: stablecoin.safeTransfer(msg.sender, amount);
// OLD: require(stablecoin.transferFrom(msg.sender, address(this), amount), "Transfer failed");
// NEW: stablecoin.safeTransferFrom(msg.sender, address(this), amount);
// OLD: IERC20(token).transfer(owner(), amount);
// NEW: IERC20(token).safeTransfer(owner(), amount);
}

Apply this pattern to all contracts.

04Section · Resolution

Resolution

Nexalo: Fixed.

Zealynx: Verified. Fixed.

Status
Fixed
F-2025-0019

oog
zealynx

Smart Contract Security Digest

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

© 2026 Zealynx