1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.0;3
4import { ERC20 } from "@solmate/tokens/ERC20.sol";5import { IPuzzle } from "./interfaces/IPuzzle.sol";6import { IERC721 } from "./interfaces/IERC721.sol";7
8/**9You are Billy the Bull, the most infamous of NFT influencers.10Your trades move markets,11 your lambos fill Instagram timelines,12 your Twitter threads are the stuff of legends.13
14There's just one problem: you're broke.15
16With just one more trade, you'd certainly earn it all back.17But your mom is done lending you money, and there's nowhere left to turn.18
19A new mint has just opened up on The NFT Outlet. You _know_ it'll be a hit.20Your strategy is clear: You need 2 NFTs. Sell one. Keep the other for your collection.21If only there were a way to get them without paying...22*/23contract BillyTheBull is IPuzzle {24 address public owner;25 NFTOutlet public nftOutlet;26 uint public nftPrice;27 uint cachedSolution;28
29 event OwnsBoth(address indexed wallet, uint tokenId1, uint tokenId2);30
31 constructor() {32 owner = address(tx.origin);33 }34
35 function initialize(address _nftOutlet, uint _startingNftPrice) external {36 require(address(nftOutlet) == address(0), "already initialized");37 nftOutlet = NFTOutlet(_nftOutlet);38 nftPrice = _startingNftPrice;39 }40
41 function name() public pure returns (string memory) {42 return "Billy the Bull";43 }44
45 function generate(address _seed) public pure returns (uint256 start) {46 start = uint256(keccak256(abi.encode(_seed)));47 }48
49 function verify(uint _start, uint _solution) public noTampering(_solution) returns (bool) {50 // decode & cache input arguments51 uint tokenId1 = _start >> 128;52 uint tokenId2 = uint(uint128(_start));53 address wallet = address(uint160(_solution));54 IERC721 nftToBuy = nftOutlet.nftDealOfTheDay();55
56 // use external logic with local storage to determine the ~~magic flag~~57 bytes32 pre = keccak256(abi.encode(owner, nftOutlet, nftPrice, cachedSolution, nftToBuy.totalSupply()));58 (, bytes memory d0) = wallet.delegatecall(abi.encodeWithSignature("getMagicFlag()"));59 bytes32 post = keccak256(abi.encode(owner, nftOutlet, nftPrice, cachedSolution, nftToBuy.totalSupply()));60 require(pre == post, "bad boy");61
62 // ensure we have a unique magic flag63 bytes memory magicFlag = abi.decode(d0, (bytes));64 require(nftOutlet.magicFlagsUsed(keccak256(magicFlag)) == false, "no reusing flags");65 nftOutlet.setMagicFlagUsed(keccak256(magicFlag));66
67 // alright houdini, pay without paying68 uint balanceBefore = nftOutlet.paymentToken().balanceOf(wallet);69 (bool s1, bytes memory d1) = address(nftOutlet).call(70 abi.encodeWithSignature("pay(address,uint256)", wallet, _incrementNFTPrice(1e18))71 );72 require(!_returnedFalse(s1, d1), "transfer must succeed");73 require(balanceBefore == nftOutlet.paymentToken().balanceOf(wallet), "sneaky sneaky");74
75 // mint an nft to your wallet76 (bool s2, bytes memory d2) = address(nftOutlet).call(77 abi.encodeWithSignature("mint(address,uint256)", wallet, tokenId1)78 );79 require(!_returnedFalse(s2, d2), "mint must succeed");80
81 // did you end up with both nfts?82 require(nftToBuy.ownerOf(tokenId1) == wallet, "must own token id 1");83 require(nftToBuy.ownerOf(tokenId2) == wallet, "must own token id 2");84 emit OwnsBoth(wallet, tokenId1, tokenId2);85
86 // you win ... if you got the magic flag right87 return uint(keccak256(magicFlag)) == _solution;88 }89
90 function _returnedFalse(bool success, bytes memory data) internal pure returns (bool) {91 return success && !abi.decode(data, (bool));92 }93
94 function _incrementNFTPrice(uint _incrementBy) public returns (uint oldPrice) {95 require(_incrementBy < 10e18, "lets keep this affordable");96 oldPrice = nftPrice;97 nftPrice = nftPrice + _incrementBy;98 }99
100 modifier noTampering(uint _solution) {101 if (cachedSolution == 0) {102 cachedSolution = _solution;103 _;104 cachedSolution = 0;105 } else {106 require(cachedSolution == _solution, "max one solution");107 _;108 }109 }110}111
112contract NFTOutlet {113 address immutable puzzle;114
115 ERC20 public paymentToken;116 IERC721 public nftDealOfTheDay;117 address treasury;118
119 mapping(address => bool) public validAssets;120 mapping(address => bool) public mintsClaimed;121 mapping(bytes32 => bool) public magicFlagsUsed;122
123 constructor(124 address _puzzle,125 address[] memory _paymentTokens,126 address[] memory _nfts127 ) {128 puzzle = _puzzle;129 paymentToken = ERC20(_paymentTokens[0]);130 nftDealOfTheDay = IERC721(_nfts[0]);131
132 for (uint256 i = 0; i < _paymentTokens.length; i++) {133 validAssets[_paymentTokens[i]] = true;134 }135
136 for (uint256 i = 0; i < _nfts.length; i++) {137 validAssets[_nfts[i]] = true;138 }139 }140
141 /////////////////////////142 /////// MODIFIERS ///////143 /////////////////////////144
145 modifier onlyPuzzle() {146 require(msg.sender == puzzle, "only puzzle");147 _;148 }149
150 modifier onlyPuzzleOwner() {151 require(msg.sender == BillyTheBull(puzzle).owner(), "only puzzle owner");152 _;153 }154
155 /////////////////////////156 //// PAYMENT ACTIONS ////157 /////////////////////////158
159 function pay(address _from, uint256 _amount) public onlyPuzzle returns (bool) {160 require(_from != address(0), "no zero address");161 try paymentToken.transferFrom(_from, address(this), _amount) returns (bool) {162 require(163 keccak256(abi.encode(_amount)) !=164 0x420badbabe420badbabe420badbabe420badbabe420badbabe420badbabe6969,165 "too immature"166 );167 return true;168 } catch {169 require(uint(uint32(_amount)) <= 4294967295, "invalid amount");170 return false;171 }172 }173
174 /////////////////////////175 //// MINTING ACTIONS ////176 /////////////////////////177
178 function mint(address _to, uint256 _tokenId) public onlyPuzzle returns (bool) {179 require(!mintsClaimed[_to], "already claimed");180 try nftDealOfTheDay.safeMint(_to, _tokenId) {181 mintsClaimed[_to] = true;182 return true;183 } catch {184 return false;185 }186 }187
188 /////////////////////////189 ///// ADMIN ACTIONS /////190 /////////////////////////191
192 function setMagicFlagUsed(bytes32 _magicFlag) onlyPuzzle public {193 magicFlagsUsed[_magicFlag] = true;194 }195
196 function changePaymentToken(address _newStablecoin) public onlyPuzzleOwner {197 require(validAssets[_newStablecoin], "no sneaky assets");198 paymentToken = ERC20(_newStablecoin);199 }200
201 function rescueERC20(address _token) public {202 ERC20(_token).transfer(treasury, ERC20(_token).balanceOf(address(this)));203 }204}205
Time Left
Solve locally (WIP)
- Clone GitHub repo + install deps
git clone https://github.com/waterfall-mkt/curta-puzzles.git && cd curta-puzzles && forge install
- Set
RPC_URL_MAINNET
in.env
.env
RPC_URL_MAINNET=""
- Write solution + run script
forge script <PATH_TO_PUZZLE> -f mainnet -vvv
This is still WIP.