Blockchain Voting Using a Chainlink Alarm Clock Oracle

Many smart contracts on Ethereum require an external source of timing to trigger an on-chain action. For dApps using on-chain blockchain voting systems, this presents a challenge since voting windows are limited to a specific time. Without timers in Solidity, a voting smart contract would require some external source to modify the state of the voting window, open or closed.

On legacy systems, timing can be a simple task. System calls allow the time to be retrieved from the NTP synced OS time, there are hardware clocks and counters, and also sleep statements to halt code for a specified range of time. In Solidity smart contracts, events are triggered by off-chain transactions, which means Ethereum and other blockchains require an off-chain alarm clock to trigger events/function calls. Fortunately, Chainlink nodes can act as a reliable alarm clock to trigger smart contracts.

Here we’ll show you how your dApp can implement simple time-gated voting, a process that’s becoming increasingly needed as more dApps move towards putting power in the users’ hands with DAOs. This setup includes the following:

  • Inheriting Chainlink functionality into your contract
  • Formatting and submitting a Chainlink sleep (“until”) request
  • Restricting vote starting to the contract owner
  • Simple KYC to confirm each address only votes once
  • Getters to check on vote status

You can find the code used in this demo on GitHub or use the easy to deploy Remix link.

Of course, this basic example is just the beginning. Any event you need to be triggered at a certain time can be handled with a Chainlink “until” request and this example can be used as a starting framework to build out your other time-sensitive smart contracts.

Contract Definition and Constructor:

pragma solidity ^0.6.6;

import "github.com/smartcontractkit/chainlink/evm-contracts/src/v0.6/ChainlinkClient.sol";

contract ChainlinkTimedVote is ChainlinkClient
{
    uint private oraclePayment;
    address private oracle;
    bytes32 private jobId;
    uint private yesCount;
    uint private noCount;
    bool private votingLive;
    mapping(address => bool) public voters;

    //only the contract owner should be able to start voting
    address payable owner;
    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }

    constructor() public {
        setPublicChainlinkToken();
        owner = msg.sender;
        oraclePayment = 0.1 * 10 ** 18; // 0.1 LINK
        //Kovan alarm oracle
        oracle = 0x2f90A6D021db21e1B2A077c5a37B3C7E75D15b7e; 
        jobId = "a7ab70d561d34eb49e9b1612fd2e044b";
        //initialize votes
        yesCount = 0;
        noCount = 0;
        votingLive = false;
    }
}

Importing ChainlinkClient and inheriting it with the “is” keyword is all it takes to get access to the functionality of making Chainlink node requests. After that, there are a few global variables we want to define such as the oracle (node) address we will be using as the timer. These addresses can be found on the Chainlinks (Testnet) page.

Additionally, we need a job spec ID, some variables to track the voting, and a mapping of the voters to a boolean in order to track if an address has voted or not. Lastly, we define a modifier that we can use later to require a message sender calling a function to be the contract owner. We then initialize that owner address along with the other variables in the constructor. Note: mapping does not require initialization, it will default to false for the bool type.

Chainlink Request to Start Voting:

function startVote(uint voteMinutes) public onlyOwner {
    Chainlink.Request memory req = buildChainlinkRequest(jobId, address(this), this.fulfill.selector);
    req.addUint("until", now + voteMinutes * 1 minutes);
    //Start voting window then submit request to sleep for $voteMinutes
    votingLive = true;
    sendChainlinkRequestTo(oracle, req, oraclePayment);
}

//Callback for startVote request
function fulfill(bytes32 _requestId) public recordChainlinkFulfillment(_requestId) {
    //$voteMinutes minutes have elapsed, stop voting
    votingLive = false;
}

Our contract’s first function, startVote, is where Chainlink requests come into play. You can see this is where we use our onlyOwner modifier to ensure voting can only be started by the contract owner.

As covered in our Connect to Any API article, the ChainlinkClient contract exposes a Chainlink.Request struct that can be formatted in various ways depending on the nature of the request. In this case, instead of adding a “get” request to the struct, we add an “until”. The until string is recognized by the Chainlink node address we defined in the constructor and upon receiving this request, the node will halt its current task pipeline for the amount of time specified, in this case “voteMinutes” number of minutes.

Immediately before submitting this request, we set votingLive to true, which opens voting since the vote function checks the status of that variable to allow or reject votes. We then submit the request, at which point the Chainlink node will pause our task pipeline. Effectively, this delays the fulfill callback by the amount of time we specified in the until request. Since our desired time window has elapsed when fulfill is called, we can now set votingLive back to false, and voting is now closed.

To recap, build an “until” request with the desired time, submit it to your specified oracle node, and the fulfill function will be called after the node has paused for the specified time. It’s that easy to implement a Chainlink timer/alarm clock. With a simple votingLive boolean, we are able to control the opening and closing of the voting by marking voting open before the Chainlink alarm request and marking it closed when the callback is received after the delay. While it’s just a boolean flag in this example, it could be adapted to any time delayed actions as needed.

Voting and Checking Vote Status:

//Increments appropriate vote counter if voting is live
function vote(bool voteCast) public {
    require(!voters[msg.sender], "Already voted!");
    //if voting is live and address hasn't voted yet, count vote  
    if(voteCast) {yesCount++;}
    if(!voteCast) {noCount++;}
    //address has voted, mark them as such
    voters[msg.sender] = true;
}
   
//Outputs current vote counts
function getVotes() public view returns (uint yesVotes, uint noVotes) {
    return(yesCount, noCount);
}

//Lets user know if their vote has been counted
function haveYouVoted() public view returns (bool) {
    return voters[msg.sender];
}

Now we need the actual voting logic, of course. The Vote function simply takes a vote in the form of true/false (yes/no), checks if the user has previously voted, counts their vote if they passed the check, and marks them as having voted by setting their address mapping to true. We use a require statement to check the address rather than an if so that the transaction is rejected in the event they’ve already voted. Finally, since Ethereum transactions do not return output, we’ve added some simple view functions to check the current vote count and also confirm if your vote has been counted.

Hopefully this guide has helped illuminate one of Chainlink’s many uses that you can develop with live on mainnet to enhance the functionality of your smart contract application. If you want to learn about additional Chainlink functionalities, check out Chainlink VRF for provable random number generation, build your first yield farming dApp using Chainlink Price Feeds, or read up on our latest research on using Chainlink oracles for Fair Sequencing Services in transaction ordering.

If you’re a developer and want to connect your smart contract to off-chain data and systems, visit the developer documentation and join the technical discussion on Discord. If you want to schedule a call to discuss the integration more in-depth, reach out here.

Need Integration Support?
Talk to an expert
Faucets
Get testnet tokens
Read the Docs
Technical documentation