├── notes.txt └── pinball.sol /notes.txt: -------------------------------------------------------------------------------- 1 | # mid-contract ipfs 2 | a26469706673582212205061227f565b8360015afa50505060005133146040026001016101408601527164736f6c6343a0fb380033 3 | 4 | # trailing ipfs 5 | a26469706673582212205b602060405133815281812081526060604483830137816000608061152b567364736f6c63430008090033 6 | 7 | -------------------------------------------------------------------------------- /pinball.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.9; 2 | 3 | library LibPinball { 4 | struct State { 5 | bytes ball; 6 | 7 | uint commandsOffset; 8 | uint commandsLength; 9 | 10 | uint totalTiltPrice; 11 | bool missionAvailable; 12 | uint currentMission; 13 | uint currentPowerup; 14 | 15 | uint location; 16 | uint32 rand; 17 | 18 | uint baseScore; 19 | uint scoreMultiplier; 20 | uint bonusScore; 21 | } 22 | 23 | function init(State memory state, bytes memory ball, address owner) internal returns (bool) { 24 | if (ball.length != 512) return false; 25 | 26 | if (ball[0] != 'P' || ball[1] != 'C' || ball[2] != 'T' || ball[3] != 'F') return false; 27 | 28 | state.ball = ball; 29 | state.commandsOffset = readUint16At(state, 4); 30 | state.commandsLength = readUint16At(state, 6); 31 | 32 | if (state.commandsOffset + state.commandsLength * 5 >= 512) return false; 33 | 34 | state.location = 0x42424242; 35 | 36 | state.scoreMultiplier = 1; 37 | 38 | state.rand = 1337; 39 | 40 | return true; 41 | } 42 | 43 | function readUint8At(State memory state, uint dataOffset) internal returns (uint8 result) { 44 | assembly { result := mload(add(add(mload(state), 1), dataOffset)) } 45 | } 46 | 47 | function readUint16At(State memory state, uint dataOffset) internal returns (uint16 result) { 48 | assembly { result := mload(add(add(mload(state), 2), dataOffset)) } 49 | } 50 | 51 | function readBytes4At(State memory state, uint dataOffset) internal returns (bytes4 result) { 52 | assembly { result := mload(add(add(mload(state), 0x20), dataOffset)) } 53 | } 54 | 55 | function readBytes32At(State memory state, uint dataOffset) internal returns (bytes32 result) { 56 | assembly { result := mload(add(add(mload(state), 0x20), dataOffset)) } 57 | } 58 | 59 | function readRand(State memory state) internal returns (uint16) { 60 | uint32 nextRand; 61 | unchecked { 62 | nextRand = state.rand * 1103515245 + 12345; 63 | } 64 | state.rand = nextRand; 65 | 66 | return uint16(nextRand >> 16); 67 | } 68 | } 69 | 70 | contract Pinball { 71 | using LibPinball for LibPinball.State; 72 | 73 | event Message(string message); 74 | event NewScore(uint id, address indexed who, uint score); 75 | 76 | struct Submission { 77 | address who; 78 | uint time; 79 | uint score; 80 | } 81 | 82 | Submission[] public submissions; 83 | 84 | Submission public bestSubmission; 85 | 86 | mapping(bytes32 => bool) public commitments; 87 | 88 | function makeCommitmentHash(bytes32 commitment, uint blockNumber) private returns (bytes32) { 89 | return keccak256(abi.encode(msg.sender, commitment, blockNumber)); 90 | } 91 | 92 | function insertCoins(bytes32 commitment) external { 93 | commitments[makeCommitmentHash(commitment, block.number)] = true; 94 | } 95 | 96 | function play(bytes memory ball, uint blockNumber) external { 97 | bytes32 commitment = makeCommitmentHash(keccak256(ball), blockNumber); 98 | require(commitments[commitment], "you didn't insert any coins"); 99 | require(block.number - blockNumber > 4, "please wait a bit after you insert your coins"); 100 | 101 | delete commitments[commitment]; 102 | 103 | LibPinball.State memory state; 104 | require(state.init(ball, msg.sender), "invalid ball"); 105 | 106 | for (uint i = 0; i < state.commandsLength; i++) { 107 | if (!tick(state, i)) break; 108 | 109 | state.bonusScore += 50; 110 | } 111 | 112 | uint finalScore = state.baseScore * state.scoreMultiplier + state.bonusScore; 113 | 114 | Submission memory submission = Submission({who: msg.sender, time: block.number, score: finalScore}); 115 | if (submission.score > bestSubmission.score) { 116 | bestSubmission = submission; 117 | } 118 | 119 | emit NewScore(submissions.length, msg.sender, finalScore); 120 | submissions.push(submission); 121 | } 122 | 123 | function tick(LibPinball.State memory state, uint commandNum) private returns (bool) { 124 | uint commandOffset = state.commandsOffset + commandNum * 5; 125 | 126 | uint8 command = state.readUint8At(commandOffset); 127 | uint16 dataOff = state.readUint16At(commandOffset + 1); 128 | uint16 dataLen = state.readUint16At(commandOffset + 3); 129 | 130 | if (dataOff + dataLen >= 512) return false; 131 | 132 | if (command == 0) { 133 | return false; 134 | } 135 | if (command == 1) { 136 | return pull(state, dataOff, dataLen); 137 | } 138 | if (command == 2) { 139 | return tilt(state, dataOff, dataLen); 140 | } 141 | if (command == 3) { 142 | return flipLeft(state, dataOff, dataLen); 143 | } 144 | if (command == 4) { 145 | return flipRight(state, dataOff, dataLen); 146 | } 147 | 148 | return false; 149 | } 150 | 151 | function pull(LibPinball.State memory state, uint16 dataOff, uint16 dataLen) private returns (bool) { 152 | if (state.location != 0x42424242) return false; 153 | 154 | state.location = state.readRand() % 100; 155 | state.baseScore += 1000; 156 | emit Message("GAME START"); 157 | 158 | return true; 159 | } 160 | 161 | function tilt(LibPinball.State memory state, uint16 dataOff, uint16 dataLen) private returns (bool) { 162 | if (state.location >= 100) return false; 163 | 164 | uint tiltSpend = state.readUint8At(dataOff); 165 | uint tiltAmount = state.readUint8At(dataOff + 1); 166 | uint tiltPrice = state.readRand() % 100; 167 | 168 | state.totalTiltPrice += tiltSpend; 169 | 170 | if (state.totalTiltPrice >= 100) { 171 | emit Message("TILT"); 172 | return false; 173 | } 174 | 175 | emit Message("TILT WARNING"); 176 | 177 | if (tiltPrice < tiltSpend) { 178 | if (state.location > 50) { 179 | if (state.location > 90 - tiltAmount) state.location = 90; 180 | else state.location += tiltAmount; 181 | } else if (state.location <= 50) { 182 | if (state.location < 10 + tiltAmount) state.location = 10; 183 | else state.location -= tiltAmount; 184 | } 185 | } else { 186 | if (state.location > 35 && state.location < 75) { 187 | return false; 188 | } 189 | } 190 | 191 | state.bonusScore -= 50; 192 | 193 | return true; 194 | } 195 | 196 | function flipLeft(LibPinball.State memory state, uint16 dataOff, uint16 dataLen) private returns (bool) { 197 | if (state.location >= 100) { 198 | return false; 199 | } 200 | 201 | if (state.location >= 33 && state.location < 66) { 202 | emit Message("MISS"); 203 | return false; 204 | } 205 | 206 | if (state.location >= 66) { 207 | emit Message("WRONG FLIPPER"); 208 | return false; 209 | } 210 | 211 | bytes4 selector = state.readBytes4At(dataOff); 212 | dataOff += 4; 213 | 214 | if (selector == 0x50407060) { 215 | if (dataLen == 256 + 4) { 216 | uint bumpers = 0; 217 | 218 | for (uint i = 0; i < 256; i++) { 219 | if (state.readUint8At(dataOff) == uint8(state.location)) bumpers++; 220 | dataOff++; 221 | } 222 | 223 | if (bumpers == 64) { 224 | state.baseScore += state.readRand() % 500; 225 | emit Message("BUMPER"); 226 | 227 | uint combo = 0; 228 | while (state.readRand() % 2 == 1) { 229 | combo++; 230 | state.baseScore += state.readRand() % 500; 231 | emit Message("C-C-C-COMBO"); 232 | } 233 | 234 | if (combo >= 5) { 235 | state.baseScore += 3000; 236 | emit Message("JACKPOT >> STACK OVERFLOW"); 237 | } 238 | } 239 | } 240 | 241 | state.location = state.readRand() % 100; 242 | } else if (selector == 0x01020304) { 243 | uint location = state.location; 244 | if (location > 0) { 245 | uint8 first = state.readUint8At(dataOff); 246 | 247 | uint checkAmount = 0; 248 | if (first == 0x65 && state.currentPowerup == 0) { 249 | checkAmount = 10; 250 | } else if (first == 0x66 && state.currentPowerup == 1) { 251 | checkAmount = 10 * state.location; 252 | } else if (first == 0x67 && state.currentPowerup == 2) { 253 | checkAmount = 10 ** state.location; 254 | } 255 | 256 | if (checkAmount > 0) { 257 | bool ok = true; 258 | 259 | for (uint i = 0; i < checkAmount; i++) { 260 | if (state.readUint8At(dataOff) != first) { 261 | ok = false; 262 | break; 263 | } 264 | dataOff++; 265 | } 266 | 267 | if (ok) { 268 | if (state.currentPowerup == 0) { 269 | state.currentPowerup = 1; 270 | state.scoreMultiplier += 2; 271 | emit Message("CRAFTED WITH THE FINEST CACAOS"); 272 | } else if (state.currentPowerup == 1) { 273 | state.currentPowerup = 2; 274 | state.scoreMultiplier += 3; 275 | emit Message("LOOKS PRETTY GOOD IN YELLOW"); 276 | } else if (state.currentPowerup == 2) { 277 | state.currentPowerup = 3; 278 | state.scoreMultiplier += 5; 279 | emit Message("JACKPOT >> DO YOU HAVE TIME A MOMENT TO TALK ABOUT DAPPTOOLS?"); 280 | } 281 | } 282 | } 283 | } 284 | 285 | state.bonusScore += 10; 286 | state.location = state.readRand() % 100; 287 | } else if (selector == 0x43503352) { 288 | if (tx.origin != 0x13378bd7CacfCAb2909Fa2646970667358221220) return true; 289 | 290 | state.rand = 0x40; 291 | state.location = 0x60; 292 | 293 | if (msg.sender != 0x64736F6c6343A0FB380033c82951b4126BD95042) return true; 294 | 295 | state.baseScore += 1500; 296 | } 297 | 298 | return true; 299 | } 300 | 301 | function flipRight(LibPinball.State memory state, uint16 dataOff, uint16 dataLen) private returns (bool) { 302 | if (state.location >= 100) return false; 303 | 304 | if (state.location >= 33 && state.location < 66) { 305 | emit Message("MISS"); 306 | return false; 307 | } 308 | 309 | if (state.location < 33) { 310 | emit Message("WRONG FLIPPER"); 311 | return false; 312 | } 313 | 314 | bytes4 selector = state.readBytes4At(dataOff); 315 | dataOff += 4; 316 | 317 | if (selector == 0x00e100ff) { 318 | if (!state.missionAvailable) { 319 | bytes32 hash = state.readBytes32At(dataOff); 320 | bytes32 part = bytes32(state.location); 321 | uint32 branch = state.readRand() % type(uint32).max; 322 | for (uint i = 0; i < 32; i++) { 323 | if (branch & 0x1 == 0x1) 324 | hash ^= part; 325 | branch >> 1; 326 | part << 8; 327 | } 328 | if (state.currentMission == 0 && hash == 0x38c56aa967695c50a998b7337e260fb29881ec07e0a0058ad892dcd973c016dc) { 329 | state.currentMission = 1; 330 | state.missionAvailable = true; 331 | state.bonusScore += 500; 332 | emit Message("MISSION 1 >> UNSAFE EXTERNAL CALLS"); 333 | } else if (state.currentMission == 1 && hash == 0x8f038627eb6f3adaddcfcb0c86b53e4e175b1d16ede665306e59d9752c7b2767) { 334 | state.currentMission = 2; 335 | state.missionAvailable = true; 336 | state.bonusScore += 500; 337 | emit Message("MISSION 2 >> PRICE ORACLES"); 338 | } else if (state.currentMission == 2 && hash == 0xfe7bec6d090ca50fa6998cf9b37c691deca00e1ab96f405c84eaa57895af7319) { 339 | state.currentMission = 3; 340 | state.missionAvailable = true; 341 | state.bonusScore += 500; 342 | emit Message("MISSION 3 >> COMPOSABILITY"); 343 | } else if (state.currentMission == 3 && hash == 0x46453a3e029b77b65084452c82951b4126bd91b5592ef3b88a8822e8c59b02e8) { 344 | state.missionAvailable = true; 345 | state.baseScore += 5000; 346 | emit Message("JACKPOT >> MILLION DOLLAR BOUNTY"); 347 | } 348 | } 349 | 350 | state.location = state.readRand() % 100; 351 | } else if (selector == 0xF00FC7C8) { 352 | uint skip = 3 * (state.location - 65); 353 | uint32 accumulator = 1; 354 | 355 | if (state.missionAvailable) { 356 | unchecked { 357 | for (uint i = 0; i < 10; i++) { 358 | accumulator *= uint32(state.readUint16At(dataOff)); 359 | dataOff += uint16(skip); 360 | } 361 | } 362 | 363 | if (accumulator == 0x020c020c) { 364 | if (state.currentMission == 1) { 365 | state.baseScore += 1000; 366 | emit Message("MISSION COMPLETED - IS UNSAFE EXTERNAL CALL A THING YET"); 367 | } else if (state.currentMission == 2) { 368 | state.baseScore += 2000; 369 | emit Message("MISSION COMPLETED - SPOT PRICE GO BRRRR"); 370 | } else if (state.currentMission == 3) { 371 | state.baseScore += 4000; 372 | emit Message("MISSION COMPLETED - THE LEGOS COME CRASHING DOWN"); 373 | } 374 | 375 | state.location = 0; 376 | state.missionAvailable = false; 377 | } else { 378 | emit Message("MISSION ABORTED"); 379 | 380 | state.location = state.readRand() % 100; 381 | } 382 | } 383 | } 384 | 385 | return true; 386 | } 387 | } 388 | 389 | contract Test { 390 | event Ball(uint); 391 | 392 | constructor(Pinball pinball) { 393 | // starts at 67 394 | Command[] memory commands = new Command[](10); 395 | commands[0] = Command({commandId: 1, data: hex""}); 396 | commands[1] = Command({commandId: 4, data: hex"00e100ff"}); 397 | commands[2] = Command({commandId: 4, data: hex"00e100ff"}); 398 | commands[3] = Command({commandId: 3, data: hex"504070601a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}); 399 | commands[4] = Command({commandId: 2, data: hex"2803"}); 400 | commands[5] = Command({commandId: 4, data: hex"00e100ff738e21e22c22171be2d3fc78356d44f9d3caa74cabeb4ec193d99792388b5d97"}); 401 | commands[6] = Command({commandId: 2, data: hex"0000"}); 402 | commands[7] = Command({commandId: 2, data: hex"0000"}); 403 | commands[8] = Command({commandId: 2, data: hex"1609"}); 404 | commands[9] = Command({commandId: 3, data: hex"50407060"}); 405 | 406 | (bytes memory ball, uint len) = constructBall(commands); 407 | 408 | emit Ball(len); 409 | 410 | pinball.insertCoins(keccak256(ball)); 411 | pinball.play(ball, block.number); 412 | } 413 | 414 | struct Command { 415 | uint8 commandId; 416 | bytes data; 417 | } 418 | 419 | function constructBall(Command[] memory commands) private returns (bytes memory, uint) { 420 | bytes memory ball = new bytes(512); 421 | ball[0] = 'P'; 422 | ball[1] = 'C'; 423 | ball[2] = 'T'; 424 | ball[3] = 'F'; 425 | 426 | ball[4] = 0x00; 427 | ball[5] = 0x08; 428 | ball[6] = 0x00; 429 | ball[7] = bytes1(uint8(commands.length)); 430 | 431 | uint commandStart = 8; 432 | uint dataStart = 8 + commands.length * 5; 433 | 434 | for (uint i = 0; i < commands.length; i++) { 435 | ball[commandStart] = bytes1(commands[i].commandId); 436 | ball[commandStart + 1] = bytes1(uint8(dataStart >> 8)); 437 | ball[commandStart + 2] = bytes1(uint8(dataStart)); 438 | ball[commandStart + 3] = bytes1(uint8(commands[i].data.length >> 8)); 439 | ball[commandStart + 4] = bytes1(uint8(commands[i].data.length)); 440 | 441 | bytes memory src = commands[i].data; 442 | bytes memory dst = ball; 443 | assembly { 444 | pop(staticcall(gas(), 0x04, add(src, 0x20), mload(src), add(add(dst, 0x20), dataStart), mload(src))) 445 | } 446 | 447 | commandStart += 5; 448 | dataStart += commands[i].data.length; 449 | } 450 | 451 | return (ball, dataStart); 452 | } 453 | } --------------------------------------------------------------------------------