Skip to content

Let’s Build An Ethereum Lottery Contract With Scheduled Triggers

February 12, 2018

If you’ve written a Solidity smart contract, then you’re familiar with the benefits of decentralization. You’re also probably aware of its limitations. One such limitation is the smart contract’s inability to execute on a schedule. In order to execute a method on the contract, it would have to be triggered from the outside. In order to demonstrate this weakness, I shall build an Ethereum Lottery smart contract.

Let’s spec it out:
– 1 ETH per entry
– Unlimited entries are allowed
– Minimum of 5 entries per drawing is required
– One winner is randomly selected. He/she wins the entire pot.
– The winner must withdraw his winnings
– Upon drawing, the lottery is reset and a new pool begins

This is what I whipped up:

pragma solidity ^0.4.11;

/**
 * @title CrankysLottery
 * @dev The CrankysLottery contract is an ETH lottery contract
 * that allows unlimited entries at the cost of 1 ETH per entry.
 * Winners are rewarded the pot.
 */
contract CrankysLottery {

    address public owner;
    uint private latestBlockNumber;
    bytes32 private cumulativeHash;
    address[] private bets;
    mapping(address => uint256) winners;

    function CrankysLottery() public {
        owner = msg.sender;
        latestBlockNumber = block.number;
        cumulativeHash = bytes32(0);
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
    	require(msg.sender == owner);
    	_;
    }

    function placeBet() public payable returns (bool) {
        uint _wei = msg.value;
        assert(_wei == 1000000000000000000);
        cumulativeHash = keccak256(block.blockhash(latestBlockNumber), cumulativeHash);
        latestBlockNumber = block.number;
        bets.push(msg.sender);
        return true;
    }

    function drawWinner() public onlyOwner returns (address) {
        assert(bets.length > 4);
        latestBlockNumber = block.number;
        bytes32 _finalHash = keccak256(block.blockhash(latestBlockNumber-1), cumulativeHash);
        uint256 _randomInt = uint256(_finalHash) % bets.length;
        address _winner = bets[_randomInt];
        winners[_winner] = 1000000000000000000 * bets.length;
        cumulativeHash = bytes32(0);
        delete bets;
        return _winner;
    }

    function withdraw() public returns (bool) {
        uint256 amount = winners[msg.sender];
        winners[msg.sender] = 0;
        if (msg.sender.send(amount)) {
            return true;
        } else {
            winners[msg.sender] = amount;
            return false;
        }
    }

    function getBet(uint256 betNumber) public view returns (address) {
        return bets[betNumber];
    }

    function getNumberOfBets() public view returns (uint256) {
        return bets.length;
    }
}

Disclaimer: This solidity contract is imperfect. I do not recommend uploading this contract and running your own lottery. This is for demonstration purposes only!

Now that we’ve got the disclaimer out of the way, let’s focus on the drawWinner() function.

We assert that there are at least 5 entries. We calculate the final hash based on each of the entrant transaction block hashes and derive a random integer with a max equal to the number of entrants. We pick our winner and log their winnings in a map. We then reset our lottery.

Unfortunately, we can’t simply annotate it with a @Scheduled(cron = “0 0 0 15 * *”) nor @Scheduled(fixedDelay = 2592000000) and call it done. In this contract, drawWinner() has to be triggered from the outside by the proper owner at the proper time every time, or a winner isn’t drawn.

Now that requires a lot of trust for a system designed around trustlessness. How do we automate execution? Let’s ask Google for a solution here:

It looks like Solidity developers have been asking for a solution:

search result 1
search result 2
search result 3
search result 4
search result 5

You’ll notice mentions of a service known as Ethereum Alarm Clock which is currently dormant (or dead?).

In many cases, contracts can (and should) be written/re-written to follow a lazy evaluation pattern. However, in other cases, an eager approach is desired and necessary.

Let’s discuss the “other case” and see how we can eagerly execute our lottery picker.

Ping Chain is a service that was built to solve this very problem.

Full Disclosure: I am part of the team that built the Ping Chain service.

Let’s see how we can refactor our lottery smart contract and have it trigger reliably on a schedule.

pragma solidity ^0.4.11;

contract PingOracle {   // Ping Chain Oracle Interface
    function isTrustedDataSource(address dataSourceAddress) public view returns (bool _isTrusted);
}

/**
 * @title CrankysLottery
 * @dev The CrankysLottery contract is an ETH lottery contract
 * that allows unlimited entries at the cost of 1 ETH per entry.
 * Winners are rewarded the pot.
 */
contract CrankysLottery {

    uint private latestBlockNumber;
    bytes32 private cumulativeHash;
    address[] private bets;
    mapping(address => uint256) winners;
    address PING_ORACLE_ADDRESS = address(0x6D0F7D4A214BF9a9F59Dd946B0211B45f6661dd4);
    PingOracle PING_ORACLE;

    function CrankysLottery() public {
	latestBlockNumber = block.number;
        cumulativeHash = bytes32(0);
        PING_ORACLE = PingOracle(PING_ORACLE_ADDRESS);
    }

    modifier pingchainOnly() {
        bool _isTrusted = PING_ORACLE.isTrustedDataSource(msg.sender);
        require(_isTrusted);
        _;
    }

    event LogCallback(bytes32 _triggerId, uint256 _triggerTimestamp, uint256 _triggerBlockNumber);

    function placeBet() public payable returns (bool) {
        uint _wei = msg.value;
        assert(_wei == 1000000000000000000);
        cumulativeHash = keccak256(block.blockhash(latestBlockNumber), cumulativeHash);
        latestBlockNumber = block.number;
        bets.push(msg.sender);
        return true;
    }

    function drawWinner() internal returns (address) {
        assert(bets.length > 4);
        latestBlockNumber = block.number;
        bytes32 _finalHash = keccak256(block.blockhash(latestBlockNumber-1), cumulativeHash);
        uint256 _randomInt = uint256(_finalHash) % bets.length;
        address _winner = bets[_randomInt];
        winners[_winner] = 1000000000000000000 * bets.length;
        cumulativeHash = bytes32(0);
        delete bets;
        return _winner;
    }

    function withdraw() public returns (bool) {
        uint256 amount = winners[msg.sender];
        winners[msg.sender] = 0;
        if (msg.sender.send(amount)) {
            return true;
        } else {
            winners[msg.sender] = amount;
            return false;
        }
    }

    function pingchainTriggerCallback(bytes32 _triggerId, uint256 _triggerTimestamp, uint256 _triggerBlockNumber) public pingchainOnly returns (bool)
    {
        drawWinner();
        LogCallback(_triggerId, _triggerTimestamp, _triggerBlockNumber);
        return true;
    }

    function getBet(uint256 betNumber) public view returns (address) {
        return bets[betNumber];
    }

    function getNumberOfBets() public view returns (uint256) {
        return bets.length;
    }
}

By converting the drawWinner() method into an internal method, implementing pingchainTriggerCallback, and adding some boilerplate pingchain code, I’ve made this contract ready for eager execution.

Now all we have to do is follow the quickstart instructions; set up triggers, fill our account with gas and PNG, and our contract is ready to go!

Source code available at github.com/cranklin/crankys-lottery.

Before I conclude, there are a few things to note:

– Ping chain is not fully decentralized. It can’t be. Ethereum smart contracts must be triggered from the outside.
– This sample lottery contract should not be uploaded. I cooked this up really quick for educational purposes. You’ll find that it costs a lot of gas to run, the random number picker isn’t perfect, and some features are missing.
– (Pseudo)Random number generators in smart contracts are not a simple task. Deterministic generation of a pseudorandom number on a public blockchain while preventing it from being manipulated or guessed is obviously difficult to secure. Current block hashes are unknown (the miner hasn’t mined the block yet). Block hashes are only known for block heights: n-256 to n-1 where n is the current block height. The rest are 0. In this system, I stored the contract construction block height for its hash to be determined at a later transaction (state change). Each transaction (state change) stores that transaction’s block height and keccak256 hashes the concatenation of the previously stored block height’s hash with the latest concatenation. The final transaction (drawWinner) concatenates its n-1 block hash and performs a final keccak256 hash before calculating its modulo to the number of entries. It’s a quick/naive approach to a complicated problem.
– And you probably should not actually run a lottery… it’s probably illegal where you live

Advertisements

From → Hacks

One Comment
  1. Hello, could you create for me a fully automated Ethereum Lottery Smart Contract?

    Features:

    There will be 3 Contracts (games/ Lotteries)

    one Contract with 10 Players
    one Contract with 100 Players
    one contract with 1000 players (participants)

    10/100/1000 Players need send 0.015 Ether to the smart contract, one of them will win. The winning share is 80% (0.12ETH/1.2ETH/12.0ETH) for the winner

    20% goes to the smart contract creator/owner/publisher (me)

    if someone send not enough the ether will be send back to the participants.

    make sure that this game will not cost more than 21000 to 200000 gas for players (when they send ether)

    i will pay you 0.006 BTC for this job. if its not enough let me know ( i don’t have much and i think its fair amount.) email me at elmexixd@gmail.com

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s

%d bloggers like this: