Smart Contract
Escrow
A.4da127056dc9ba3f.Escrow
1/*
2 Escrow Contract for managing NFTs in a Leaderboard Context.
3 Holds NFTs in Escrow account awaiting transfer or burn.
4
5 Authors:
6 Corey Humeston: corey.humeston@dapperlabs.com
7 Deewai Abdullahi: innocent.abdullahi@dapperlabs.com
8*/
9
10import NonFungibleToken from 0x1d7e57aa55817448
11import NFTLocker from 0xb6f2481eba4df97b
12
13access(all) contract Escrow {
14 // Event emitted when a new leaderboard is created.
15 access(all) event LeaderboardCreated(name: String, nftType: Type)
16
17 // Event emitted when an NFT is deposited to a leaderboard.
18 access(all) event EntryDeposited(leaderboardName: String, nftID: UInt64, owner: Address)
19
20 // Event emitted when an NFT is returned to the original collection from a leaderboard.
21 access(all) event EntryReturnedToCollection(leaderboardName: String, nftID: UInt64, owner: Address)
22
23 // Event emitted when an NFT is burned from a leaderboard.
24 access(all) event EntryBurned(leaderboardName: String, nftID: UInt64)
25
26 // Named Paths
27 access(all) let CollectionStoragePath: StoragePath
28 access(all) let CollectionPublicPath: PublicPath
29
30 // The resource representing an NFT leaderboard's info.
31 access(all) struct LeaderboardInfo {
32 access(all) let name: String
33 access(all) let nftType: Type
34 access(all) let entriesLength: Int
35
36 // LeaderboardInfo struct initializer.
37 init(name: String, nftType: Type, entriesLength: Int) {
38 self.name = name
39 self.nftType = nftType
40 self.entriesLength = entriesLength
41 }
42 }
43
44 // The resource representing a leaderboard.
45 access(all) resource Leaderboard {
46 access(all) var collection: @{NonFungibleToken.Collection}
47 access(all) var entriesData: {UInt64: LeaderboardEntry}
48 access(all) let name: String
49 access(all) let nftType: Type
50 access(all) var entriesLength: Int
51 access(all) var metadata: {String: AnyStruct}
52
53 // Adds the provided NFT to the leaderboard and creates the corresponding entry with the provided ownerAddress, which is
54 // used to validate the deposit capability when calling transferNftToCollection.
55 access(contract) fun addEntryToLeaderboard(nft: @{NonFungibleToken.NFT}, ownerAddress: Address) {
56 pre {
57 nft.isInstance(self.nftType): "This NFT cannot be used for leaderboard. NFT is not of the correct type."
58 }
59
60 let nftID = nft.id
61
62 // Create the entry and add it to the Leaderboard's entries map.
63 self.entriesData[nftID] = LeaderboardEntry(
64 nftID: nftID,
65 ownerAddress: ownerAddress,
66 metadata: {},
67 )
68
69 // Deposit the NFT into the leaderboard's NFT collection.
70 self.collection.deposit(token: <-nft)
71
72 // Increment entries length.
73 self.entriesLength = self.entriesLength + 1
74
75 emit EntryDeposited(leaderboardName: self.name, nftID: nftID, owner: ownerAddress)
76 }
77
78 // Transfers an NFT entry from the leaderboard to an account.
79 access(contract) fun transferNftToCollection(nftID: UInt64, depositCap: Capability<&{NonFungibleToken.Collection}>) {
80 pre {
81 depositCap.check() : "Deposit capability is not valid"
82 }
83 // Remove the NFT entry's data from the leaderboard.
84 self.entriesData.remove(key: nftID)!
85
86 // Transfer the NFT to the receiver's collection.
87 let receiverCollection = depositCap.borrow()
88 ?? panic("Could not borrow the NFT receiver from the capability")
89 receiverCollection.deposit(token: <- self.collection.withdraw(withdrawID: nftID))
90 emit EntryReturnedToCollection(leaderboardName: self.name, nftID: nftID, owner: depositCap.address)
91
92 // Decrement entries length.
93 self.entriesLength = self.entriesLength - 1
94 }
95
96 // Burns an NFT entry from the leaderboard.
97 access(contract) fun burn(nftID: UInt64) {
98 if(self.entriesData[nftID] == nil) {
99 return
100 }
101
102 // Remove the NFT entry's data from the leaderboard.
103 self.entriesData.remove(key: nftID)!
104
105 // Burn the NFT.
106 destroy <- self.collection.withdraw(withdrawID: nftID)
107 emit EntryBurned(leaderboardName: self.name, nftID: nftID)
108
109 // Decrement entries length.
110 self.entriesLength = self.entriesLength - 1
111 }
112
113 // Leaderboard resource initializer.
114 init(name: String, nftType: Type, collection: @{NonFungibleToken.Collection}) {
115 self.name = name
116 self.nftType = nftType
117 self.collection <- collection
118 self.entriesLength = 0
119 self.metadata = {}
120 self.entriesData = {}
121 }
122 }
123
124 // The resource representing an NFT entry in a leaderboard.
125 access(all) struct LeaderboardEntry {
126 access(all) let nftID: UInt64
127 access(all) let ownerAddress: Address
128 access(all) var metadata: {String: AnyStruct}
129
130 // LeaderboardEntry struct initializer.
131 init(nftID: UInt64, ownerAddress: Address, metadata: {String: AnyStruct}) {
132 self.nftID = nftID
133 self.ownerAddress = ownerAddress
134 self.metadata = metadata
135 }
136 }
137
138 // An interface containing the Collection function that gets leaderboards by name.
139 access(all) resource interface ICollectionPublic {
140 access(all) fun getLeaderboardInfo(name: String): LeaderboardInfo?
141 access(all) fun addEntryToLeaderboard(nft: @{NonFungibleToken.NFT}, leaderboardName: String, ownerAddress: Address)
142 access(Operate) fun createLeaderboard(name: String, nftType: Type, collection: @{NonFungibleToken.Collection})
143 access(Operate) fun transferNftToCollection(leaderboardName: String, nftID: UInt64, depositCap: Capability<&{NonFungibleToken.Collection}>)
144 access(Operate) fun burn(leaderboardName: String, nftID: UInt64)
145 }
146
147 // Entitlement that grants the ability to operate the Escrow Collection
148 access(all) entitlement Operate
149
150 // Deprecated in favor of Operate entitlement
151 access(all) resource interface ICollectionPrivate: ICollectionPublic {}
152
153 // The resource representing a collection.
154 access(all) resource Collection: ICollectionPublic, ICollectionPrivate {
155 // A dictionary holding leaderboards.
156 access(self) var leaderboards: @{String: Leaderboard}
157
158 // Creates a new leaderboard and stores it.
159 access(Operate) fun createLeaderboard(name: String, nftType: Type, collection: @{NonFungibleToken.Collection}) {
160 pre{
161 self.leaderboards.containsKey(name) == false: "Leaderboard already exists with this name"
162 }
163
164 // Create and store leaderboard resource in the leaderboards dictionary.
165 self.leaderboards[name] <-! create Leaderboard(name: name, nftType: nftType, collection: <-collection)
166
167 // Emit the event.
168 emit LeaderboardCreated(name: name, nftType: nftType)
169 }
170
171 // Returns leaderboard info with the given name.
172 access(all) fun getLeaderboardInfo(name: String): LeaderboardInfo? {
173 if let leaderboard = &self.leaderboards[name] as &Leaderboard? {
174 return LeaderboardInfo(
175 name: leaderboard.name,
176 nftType: leaderboard.nftType,
177 entriesLength: leaderboard.entriesLength
178 )
179 }
180 return nil
181 }
182
183 // Adds the provided NFT to the leaderboard and creates the corresponding entry with the provided ownerAddress, which is
184 // used to validate the deposit capability when calling transferNftToCollection.
185 access(all) fun addEntryToLeaderboard(nft: @{NonFungibleToken.NFT}, leaderboardName: String, ownerAddress: Address) {
186 let leaderboard = &self.leaderboards[leaderboardName] as &Leaderboard?
187 ?? panic("Leaderboard does not exist with this name")
188 leaderboard.addEntryToLeaderboard(nft: <-nft, ownerAddress: ownerAddress)
189 }
190
191 // transferNftToCollection transfers the moment back to the owner's collection once the leaderboard is completed.
192 // This is used for entries of users who didn't win the leaderboard
193 access(Operate) fun transferNftToCollection(leaderboardName: String, nftID: UInt64, depositCap: Capability<&{NonFungibleToken.Collection}>) {
194 let leaderboard= &self.leaderboards[leaderboardName] as &Leaderboard?
195 ?? panic("Leaderboard does not exist with this name")
196
197 if(leaderboard.entriesData[nftID] == nil) {
198 return
199 }
200
201 if(depositCap.address != leaderboard.entriesData[nftID]!.ownerAddress){
202 panic("Only the owner of the entry can withdraw it")
203 }
204
205 leaderboard.transferNftToCollection(nftID: nftID, depositCap: depositCap)
206 }
207
208 // adminTransferNftToCollection transfers the moment back to an admin collection once the leaderboard is completed.
209 // This is used for entries of users who won the leaderboard
210 access(Operate) fun adminTransferNftToCollection(leaderboardName: String, nftID: UInt64, depositCap: Capability<&{NonFungibleToken.Collection}>) {
211 let leaderboard= &self.leaderboards[leaderboardName] as &Leaderboard?
212 ?? panic("Leaderboard does not exist with this name")
213
214 if(leaderboard.entriesData[nftID] == nil) {
215 return
216 }
217
218 leaderboard.transferNftToCollection(nftID: nftID, depositCap: depositCap)
219 }
220
221 // burn destroys the NFT from the leaderboard.
222 // this is used for entries of users who won the leaderboard
223 access(Operate) fun burn(leaderboardName: String, nftID: UInt64) {
224 let leaderboard = &self.leaderboards[leaderboardName] as &Leaderboard?
225 ?? panic("Leaderboard does not exist with this name")
226 leaderboard.burn(nftID: nftID)
227 }
228
229 // Collection resource initializer.
230 init() {
231 self.leaderboards <- {}
232 }
233 }
234
235 // Handler for depositing NFTs to the Escrow Collection, used by the NFTLocker contract.
236 access(all) struct DepositHandler: NFTLocker.IAuthorizedDepositHandler {
237 access(all) fun deposit(nft: @{NonFungibleToken.NFT}, ownerAddress: Address, passThruParams: {String: AnyStruct}) {
238 // Get leaderboard name from pass-thru parameters
239 let leaderboardName = passThruParams["leaderboardName"] as! String?
240 ?? panic("Missing or invalid 'leaderboardName' entry in pass-thru parameters map")
241
242 // Get the Escrow Collection public reference
243 let escrowCollectionPublic = Escrow.account.capabilities.borrow<&Escrow.Collection>(Escrow.CollectionPublicPath)
244 ?? panic("Could not borrow a reference to the public leaderboard collection")
245
246 // Add the NFT to the escrow leaderboard
247 escrowCollectionPublic.addEntryToLeaderboard(nft: <-nft, leaderboardName: leaderboardName, ownerAddress: ownerAddress)
248 }
249 }
250
251 // Escrow contract initializer.
252 init() {
253 // Initialize paths.
254 self.CollectionStoragePath = /storage/EscrowLeaderboardCollection
255 self.CollectionPublicPath = /public/EscrowLeaderboardCollectionInfo
256
257 // Create and save a Collection resource to account storage.
258 self.account.storage.save(<- create Collection(), to: self.CollectionStoragePath)
259
260 // Create a public capability to the Collection resource and publish it publicly.
261 self.account.capabilities.publish(
262 self.account.capabilities.storage.issue<&Collection>(self.CollectionStoragePath),
263 at: self.CollectionPublicPath
264 )
265 }
266}
267