Dx Protocol: Repeated Withdrawals via unlockToken() Timing Bug

BSC • Address: 0xeb3a…e449

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

Impact

Estimated exposure was approximately $5.2M, hidden entirely in bytecode due to the contract being unverified at the time of analysis.

Fix

Reproduce

  1. Load the address in the EVMDecompiler.
  2. Locate unlockToken() in the decompiled output.
  3. Observe the conditional write to isLocked versus the unconditional transfer path.

Disclosure note: This post is for educational purposes and responsible vulnerability awareness.