Smart Contract
randomizeBreak
A.8d75e1dff5f8af66.randomizeBreak
1// TeamAssignment.cdc
2// Smart contract for randomly assigning NBA Top Shot usernames to NBA teams
3// Uses Flow's commit-reveal pattern with RandomConsumer VRF for provably fair randomness
4// Based on Flow team's recommended approach using RandomConsumer contract
5
6import RandomConsumer from 0x45caec600164c9e6
7import Burner from 0xf233dcee88fe0abe
8
9access(all) contract randomizeBreak {
10
11 // ============ Events (Matching Flow's Pattern) ============
12
13 // Event emitted when a commitment is made (lock phase)
14 // Note: RandomnessSourced event is automatically emitted by RandomConsumer.requestRandomness()
15 access(all) event CommitmentMade(commitmentId: UInt64, lockBlock: UInt64, usernames: [String], teams: [String], combos: [String])
16
17 // Event emitted when an assignment is made (matches Flow's ParticipantAssigned pattern)
18 access(all) event ParticipantAssigned(username: String, team: String, assignmentIndex: UInt64, receiptID: String)
19
20 // Event emitted when all assignments are complete (matches Flow's completion pattern)
21 // Note: RandomnessFulfilled event is automatically emitted by RandomConsumer.fulfillRandomRequest()
22 access(all) event TeamAssignmentComplete(
23 commitmentId: UInt64,
24 lockBlock: UInt64,
25 revealBlock: UInt64,
26 receiptID: String,
27 verificationNote: String,
28 assignments: {UInt64: Assignment}
29 )
30
31 // ============ Data Structures ============
32
33 // Assignment structure
34 access(all) struct Assignment {
35 access(all) let username: String
36 access(all) let team: String
37 access(all) let assignmentIndex: UInt64
38 access(all) let timestamp: UFix64
39
40 init(username: String, team: String, assignmentIndex: UInt64, timestamp: UFix64) {
41 self.username = username
42 self.team = team
43 self.assignmentIndex = assignmentIndex
44 self.timestamp = timestamp
45 }
46 }
47
48 // Receipt resource - returned from commit, submitted to reveal
49 // Conforms to RandomConsumer.RequestWrapper for VRF integration
50 access(all) resource Receipt {
51 access(all) let usernames: [String]
52 access(all) let teams: [String]
53 access(all) let combos: [String]
54 access(all) let lockBlock: UInt64 // Store block height when request is created
55 access(all) var request: @RandomConsumer.Request?
56
57 init(
58 usernames: [String],
59 teams: [String],
60 combos: [String],
61 request: @RandomConsumer.Request
62 ) {
63 self.usernames = usernames
64 self.teams = teams
65 self.combos = combos
66 self.lockBlock = request.block // Store block height before moving request
67 self.request <- request
68 }
69
70 // Required by RequestWrapper interface
71 access(all) fun popRequest(): @RandomConsumer.Request? {
72 let req <- self.request <- nil
73 return <-req
74 }
75
76 // Get the block height from the request
77 access(all) fun getRequestBlock(): UInt64? {
78 return self.lockBlock
79 }
80 }
81
82 // ============ Contract State ============
83
84 // Storage for all completed assignments
85 access(all) var assignments: {UInt64: Assignment}
86
87 // Consumer resource for VRF randomness
88 access(self) let consumer: @RandomConsumer.Consumer
89
90 // Counter for commitment IDs (for receiptID generation)
91 access(all) var nextCommitmentId: UInt64
92
93 // ============ Contract Initialization ============
94
95 init() {
96 self.assignments = {}
97 self.consumer <- RandomConsumer.createConsumer()
98 self.nextCommitmentId = 0
99 }
100
101 // ============ Commit Phase ============
102
103 // Lock in the assignment - returns a Receipt for later reveal
104 // The randomness is requested but NOT revealed yet
105 // RandomConsumer.requestRandomness() automatically emits RandomnessSourced event
106 access(all) fun commitAssignment(
107 usernames: [String],
108 teams: [String],
109 combos: [String]
110 ): @Receipt {
111 pre {
112 usernames.length > 0: "Must provide at least one username"
113 (teams.length + combos.length) == usernames.length: "Number of single teams plus combos must equal number of usernames"
114 }
115
116 // Request randomness from the VRF
117 // This automatically emits RandomnessSourced event with block, randomSource, and requestUUID
118 let request <- self.consumer.requestRandomness()
119 let lockBlock = request.block
120
121 // Generate commitment ID for tracking
122 let commitmentId = self.nextCommitmentId
123 self.nextCommitmentId = commitmentId + 1
124
125 // Create the receipt
126 let receipt <- create Receipt(
127 usernames: usernames,
128 teams: teams,
129 combos: combos,
130 request: <-request
131 )
132
133 // Emit our commitment event
134 emit CommitmentMade(
135 commitmentId: commitmentId,
136 lockBlock: lockBlock,
137 usernames: usernames,
138 teams: teams,
139 combos: combos
140 )
141
142 return <- receipt
143 }
144
145 // ============ Reveal Phase ============
146
147 // Reveal the assignments using the committed randomness
148 // This consumes the receipt and returns the final assignments
149 // RandomConsumer.fulfillRandomRequest() automatically emits RandomnessFulfilled event
150 access(all) fun revealAndAssign(receipt: @Receipt): {UInt64: Assignment} {
151 // Get values from receipt before we move/destroy it
152 let lockBlock = receipt.getRequestBlock()!
153 let revealBlock = getCurrentBlock().height
154 let currentTime = getCurrentBlock().timestamp
155 let receiptUUID = receipt.uuid
156
157 // Generate receipt ID for events (using receipt's UUID)
158 let receiptID = receiptUUID.toString()
159
160 // Get the usernames and team options from receipt
161 var remainingUsernames = receipt.usernames
162
163 // Combine single teams and combos
164 var allAssignments: [String] = []
165 var i = 0
166 while i < receipt.teams.length {
167 allAssignments.append(receipt.teams[i])
168 i = i + 1
169 }
170 i = 0
171 while i < receipt.combos.length {
172 allAssignments.append(receipt.combos[i])
173 i = i + 1
174 }
175
176 var result: {UInt64: Assignment} = {}
177 var index: UInt64 = 0
178
179 // Pop the randomness request from the receipt and get the committed random seed
180 let request <- receipt.popRequest()
181
182 // Ensure we have a request
183 if request == nil {
184 panic("Receipt does not contain a valid request")
185 }
186
187 // Fulfill the committed randomness to get the seed value
188 // This automatically emits RandomnessFulfilled event with randomResult and requestUUID
189 // This uses the randomness from the LOCK block, not the reveal block
190 var seedValue = self.consumer.fulfillRandomRequest(<-request!)
191
192 // Perform the random assignments using the committed seed
193 // Use XOR shift algorithm to derive multiple random values (PRG)
194 while remainingUsernames.length > 0 {
195 // XOR shift algorithm (no multiplication, no overflow)
196 seedValue = seedValue ^ (seedValue << 13)
197 seedValue = seedValue ^ (seedValue >> 7)
198 seedValue = seedValue ^ (seedValue << 17)
199 let usernameIndex = seedValue % UInt64(remainingUsernames.length)
200
201 // Another round for assignment index
202 seedValue = seedValue ^ (seedValue << 13)
203 seedValue = seedValue ^ (seedValue >> 7)
204 seedValue = seedValue ^ (seedValue << 17)
205 let assignmentIndex = seedValue % UInt64(allAssignments.length)
206
207 // Get selected username and team
208 let selectedUsername = remainingUsernames[Int(usernameIndex)]
209 let selectedAssignment = allAssignments[Int(assignmentIndex)]
210
211 // Create assignment record
212 let assignment = Assignment(
213 username: selectedUsername,
214 team: selectedAssignment,
215 assignmentIndex: index,
216 timestamp: currentTime
217 )
218
219 // Store assignment
220 result[index] = assignment
221 self.assignments[index] = assignment
222
223 // Emit individual assignment event (for real-time UI updates)
224 emit ParticipantAssigned(
225 username: selectedUsername,
226 team: selectedAssignment,
227 assignmentIndex: index,
228 receiptID: receiptID
229 )
230
231 // Remove assigned username and team from pools
232 remainingUsernames = self.removeElement(arr: remainingUsernames, index: Int(usernameIndex))
233 allAssignments = self.removeElement(arr: allAssignments, index: Int(assignmentIndex))
234
235 index = index + 1
236 }
237
238 // Generate commitment ID for the completion event (we'll use receipt UUID as base)
239 let commitmentId = receiptUUID % 1000000
240
241 // Destroy the receipt (it's been used)
242 Burner.burn(<-receipt)
243
244 // Create verification note
245 let verificationNote = "This assignment used Flow's Verifiable Random Function (VRF). The participants and teams were locked at block ".concat(lockBlock.toString()).concat(" and revealed at block ").concat(revealBlock.toString()).concat(". The random seed was determined by the blockchain AFTER the lock, making it impossible for anyone to predict or manipulate the results.")
246
247 // Emit the main completion event (shareable proof of fairness)
248 emit TeamAssignmentComplete(
249 commitmentId: commitmentId,
250 lockBlock: lockBlock,
251 revealBlock: revealBlock,
252 receiptID: receiptID,
253 verificationNote: verificationNote,
254 assignments: result
255 )
256
257 return result
258 }
259
260 // ============ Helper Functions ============
261
262 // Remove element from array at index
263 access(all) fun removeElement(arr: [String], index: Int): [String] {
264 var result: [String] = []
265 var i = 0
266 while i < arr.length {
267 if i != index {
268 result.append(arr[i])
269 }
270 i = i + 1
271 }
272 return result
273 }
274
275 // ============ View Functions ============
276
277 // Get all completed assignments
278 access(all) fun getAllAssignments(): {UInt64: Assignment} {
279 return self.assignments
280 }
281
282 // Get assignment by index
283 access(all) fun getAssignment(index: UInt64): Assignment? {
284 return self.assignments[index]
285 }
286}
287