-
Notifications
You must be signed in to change notification settings - Fork 0
/
betacorn.cpp
365 lines (292 loc) · 12.8 KB
/
betacorn.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
#include <betacorn.hpp>
namespace eosio {
void dice::withdraw( name to, asset quantity ) {
require_auth( to );
check( quantity.symbol == ACORN_SYMBOL, "you can only withdraw acornaccount::ACORN" );
check( quantity.is_valid(), "invalid quantity" );
check( quantity.amount > 0, "must withdraw positive quantity" );
// subtract balance
sub_balance( to, quantity, true );
// withdraw ACORNs
pay( to, quantity, "" );
}
void dice::commit( name host, const checksum256& commitment ) {
require_auth( host );
// Commitments are game proposals, and at first they are not matched to a player
// and to a bet size. When a player wants to play, they are matched with a
// commitment that belongs to a host that can cover the player bet with their
// current ACORN deposit balance.
// Check that the host has a positive deposit balance.
// Hosts can only propose commitments AFTER they have Shown Us The Money.
accounts acnts( _self, _self.value );
auto owner_accounts = acnts.get_index<"byowner"_n>();
auto it = owner_accounts.find( host.value );
check( it != owner_accounts.end(), "cannot commit with a bankroll of zero" );
// Check that the commitment's first 64 bits are unique among all commitments
// in the matches table.
// That works because the matches table is a superset of the games table. When
// an empty game is created, a dummy clone entry is added to the match table
// as well (because intercepted player transfers--bets) cannot allocate RAM.
// So we just search the matches table here.
uint64_t hash_prefix = get_hash_prefix( commitment );
check( hash_prefix != ZERO_SOURCE, "A zeroed-out checksum256 is not an acceptable commitment source" );
matches mts( _self, _self.value );
auto mt_collision_it = mts.find( hash_prefix );
check( mt_collision_it == mts.end(), "commitment already exists or was generated from a bad seed" );
// It is unique, so create an entry for it (host pays RAM, so no other checks or limitations needed)
games gms( _self, _self.value );
gms.emplace( host, [&]( auto& g ){
g.commitment = commitment;
g.host = host;
});
// We need to preallocate a mirror, dummy match entry because the player won't
// be able to pay for RAM.
mts.emplace( host, [&]( auto& m ){
m.commitment = commitment;
m.host = host;
m.guess = NULL_GUESS;
m.player = NULL_NAME;
m.bet = ZERO_ACORNS;
m.deadline = time_point_sec(get_current_time()); // just debug info
});
}
void dice::cancelcommit( name host, const checksum256& commitment ) {
require_auth( host );
// find the match
uint64_t hash_prefix = get_hash_prefix( commitment );
matches mts( _self, _self.value );
auto mit = mts.find( hash_prefix );
check( mit != mts.end(), "commitment not found" );
const match & em = *mit;
// can only cancel commitments that are not waiting for a reveal already.
check( em.guess == NULL_GUESS, "cannot cancel commitment: already in play" );
// delete match entry
mts.erase( mit );
// delete game entry
games gms( _self, _self.value );
auto git = gms.find( hash_prefix );
gms.erase( git );
}
void dice::reveal( const checksum256& commitment, const checksum256& source ) {
// check that the provided source and commitment parameters match
const auto & source_array = source.extract_as_byte_array();
assert_sha256( (char *)&source_array[0], 32, commitment );
// find the match
uint64_t hash_prefix = get_hash_prefix( commitment );
matches mts( _self, _self.value );
auto mit = mts.find( hash_prefix );
check( mit != mts.end(), "commitment not found" );
const match & em = *mit;
// Figure out who to pay for what.
if (em.guess != NULL_GUESS) {
// build the payout value. we have to remove 0.0001 ACORN from it because we pay out
// 0.0001 ACORN when the player loses in order to notify them of the loss.
asset win_quantity = (em.bet * 2) - ACORN_SHELL;
// check who won and issue the correct payout transaction to the player and calculate
// the corresponding payout amount to the host.
asset host_payout;
asset player_payout;
string player_message;
char result = source_array[31] & 1;
if (result == em.guess) {
host_payout = ACORN_SHELL;
player_payout = win_quantity;
player_message = "Win!";
} else {
host_payout = win_quantity;
player_payout = ACORN_SHELL;
player_message = "Lose";
}
// notify and/or pay player
pay( em.player, player_payout, player_message );
// update host balance
add_balance( em.host, host_payout, false );
} else {
// This is a reveal without a player, i.e. the "match" was just the placeholder match entry
// that we created because we can't charge RAM to the player.
// So this is just another way to do a cancel_commit().
// Since the match was still open, there is a game entry that needs to be cleaned up as well.
games gms( _self, _self.value );
auto git = gms.find( hash_prefix );
gms.erase( git );
}
// delete match entry
mts.erase( mit );
}
void dice::collect( name player ) {
// for every match that player is in (search byplayer)
matches mts( _self, _self.value );
auto player_matches = mts.get_index<"byplayer"_n>();
auto it = player_matches.find( player.value );
uint32_t now = get_current_time();
while ( it != player_matches.end() ) {
const match & em = *it;
// if match has timed out
if (now > em.deadline.sec_since_epoch()) {
// send the player their winnings, which is everything (full penalty for timeouts)
pay( player, em.bet * 2, "Win! (Timeout)");
// delete match entry & move iterator to next match entry
it = player_matches.erase( it );
} else {
++it;
}
}
}
void dice::acorn_transfer( name from, name to, asset quantity, string memo ) {
// Not interested in actions where we are paying others.
if ( from == _self )
return;
check( quantity.symbol == ACORN_SYMBOL, "you can only deposit acornaccount::ACORN" );
check( quantity.is_valid(), "invalid quantity" );
check( quantity.amount >= MIN_TRANSFER_SHELLS, "minimum quantity not met" ); // avoid deposit spam & serves as minimum bet guard
check( memo.size() <= 256, "memo has more than 256 bytes" );
// If memo is exactly "deposit", this is a host funding its games.
// If memo is exactly "odd" or "0", this is a bet for an odd number.
// If memo is exactly "even" or "1", this is a bet for an even number.
// If memo is anything else, the transaction is rejected.
if (memo == "odd" || memo == "Odd" || memo == "ODD" || memo == "1") {
do_bet(from, quantity, 1);
} else if (memo == "even" || memo == "Even" || memo == "EVEN" || memo == "0") {
do_bet(from, quantity, 0);
} else if (memo == "deposit" || memo == "Deposit" || memo == "DEPOSIT") {
add_balance( from, quantity, true );
} else {
check( false, "memo must be: 'odd', 'even' or 'deposit'.");
}
}
void dice::do_bet( name player, asset quantity, char guess ) {
// First we search for a host that has a sufficient balance to cover our bet.
asset max_bankroll = ZERO_ACORNS;
accounts acnts( _self, _self.value );
auto ait = acnts.begin();
while (ait != acnts.end()) {
const account & acct = *ait;
// At most 1% of a host's current bankroll is at risk in a bet.
if ((acct.balance / MAX_BET_TO_BANKROLL_RATIO) >= quantity) {
// 1% of the acct balance can cover the bet.
// Now let's search for any open game (commitment) that this acct hosts.
games gms( _self, _self.value );
auto host_games = gms.get_index<"byhost"_n>();
auto git = host_games.find( acct.owner.value );
if (git != host_games.end()) {
// We got one free commitment.
const game & eg = *git;
// Fund the match by subtracting from the host's account balance.
sub_balance( acct.owner, quantity, false );
// Find and fill in the dummy match entry with an actual player now
matches mts( _self, _self.value );
auto mit = mts.find( get_hash_prefix( eg.commitment ) );
mts.modify( mit, same_payer, [&]( auto& m ){
m.guess = guess;
m.player = player;
m.bet = quantity;
m.deadline = time_point_sec(get_current_time() + GAME_TIMEOUT_SECS);
});
// Remove the game entry (open game offer), leaving only the ongoing,
// active match entry.
host_games.erase( git );
// And we are done.
return;
}
} else if (acct.balance > max_bankroll) {
// If this host has an open game offer, we can record their bankroll
// as the new maximum bankroll availble to cover bets.
games gms( _self, _self.value );
auto host_games = gms.get_index<"byhost"_n>();
auto git = host_games.find( acct.owner.value );
if (git != host_games.end()) {
max_bankroll = acct.balance;
}
}
++ait;
}
// Did not find a single game to match this player's bet, so refuse the player's ACORN transfer.
max_bankroll /= MAX_BET_TO_BANKROLL_RATIO; // Max bet is actually 1% of the max bankroll.
if (max_bankroll.amount < MIN_TRANSFER_SHELLS) {
check( false, "no bets available" );
} else {
string msg = "the current maximum bet is ";
msg.append( max_bankroll.to_string() );
check( false, msg );
}
}
void dice::pay( name to, asset quantity, string memo ) {
action(
permission_level{ _self, "active"_n },
"acornaccount"_n, "transfer"_n,
std::make_tuple(_self, to, quantity, memo)
).send();
}
void dice::add_balance( name owner, asset value, bool enforce_min ) {
accounts acnts( _self, _self.value );
auto owner_accounts = acnts.get_index<"byowner"_n>();
auto it = owner_accounts.find( owner.value );
if( it == owner_accounts.end() ) {
if (enforce_min) {
// Enforce a minimum balance to allow the creation of an account.
// This helps because players iterate over all accounts to find a suitable game host.
check( value >= MIN_BALANCE, "deposit does not meet minimum balance requirement" );
}
// The RAM payer needs to be the contract itself. We can't charge RAM during
// an incoming ACORN transfer.
// (Not sure how this was working at all before, with "owner" being charged
// for the RAM.)
// The only other way to do this is create an "open account" action that charges
// RAM to the caller. For now let's just do this.
acnts.emplace( _self, [&]( auto& a ){
a.owner = owner;
a.balance = value;
});
} else {
owner_accounts.modify( it, same_payer, [&]( auto& a ) {
a.balance += value;
});
}
}
void dice::sub_balance( name owner, asset value, bool enforce_min ) {
accounts acnts( _self, _self.value );
auto owner_accounts = acnts.get_index<"byowner"_n>();
auto it = owner_accounts.find( owner.value );
check( it != owner_accounts.end(), "no account object found" );
const auto& owner_account = *it;
check( owner_account.balance.amount >= value.amount, "overdrawn balance" );
asset result = owner_account.balance - value;
if (result.amount == 0) {
owner_accounts.erase( it );
// Wiping your host account balance clean is an implicit request to cancel every single game
// offer that has not been taken yet.
matches mts( _self, _self.value );
games gms( _self, _self.value );
auto host_games = gms.get_index<"byhost"_n>();
auto git = host_games.find( owner.value );
while (git != host_games.end()) {
auto mit = mts.find( git->primary_key() ); // any match that has a corresponding game is by definition empty
mts.erase( mit ); // so just erase it, no need to test mit->guess == NULL_GUESS (it is.)
git = host_games.erase( git );
}
} else {
if (enforce_min) {
// When withdrawing ACORN to an external account, either you are withdrawing everything, or
// you need to leave a minimum balance, in order to prevent host-balance (account entry) spam.
check( result >= MIN_BALANCE,
"withdrawal must either withdraw the full balance, or the remainder must meet the minimum balance requirement" );
// In addition, you cannot withdraw less than the minimum transfer amount if you're not
// emptying the account.
check( value.amount >= MIN_TRANSFER_SHELLS, "withdrawals below the minimum transfer are only allowed when emptying the account" );
}
owner_accounts.modify( it, same_payer, [&]( auto& a ) {
a.balance = result;
});
}
}
extern "C" {
void apply(uint64_t receiver, uint64_t code, uint64_t action) {
if (code == "acornaccount"_n.value && action == "transfer"_n.value) {
eosio::execute_action(eosio::name(receiver), eosio::name(code), &dice::acorn_transfer);
} else if (code == receiver) {
switch (action) { EOSIO_DISPATCH_HELPER(dice, (withdraw)(commit)(cancelcommit)(reveal)(collect)) }
}
eosio_exit(0);
}
}
} /// namespace eosio