Back to Blog

#10 Panoptic Incident: XOR Linearity Breaks the Position Fingerprint Scheme

Code Auditing
February 13, 2026
8 min read

On August 25, 2025, with the assistance of Cantina and Seal911, Panoptic conducted a white hat rescue operation, securing approximately $400K in at-risk funds [1]. The root cause was a flaw in the construction of s_positionsHash: the protocol used XOR to aggregate Keccak256 hashes of position IDs into a single fingerprint. While individual Keccak256 hashes remain collision-resistant, XOR's mathematical linearity makes the composite fingerprint insecure. An attacker can generate a set of forged position IDs whose XOR-aggregated hash matches any target fingerprint, bypassing the protocol's position verification and withdrawing collateral without repaying debt.

Background

Panoptic is a decentralized perpetual options trading protocol built on Ethereum that enables users to trade put and call options.

The function withdraw() has an input parameter positionList, which consists of a set of tokenIds. Each tokenId represents a position. The function withdraw() checks the debt status of each position based on s_positionsHash and then retrieves the user's collateral.

To save gas, the protocol does not store every position (i.e., tokenId) of the user. Instead, it calculates a fingerprint based on all the user's tokenIds and records it in s_positionsHash. The most significant 8 bits of each s_positionsHash represent numLegs, while the lower 248 bits represent the user position hash.

For each passed tokenId, the protocol calculates its hash, takes the lower 248 bits, and updates the user position hash by performing a bitwise XOR with the lower 248 bits of the current s_positionsHash.

Simultaneously, countLegs is adjusted based on the numerical range of the tokenId: it remains unchanged when tokenId is below 2642^{64}, and increases by 1, 2, or 3 when in the ranges (264,2112)(2^{64}, 2^{112}), (2112,2168)(2^{112}, 2^{168}), and (2168,2208)(2^{168}, 2^{208}) respectively. This is finally written to the top 8 bits of s_positionsHash to update numLegs.

Vulnerability Analysis

The root cause is a flaw in the contract's algorithm for constructing s_positionsHash, specifically the use of the XOR operation to aggregate Keccak256 hash results. While a single hash function remains secure, the mathematical linearity of the XOR operation renders the overall fingerprint algorithm (i.e., the XOR sum of hashes) insecure [2].

Linearity implies that an attacker does not need to break the hash function itself (i.e., reversing the tokenId from the hash). Instead, they can employ a combinatorial strategy: generating a large number of random tokenIds, calculating their Keccak256(tokenId), and from these hash values, selecting a specific subset such that the XOR sum of these tokenId hashes exactly matches the victim's target fingerprint.

This allows an attacker to pass a set of forged tokenIds when calling withdraw() to pass the health check and retrieve all collateral corresponding to s_positionsHash.

Theory

Assume the user's position fingerprint s_positionsHash has a user position hash of TT and numLegs of kk, with user tokenIds {t1,t2,…,tn}\{t_1, t_2, \dots, t_n\}. Thus:

⨁i=1k[Hash(ti)(mod2248)]=T\bigoplus_{i=1}^{k} [\text{Hash}(t_i) \pmod{2^{248}}] = T

Here, each 248-bit hash value can be viewed as a 248-dimensional vector.

Hash(ti)(mod2248)=[b0,b1,…,b247]T,where bi∈{0,1}\text{Hash}(t_i) \pmod{2^{248}} = [b_0, b_1, \dots, b_{247}]^T, where\ b_i \in \{0, 1\}

Therefore, TT resides in a 248-dimensional space composed of 0s and 1s (F2248\mathbb{F}_2^{248}). In this space, the XOR operation is equivalent to vector addition (Appendix I).

The attacker's goal is to find nn 248-dimensional vectors {v1,v2,…,vn}\{v_1, v_2, \dots, v_n\} from all of the available vectors such that the lower 248 bits of their XOR sum equal TT. Thus, we can formulate the attacker's objective as a system of linear equations:

x1v1+x2v2+⋯+xnvn=Tx_1 v_1 + x_2 v_2 + \dots + x_n v_n = T

Specifically, we do not need to try to construct TT directly. Instead, we select nn linearly independent hash vectors {v1,v2,…,vn}\{v_1, v_2, \dots, v_n\} (where nn equals the dimension 248) and use them as column vectors to construct an n×nn \times n matrix AA:

A=[v1,v2,…,vn]A = [v_1, v_2, \dots, v_n]

The problem then transforms into finding a coefficient vector xx such that:

A⋅x=TA \cdot x = T

Where x=[x1,x2,…,xn]Tx = [x_1, x_2, \dots, x_n]^T, and xi∈{0,1}x_i \in \{0, 1\}.

According to linear algebra theory, as long as matrix AA is full rank, it spans the entire nn-dimensional space. This means that for any target TT, the system of equations has a unique solution. After solving for xx, we simply retain those vectors viv_i for which xi=1x_i=1; their XOR sum is TT.

Example

To facilitate understanding, we demonstrate this construction process with a case study. Assume the vectors are 3-dimensional, with each dimension consisting only of 0 or 1, and the target hash value is 101, i.e., T=[1,0,1]TT = [1, 0, 1]^T.

Next, we randomly generate 3 hash values:

  • v1=[1,1,0]Tv_1 = [1, 1, 0]^T
  • v2=[0,1,0]Tv_2 = [0, 1, 0]^T
  • v3=[0,1,1]Tv_3 = [0, 1, 1]^T

We construct matrix AA using these three vectors as columns and set up the equation Ax=TAx=T:

[100111001][x1x2x3]=[101]\begin{bmatrix} 1 & 0 & 0 \\ 1 & 1 & 1 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \\ x_3 \end{bmatrix} = \begin{bmatrix} 1 \\ 0 \\ 1 \end{bmatrix}

Solving via Gaussian elimination, we obtain x=[1,0,1]Tx = [1, 0, 1]^T. This means we need to select v1v_1 and v3v_3 (corresponding to x1=1,x3=1x_1=1, x_3=1), and their XOR sum exactly equals the target TT.

Selecting n Linearly Independent Vectors

As previously mentioned, to solve Ax=TAx=T, we need to construct a full-rank matrix AA. Given that we are dealing with an extremely large vector space (m=2248m = 2^{248}), the core question is: How do we quickly select nn linearly independent vectors from this vast space?

Consider the simplest method: randomly generating nn tokenIds and selecting their hash results as vectors.

Over F2\mathbb{F}_2, the probability PP that nn randomly selected nn-dimensional vectors form a full-rank matrix is:

P(n)=∏k=0n−1(1−2k2n)P(n) = \prod_{k=0}^{n-1} \left(1 - \frac{2^k}{2^n}\right)

When nn is large (in this example, n=248n=248), this probability converges to a constant (Appendix II):

lim⁡n→∞P(n)≈0.28879\lim_{n \to \infty} P(n) \approx 0.28879

This means each random attempt has approximately a 28.9% probability of success. On average, an attacker only needs about 3.5 attempts to find a set of linearly independent vectors. Therefore, the computational cost is extremely low, and the attacker can quickly construct a matrix AA that satisfies the conditions.

To control the countLegs in the top 8 bits, we only need to adjust the range of the randomly generated tokenId values based on the target countLegs.

For example, if we want the numLegs in the forged fingerprint to be 0, we simply ensure that the nn randomly generated tokenIds are all less than 2642^{64}. Since the leg increment for tokenIds in this range is 0, regardless of which vectors the Gaussian elimination solution xx selects, the final accumulated numLegs will inevitably be 0.

Attack Analysis

The white hat rescuer initiated multiple rescue transactions. For simplicity, the following discussion is based on just one of these transactions [3].

The core logic consists of the following 5 steps:

  1. Borrow 0.23e8 WBTC and 28e18 WETH via flash loan from Aave.
  2. Deposit 0.23e8 WBTC and 28e18 WETH into the poWBTC contract and the 0x1f8d_poWETH contract.
  3. Call mintOptions() of the PanopticPool contract with a normal positionIdList to borrow funds and open a leveraged position.
  4. Call withdraw(), passing the forged tokenIds. Because these forged positions have a positionSize of 0, the function returns tokenRequired as 0, meaning the total collateral required for all positions is erroneously calculated as zero. Meanwhile, the s_positionsHash generated by these tokenIds is exactly the same as the one generated in step 3, allowing the rescuer to retrieve all collateral without repaying any debt.
  5. Repay the flash loan and execute the next round.

Summary

This incident highlights two compounding flaws that together enabled unauthorized collateral withdrawal.

  • XOR is not a secure aggregation function for hashes. Keccak256 is collision-resistant, but XOR's linearity means the XOR sum of multiple hashes does not inherit that property. Constructing a set of inputs whose hashes XOR to any target value reduces to solving a system of linear equations over F2\mathbb{F}_2, which is computationally trivial. Hash composition requires operations that preserve collision resistance, such as concatenation followed by re-hashing.
  • Missing validation of position IDs. The protocol did not verify whether passed positionIds corresponded to valid option positions. Values below 2642^{64} carry a countLegs of 0 and a positionSize of 0, meaning the forged positions incur no debt. This allowed the attacker to pass the health check with zero collateral requirement while matching the target fingerprint.

Reference

  1. https://x.com/Panoptic_xyz/status/1961187739866644524

  2. https://cseweb.ucsd.edu/~mihir/papers/inchash.pdf

  3. https://app.blocksec.com/explorer/tx/eth/0x67a45dfe5ff4b190058674d7c791bbdc48e889f319f937c24fa13a5f9093f088

Appendix

The following two appendices provide a mathematical explanation and proof of the statements in the main text, namely that the XOR operation is equivalent to vector addition (Appendix I) and the probability that a random matrix is full rank (Appendix II).

Appendix I

In the finite field F2={0,1}\mathbb{F}_2 = \{0, 1\}, addition is defined as addition modulo 2:

0+0=00+1=11+0=11+1=0(mod2)\begin{aligned} 0 + 0 &= 0 \\ 0 + 1 &= 1 \\ 1 + 0 &= 1 \\ 1 + 1 &= 0 \pmod 2 \end{aligned}

Observation shows this is identical to the Exclusive OR (XOR, symbol ⊕\oplus) logic operation.

For the nn-dimensional vector space F2n\mathbb{F}_2^n (in this case n=248n=248), the addition of two vectors u=[u1,…,un]Tu = [u_1, \dots, u_n]^T and v=[v1,…,vn]Tv = [v_1, \dots, v_n]^T is defined as component-wise modulo 2 addition:

u+v=[u1+v1(mod2)⋮un+vn(mod2)]=[u1⊕v1⋮un⊕vn]=u⊕vu + v = \begin{bmatrix} u_1 + v_1 \pmod 2 \\ \vdots \\ u_n + v_n \pmod 2 \end{bmatrix} = \begin{bmatrix} u_1 \oplus v_1 \\ \vdots \\ u_n \oplus v_n \end{bmatrix} = u \oplus v

Therefore, in the F2n\mathbb{F}_2^{n} vector space, vector addition is equivalent to bitwise XOR operation on vector components.

Appendix II

To make the matrix full rank, these nn vectors must be linearly independent.

The first vector v1v_1 can be any vector in F2n\mathbb{F}_2^n except the zero vector, providing 2n−12^n - 1 choices; the second vector v2v_2 must not reside in the subspace spanned by {v1}\{v_1\}, which contains 212^1 vectors, leaving 2n−22^n - 2 choices; following this logic, the kk-th vector vkv_k cannot be in the subspace spanned by the previous k−1k-1 vectors, resulting in 2n−2k−12^n - 2^{k-1} possible choices.

The total number of such matrices (i.e., the order of GL(n,F2)GL(n, \mathbb{F}_2)) is:

N=∏k=0n−1(2n−2k)=(2n−1)(2n−2)(2n−4)⋯(2n−2n−1)N = \prod_{k=0}^{n-1} (2^n - 2^k) = (2^n - 1)(2^n - 2)(2^n - 4)\cdots(2^n - 2^{n-1})

And the total number of all possible n×nn \times n matrices is (2n)n=2n2(2^n)^n = 2^{n^2}.

Thus, the probability PP is:

P=∏k=0n−1(2n−2k)2n2=∏k=0n−1(2n−2k2n)=∏k=0n−1(1−2k2n)P = \frac{\prod_{k=0}^{n-1} (2^n - 2^k)}{2^{n^2}} = \prod_{k=0}^{n-1} \left( \frac{2^n - 2^k}{2^n} \right) = \prod_{k=0}^{n-1} (1 - \frac{2^k}{2^n})

Letting j=n−kj = n - k, we can rewrite this expression as:

P=∏j=1n(1−12j)P = \prod_{j=1}^{n} \left(1 - \frac{1}{2^j}\right)

As n→∞n \to \infty, this product converges to a constant:

P≈0.288788P \approx 0.288788

This means that for large nn, the probability that a random matrix is full rank is approximately 28.9%.


About BlockSec

BlockSec is a full-stack blockchain security and crypto compliance provider. We build products and services that help customers to perform code audit (including smart contracts, blockchain and wallets), intercept attacks in real time, analyze incidents, trace illicit funds, and meet AML/CFT obligations, across the full lifecycle of protocols and platforms.

BlockSec has published multiple blockchain security papers in prestigious conferences, reported several zero-day attacks of DeFi applications, blocked multiple hacks to rescue more than 20 million dollars, and secured billions of cryptocurrencies.

Sign up for the latest updates
~$5.57M Lost: X100 Freezer, NEM Exploits | BlockSec Weekly
Security Insights

~$5.57M Lost: X100 Freezer, NEM Exploits | BlockSec Weekly

This weekly report covers two security incidents on Ethereum and BNB Chain from October 19 to October 25, 2026. It examines a flash-loan-funded governance takeover of X100 Freezer and a tax-settlement flaw affecting a NEM token liquidity pool on BNB Chain. Estimated losses within the report's scope total approximately $5.57M: ~$4.75M from the representative X100 transaction and ~$820K from the NEM pool incident. The total excludes separately reported follow-on X100 extractions.

~$3.87M Lost: Autheo Network, UNCX Hacks | BlockSec Weekly
Security Insights

~$3.87M Lost: Autheo Network, UNCX Hacks | BlockSec Weekly

This weekly report covers 3 security incidents from October 12 to October 18, 2026, with approximately $3.87M in total estimated losses. Autheo Network suffered a ~$1.76M private key compromise, Qubic Wallet lost ~$1.30M due to weak seed generation, and UNCX Network's Smart Token Vesting V2 contracts suffered an ~$807K accounting error across Ethereum, BNB Chain, Base, and Robinhood Chain. The highlighted UNCX analysis explains how a flawed aggregate share calculation mixed vesting tranches created at different token-to-share ratios and enabled excess withdrawals from shared pools.

~$542K Lost: MakerDaoBot Exploit | BlockSec Weekly
Security Insights

~$542K Lost: MakerDaoBot Exploit | BlockSec Weekly

This report covers 1 security incident on Ethereum from 2026/10/05 - 2026/10/11, with an estimated loss of approximately $542K. An attack contract deployed a malicious adapter and passed it to a MakerDaoBot function that lacked access control, causing the bot to grant the adapter permission over its internal MakerDAO balances. The adapter settled four legacy ETH-A auctions and converted 200 units of ETH-A collateral to WETH; the attack contract unwrapped it and sent 200 ETH to the attacker.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit