Abstract
We discovered a critical timing vulnerability in Dx Protocol's token locking mechanism through EVM decompilation. The unlockToken() function contains a race condition that allows attackers to withdraw locked tokens multiple times, potentially draining the entire contract balance. This analysis demonstrates how decompiling unverified contracts can reveal hidden security flaws that put millions of dollars at risk.
We decompiled an unverified Dx Protocol contract on BSC using EVMDecompiler and uncovered a logic flaw in unlockToken()
.
The issue
If unlockToken()
is called before unlockTime
, the function still transfers tokens to the caller but leaves isLocked
set to true
. This allows the caller to repeatedly invoke unlockToken()
and withdraw again and again.
Decompiled fragment
function unlockToken(uint256 _tokenId) public {
require(tokenLocks[msg.sender][_tokenId].isLocked, "Token is already unlocked");
require(tokenLocks[msg.sender][_tokenId].unlockTime != 0, "Token is not locked");
if (block.timestamp > tokenLocks[msg.sender][_tokenId].unlockTime) {
tokenLocks[msg.sender][_tokenId].isLocked = false;
}
uint256 amount = tokenLocks[msg.sender][_tokenId].amount;
require(IERC20(tokenLocks[msg.sender][_tokenId].tokenAddress).balanceOf(address(this)) >= amount, "Not enough balance");
require(IERC20(tokenLocks[msg.sender][_tokenId].tokenAddress).transfer(msg.sender, amount), "Failed transfer");
emit Unlocked(msg.sender, tokenLocks[msg.sender][_tokenId].tokenAddress, amount, block.timestamp);
}
Why it breaks
isLocked
is only flipped tofalse
whenblock.timestamp > unlockTime
.- When called earlier, the state remains locked but the transfer proceeds, enabling repeated withdrawals.
Impact
Estimated exposure was approximately $5.2M, hidden entirely in bytecode due to the contract being unverified at the time of analysis.
Fix
- Require
block.timestamp >= unlockTime
before any transfer logic. - Set
isLocked = false
(and/or zero outunlockTime
) before transferring tokens to close re-entry style repeated calls.
Reproduce
- Load the address in the EVMDecompiler.
- Locate
unlockToken()
in the decompiled output. - Observe the conditional write to
isLocked
versus the unconditional transfer path.
Disclosure note: This post is for educational purposes and responsible vulnerability awareness.