The Merkle proof verifier (VoidboundMerkle._merkleProof) does not enforce the required proof length.
By choosing a blade id with a 1-bit in the right place and crafting a Blade whose leaf equals an
internal node of the relic tree, you can produce a short proof that still validates against the
current world root. Then bind a blade with edge = relic0.sigil (3e12) and one-shot the Shogun.
This writeup matches the live instance used at:
0xe6510CC3cD4E1a2ae336c074E1d6015994e5b24f (Sepolia)
VoidboundMerkle._merkleProof accepts any proof length:
for (uint256 i; i < proof.length; i++) { ... }
return root == hashed;
So if we can make hashed equal the root after fewer steps, the proof passes.
We exploit this by:
That lets us “skip” most of the Merkle tree.
performKata(bytes) forbids a direct call to enterSanctum() by selector, but does not check
calldata length vs payload length. We can call performKata() with a payload that contains
enterSanctum() later in calldata and still passes shadowTorii.
Minimal helper contract (GatePass) that calls performKata() with a crafted payload works.
Once enterSanctum() runs, whitelist[tx.origin] = true.
Call:
pledgeClan(0)
awakenRonin()
attuneRelic() adds blade.tempo to relics[0].attunement.
Use mirrorRite to call voidAttuneBatch in one go. For example:
Then call:
mirrorRite(abi.encodeWithSelector(
voidAttuneBatch.selector,
[6..21]
))
Verify:
getRelic(0).attunement == 129
bindBlade requires the blade id already exists and is owned.
So create blades until:
getBladeCount() == 130
Goal: make merkleizeBlade(blade) == b1, the right half of relic0’s Merkle leaf.
Relic leaf structure:
R0 = hash(b0, b1)
b1 = hash(a2, a3)
a2 = hash(hash(attunement), hash(sigil))
a3 = hash(hash(isSealed), hash(isSealed))
To match this with a Blade:
Blade.id = attunement = 129
Blade.edge = relic0.sigil (3_000_000_000_000)
Blade.tempo = 1
Blade.roninId = 1 // because bool true hashes same as uint256(1)
So:
merkleizeBlade(Blade) == b1
Now choose blade id 129.
Its Merkle path bits match the internal-node shortcut, allowing a short proof.
The proof depends on current on-chain state (blades_root changes as you forge). Use the provided script:
python E:\ctf\compute_proof.py
It prints:
Bind:
bindBlade(
[129, 3000000000000, 1, 1],
[<9 hashes from script>]
)
Then:
duelShogun()
Damage = edge * tempo + level = 3e12, so the Shogun dies instantly.
E:\ctf\compute_proof.py — generates the correct proof for the current state.mirrorRite, raise gas limit (~1,000,000).