In a previous article I wrote about various security flaws that can show up in Solidity, with some rules for avoiding them. There's another whole class of vulnerabilities based on manipulating the underlying platform. It can be pretty easy for these to sneak in.
For example, a while back I was sad that so far, if you do offline transactions in MyEtherWallet, you don't get a nice interface for calling contract functions. I came up with something I briefly thought was clever: just make a contract that can be controlled by simple ether sends.
So I made this 2-of-3 multisig wallet. To withdraw funds, send a small amount of ether from one of your addresses; that address becomes the recipient, and the contract will be set up to withdraw a million times as much ether to that recipient. Then do another send from one of the other addresses, and the transfer completes. Here's the horrible code I made:
//DO NOT USE THIS
contract HorribleMultisig {
address a;
address b;
address c;
address payee;
uint payamount;
function HorribleMultisig(address _a, address _b, address _c) {
a = _a;
b = _b;
c = _c;
}
function () {
//if not an approved address, do nothing but keep any ether
if (msg.sender != a &&
msg.sender != b &&
msg.sender != c) return;
//if payee isn't set, this is first one
if (payee == 0) {
payee = msg.sender;
payamount = msg.value * 1000000;
msg.sender.send(msg.value); //ok if fails, want fixed gas
return;
}
//if payee sends again, reset all to zero
if (payee == msg.sender) {
payamount = 0;
payee = 0;
return;
}
//payee has been set so this is second send
//so actually transfer, and reset to zero
uint willpay = payamount;
address willpayto = payee;
payamount = 0;
payee = 0;
if (willpay > this.balance) willpay = this.balance;
if (!willpayto.call.value(willpay)()) throw;
}
}
I posted this abomination on reddit, and people made minor stylistic suggestions. The next day I realized it's completely insecure. It seems like you might be able to keep one key in a convenient but less-secure location, because your funds are locked by the other keys...but in fact, an attacker with just one key can steal all your funds!
All the attacker has to do is monitor network traffic, waiting for you to send ether to this contract using one of the uncompromised keys. Then the attacker can quickly send his own transaction, using the compromised key. If the attacker's transaction gets into the blockchain first, then that key becomes the recipient, and the transaction you sent will authorize the transfer. And since it's the attacker who sets the value, the attacker can take everything.
You could sorta fix this, by canceling the transfer if the second transaction includes any ether. But you wouldn't want to trust your funds to that; if you accidentally send your first transaction with zero ether, you're toast. You could require both keys to send matching ether amounts, so at least an attacker can't steal everything, but that's not exactly ideal either.
You won't find problems like this by calling Solidity functions in the online compiler, or by running unit tests. What happens on the blockchain might be just fine. You have to think about what might happen before the code even runs on the blockchain. You have to assume that attackers can see your transactions before the blockchain does, and issue their own transactions, which may get to the chain before yours.
Copyright Registration
A nifty little idea is to register copyright on the blockchain. Submit a hash and you register yourself as the owner of the hash. At various times I've seen code like this:
contract HorribleRegistrar {
mapping (bytes32 => address) public hashes;
function register(bytes32 hash) {
if (hashes[hash] == 0) {
hashes[hash] = msg.sender;
}
}
}
Once again, an attacker can take your stuff. He can listen for hashes submitted to the registrar, submit the hash himself, and sometimes his transaction will get in first and he'll own that hash for the rest of time...or at least, for as long as anyone pays attention to this silly contract.
To fix this one, you have to get a little fancier. Don't submit sha3(yourContent), submit sha3(yourAddress, sha3(yourContent)).
contract BetterRegistrar {
mapping (bytes32 => address) hashes;
mapping (bytes32 => address) public registrations;
function stepOne(bytes32 hash) {
if (hashes[hash] == 0) {
hashes[hash] = msg.sender;
}
}
function stepTwo(bytes32 outerhash, bytes32 innerhash) {
if (hashes[outerhash] == msg.sender &&
sha3(msg.sender, innerhash) == outerhash) {
hashes[outerhash] = 0;
registrations[innerhash] = msg.sender;
}
}
}
Now the attacker can't do any harm. If he repeats step one, he's just saved a hash of a statement giving you ownership. If he takes the innerhash from step two and submits his own transaction making himself the owner, it'll fail because it won't match the hash saved in step one.
Oyente
There's actually a tool that can alert you to problems like this, called Oyente. Here's their github and paper (pdf). It tries to find several other problems too. It's not perfect; when I tested the above contracts, it warned of a time dependency problem in HorribleMultisig, but HorribleRegistrar got through without warnings. If Oyente does throw warnings, it's not that easy to interpret them.
Still, it might find stuff you missed. They recommend using it with docker:
Install:
docker pull hrishioa/oyente
Run:
docker run -it hrishioa/oyente
Get the docker container name you need:
docker ps
(Look in the Names column. An example is "modest_sinoussi")
Copy the contract you want to test into the docker container (ignore wraparound, this is all one line):
docker cp MyContract.sol modest_sinoussi:/home/oyente/oyente/MyContract.sol
Now you're finished with your regular terminal. From within docker:
cd /home/oyente/oyente
source ../dependencies/venv/bin/activate
python oyente.py MyContract.sol
Without that second line you won't have the dependencies, so oyente won't run. The last line can be repeated at will to run more tests. If your solidity doesn't compile, oyente will just terminate without output. (When I downloaded it, their image came with solc version 0.4.2, and the latest solc was 0.4.6.)
Finally, to shut the docker container back down, go back to your regular terminal and:
docker stop modest_sinoussi
...or whatever name docker gave you.