This repository has been archived by the owner on Oct 7, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 19
/
RockPaperScissors.sol
315 lines (285 loc) · 11.9 KB
/
RockPaperScissors.sol
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
pragma solidity ^0.5.11;
pragma experimental ABIEncoderV2;
import '@statechannels/nitro-protocol/contracts/interfaces/ForceMoveApp.sol';
import '@statechannels/nitro-protocol/contracts/Outcome.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
/**
* @dev The RockPaperScissors contract complies with the ForceMoveApp interface and implements a commit-reveal game of Rock Paper Scissors (henceforth RPS).
* The following transitions are allowed:
*
* Start -> RoundProposed [ PROPOSE ]
* RoundProposed -> Start [ REJECT ]
* RoundProposed -> RoundAccepted [ ACCEPT ]
* RoundAccepted -> Reveal [ REVEAL ]
* Reveal -> Start [ FINISH ]
*
*/
contract RockPaperScissors is ForceMoveApp {
using SafeMath for uint256;
enum PositionType {Start, RoundProposed, RoundAccepted, Reveal}
enum Weapon {Rock, Paper, Scissors}
struct RPSData {
PositionType positionType;
uint256 stake; // this is contributed by each player. If you win, you get your stake back as well as the stake of the other player. If you lose, you lose your stake.
bytes32 preCommit;
Weapon aWeapon; // playerOneWeapon
Weapon bWeapon; // playerTwoWeapon
bytes32 salt;
}
/**
* @notice Decodes the appData.
* @dev Decodes the appData.
* @param appDataBytes The abi.encode of a RPSData struct describing the application-specific data.
* @return An RPSData struct containing the application-specific data.
*/
function appData(bytes memory appDataBytes) internal pure returns (RPSData memory) {
return abi.decode(appDataBytes, (RPSData));
}
/**
* @notice Encodes the RPS update rules.
* @dev Encodes the RPS update rules.
* @param fromPart State being transitioned from.
* @param toPart State being transitioned to.
* @return true if the transition conforms to the rules, false otherwise.
*/
function validTransition(
VariablePart memory fromPart,
VariablePart memory toPart,
uint256, /* turnNumB */
uint256 /* nParticipants */
) public pure returns (bool) {
Outcome.AllocationItem[] memory fromAllocation = extractAllocation(fromPart);
Outcome.AllocationItem[] memory toAllocation = extractAllocation(toPart);
_requireDestinationsUnchanged(fromAllocation, toAllocation);
// decode application-specific data
RPSData memory fromGameData = appData(fromPart.appData);
RPSData memory toGameData = appData(toPart.appData);
// deduce action
if (fromGameData.positionType == PositionType.Start) {
require(
toGameData.positionType == PositionType.RoundProposed,
'Start may only transition to RoundProposed'
);
requireValidPROPOSE(
fromPart,
toPart,
fromAllocation,
toAllocation,
fromGameData,
toGameData
);
return true;
} else if (fromGameData.positionType == PositionType.RoundProposed) {
if (toGameData.positionType == PositionType.Start) {
requireValidREJECT(fromAllocation, toAllocation, fromGameData, toGameData);
return true;
} else if (toGameData.positionType == PositionType.RoundAccepted) {
requireValidACCEPT(fromAllocation, toAllocation, fromGameData, toGameData);
return true;
}
revert('Proposed may only transition to Start or RoundAccepted');
} else if (fromGameData.positionType == PositionType.RoundAccepted) {
require(
toGameData.positionType == PositionType.Reveal,
'RoundAccepted may only transition to Reveal'
);
requireValidREVEAL(fromAllocation, toAllocation, fromGameData, toGameData);
return true;
} else if (fromGameData.positionType == PositionType.Reveal) {
require(
toGameData.positionType == PositionType.Start,
'Reveal may only transition to Start'
);
requireValidFINISH(fromAllocation, toAllocation, fromGameData, toGameData);
return true;
}
revert('No valid transition found');
}
// action requirements
function requireValidPROPOSE(
VariablePart memory fromPart,
VariablePart memory toPart,
Outcome.AllocationItem[] memory fromAllocation,
Outcome.AllocationItem[] memory toAllocation,
RPSData memory fromGameData,
RPSData memory toGameData
)
private
pure
outcomeUnchanged(fromPart, toPart)
stakeUnchanged(fromGameData, toGameData)
allocationsNotLessThanStake(fromAllocation, toAllocation, fromGameData, toGameData)
{}
function requireValidREJECT(
Outcome.AllocationItem[] memory fromAllocation,
Outcome.AllocationItem[] memory toAllocation,
RPSData memory fromGameData,
RPSData memory toGameData
)
private
pure
allocationUnchanged(fromAllocation, toAllocation)
stakeUnchanged(fromGameData, toGameData)
{}
function requireValidACCEPT(
Outcome.AllocationItem[] memory fromAllocation,
Outcome.AllocationItem[] memory toAllocation,
RPSData memory fromGameData,
RPSData memory toGameData
) private pure stakeUnchanged(fromGameData, toGameData) {
require(fromGameData.preCommit == toGameData.preCommit, 'Precommit should be the same.');
// Since Player A has the unique privilege of knowing the result of the game after receiving this 'to' state,
// Player B should modify the allocations as if they had won the game and Player A had lost.
// This is to incentivize Player A to continue with the REVEAL (rather than disconnecting)
// in the case where Player A knows they have lost.
require(
toAllocation[0].amount == fromAllocation[0].amount.sub(toGameData.stake),
'Allocation for player A should be decremented by 1x stake'
);
require(
toAllocation[1].amount == fromAllocation[1].amount.add(toGameData.stake),
'Allocation for player B should be incremented by 1x stake.'
);
}
function requireValidREVEAL(
Outcome.AllocationItem[] memory fromAllocation,
Outcome.AllocationItem[] memory toAllocation,
RPSData memory fromGameData,
RPSData memory toGameData
) private pure stakeUnchanged(fromGameData, toGameData) {
require(
toGameData.bWeapon == fromGameData.bWeapon,
"Player Second's weapon should be the same between commitments."
);
// check hash matches
// need to convert Weapon -> uint256 to get hash to work
bytes32 hashed = keccak256(abi.encode(uint256(toGameData.aWeapon), toGameData.salt));
require(hashed == fromGameData.preCommit, 'The hash needs to match the precommit');
// Recall that on the 'from' state, the allocations are as if Player A has lost.
// First, undo this
uint256 correctAmountA = fromAllocation[0].amount.add(fromGameData.stake);
uint256 correctAmountB = fromAllocation[1].amount.sub(fromGameData.stake);
// Next, transfer one "stake" from Loser to Winner
if (toGameData.aWeapon == toGameData.bWeapon) {
// a draw
} else if ((toGameData.aWeapon == Weapon.Rock && toGameData.bWeapon == Weapon.Scissors) || (toGameData.aWeapon > toGameData.bWeapon)) {
// player A won
correctAmountA = correctAmountA.add(fromGameData.stake);
correctAmountB = correctAmountB.sub(fromGameData.stake);
} else {
// player B won
correctAmountA = correctAmountA.sub(fromGameData.stake);
correctAmountB = correctAmountB.add(fromGameData.stake);
}
require(
toAllocation[0].amount == correctAmountA,
"Player A's allocation should reflect the result of the game."
);
require(
toAllocation[1].amount == correctAmountB,
"Player B's allocation should reflect the result of the game."
);
}
function requireValidFINISH(
Outcome.AllocationItem[] memory fromAllocation,
Outcome.AllocationItem[] memory toAllocation,
RPSData memory fromGameData,
RPSData memory toGameData
)
private
pure
allocationUnchanged(fromAllocation, toAllocation)
stakeUnchanged(fromGameData, toGameData)
{}
function extractAllocation(VariablePart memory variablePart)
private
pure
returns (Outcome.AllocationItem[] memory)
{
Outcome.OutcomeItem[] memory outcome = abi.decode(variablePart.outcome, (Outcome.OutcomeItem[]));
require(outcome.length == 1, 'RockPaperScissors: Only one asset allowed');
Outcome.AssetOutcome memory assetOutcome = abi.decode(
outcome[0].assetOutcomeBytes,
(Outcome.AssetOutcome)
);
require(
assetOutcome.assetOutcomeType == uint8(Outcome.AssetOutcomeType.Allocation),
'RockPaperScissors: AssetOutcomeType must be Allocation'
);
Outcome.AllocationItem[] memory allocation = abi.decode(
assetOutcome.allocationOrGuaranteeBytes,
(Outcome.AllocationItem[])
);
require(
allocation.length == 2,
'RockPaperScissors: Allocation length must equal number of participants (i.e. 2)'
);
return allocation;
}
function _requireDestinationsUnchanged(
Outcome.AllocationItem[] memory fromAllocation,
Outcome.AllocationItem[] memory toAllocation
) private pure {
require(
toAllocation[0].destination == fromAllocation[0].destination,
'RockPaperScissors: Destimation playerA may not change'
);
require(
toAllocation[1].destination == fromAllocation[1].destination,
'RockPaperScissors: Destimation playerB may not change'
);
}
// modifiers
modifier outcomeUnchanged(VariablePart memory a, VariablePart memory b) {
require(
keccak256(b.outcome) == keccak256(a.outcome),
'RockPaperScissors: Outcome must not change'
);
_;
}
modifier stakeUnchanged(RPSData memory fromGameData, RPSData memory toGameData) {
require(
fromGameData.stake == toGameData.stake,
'The stake should be the same between commitments'
);
_;
}
modifier allocationsNotLessThanStake(
Outcome.AllocationItem[] memory fromAllocation,
Outcome.AllocationItem[] memory toAllocation,
RPSData memory fromGameData,
RPSData memory toGameData
) {
require(
fromAllocation[0].amount >= toGameData.stake,
'The allocation for player A must not fall below the stake.'
);
require(
fromAllocation[1].amount >= toGameData.stake,
'The allocation for player B must not fall below the stake.'
);
_;
}
modifier allocationUnchanged(
Outcome.AllocationItem[] memory fromAllocation,
Outcome.AllocationItem[] memory toAllocation
) {
require(
toAllocation[0].destination == fromAllocation[0].destination,
'RockPaperScissors: Destination playerA may not change'
);
require(
toAllocation[1].destination == fromAllocation[1].destination,
'RockPaperScissors: Destination playerB may not change'
);
require(
toAllocation[0].amount == fromAllocation[0].amount,
'RockPaperScissors: Amount playerA may not change'
);
require(
toAllocation[1].amount == fromAllocation[1].amount,
'RockPaperScissors: Amount playerB may not change'
);
_;
}
}