4.5 Workshop: Fear of Missing Out Generalized
In this workshop, we will extend our Fear of Missing Out application with the ability to split the reward between the N most recent Buyers.
In this version, the Funder will have the advantage that, if there are less than N Buyers, the Funder will earn the rewards for every absent Buyer. For example, if the auction is set to have 5 winners, yet only 3 Buyers bid, the first three Buyers will receive 1/5 of the funds each, and the Funder will receive the remaining 2/5 of the funds.
This workshop assumes that you have recently completed Workshop: Fear of Missing Out.
And it will run Reach.
4.5.1 Problem Analysis
Our problem analysis is practically the same as the original Fear of Missing Out application, except for one difference:
What funds change ownership during the application and how?
Stop!
Write down the problem analysis of this program as a comment.
Let’s compare answers for how funds should change ownership in this generalized version:
Buyers continually add funds to the balance during execution until the last N Buyers, and potentially the Funder, split the balance.
4.5.2 Data Definition
The data type representation of this program will basically be the same as the
regular Fear of Missing Out program. However, instead of tracking the latest Buyer as an
Address
, we will track the last N Buyers as an Array(Address, N)
.
You should take the time now to fill out the interaction interface for the participants.
Stop!
Write down the data definitions for this program as definitions.
Our participant interact interface, with the addition of some handy logging functions, looks like this so far:
It is worth noting that Reach does not support arbitrarily sized arrays, so we could not determine NUM_OF_WINNERS at runtime, e.g. from the interaction interface. However, we can still write a program that is generic in the size of the array, then specialize it when we compile.
.. // ...
4 const NUM_OF_WINNERS = 3;
5
6 const CommonInterface = {
7 showOutcome: Fun([Array(Address, NUM_OF_WINNERS)], Null),
8 };
9
10 const FunderInterface = {
11 ...CommonInterface,
12 getParams: Fun([], Object({
13 deadline: UInt, // relative deadline
14 ticketPrice: UInt,
15 })),
16 };
17
18 const BuyerInterface = {
19 ...CommonInterface,
20 shouldBuyTicket: Fun([UInt], Bool),
21 showPurchase: Fun([Address], Null),
22 };
.. // ...
At this point, you can modify your JavaScript file (index.mjs) to contain defintions of these values, although you may want to use a placeholders for the actual value. When you’re writing a Reach program, especially in the early phases, you should have these two files open side-by-side and update them in tandem as you’re deciding the participant interact interface.
4.5.3 Communication Construction
A fundamental aspect of a decentralized application is the pattern of communication and transfer among the participants. We should write down this structure as comments in our program to serve as an outline and guide us in implementation. In our original Fear of Missing Out implementation, we outlined the pattern of communication as follows:
// 1. The Funder publishes the ticket price and deadline
// 2. While the deadline has yet to be reached:
// 2a. Allow a Buyer to purchase a ticket
// 2b. Keep track of last Buyer
// 3. Transfer the balance to the last person who bought a ticket
This outline will need to be updated for our generalized version. You should do this now, in your Reach program (index.rsh).
Stop!
Write down the communication pattern for this program as comments.
Here’s what we wrote for our outline:
// 1. The Funder publishes the ticket price and deadline
// 2. While the deadline has yet to be reached:
// 2a. Allow a Buyer to purchase a ticket
// 2b. Keep track of the winners (last N Buyers)
// 3. Divide the balance evenly amongst the winners.
// 4. Transfer the reward to each winner.
Now, this outline needs to be converted to a real program.
Stop!
Write down the communication pattern for this program as code.
The body of your application should look something like this:
// 1. The Funder publishes the ticket price and deadline
Funder.publish(ticketPrice, deadline);
const initialWinners = Array.replicate(NUM_OF_WINNERS, Funder);
const [ keepGoing, winners, ticketsSold ] =
// 2. While the deadline has yet to be reached:
parallel_reduce([ true, initialWinners, 0 ])
.invariant(balance() == ticketsSold * ticketPrice)
.while(keepGoing)
.case(
Buyer,
// 2a. Allow a Buyer to purchase a ticket
(() => ({
when: declassify(interact.shouldBuyTicket(ticketPrice)) })),
(() => ticketPrice),
() => {
const buyer = this;
// 2b. Keep track of the winners (last N Buyers)
const idx = ticketsSold % NUM_OF_WINNERS;
const newWinners =
Array.set(winners, idx, buyer);
return [ true, newWinners, ticketsSold + 1 ]; })
.timeout(deadline, () => {
race(Buyer, Funder).publish();
return [ false, winners, ticketsSold ]; });
// 3. Divide the balance evenly amongst the winners.
transfer(balance() % NUM_OF_WINNERS).to(Funder);
const reward = balance() / NUM_OF_WINNERS;
// 4. Transfer the reward to each winner.
winners.forEach(winner =>
transfer(reward).to(winner));
commit();
Extending this program to track an array of Address
es, as opposed to a single
Address
is fairly straightforward. We maintain an array of size NUM_OF_WINNERS
and implement a ring buffer to keep it up to date with the most recent N winners, as
demonstrated in step 2b.
Another aspect of this code worth highlighting is step 3.
We transfer balance() % NUM_OF_WINNERS
to the winner because the total balance may not be evenly
divisible by the number of winners.
For example, if the ticket price is 4 ETH and there are 10 tickets purchased by Buyers, then the total balance will be 40 ETH. However, if the application is set to select 3 winners, then 40 cannot be evenly distributed to 3 participants. So, we will transfer 1 ETH to the Funder, and split the remaining 39 ETH between the 3 Buyers.
4.5.4 Assertion Insertion
This program doesn’t have many interesting properties to prove
as assertions, beyond the token linearity property. The
only property of interest is the parallel_reduce
invariant
which states that the balance must be equal to the number of tickets
sold multiplied by the ticket price.
4.5.5 Interaction Introduction
Next, we need to insert the appropriate calls to interact
.
In this case, our program is very simple and we expect you’ll do a great job without further discussion.
Stop!
Insertinteract
calls to the frontend into the program.
Let’s look at our whole program now:
1 'reach 0.1';
2
3 // FOMO Workshop generalized to last N winners
4 const NUM_OF_WINNERS = 3;
5
6 const CommonInterface = {
7 showOutcome: Fun([Array(Address, NUM_OF_WINNERS)], Null),
8 };
9
10 const FunderInterface = {
11 ...CommonInterface,
12 getParams: Fun([], Object({
13 deadline: UInt, // relative deadline
14 ticketPrice: UInt,
15 })),
16 };
17
18 const BuyerInterface = {
19 ...CommonInterface,
20 shouldBuyTicket: Fun([UInt], Bool),
21 showPurchase: Fun([Address], Null),
22 };
23
24 export const main = Reach.App(
25 { },
26 [
27 Participant('Funder', FunderInterface), ParticipantClass('Buyer', BuyerInterface),
28 ],
29 (Funder, Buyer) => {
30 const showOutcome = (winners) =>
31 each([Funder, Buyer], () => interact.showOutcome(winners));
32
33 Funder.only(() => {
34 const { ticketPrice, deadline } = declassify(interact.getParams()); });
35 Funder.publish(ticketPrice, deadline);
36
37 const initialWinners = Array.replicate(NUM_OF_WINNERS, Funder);
38
39 // Until deadline, allow buyers to buy ticket
40 const [ keepGoing, winners, ticketsSold ] =
41 parallel_reduce([ true, initialWinners, 0 ])
42 .invariant(balance() == ticketsSold * ticketPrice)
43 .while(keepGoing)
44 .case(
45 Buyer,
46 (() => ({
47 when: declassify(interact.shouldBuyTicket(ticketPrice)) })),
48 (() => ticketPrice),
49 () => {
50 const buyer = this;
51 Buyer.only(() => interact.showPurchase(buyer));
52 const idx = ticketsSold % NUM_OF_WINNERS;
53 const newWinners =
54 Array.set(winners, idx, buyer);
55 return [ true, newWinners, ticketsSold + 1 ]; })
56 .timeout(deadline, () => {
57 race(Buyer, Funder).publish();
58 return [ false, winners, ticketsSold ]; });
59
60 transfer(balance() % NUM_OF_WINNERS).to(Funder);
61 const reward = balance() / NUM_OF_WINNERS;
62
63 winners.forEach(winner =>
64 transfer(reward).to(winner));
65
66 commit();
67 showOutcome(winners);
68 });
4.5.6 Deployment Decisions
Next, it is time to test our program. As usual, we’ll present a completely automated test deployment, rather than an interactive one.
The program is fairly straightfoward to test. We just create test accounts for the Funder and any number of Buyers. The decision to purchase a ticket by a Buyer will rely simply on generating a random boolean.
Stop!
Decide how you will deploy and use this application.
Here’s the JavaScript frontend we wrote:
1 import {loadStdlib} from '@reach-sh/stdlib';
2 import * as backend from './build/index.main.mjs';
3
4 const numOfBuyers = 10;
5
6 (async () => {
7 const stdlib = await loadStdlib();
8 const startingBalance = stdlib.parseCurrency(100);
9
10 const accFunder = await stdlib.newTestAccount(startingBalance);
11 const accBuyerArray = await Promise.all(
12 Array.from({ length: numOfBuyers }, () =>
13 stdlib.newTestAccount(startingBalance)
14 )
15 );
16
17 const ctcFunder = accFunder.deploy(backend);
18 const ctcInfo = ctcFunder.getInfo();
19
20 const funderParams = {
21 ticketPrice: stdlib.parseCurrency(3),
22 deadline: 8,
23 };
24
25 const resultText = (outcome, addr) =>
26 outcome.includes(addr) ? 'won' : 'lost';
27
28 await Promise.all([
29 backend.Funder(ctcFunder, {
30 showOutcome: (outcome) =>
31 console.log(`Funder saw they ${resultText(outcome, accFunder.networkAccount.address)}`),
32 getParams: () => funderParams,
33 }),
34 ].concat(
35 accBuyerArray.map((accBuyer, i) => {
36 const ctcBuyer = accBuyer.attach(backend, ctcInfo);
37 const Who = `Buyer #${i}`;
38 return backend.Buyer(ctcBuyer, {
39 showOutcome: (outcome) =>
40 console.log(`${Who} saw they ${resultText(outcome, accBuyer.networkAccount.address)}`),
41 shouldBuyTicket : () => Math.random() < 0.5,
42 showPurchase: (addr) => {
43 if (stdlib.addressEq(addr, accBuyer)) {
44 console.log(`${Who} bought a ticket.`);
45 }
46 }
47 });
48 })
49 ));
50 })();
Let’s see what it looks like when we run the program:
$ ../reach run |
Buyer #6 bought a ticket. |
Buyer #3 bought a ticket. |
... |
Buyer #4 bought a ticket. |
Buyer #1 bought a ticket. |
Buyer #3 bought a ticket. |
Buyer #8 bought a ticket. |
Buyer #6 bought a ticket. |
Funder saw they lost |
Buyer #1 saw they lost |
Buyer #5 saw they lost |
Buyer #9 saw they lost |
Buyer #0 saw they lost |
Buyer #8 saw they won |
Buyer #7 saw they lost |
Buyer #2 saw they lost |
Buyer #6 saw they won |
Buyer #3 saw they won |
Buyer #4 saw they lost |
4.5.7 Discussion and Next Steps
Great job!
You’ve now implemented a generalized Fear of Missing Out game. You can try extending this application with additional features such as:
Slightly increasing the ticket price with each purchase.
Introducing a small payout system (dividends) to Buyers as the game progresses. e.g. every time the ring buffer is filled.
If you found this workshop rewarding, please let us know on the Discord community!