Puzzle #15
Billy the Bull
SoliditySolidity's logo.Puzzle
Curtacallsverify()
1
// SPDX-License-Identifier: MIT
2
pragma solidity ^0.8.0;
3
4
import { ERC20 } from "@solmate/tokens/ERC20.sol";
5
import { IPuzzle } from "./interfaces/IPuzzle.sol";
6
import { IERC721 } from "./interfaces/IERC721.sol";
7
8
/**
9
You are Billy the Bull, the most infamous of NFT influencers.
10
Your trades move markets,
11
your lambos fill Instagram timelines,
12
your Twitter threads are the stuff of legends.
13
14
There's just one problem: you're broke.
15
16
With just one more trade, you'd certainly earn it all back.
17
But your mom is done lending you money, and there's nowhere left to turn.
18
19
A new mint has just opened up on The NFT Outlet. You _know_ it'll be a hit.
20
Your strategy is clear: You need 2 NFTs. Sell one. Keep the other for your collection.
21
If only there were a way to get them without paying...
22
*/
23
contract 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 arguments
51
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 flag
63
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 paying
68
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 wallet
76
(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 right
87
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
112
contract 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 _nfts
127
) {
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
First Blood
jinu.eth
01:52:00
20
Time Left

Solve locally (WIP)

  1. Clone GitHub repo + install deps
git clone https://github.com/waterfall-mkt/curta-puzzles.git && cd curta-puzzles && forge install
  1. Set RPC_URL_MAINNET in .env
.env
RPC_URL_MAINNET=""
  1. Write solution + run script
forge script <PATH_TO_PUZZLE> -f mainnet -vvv
This is still WIP.
Waterfall