Lessons Learned from Developing an NFL Survivor Pool on Ethereum

Matt Solomon
Coinmonks
7 min readAug 26, 2018

--

I recently began developing an NFL Survivor application on Ethereum. For those unfamiliar with survivor pools, the basic gameplay rules are as follows:

  • Pay the required entry fee to join a Survivor pool
  • Each week, choose one team that you think will win their game. If your team wins, you move on to the next week. If your team loses, you are out.
  • Each team can only be picked once
  • Last person standing wins the pot
  • If all remaining players are eliminated at once, or multiple players remain when the NFL season is over, the pot it split equally among them
Source

This turned out to be a bit more complicated than I initially expected. Below are the various challenges I faced during development and how I tackled them.

Obtaining Game Results

In order to determine which players remain after each week, we need to know which teams won their NFL games, and then we need to compare those winners to each players’ picks. Getting outside data into the blockchain in a trustless manner is a tough problem, especially when you need to compare that data against every player who joined.

Trustless Approach Using Oraclize

A fairly trustless solution is to use Oraclize (well, you do need to trust Oraclize). There are two ways we can use this service to determine the winners of NFL games each :

  1. Use a standard API call, as detailed in a tutorial I’ve previously written
  2. Use the computation data source to not only get the game winners, but to also determine which players remain and which are eliminated.

In the worst case, we need to reach out to an Oracle 17 total times (once a week for each of the 17 weeks in a NFL season). Using option 1, assuming we can get all game results in one API call, the Oraclize default gas parameters will cost us 0.004 ETH per week or 0.068 ETH over the full season. This means, at current price levels of ~$300/ETH, the contract will spend about $20 over the season just to determine the winner. And this is before adding in gas costs for string parsing and manipulation (which is complicated and costly in Solidity) and for loops needed to determine the remaining players. So option 1 is eliminated, due to the cost and complexity of parsing the result and looping over all players to determine the remaining players.

Let’s look at option 2. The Oraclize computation data source requires the script to print the result of the computation as the last line. This means we could either:

  1. Print a string of the winning (or losing) teams and parse them in Solidity, or
  2. Use a computation that goes one step further and prints a string that tells us which players remain

Approach 1 still introduces the complications discussed of string manipulation and loops described above, which we’d like to avoid if possible, so it’s not a great option.

To clarify what approach 2 is suggesting: say we have an array of addresses called playersEntered which stores the address of each entered player, and an array of booleans called isNotEliminated that stores whether each player is eliminated. We can then have a script return a string such as '1001110' where the first digit indicates that players[0] is still alive, but players[1] is eliminated. This approach, like all other approaches above, requires us to loop through each player to update the contract’s state appropriately. This can get costly as the number of players gets large, so is also not favorable.

A More Trusting Approach

There’s a trade-off between trust, cost, and complexity that was can explore a bit.

It would be pretty obvious to all players if they were wrongly eliminated due to fake game results used for the benefit of the contract owner. So another approach is to, on our own server, call the API to get the NFL winners, then determine the remaining players on our own server. Next, still using our server, we can call a contract function from our server and provide an array remaining players. We can update our contract’s variables that store player status with the ones provided by the API, and thus prevent the need for any loops!

However, even if it is obvious that there is manipulation, we currently don’t provide a way for players to get their money back. If you want to increase the trust a bit more, one idea is to use some type of voting system. Implement a function that users call if they believe they were wrongly eliminated, and if over X% of users call this function, all user funds are returned. The risk here is shows up later in the season when only a handful of players remain. What’s to stop all the losing players from calling this function to get their money back, regardless of whether or not the results have been faked?

For now, I chose to go with the approach described in this section for its simplicity and cost-effectiveness, although I don’t like that it does require trust in the contract owner’s own API to determine the remaining players. This is something I’d like to update in a future version of this contract.

Scheduling Future Transactions

Each week, various contract functions need to be called to do things such as fetch game results from an API and update this week’s pick deadline. Ideally, these functions would be called automatically so the contract deployer does not need to manually call them each week. How can we do this?

One interesting solution is to use Ethereum Alarm Clock, but unfortunately it’s not yet deployed on the mainnet it is deployed on the Ropsten and Kovan testnets, though). It also relies on incentivizing humans to make the function call, meaning you won’t be able to test this approach on a private network.

Alternatively, if you run a node using Geth or Parity, you can schedule transactions through your node. The answers to this StackExchange question explains how you can do that. However, I do not run a node and did not want to set one up, so this option was off the table. Another downside is that it is not explicit that this functionality is implemented when reading through the contract.

A third approach is to incentivize the players to call the required functions for us. We can do this by providing them a small payment as a reward for calling the function. The obvious downside here is that there is still no guarantee the function will be called, especially if the reward is not high enough. And the reward may become less enticing, or too high, if the ETH/USD price changes. Furthermore, adding another function that users can call increases the contract’s attack surface.

The next approach I’m aware of is to use Oraclize, either with or without an API call. This is not a standard Oraclize use case, so let’s go into a bit more depth here. When using Oraclize, the typical execution flow looks like this:

  1. Manually call some function foo() in your contract
  2. foo() calls oraclize_query() with the API query as one of the inputs to oraclize_query()
  3. Oraclize reaches out to the specified API and waits for a response
  4. Oraclize calls a function in your contract called __callback() and passes it the result of the query

The “trick” comes in step 2, where oraclize_query() actually allows you to pass in a delay parameter, which postpones the API call for the specified amount of time. If you enter the API query input as an empty string, Oraclize will not throw an error, and will now act as a transaction scheduler. See this StackExchange answer for a simple example.

An additional benefit of the Oraclize approach is that it can be easily used to schedule recursive queries, making it easy to repeat our function call each week. As discussed above, the downside to this approach is that it can be quite costly. Due to its ease of implementation, and the fact that I’m already familiar with Oraclize from my previous tutorial, I decided to take the cost hit and use this approach. Using the numbers from the previous section, the default cost of an Oraclize query costs 0.004 ETH, which works out to about $1.20/week.

One last option worth mentioning is to set up a cron job on your own server to schedule the function calls as needed from a regular account. Like scheduling transactions from a node it is not apparent that this implemented when reading the contract.

Testing Future Transactions

Often times smart contract functions can only be called during certain times. In this case, we must prevent players from choosing their teams after the games have started. So how can we do this?

We’ll be writing our tests using ganache-cli and Truffle, and utilizing a feature of ganache called evm_increaseTime to alter the time. This method allows us to arbitrarily increase the EVM time by as much as we want, so we can simulate calls at a future date.

However, it seems there are some issues with this method as I could not get it to work properly. The function below was used to increase the EVM time using the evm_increaseTime method:

However, as seen from the comments in that snippet, this does not work properly. According to this issue, we need to mine a block after we change the time to actually get the time change to take affect. We do that using the evm_mine method, and this does seem to work if we check the ganache-cli timestamp. However, if we add a function to our contract that simply returns now or block.timestamp, we see the time change is not honored. Similarly, tests depending on now fail since the time change is not recognized by the contract. This is demonstrated in the snippet below:

There is an open issue for this here, and as a result I have not been able to test future transactions. This is a bit disappointing, since this contract is quite reliant on things happening at certain time. If anyone has suggestions on how to get this working properly, I’d love to hear it!

Get Best Software Deals Directly In Your Inbox

--

--