2.4 Bets and Wagers

Although it’s fun to play Rock, Paper, Scissors! with friends for a laugh, it’s even better to play it with enemies and your entire life-savings on the line! Let’s change our program so that Alice can offer a wager to Bob and whoever wins will take the pot.

This time, let’s start with changes to the JavaScript frontend and then we’ll go back into the Reach code and connect the new methods up.

Since we’re going to be having funds get transfered, we’ll record the balances of each participant before the game starts, so we can more clearly show what they won at the end. We’ll add this code in between account creation and contract deployment.

..    // ...
 8    const accBob = await stdlib.newTestAccount(toNetworkFormat('10'));
 9    
10    const getBalance = async (who) => stdlib.fromWei( await stdlib.balanceOf(who) );
11    const beforeAlice = await getBalance(accAlice);
12    const beforeBob = await getBalance(accBob);
..    // ...

Next, we’ll update Alice’s interface object to include her wager.

..    // ...
27    backend.Alice(
28      stdlib, ctcAlice,
29      { ...Player('Alice'),
30        wager: toNetworkFormat('5')
31      }),
..    // ...

For Bob, we’ll modify his interface to show the wager and immediately accept it by returning.

..    // ...
32    backend.Bob(
33      stdlib, ctcBob,
34      { ...Player('Bob'),
35        acceptWager: (amt) =>
36        console.log(`Bob accepts the wager of ${stdlib.fromWei(amt)}.`) } )
..    // ...

Finally, after the computation is over, we’ll get the balance again and show a message summarizing the effect.

..    // ...
37    ]);
38    
39    const afterAlice = await getBalance(accAlice);
40    const afterBob = await getBalance(accBob);
41    
42    console.log(`Alice went from ${beforeAlice} to ${afterAlice}.`);
43    console.log(`Bob went from ${beforeBob} to ${afterBob}.`);
44    
45    })();

These changes to the frontend only deal with issues of presentation and interfacing. The actual business logic of making the wager and transferring the funds will happen in the Reach code.

Let’s look at that now.

First, we need to update the participant interact interface.

 1    'reach 0.1';
 2    
 3    const Player =
 4          { getHand: Fun([], UInt256),
 5            seeOutcome: Fun([UInt256], Null) };
 6    const Alice =
 7          { ...Player,
 8            wager: UInt256 };
 9    const Bob =
10          { ...Player,
11            acceptWager: Fun([UInt256], Null) };
12    
13    export const main =
14      Reach.App(
15        {},
16        [['Alice', Alice], ['Bob', Bob]],
17        (A, B) => {
..          // ...
42          exit(); });

Each of the three parts of the application have to be updated to deal with the wager. Let’s look at Alice’s first step first.

..    // ...
18    A.only(() => {
19      const wager = declassify(interact.wager);
20      const handA = declassify(interact.getHand()); });
21    A.publish(wager, handA)
22      .pay(wager);
23    commit();
..    // ...

Next, Bob needs to be shown the wager and given the opportunity to accept it and transfer his funds.

..    // ...
25    B.only(() => {
26      interact.acceptWager(wager);
27      const handB = declassify(interact.getHand()); });
28    B.publish(handB)
29      .pay(wager);
..    // ...

The DApp is now running in a consensus step and the contract itself now holds twice the wager amount. Before, it would compute the outcome and then commit the state; but now, it needs to look at the outcome and use it to balance the account.

..    // ...
31    const outcome = (handA + (4 - handB)) % 3;
32    const [forA, forB] =
33          outcome == 2 ? [2, 0] :
34          outcome == 0 ? [0, 2] :
35          [1, 1];
36    transfer(forA * wager).to(A);
37    transfer(forB * wager).to(B);
38    commit();
..    // ...

At this point, we can run the program and see its output by running

  $ ./reach run

Since the players act randomly, the results will be different every time. When I ran the program three times, this is the output I got:

$ ./reach run

Alice played Paper

Bob accepts the wager of 5.0.

Bob played Rock

Alice saw outcome Alice wins

Bob saw outcome Alice wins

Alice went from 10.0 to 14.999999999999687163.

Bob went from 10.0 to 4.999999999999978229.

 

$ ./reach run

Alice played Paper

Bob accepts the wager of 5.0.

Bob played Scissors

Alice saw outcome Bob wins

Bob saw outcome Bob wins

Alice went from 10.0 to 4.999999999999687163.

Bob went from 10.0 to 14.999999999999978246.

 

$ ./reach run

Alice played Rock

Bob accepts the wager of 5.0.

Bob played Scissors

Alice saw outcome Alice wins

Bob saw outcome Alice wins

Alice went from 10.0 to 14.999999999999687175.

Bob went from 10.0 to 4.999999999999978229.

How come Alice and Bob’s balance goes back to 10.0 each time? It’s because every time we run ./reach run, it starts a completely fresh instance of the testing network and creates new accounts for each player.

How come the balances aren’t exactly 10, 15, and 5? It’s because Ethereum transactions cost "gas" to run.

Why does Alice win slightly less than Bob when she wins? She has to pay to deploy the contract, because she calls acc.deploy in her frontend. The guide section on deployment discusses how to avoid this difference.

Alice is doing okay, if she keeps this up, she’ll make a fortune on Rock, Paper, Scissors!!

If your version isn’t working, look at the complete versions of tut-3/index.rsh and tut-3/index.mjs to make sure you copied everything down correctly!

Now that there is a reason to play this game, it turns out that there’s a major security vulnerability. We’ll fix this in the next step; make sure you don’t launch with this version, or Alice is going to go broke!