How Someone Tried to Exploit a Flaw in Our Smart Contract and Steal All of Its Ether
On March 7th, we noticed some weird transactions happening on CityMayor. First, someone used our contract directly instead of going through the dAPP. We know that, because a nickname is mandatory if you want to use the DAPP to interact with the CityMayor contract.
Now, don’t get me wrong, that’s pretty neat! We are not against people using the contract directly, and we actually encourage you to do so :) We’ve extensively documented the smart contract on etherscan.io so that anyone can take a look at it (it’s pretty short), try to understand it and interact with it directly.
The weird transactions can be observed in the “Internal Transactions” tab of the CityMayor contract. The highlighted address is CityMayor’s smart contract.
Why are these “internal” transactions? Because these transactions happened as part of the execution of something else! Looking at 0x6fb62bec140218ff7c43b20232e06a05f9b25253 we can see that a smart contract is calling CityMayor, not a normal Ethereum user!
We can see that the smart contract has been deployed especially for interacting with CityMayor, and that it was later killed. Fortunately everything is in clear on the blockchain and we managed to recover the bytecode of the malicious contract. The contract code can then be reversed engineer with a decompiler like porosity or other reverse engineering tools. Unfortunately we have to do this because our attacker did not publish the Solidity source code, but this would be for another blog post.
We will just use Etherscan to analyze what our mysterious user called on his malicious contract:
1. the first one uses setContract(address _ownerContract)
with the address of our contract.
Note that in reality, we can’t see the protype of the function called, but only its signature (
0x75f890ab
) and its input. This is because we don’t have access to the contract’s ABI or source code. How does Etherscan shows us the prototype of the function called then? Etherscan must have a table of known function prototypes and their associated signature. This way, Etherscan knows (by having learned it previously, somehow) thatsetContract(address _ownerContract)
is associated to0x75f890ab
.
2. the second transaction executes the function with signature be4d6211
(Etherscan doesn’t seem to know the prototype for that one) and sends it 0.25 ETH. The execution seems to trigger the execution of the CityMayor contract (we can see that in the “internal transaction” tab again) and sends it the attacker’s 0.25 ETH. We detect that an offer has been made, so we guess that the attacker has used his or her contract to make an indirect offer to buy a city (Moscow).
3. the third transaction calls the function fc64b60b
with input 0x17
. No idea what that is doing.
4. the fourth transaction is the interesting one! Our attacker calls the getFunds()
function of his/her malicious contract, which seems to trigger the cancelOfferForCity()
function of CityMayor. You guessed it right, he or she is trying to cancel his or her offer. But what happens next? CityMayor reimburses the attacker’s contract by sending it the 0.25 ETH but the transaction fails. Odd right?
What exactly happened? Let’s take a look at our cancelOfferForCity()
function:
What it does is pretty straight forward:
- it gets the offer via the offer ID.
- it checks if the caller is indeed the one who made the offer
- it refunds the caller via the
msg.sender.transfer()
function - it deletes the offer
- it emits an event that we can detect to update our dapp
What is the attacker trying to do here? Well, what the attacker has seen in this code, is what we call a reentrancy vulnerability.
When msg.sender.transfer()
is called to send money to the msg.sender
address, if the receiving address is a smart contract (which is what is actually happening here) and if it has a fallback function, then it will be executed. A fallback function is an “everything-goes-in” kind of function that will be executed if no method is explicitely called (our case since we’re just sending some ETH to the contract) or if the method called doesn’t exist. It looks like this:
function () payable {
// do something...
}
Wait what? That means that the third line of our function, which refunds the caller, is leading to the code execution of another contract? Yup you heard it right. That’s a surprising artifact of smart contracts and of how the Ethereum VM (EVM) behaves! Interesting right?
What’s even more interesting, is that this fallback function could call the CityMayor contract again. Like this:
At this point what’s happening is basic nested call 101, but what our attacker saw is that if this works: the second execution of cancelOfferForCity()
will still be valid since the offer still hasn’t been deleted in the first execution of cancelOfferForCity()
.
The second execution will lead to a second execution of msg.sender.transfer()
, which would allow our attacker to cancel his or her offer twice and to double the amount money that he or she initially put in!
Why didn’t it work then? Well, unfortunately for our attacker, this attack doesn’t work with the transfer()
function CityMayor uses. Why? Because transfer()
limits the amount of gas that the nested call will be able to use. Gas is an important concept in Ethereum, each instruction consumes some gas and each transaction needs some amount of gas to run to its full completion. transfer()
limits the amount of gas of the following transaction, it makes sense right? it is meant to transfer money, not execute code. If CityMayor’s code had used a low level call instead, like call()
which doesn’t limit gas in the same way, the attack would probably have worked.
By the way, if you’ve heard of the DAO attack, this is basically the same thing.
Good job to our mysterious attacker 0xafc64ad0eb7283ebfc9b9f706c914b633e1a78c3! We’ve sent him or her 10,000 CITYs (the token of CityMayor) for the effort and for motivating this blog post :)
The United Nations
Building an Ethereum / EOS / Bitcoin dApp?
Nodablock helps Citymayor to access the Ethereum network without any hassle
Nodablock provides an easy access to the main Blockchain networks (Ethereum, EOS, bitcoin) with a secure, reliable, and scalable API. It saves developers time and money, and remove the need to maintain your own node.