Let’s Play — Capture the Ether : Lotteries (Part II)

Forest Fang
8 min readApr 15, 2018

--

This is the third installment of stories on my journey of Capture the Ether, a game where we hack Ethereum Solidity code for fun and learn about smart contract security. Read the introduction post for background, what to expect, and link to other sections.

What is Capture the Ether?

In Capture the Ether, we win points by completing challenges. Each challenge comes with a smart contract to deploy and objectives can be completed by exploiting the contract into a specific state. Challenges are grouped into categories that focus on specific areas of security vulnerabilities in smart contracts.

Spoiler Alert

We will cover Guess the new number, Predict the future, and Predict the block hash challenges in this article. These challenges demonstrate it is difficult to source randomness from a deterministic programming model in EVM.

I recommend you read, think about, attempt the problem on your own first. For each problem, I will discuss the high level approach followed by detailed solution.

Guess the new number

Approach

This time we need to know the current block timestamp before we send transaction.

This may seems impossible first, except that if we mine the blocks ourself, we can inject transaction with a timestamp that we will use in our current block header. This is related to miner front-running commonly found in decentralized exchange implementation. That is, miner might have mis-aligned incentive when transactions are public and the transaction has economic value. They can sway miners’ behaviors away from fair incentive provided by gas collected in the block. Therefore it is almost never a good idea to use block hash or timestamp as a source of randomness whether or not it could be programmatically exploited or not.

On the other hand, there is a far easier way to exploit this contract by simply creating a relay contract that compute the answer itself before sending transaction to the GuessTheNewNumberChallenge contract. The reward Ether can then be passed back to your account as the original sender.

Solution Details

While it is perfectly feasible to write this relay contract in Remix, you will likely need to spend some moments debug this one as it has some hidden pitfalls if you haven’t called another contract from Solidity before.

We will introduce you to the Truffle framework which helps you to scaffold, organize, compile, test and deploy your DApp (Decentralized Application). It excels at building web application that directly interacts with blockchain with or without a centralized server. Today we will only use it to compile and test our Solidity smart contract.

Truffle has excellent tutorials (e.g. your decentralized Pet Shop) and project starting templates called boxes. We will begin with a blank template instead:

npm install -g truffle
mkdir capture-the-ether && cd capture-the-ether
truffle init

Next we will import the challenge contract code into contracts/GuessTheNewNumberChallenge.sol and add test skeleton in test/GuessTheNewNumberChallenge.test.js . I recommend using Visual Studio Code with Solidity plugin as your IDE.

We can enter Truffle development console with truffle develop which spawns a development blockchain. You will be greeted with ten test accounts with 100 ether loaded in each account.

Now let’s run migrate --reset to compile our challenge contract, and test to give our unit test a try.

truffle(develop)> test
Using network 'develop'.
Contract: GuessTheNewNumberChallenge
1) should be completed
> No events were emitted
0 passing (91ms)
1 failing
1) Contract: GuessTheNewNumberChallenge should be completed:
AssertionError: Challenge has not been completed!
at Context.it (test/GuessTheNewNumberChallenge.test.js:7:5)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:182:7)

We see test fails as expected since isComplete is false in the beginning. 🎉 I hope you enjoy the async/await syntax after drowning in the callback hell from previous sessions.

Next we will add the solver contract that will call the challenge contract and relay the computed guess.

To start, we simply relay the isComplete function as a simple test. Running migrate and test should print the same failure message we saw before.

With that let’s implement the guess function and modify the test like so:

Running migrate and test , we got this daunting error:

truffle(develop)> test
Using network 'develop'.
Contract: GuessTheNewNumberSolver
1) should solve the challenge
> No events were emitted
0 passing (261ms)
1 failing
1) Contract: GuessTheNewNumberSolver should solve the challenge:
Error: VM Exception while processing transaction: revert
at Object.InvalidResponse (/usr/local/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/errors.js:38:1)
at /usr/local/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/requestmanager.js:86:1
at /usr/local/lib/node_modules/truffle/build/webpack:/~/truffle-provider/wrapper.js:134:1

What happened? revert happens when there is no safe way to continue execution and all changes are reverted rendering whole transaction effect-less.

Below is an excellent tutorial debugging smart contract via stepping through the execution code. One thing to call out is truffle develop --log starts a development console without interactive prompt. This is intentional as truffle develop --log is meant to be run side by side with truffle develop .

For our purpose, you should run truffle develop --log in a separate session, and as you run test in the original console, you should see transaction hash being logged:

develop:testrpc eth_sendTransaction +7ms
develop:testrpc +115ms
develop:testrpc Transaction: 0x71f23cf0b6e38b71c0ad1e3bd5bb6644cebf8d1688bf7cad06d511d642ff3a1e +0ms
develop:testrpc Gas usage: 39241 +0ms
develop:testrpc Block Number: 320 +0ms
develop:testrpc Block Time: Sat Apr 14 2018 19:00:39 GMT-0700 (PDT) +1ms
develop:testrpc Runtime Error: revert +0ms
develop:testrpc +0ms
develop:testrpc eth_getLogs +8ms

We can use this hash in the development console to replay the transaction in debug mode: debug <tx hash found in develop --log console>

As we step into GuessTheNewNumberChallenge we found we made it through n == answer just fine, and returned back to GuessTheNewNumberSolver after msg.sender.transfer(2 ether); just before triggering revert.

If we experiment commenting out msg.sender.transfer(2 ether); , the transaction went through fine. The test still fails since the challenge contract didn’t empty its pocket even when guess is correct.

So what gives? 😕 The answer lies in Fallback Function:

A contract can have exactly one unnamed function. This function cannot have arguments and cannot return anything. It is executed on a call to the contract if none of the other functions match the given function identifier (or if no data was supplied at all).

Furthermore, this function is executed whenever the contract receives plain Ether (without data). Additionally, in order to receive Ether, the fallback function must be marked payable. If no such function exists, the contract cannot receive Ether through regular transactions.

Now we think about it, we never specified what happened when the relay contract gets paid with 2 ether. You don’t want to lose your ether, do you? 🙄

We implement the fallback to send the ether back to us. Since we do not have access to the original msg.sender anymore, we need to remember the owner of the contract during deploy time with the constructor function.

🤞Does it work? Nope…we still get Error: VM Exception while processing transaction: revert . If we run debugger again, we found the error now stems from owner.transfer(msg.value); and if we comment out that line we are able to complete the challenge (and lose our 2 ethers 😢)

Maybe you are smarter than I was and read the full documentation before:

In the worst case, the fallback function can only rely on 2300 gas being available (for example when send or transfer is used), leaving not much room to perform other operations except basic logging. The following operations will consume more gas than the 2300 gas stipend:

* Writing to storage

* Creating a contract

* Calling an external function which consumes a large amount of gas

* Sending Ether

Fallback function in fact cannot send ether due to lack of gas. Therefore we need to add a withdraw function:

function () public payable { }function withdraw() public {  require(msg.sender == owner);  owner.transfer(address(this).balance);}

Now our test should happily pass:

truffle(develop)> test
Using network 'develop'.
Compiling ./contracts/GuessTheNewNumberChallenge.sol...
Compiling ./contracts/GuessTheNewNumberSolver.sol...
Contract: GuessTheNewNumberSolver
✓ should solve the challenge (212ms)
1 passing (227ms)

A Word on Fallback Function

Fallback function is a powerful feature for smart contract in Ethereum but it also comes with hidden complexity. Historically it has been misused by many contracts leading to massive loss of Ether such as the DAO hack and the Parity Multisig Wallet bug.

Don’t be overly permissive in your smart contract code. The immutability of smart contract sometimes incentivizes developers away from being restrictive and at the same time make it impossible to fix any bugs when they surface. Developers have been experimenting with upgradable smart contracts using technique such as proxy contract. [1] [2] [3]

Vulnerability Remediation

It is possible to determine if an address is a contract address instead of a normal account by checking code size. However it is debatable whether this is a good solution, namely this prevents smart contract interaction which is an outstanding feature of Ethereum smart contract.

Predict the future

Approach

The answer must be between 0 and 9.

guess is locked in in blocks before settle can be made.

By using a relay contract we can test if answer would be equal to settle .

In other words, the relay contract can avoid calling settle if the answer in the upcoming block is not the same as guess .

Solution Details

This looks to be a trivial extension of the last solution:

We just need to make sure it was the relay contract calling lockInGuess so guesser validation can be satisfied at settle .

If you find it tedious to keep trying your luck by authorizing Metamask transaction, you may consider exporting your private key and run an automated loop using web3.js instead.

Predict the block hash

Approach

This one looks tricky since you need to know the result of block.blockhash(settlementBlockNumber); where settlementBlockNumber = block.number + 1

If you have been using Visual Studio Code with Solidity extension, you might have seen this static analysis warning: no-block-members (Discourage use of members ‘blockhash’ & ‘timestamp’ (and alias ‘now’) of ‘block’ global variable)

There is a reason for that and there is plenty of warnings on the block.blockhash function itself.

In fact, block.blockhash becomes deterministic after certain conditions. If you haven’t figured it out yet, re-read the documentation again.

Solution Details

The secret lies in the implementation of block.blockhash :

The block hashes are not available for all blocks for scalability reasons. You can only access the hashes of the most recent 256 blocks, all other values will be zero.

So we simply need to wait for 256 blocks to settle with a guess of 0x . Let’s test this with Truffle:

After a while, you should see the test passes! In real world, you can either check back on Etherscan to see how many block confirmation your lock in transaction has, or use web3.js to listen for new blocks event to wait for 256 blocks to pass.

Conclusion

In this post, you have learnt to write smart contract with Solidity and test with web3.js using the Truffle framework, reference and interact with another contract in Solidity, pitfalls of fallback function, and unreliability of block members as a source of randomness.

You can find most of the Solidity and web3.js test code at my Github.

Next time, we will switch gears and look at the Math section to learn how to do math safely in Solidity and what can go wrong if we are not careful.

Capture the Ether is brought to you by @smarx, who blogs about smart contract development at Program the Blockchain.

I am not affiliated with the game itself and the awesome company behind it. Views expressed are solely my own and do not express the views or opinions of any entity with which I have been, am now, and will be affiliated.

--

--

Forest Fang

functional programming advocate; visualization addict; blockchain enthusiast;