diff --git a/src/Curta/1_TwoTimesFourIsEight/Exploit.t.sol b/src/Curta/1_TwoTimesFourIsEight/Exploit.t.sol new file mode 100644 index 0000000..307f52f --- /dev/null +++ b/src/Curta/1_TwoTimesFourIsEight/Exploit.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test, console2} from "forge-std/Test.sol"; +import {Curta} from "../general/CurtaLocal.sol"; +import {TwoTimesFourIsEight} from "./challenge/Challenge.sol"; + +contract ExploitTest is Test { + address playerAddr = makeAddr("player"); + Curta curta; + TwoTimesFourIsEight puzzle; + uint256 puzzleId = 1; + + function setUp() public { + curta = new Curta(); + curta.setPuzzleId(puzzleId - 1); + puzzle = new TwoTimesFourIsEight(); + curta.addPuzzle(puzzle, 0); + vm.deal(playerAddr, 1 ether); + } + + function test() public { + vm.startPrank(playerAddr, playerAddr); + + curta.solve(puzzleId, 58841883346347349032075282154728593374741534902604841931998904142147573277043); + + vm.stopPrank(); + } +} diff --git a/src/Curta/1_TwoTimesFourIsEight/challenge/Challenge.sol b/src/Curta/1_TwoTimesFourIsEight/challenge/Challenge.sol new file mode 100644 index 0000000..89b5ca3 --- /dev/null +++ b/src/Curta/1_TwoTimesFourIsEight/challenge/Challenge.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.17; + +import "../../general/IPuzzle.sol"; + +/// @title 2 × 4 = 8 +/// @custom:subtitle Sodoku +/// @author fiveoutofnine +/// @notice A modified version of the classic Sodoku puzzle, for an 8 × 8 grid. +/// As usual, each row and column must contain [1, ..., 8] exactly once. +/// However, unlike in regular Sodoku, we now check for 2 × 4 subgrids, rather +/// than 3 × 3 subgrids. +contract TwoTimesFourIsEight is IPuzzle { + /// @notice A mapping of from indices to which checks must be performed at + /// that index. + /// @dev We reserve 3 bits for each check as follows: + /// * 0th bit is `1`: check subgrid; + /// * 1st bit is `1`: check column; + /// * 2nd bit is `1`: check row. + /// + /// For clarity, the following table lays out the bitpacked values: + /// | Index | Row | Column | Subgrid | Value | + /// |-------+---------+---------+---------+-------| + /// | 0 | 1 | 1 | 1 | 0b111 | + /// | 1 | 0 | 1 | 0 | 0b010 | + /// | 2 | 0 | 1 | 0 | 0b010 | + /// | 3 | 0 | 1 | 0 | 0b010 | + /// | 4 | 0 | 1 | 1 | 0b011 | + /// | 5 | 0 | 1 | 0 | 0b010 | + /// | 6 | 0 | 1 | 0 | 0b010 | + /// | 7 | 0 | 1 | 0 | 0b010 | + /// | 8 | 1 | 0 | 0 | 0b100 | + /// | 16 | 1 | 0 | 1 | 0b101 | + /// | 20 | 0 | 0 | 1 | 0b001 | + /// | 24 | 1 | 0 | 0 | 0b100 | + /// | 32 | 1 | 0 | 1 | 0b101 | + /// | 36 | 0 | 0 | 1 | 0b001 | + /// | 40 | 1 | 0 | 0 | 0b100 | + /// | 48 | 1 | 0 | 1 | 0b101 | + /// | 52 | 0 | 0 | 1 | 0b001 | + /// | 56 | 1 | 0 | 0 | 0b100 | + uint256 private constant CHECKS = 0x400010005000000040001000500000004000100050000000422232227; + + /// @notice A bitpacked value that indicates how many bits to shift by to + /// get to the next value in the row. + /// @dev We reserve 6 bits for each value, and the following are packed + // left-to-right: `[4, 4, 4, 4, 4, 4, 4, 4]`. + uint256 private constant ROW_SHIFTS = 0x104104104104; + + /// @notice A bitpacked value that indicates how many bits to shift by to + /// get to the next value in the column. + /// @dev We reserve 6 bits for each value, and the following are packed + // left-to-right: `[32, 32, 32, 32, 32, 32, 32, 32]`. + uint256 private constant COL_SHIFTS = 0x820820820820; + + /// @notice A bitpacked value that indicates how many bits to shift by to + /// get to the next value in the 2 × 4 subgrid. + /// @dev We reserve 6 bits for each value, and the following are packed + // left-to-right: `[4, 4, 4, 20, 4, 4, 4, 4]`. + uint256 private constant SUBGRID_SHIFTS = 0x104104504104; + + /// @notice A bitmap to denote that each of [1, ..., 8] has been seen. + /// @dev Bits 1-8 should be set to 1, with everything else set to 0 (i.e. + /// `0b111111110 = 0xFE`). + uint256 private constant FILLED_BITMAP = 0x1FE; + + /// @inheritdoc IPuzzle + function name() external pure returns (string memory) { + return unicode"2 × 4 = 8"; + } + + /// @inheritdoc IPuzzle + function generate(address _seed) external pure returns (uint256) { + uint256 seed = uint256(keccak256(abi.encodePacked(_seed))); + uint256 puzzle; + + // We use this to keep track of which indices [0, ..., 63] have been + // filled. See the next comment for why the value is initialized to + // `1 << 64`. + uint256 bitmap = 1 << 64; + // Note that the bitmap only intends on reserving bits 0-63 to represent + // the slots that have been filled. Thus, if we set `index` to 64, it + // is a sentinel value that will always yield 0 when using it to + // retrieve from the bitmap. + uint256 index = 64; + // We fill the puzzle randomly with 1 of [1, ..., 8]. This way, every + // puzzle is solvable. + for (uint256 i = 1; i < 9;) { + // We have exhausted the seed, so stop iterating. + if (seed == 0) break; + + // Loop through until we find an unfilled index. + while ((bitmap >> index) & 1 == 1 && seed != 0) { + // Retrieve 6 random bits from `seed` to determine which index + // to fill. + index = seed & 0x3F; + seed >>= 6; + } + // Set the bit in the bitmap to indicate that the index has + // been filled. + bitmap |= 1 << index; + + // Place the number into the slot that was just filled. + puzzle |= (i << (index << 2)); + index = 64; + unchecked { + ++i; + } + } + + return puzzle; + } + + /// @inheritdoc IPuzzle + function verify(uint256 _start, uint256 _solution) external pure returns (bool) { + // Iterate through the puzzle. + for (uint256 index; index < 256;) { + // Check that the starting position is included in the solution. + if (_start & 0xF != 0 && _start & 0xF != _solution & 0xF) { + return false; + } + + // Retrieve how many checks to perform. + uint256 checks = (CHECKS >> index) & 7; + if (checks & 4 == 4 && !check(_solution, ROW_SHIFTS)) return false; + if (checks & 2 == 2 && !check(_solution, COL_SHIFTS)) return false; + if (checks & 1 == 1 && !check(_solution, SUBGRID_SHIFTS)) { + return false; + } + + _start >>= 4; + _solution >>= 4; + unchecked { + index += 4; + } + } + + return true; + } + + /// @notice Checks whether a row, column, or box is filled in a valid way. + /// @param _shifted The puzzle shifted to the index it should start checking + /// from. + /// @param _shifts A bitpacked value that indicates how many bits to shift + /// by after each iteration in the loop. + /// @return Whether the check is valid. + function check(uint256 _shifted, uint256 _shifts) internal pure returns (bool) { + uint256 shifted = _shifted; + // Used to keep track of which numbers [1, ..., 8] have been seen. + uint256 bitmap; + + while (_shifts != 0) { + // Set the bit in the bitmap to indicate that the number has been + // seen. + bitmap |= 1 << (shifted & 0xF); // `shifted & 0xF` reads the number. + // Retrieve 6 bits from `_shifts` to determine how many bits to + // shift the puzzle by. + shifted >>= (_shifts & 0x3F); + _shifts >>= 6; + } + + return bitmap == FILLED_BITMAP; + } +} diff --git a/src/Curta/1_TwoTimesFourIsEight/solve.py b/src/Curta/1_TwoTimesFourIsEight/solve.py new file mode 100644 index 0000000..f9a7d5d --- /dev/null +++ b/src/Curta/1_TwoTimesFourIsEight/solve.py @@ -0,0 +1,41 @@ +import neko.algo.sudoku as sudoku + +def puzzle_to_2d_array(puzzle): + # Convert the puzzle to a binary string + puzzle_bin = format(puzzle, '0256b') + + # Split the binary string into 4-bit chunks + cells = [puzzle_bin[i:i+4] for i in range(0, len(puzzle_bin), 4)] + + # Convert each 4-bit chunk to an integer + cells = [int(cell, 2) for cell in cells] + + # Group the cells into rows to form a 2D array + puzzle_2d = [cells[i:i+8] for i in range(0, len(cells), 8)] + + return puzzle_2d + +def array_to_puzzle(array): + # Convert the 2D array to a 1D array + cells = [cell for row in array for cell in row] + + # Convert each cell to a 4-bit binary string + cells = [format(cell, '04b') for cell in cells] + + # Join the 4-bit binary strings into a single binary string + puzzle_bin = ''.join(cells) + + # Convert the binary string to an integer + puzzle = int(puzzle_bin, 2) + + return puzzle + + +puzzle = 1961977486345643953169794982364451687158713042345442410496 +instance = puzzle_to_2d_array(puzzle) +assert array_to_puzzle(instance) == puzzle + +answer = array_to_puzzle(sudoku.solve(instance, 1, N=8, MI=2, MJ=4)[0]) + +print(answer) + diff --git a/src/Curta/general/CurtaLocal.sol b/src/Curta/general/CurtaLocal.sol new file mode 100644 index 0000000..3e09d80 --- /dev/null +++ b/src/Curta/general/CurtaLocal.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +// from: https://github.com/waterfall-mkt/curta/blob/main/src/Curta.sol +pragma solidity ^0.8.17; + +import { ICurta } from "./ICurta.sol"; +import { IPuzzle } from "./IPuzzle.sol"; + +contract Curta is ICurta { + uint32 public puzzleId; + mapping(uint32 => PuzzleData) public getPuzzle; + + function solve(uint32 _puzzleId, uint256 _solution) external payable { + PuzzleData memory puzzleData = getPuzzle[_puzzleId]; + IPuzzle puzzle = puzzleData.puzzle; + + if (!puzzle.verify(puzzle.generate(msg.sender), _solution)) { + revert IncorrectSolution(); + } + + emit SolvePuzzle({ id: _puzzleId, solver: msg.sender, solution: _solution, phase: 0 }); + } + + function addPuzzle(IPuzzle _puzzle, uint256 /* _tokenId */) external { + uint32 curPuzzleId = ++puzzleId; + unchecked { + getPuzzle[curPuzzleId] = PuzzleData({ + puzzle: _puzzle, + addedTimestamp: uint40(block.timestamp), + firstSolveTimestamp: 0 + }); + } + } + + function setPuzzleId(uint32 _puzzleId) external { + puzzleId = _puzzleId; + } +} diff --git a/src/Curta/general/ICurta.sol b/src/Curta/general/ICurta.sol new file mode 100644 index 0000000..b490a9b --- /dev/null +++ b/src/Curta/general/ICurta.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +// from: https://github.com/waterfall-mkt/curta/blob/main/src/interfaces/ICurta.sol +pragma solidity ^0.8.17; +import { IPuzzle } from "./IPuzzle.sol"; + +interface ICurta { + + error IncorrectSolution(); + + event SolvePuzzle(uint32 indexed id, address indexed solver, uint256 solution, uint8 phase); + + struct PuzzleData { + IPuzzle puzzle; + uint40 addedTimestamp; + uint40 firstSolveTimestamp; + } + function solve(uint32 _puzzleId, uint256 _solution) external payable; +} diff --git a/src/Curta/general/IPuzzle.sol b/src/Curta/general/IPuzzle.sol new file mode 100644 index 0000000..f40c444 --- /dev/null +++ b/src/Curta/general/IPuzzle.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +// from https://github.com/waterfall-mkt/curta/blob/main/src/interfaces/IPuzzle.sol +pragma solidity ^0.8.17; + +interface IPuzzle { + function name() external pure returns (string memory); + + function generate(address _seed) external returns (uint256); + + function verify(uint256 _start, uint256 _solution) external returns (bool); +} diff --git a/src/ParadigmCTF2023/README.md b/src/ParadigmCTF2023/README.md index 052c498..37e1d18 100644 --- a/src/ParadigmCTF2023/README.md +++ b/src/ParadigmCTF2023/README.md @@ -1,4 +1,4 @@ -# Paradigm CTF 2023 (WIP) +# Paradigm CTF 2023 Paradigm CTF: https://twitter.com/paradigm_ctf @@ -10,8 +10,6 @@ Result: I spent the first several hours working on some jeopardy challenges and managed to solve Black Sheep, Grains of Sand, and Skill Based Game. After that, I dedicated the remainder of my time to King-of-the-Hill challenges. -Currently, only my solver codes for the jeopardy challenges are here. - --- ## Jeopardy