0

I have a betting contract, such that each week the prior week's bets are no longer relevant. I'm thinking about the most efficient way to store and process these bets. The alternatives are many, and so I just want to present my favorite and see if there are any obvious deficiencies. I have simplified/omitted things to be clear, hopefully not too much. I'm leaning towards a method that puts new bets into separate dynamic arrays, and then deleting them each week.

Dynamic arrays capture the relevant data: when the bettor puts 1.45 ETH on the Chicago Bears to beat the point spread. The are dynamic arrays, state variable:

address[] public bettorAddress; // eg, 0x123...
uint[] public betAmount;        // eg, 145e16
uint[] public betPick;         // eg, 7

For any bet the relevant data needed to process are all in the same slot, as each bet is processed with a method such as the following:

function takeBet(bet1, team)
    external
    {
    betAmount.push(bet1);
    bettorAddress.push(msg.sender);
    betPick.push(team);
 }

I can process the winners and losers in a loop by running it in chunks (say, 100 max). A variable (loopCounter in this example) is set to zero each week. At the end of the week, each team corresponds to a slot in the bool array resultsVector to indicate whether the bettor won or not (eg, if the Chicago Bears are team #7, 'resultsVector[7] = true' if the Bears beat the spread).

Thus, if there are 589 bets, and processing 100 bets costs 4e6 in gas, that's 6 separate transactions if I break it into chunks of 100. The settlement function would work as follows:

  function settleBook()
        external
        {
        uint top = min(loopCounter + 100, betAmount.length);
         // other code... 
        uint i;
 /** state variable loopCounter = 0 at start 
 **/
                for (i = loopCounter; i < top; i++) {
 /** results for 32 teams updated outside this function, in state variable: bool public resultsVector[32] 
 **/
                    if (resultsVector[betPick[i]]) {
 /** processes the results by giving bettor[i] appropriate credit for his successful pick. State variable bettorBalance defined as: mapping(address => uint) public bettorBalance
**/
                        bookiePot -= betAmount[i]; 
/** state variable bettorBalance defined as:  mapping(address => uint) public bettorBalance; 
**/
                        bettorBalance[bettorAddress[i]] += betAmount[i];
                    }
                }
/** advance the loopCounter to start at the next unprocessed bettor on the next function call
**/ 
                    loopCounter = i;
/** if all done, delete and reset loopCounter for the next week
**/ 
                    if (i == betAmount.length) {
                        // other code....
                        loopCounter = 0;
                        delete betAmount;
                        delete bettorAddress;
                        delete betPick;
                    }
  }

It works, but my biggest concern is that while it seems linear, I'm not sure it is: if I had 8765 bets, requiring 88 function calls to ultimately process, would this still work?

I could require users to process their bets, which breaks it up into single calls. Yet, some would not respond, and then I would need a method to purge lazy/distracted user data that creates problems. Further, it's more gas efficient to process them all at once.

Criticisms and suggestions welcome.

1

As a general rule, you should try to structure things so iteration isn't necessary, at all. Here are some general thoughts on the topic: https://blog.b9lab.com/getting-loopy-with-solidity-1d51794622ad

Unless I miss my guess the primary concern is awarding winnings to correct betters. A secondary concern is minimizing gas cost, and you can consider garbage collection to clean up data you don't need anymore. Deleting storage will give the sender a gas refund and reduce chain bloat.

Inverting the structure

Consider:

  1. Winners are motivated to claim prizes.
  2. A contract can check if the claim is valid and pay the prize.
  3. The end.

For step 2, you would want to be able to look up the msg.sender and their bet in one hop - go there directly, i.e. O(1). So, you will want to organize by the user and bet first, then tuck the details there for later verification. You'll add another dimension for games.

So, the key could be hash(game, team, user) and that could lead to a struct with things like amount.

You can get the same effect with something like:

mapping(bytes32 => mapping(bytes32 => mapping(address => Bet))) bets;

Garbage collection

Garbage collection is a little trickier with the suggested structure because it (so far) has no way to iterate the 'book`. You might find it worthwhile to create dynamic lists of keys that exist. Hashes would be handy here, also. Possibly you want a list of games that exist and the bets.

bytes32[] gameIds;
bytes32[] betIds; // the hash of user address and team they betted on

Knowing the keys that exist will make it possible to delete a few as background garbage collection process. If the data is truly irrelevant then it doesn't matter who deletes and every user will appreciate the lower gas cost from the refund if they delete something from the mapping which they place a new bet, claim a prize or anything else that supplies gas.

Consider modifiers attached to unrelated functions:

function doSomething() collectGarbage {

It might be very useful to remove elements from the arrays. It costs more in the constructive phase of the operation but you can get most of the gas back with the delete functions. Have a look over here for arrays with delete and random access by key. https://medium.com/robhitchens/solidity-crud-epilogue-e563e794fde

Hope it helps.

5
  • I've read your articles on loops and was hoping you would comment. I could create a mapping from hash(game,week,team) to an outcome (win/lose). A user could retrieve their winnings if that hash mapped to win (true), which would be in a struct with their address and bet amount. While this avoids looping, the size of the mapping and number of structs accumulates over time. As half will be unattended because they have no value, would the accumulation of unredeemed mappings and structs create problems, such as making it more costly to access these mappings and structs as they grow in number? – Eric Falkenstein Aug 28 '20 at 14:56
  • I assume more than one winner is possible. You would still have to update the flag which would be expensive. On the other hand, if the team is part of the bet to consider, and the winning team was set (once) then it should be possible determine if a prize claim is valid. So, if you can get to the bet in one move and you can confirm it is a winning bet then the contract can settle with that better. That should be all you need. – Rob Hitchens Aug 29 '20 at 6:21
  • Thanks for accepting my answer ;-) – Rob Hitchens Aug 29 '20 at 6:21
  • (forgot to accept!). Is there a cost to a mapping to a struct growing linearly over time? – Eric Falkenstein Aug 29 '20 at 14:31
  • 1
    If the cost of storing the struct is c, it will always be c. The OP doesnt increase in relation to previously stored data or overall size. Dynamic arrays have an additional write to update the array length but that's not a factor for mappings. – Rob Hitchens Aug 29 '20 at 22:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.