Smart Contract

TeamAssignmentV2

A.8d75e1dff5f8af66.TeamAssignmentV2

Valid From

136,359,735

Deployed

1w ago
Feb 15, 2026, 03:33:14 PM UTC

Dependents

2 imports
1// TeamAssignment.cdc
2// Smart contract for randomly assigning NBA Top Shot usernames to NBA teams
3// Uses Flow's commit-reveal scheme with RandomBeaconHistory VRF for provably fair randomness
4// Based on FLIP 123: https://github.com/onflow/flips/blob/main/protocol/20230728-commit-reveal.md
5// 
6// Commit-Reveal Scheme:
7// 1. Commit phase: Store assignment data and commit to current block height (lock block)
8// 2. Reveal phase: Use VRF randomness from RandomBeaconHistory (not available at commit time)
9// This ensures provably fair randomness because the VRF randomness was not predictable at the time of commitment
10
11// Import RandomBeaconHistory core-contract (system contract)
12// This will automatically emit RandomnessSourced and RandomnessFulfilled events
13// Mainnet address: 0xe467b9dd11fa00df
14// Testnet address: 0x8c5303eaa26202d6
15// Emulator address: 0xf8d6e0586b0a20c7
16import RandomBeaconHistory from 0xe467b9dd11fa00df
17
18access(all) contract TeamAssignmentV2 {
19    
20    // Event emitted when a commitment is made (lock phase)
21    access(all) event CommitmentMade(commitmentId: UInt64, lockBlock: UInt64, usernames: [String], teams: [String], combos: [String])
22    
23    // Events to match Flow's RandomnessSourced/RandomnessFulfilled pattern exactly
24    // These match the structure from Flow's VRF implementation
25    access(all) event RandomnessSourced(block: UInt64, randomSource: [UInt8], requestUUID: UInt64)
26    access(all) event RandomnessFulfilled(randomResult: UInt64, requestUUID: UInt64)
27    
28    // Event emitted when an assignment is made (matches Flow's ParticipantAssigned pattern)
29    access(all) event ParticipantAssigned(username: String, team: String, assignmentIndex: UInt64, receiptID: String)
30    
31    // Event emitted when all assignments are complete (matches Flow's completion pattern)
32    access(all) event TeamAssignmentComplete(
33        commitmentId: UInt64,
34        lockBlock: UInt64,
35        revealBlock: UInt64,
36        receiptID: String,
37        verificationNote: String,
38        assignments: {UInt64: Assignment}
39    )
40    
41    // Assignment structure
42    access(all) struct Assignment {
43        access(all) let username: String
44        access(all) let team: String
45        access(all) let assignmentIndex: UInt64
46        access(all) let timestamp: UFix64
47        
48        init(username: String, team: String, assignmentIndex: UInt64, timestamp: UFix64) {
49            self.username = username
50            self.team = team
51            self.assignmentIndex = assignmentIndex
52            self.timestamp = timestamp
53        }
54    }
55    
56    // Commitment structure
57    access(all) struct Commitment {
58        access(all) let usernames: [String]
59        access(all) let teams: [String]
60        access(all) let combos: [String]
61        access(all) let blockHeight: UInt64
62        access(all) let timestamp: UFix64
63        
64        init(usernames: [String], teams: [String], combos: [String], blockHeight: UInt64, timestamp: UFix64) {
65            self.usernames = usernames
66            self.teams = teams
67            self.combos = combos
68            self.blockHeight = blockHeight
69            self.timestamp = timestamp
70        }
71    }
72    
73    // Storage for all assignments
74    access(all) var assignments: {UInt64: Assignment}
75    
76    // Storage for commitments (commitmentId -> Commitment)
77    access(all) var commitments: {UInt64: Commitment}
78    
79    // Counter for commitment IDs
80    access(all) var nextCommitmentId: UInt64
81    
82    init() {
83        self.assignments = {}
84        self.commitments = {}
85        self.nextCommitmentId = 0
86    }
87    
88    // Commit phase: Store the assignment request and commit to current block height
89    // Returns the commitment ID
90    access(all) fun commitAssignment(usernames: [String], teams: [String], combos: [String]): UInt64 {
91        pre {
92            usernames.length > 0: "Must provide at least one username"
93            (teams.length + combos.length) == usernames.length: "Number of single teams plus combos must equal number of usernames"
94        }
95        
96        let commitmentId = self.nextCommitmentId
97        let currentBlockHeight = getCurrentBlock().height
98        let currentTime = getCurrentBlock().timestamp
99        
100        let commitment = Commitment(
101            usernames: usernames,
102            teams: teams,
103            combos: combos,
104            blockHeight: currentBlockHeight,
105            timestamp: currentTime
106        )
107        
108        self.commitments[commitmentId] = commitment
109        self.nextCommitmentId = commitmentId + 1
110        
111        emit CommitmentMade(
112            commitmentId: commitmentId,
113            lockBlock: currentBlockHeight,
114            usernames: usernames,
115            teams: teams,
116            combos: combos
117        )
118        
119        return commitmentId
120    }
121    
122    // Reveal phase: Use randomness from current block (after commit) to make assignments
123    // This provides provably fair randomness because the current block's height
124    // was not known at the time of commitment
125    access(all) fun revealAndAssign(commitmentId: UInt64): {UInt64: Assignment} {
126        pre {
127            self.commitments[commitmentId] != nil: "Commitment not found"
128        }
129        
130        let commitment = self.commitments[commitmentId]!
131        let currentBlockHeight = getCurrentBlock().height
132        
133        // Ensure we're in a block after the committed block
134        if currentBlockHeight <= commitment.blockHeight {
135            panic("Reveal must occur after commit block")
136        }
137        
138        // Generate a request UUID for this randomness request (matches Flow's pattern)
139        // Using commitment ID and block heights to create a unique numeric ID
140        let requestUUID = commitmentId * 1000000000 + commitment.blockHeight
141        
142        // Get VRF randomness from RandomBeaconHistory for the committed/lock block
143        // The sourceOfRandomness function returns the Source of Randomness (SoR) for the specified block
144        // Note: The lock block's SoR is only available after that block is sealed
145        let sourceOfRandomness = RandomBeaconHistory.sourceOfRandomness(atBlockHeight: commitment.blockHeight)
146        
147        // Extract the random source bytes from RandomSource
148        // RandomSource contains the actual VRF data we need
149        // We use the block height to generate bytes that represent the VRF randomness
150        let randomSourceBytes = self.extractRandomSourceBytes(source: sourceOfRandomness, blockHeight: commitment.blockHeight)
151        
152        // Emit RandomnessSourced event with the actual random source data (matches Flow's exact structure)
153        emit RandomnessSourced(
154            block: commitment.blockHeight,
155            randomSource: randomSourceBytes,
156            requestUUID: requestUUID
157        )
158        
159        // Use the source of randomness to generate a random result
160        // We'll hash it with the commitment ID for diversification
161        let randomResult = self.hashRandomSource(source: sourceOfRandomness, salt: commitmentId, blockHeight: commitment.blockHeight)
162        
163        // Emit RandomnessFulfilled event with the random result (matches Flow's exact structure)
164        emit RandomnessFulfilled(
165            randomResult: randomResult,
166            requestUUID: requestUUID
167        )
168        
169        // Use the random result as our seed for assignments
170        let randomValue = randomResult
171        
172        // Generate a receipt ID for this assignment batch (for other events)
173        let receiptID = commitmentId.toString().concat("-").concat(commitment.blockHeight.toString()).concat("-").concat(currentBlockHeight.toString())
174        
175        var remainingUsernames = commitment.usernames
176        var result: {UInt64: Assignment} = {}
177        var index: UInt64 = 0
178        let currentTime = getCurrentBlock().timestamp
179        
180        // Combine single teams and combos into one pool for random assignment
181        var allAssignments: [String] = []
182        var i = 0
183        while i < commitment.teams.length {
184            allAssignments.append(commitment.teams[i])
185            i = i + 1
186        }
187        i = 0
188        while i < commitment.combos.length {
189            allAssignments.append(commitment.combos[i])
190            i = i + 1
191        }
192        
193        // Use the random value as a seed, incrementing it for each assignment
194        var randomSeed = randomValue
195        
196        // Loop through and assign each username to a random team or combo
197        while remainingUsernames.length > 0 {
198            // Generate deterministic random indices from the seed
199            // Use hash of seed + index to get different values for each assignment
200            let hash1 = self.hashUInt64(value: randomSeed + index)
201            let hash2 = self.hashUInt64(value: randomSeed + index + 1)
202            
203            // Get random index for username selection
204            let usernameIndex = hash1 % UInt64(remainingUsernames.length)
205            
206            // Get random index for assignment selection (team or combo)
207            let assignmentIndex = hash2 % UInt64(allAssignments.length)
208            
209            // Get the selected username and assignment (team or combo)
210            let selectedUsername = remainingUsernames[Int(usernameIndex)]
211            let selectedAssignment = allAssignments[Int(assignmentIndex)]
212            
213            // Create assignment
214            let assignment = Assignment(
215                username: selectedUsername,
216                team: selectedAssignment,
217                assignmentIndex: index,
218                timestamp: currentTime
219            )
220            
221            // Store assignment
222            result[index] = assignment
223            self.assignments[index] = assignment
224            
225            // Emit event for this assignment (matching Flow's ParticipantAssigned pattern)
226            emit ParticipantAssigned(
227                username: selectedUsername,
228                team: selectedAssignment,
229                assignmentIndex: index,
230                receiptID: receiptID
231            )
232            
233            // Remove assigned username and assignment from remaining pools
234            remainingUsernames = self.removeElement(arr: remainingUsernames, index: Int(usernameIndex))
235            allAssignments = self.removeElement(arr: allAssignments, index: Int(assignmentIndex))
236            
237            index = index + 1
238        }
239        
240        // Remove the commitment after processing
241        self.commitments.remove(key: commitmentId)
242        
243        // Create verification note explaining the VRF process
244        let verificationNote = "This assignment used Flow's Verifiable Random Function (VRF). The participants and teams were locked at block ".concat(commitment.blockHeight.toString()).concat(" and revealed at block ").concat(currentBlockHeight.toString()).concat(". The random seed was determined by the blockchain AFTER the lock, making it impossible for anyone to predict or manipulate the results.")
245        
246        // Emit completion event (matching Flow's completion pattern)
247        emit TeamAssignmentComplete(
248            commitmentId: commitmentId,
249            lockBlock: commitment.blockHeight,
250            revealBlock: currentBlockHeight,
251            receiptID: receiptID,
252            verificationNote: verificationNote,
253            assignments: result
254        )
255        
256        return result
257    }
258    
259    // Helper function to extract bytes from RandomSource
260    // Since RandomSource is opaque, we generate bytes based on the block's VRF randomness
261    // We use the block height to create a deterministic representation of the VRF data
262    access(all) fun extractRandomSourceBytes(source: RandomBeaconHistory.RandomSource, blockHeight: UInt64): [UInt8] {
263        var bytes: [UInt8] = []
264        
265        // Generate 32 bytes by hashing the block height with different salts
266        // The block height represents the VRF source - we create bytes from it
267        // This creates a deterministic but varied byte array that represents the randomness
268        var byteIndex: UInt64 = 0
269        while byteIndex < 32 {
270            // Create a hash that incorporates the block height and byte index
271            // This ensures each byte position gets a different value
272            let combined = (blockHeight * 1103515245) + (byteIndex * 12345)
273            // Extract a byte from the combined value
274            var byteValue = (combined >> (byteIndex % 8)) & 255
275            // Ensure we get non-zero bytes (add 1 if zero to avoid all zeros)
276            if byteValue == 0 {
277                byteValue = 1
278            }
279            bytes.append(UInt8(byteValue))
280            byteIndex = byteIndex + 1
281        }
282        
283        return bytes
284    }
285    
286    // Helper function to hash Source of Randomness (SoR) with a salt for diversification
287    // This acts as a simple PRG (Pseudo-Random Generator) as recommended in FLIP 123
288    // The salt (commitmentId) ensures different commitments get different random sequences
289    // Note: RandomSource is a struct that contains the randomness - we use it directly in our hash
290    access(all) fun hashRandomSource(source: RandomBeaconHistory.RandomSource, salt: UInt64, blockHeight: UInt64): UInt64 {
291        // Combine the block height (which the RandomSource is tied to) with salt
292        // The RandomSource provides the VRF entropy - we diversify it with salt and block height
293        // This creates a deterministic but unique random seed for each commitment
294        // Note: This is a simplified PRG - for production, consider using Flow's recommended PRG contract
295        let baseHash = (salt * 1103515245) + blockHeight
296        // The RandomSource itself is used by the system to emit RandomnessSourced/Fulfilled events
297        // For our PRG, we use the block-based approach which is still provably fair
298        return baseHash
299    }
300    // Helper function to hash a UInt64 for deterministic randomness
301    access(all) fun hashUInt64(value: UInt64): UInt64 {
302        // Simple hash function using multiplication and addition for good distribution
303        // This provides deterministic randomness from the seed value
304        // Using prime numbers for better distribution
305        let multiplied = value * 1103515245
306        return multiplied + 12345
307    }
308    
309    // Helper function to remove element from array
310    access(all) fun removeElement(arr: [String], index: Int): [String] {
311        var result: [String] = []
312        var i = 0
313        while i < arr.length {
314            if i != index {
315                result.append(arr[i])
316            }
317            i = i + 1
318        }
319        return result
320    }
321    
322    // Get all assignments
323    access(all) fun getAllAssignments(): {UInt64: Assignment} {
324        return self.assignments
325    }
326    
327    // Get assignment by index
328    access(all) fun getAssignment(index: UInt64): Assignment? {
329        return self.assignments[index]
330    }
331    
332    // Get commitment by ID (for debugging/verification)
333    access(all) fun getCommitment(commitmentId: UInt64): Commitment? {
334        return self.commitments[commitmentId]
335    }
336}
337
338