F-2025-0021·uninitialized-state

Uninitialized lastWithdrawalTime allows immediate first withdrawal bypassing 30-day timelock

Acknowledgedrafflelotteryvrf
TL;DR

lastWithdrawalTime defaults to 0, so the first call to withdrawForStaking compares block.timestamp >= 30 days, which is always true.

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

Description

The withdrawForStaking function enforces a 30-day minimum interval between withdrawals to protect user funds from rapid extraction. However, the lastWithdrawalTime state variable is never initialized and defaults to 0, allowing the first withdrawal to bypass the timelock completely.

solidity
function withdrawForStaking(uint256 amount) external onlyOwner {
require(msg.sender == founder || msg.sender == owner(), "Only founder");
require(stakingActive, "Staking not active");
require(amount > 0, "Amount must be > 0");
require(
block.timestamp >= lastWithdrawalTime + MIN_WITHDRAWAL_INTERVAL,
"Must wait 30 days"
);
uint256 availableBalance = stablecoin.balanceOf(address(this));
require(availableBalance >= amount, "Insufficient balance");
lastWithdrawalTime = block.timestamp;
totalWithdrawnForStaking += amount;
require(stablecoin.transfer(founder, amount), "Transfer failed");
emit FundsWithdrawnForStaking(founder, amount);
}

The timelock check at line 104-105 compares block.timestamp against lastWithdrawalTime + MIN_WITHDRAWAL_INTERVAL. When lastWithdrawalTime = 0, this becomes block.timestamp >= 0 + 30 days, which is always true since current Unix timestamps are far greater than 30 days (current time is ~1.7 billion seconds since epoch).

The 30-day withdrawal timelock is bypassed for the first withdrawal, allowing immediate extraction of all accumulated treasury funds. This defeats the purpose of the timelock protection, which is meant to prevent rapid fund extraction and provide transparency/security for users who contribute to the treasury through lottery participation. While the timelock functions correctly after the first withdrawal, the initial bypass creates a window where user funds are not adequately protected.

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);
}
04Section · Resolution

Resolution

Nexalo: Removed withdrawForStaking function.

Zealynx: With this design choice, the protocol no longer supports direct withdrawal of treasury funds for staking purposes.

F-2025-0021

oog
zealynx

Smart Contract Security Digest

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

© 2026 Zealynx