Smart Contract
Flowty
A.5c57f79c6694797f.Flowty
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import LostAndFound from 0x473d6a2c37eab5be
5import Burner from 0xf233dcee88fe0abe
6
7import FlowtyUtils from 0x3cdbb3d569211ff3
8import FlowtyListingCallback from 0x3cdbb3d569211ff3
9import DNAHandler from 0x3cdbb3d569211ff3
10
11// Flowty
12//
13// A smart contract responsible for the main lending flows.
14// It is facilitating the lending deal, allowing borrowers and lenders
15// to be sure that the deal would be executed on their agreed terms.
16//
17// Each account that wants to list a loan installs a Storefront,
18// and lists individual loan within that Storefront as Listings.
19// There is one Storefront per account, it handles loans of all NFT types
20// for that account.
21//
22// Each Listing can have one or more "cut"s of the requested loan amount that
23// goes to one or more addresses. Cuts are used to pay listing fees
24// or other considerations.
25// Each NFT may be listed in one or more Listings, the validity of each
26// Listing can easily be checked.
27//
28// Lenders can watch for Listing events and check the NFT type and
29// ID to see if they wish to fund the listed loan.
30//
31access(all) contract Flowty {
32
33 // permits creating new listings on a storefront resource
34 access(all) entitlement List
35
36 // permits removing listings from a storefront resource
37 access(all) entitlement Cancel
38
39 access(all) entitlement Administrator
40
41 // FlowtyInitialized
42 // This contract has been deployed.
43 // Event consumers can now expect events from this contract.
44 //
45 access(all) event FlowtyInitialized()
46
47 // FlowtyStorefrontInitialized
48 // A FlowtyStorefront resource has been created.
49 // Event consumers can now expect events from this FlowtyStorefront.
50 // Note that we do not specify an address: we cannot and should not.
51 // Created resources do not have an owner address, and may be moved
52 // after creation in ways we cannot check.
53 // ListingAvailable events can be used to determine the address
54 // of the owner of the FlowtyStorefront (...its location) at the time of
55 // the listing but only at that precise moment in that precise transaction.
56 // If the seller moves the FlowtyStorefront while the listing is valid,
57 // that is on them.
58 //
59 access(all) event FlowtyStorefrontInitialized(flowtyStorefrontResourceID: UInt64)
60
61 // FlowtyMarketplaceInitialized
62 // A FlowtyMarketplace resource has been created.
63 // Event consumers can now expect events from this FlowtyStorefront.
64 // Note that we do not specify an address: we cannot and should not.
65 // Created resources do not have an owner address, and may be moved
66 // after creation in ways we cannot check.
67 // ListingAvailable events can be used to determine the address
68 // of the owner of the FlowtyStorefront (...its location) at the time of
69 // the listing but only at that precise moment in that precise transaction.
70 // If the seller moves the FlowtyStorefront while the listing is valid,
71 // that is on them.
72 //
73 access(all) event FlowtyMarketplaceInitialized(flowtyMarketplaceResourceID: UInt64)
74
75 // FlowtyStorefrontDestroyed
76 // A FlowtyStorefront has been destroyed.
77 // Event consumers can now stop processing events from this FlowtyStorefront.
78 // Note that we do not specify an address.
79 //
80 access(all) event FlowtyStorefrontDestroyed(flowtyStorefrontResourceID: UInt64)
81
82 // FlowtyMarketplaceDestroyed
83 // A FlowtyMarketplace has been destroyed.
84 // Event consumers can now stop processing events from this FlowtyMarketplace.
85 // Note that we do not specify an address.
86 //
87 access(all) event FlowtyMarketplaceDestroyed(flowtyStorefrontResourceID: UInt64)
88
89 // ListingAvailable
90 // A listing has been created and added to a FlowtyStorefront resource.
91 // The Address values here are valid when the event is emitted, but
92 // the state of the accounts they refer to may be changed outside of the
93 // FlowtyMarketplace workflow, so be careful to check when using them.
94 //
95 access(all) event ListingAvailable(
96 flowtyStorefrontAddress: Address,
97 flowtyStorefrontID: UInt64,
98 listingResourceID: UInt64,
99 nftType: String,
100 nftID: UInt64,
101 amount: UFix64,
102 interestRate: UFix64,
103 term: UFix64,
104 enabledAutoRepayment: Bool,
105 royaltyRate: UFix64,
106 expiresAfter: UFix64,
107 paymentTokenType: String,
108 repaymentAddress: Address?
109 )
110
111 // ListingCompleted
112 // The listing has been resolved. It has either been funded, or removed and destroyed.
113 //
114 access(all) event ListingCompleted(
115 listingResourceID: UInt64,
116 flowtyStorefrontID: UInt64,
117 funded: Bool,
118 nftID: UInt64,
119 nftType: String,
120 flowtyStorefrontAddress: Address
121 )
122
123 // FundingAvailable
124 // A funding has been created and added to a FlowtyStorefront resource.
125 // The Address values here are valid when the event is emitted, but
126 // the state of the accounts they refer to may be changed outside of the
127 // FlowtyMarketplace workflow, so be careful to check when using them.
128 //
129 access(all) event FundingAvailable(
130 fundingResourceID: UInt64,
131 listingResourceID: UInt64,
132 borrower: Address,
133 lender: Address,
134 nftID: UInt64,
135 nftType: String,
136 repaymentAmount: UFix64,
137 enabledAutoRepayment: Bool,
138 repaymentAddress: Address?
139 )
140
141 // FundingRepaid
142 // A funding has been repaid.
143 //
144 access(all) event FundingRepaid(
145 fundingResourceID: UInt64,
146 listingResourceID: UInt64,
147 borrower: Address,
148 lender: Address,
149 nftID: UInt64,
150 nftType: String,
151 repaymentAmount: UFix64,
152 repaymentAddress: Address?
153 )
154
155 // FundingSettled
156 // A funding has been settled.
157 //
158 access(all) event FundingSettled(
159 fundingResourceID: UInt64,
160 listingResourceID: UInt64,
161 borrower: Address,
162 lender: Address,
163 nftID: UInt64,
164 nftType: String,
165 repaymentAmount: UFix64,
166 repaymentAddress: Address?
167 )
168
169 access(all) event CollectionSupportChanged(
170 collectionIdentifier: String,
171 state: Bool
172 )
173
174 access(all) event RoyaltyAdded(
175 collectionIdentifier: String,
176 rate: UFix64
177 )
178
179 access(all) event RoyaltyEscrow(
180 fundingResourceID: UInt64,
181 listingResourceID: UInt64,
182 lender: Address,
183 amount: UFix64
184 )
185
186 // FlowtyStorefrontStoragePath
187 // The location in storage that a FlowtyStorefront resource should be located.
188 access(all) let FlowtyStorefrontStoragePath: StoragePath
189
190 // FlowtyMarketplaceStoragePath
191 // The location in storage that a FlowtyMarketplace resource should be located.
192 access(all) let FlowtyMarketplaceStoragePath: StoragePath
193
194 // FlowtyStorefrontPublicPath
195 // The public location for a FlowtyStorefront link.
196 access(all) let FlowtyStorefrontPublicPath: PublicPath
197
198 // FlowtyMarketplacePublicPath
199 // The public location for a FlowtyMarketplace link.
200 access(all) let FlowtyMarketplacePublicPath: PublicPath
201
202 // FlowtyAdminStoragePath
203 // The location in storage that an FlowtyAdmin resource should be located.
204 access(all) let FlowtyAdminStoragePath: StoragePath
205
206 // FusdVaultStoragePath
207 // The location in storage that an FUSD Vault resource should be located.
208 access(all) let FusdVaultStoragePath: StoragePath
209
210 // FusdReceiverPublicPath
211 // The public location for a FUSD Receiver link.
212 access(all) let FusdReceiverPublicPath: PublicPath
213
214 // FusdBalancePublicPath
215 // The public location for a FUSD Balance link.
216 access(all) let FusdBalancePublicPath: PublicPath
217
218 // ListingFee
219 // The fixed fee in FUSD for a listing.
220 access(all) var ListingFee: UFix64
221
222 // FundingFee
223 // The percentage fee on funding, a number between 0 and 1.
224 access(all) var FundingFee: UFix64
225
226 // SuspendedFundingPeriod
227 // The suspended funding period in seconds(started on listing).
228 // So that the borrower has some time to delist it.
229 access(all) var SuspendedFundingPeriod: UFix64
230
231 // A dictionary for the Collection to royalty configuration.
232 access(account) var Royalties: {String:Royalty}
233 access(account) var TokenPaths: {String:PublicPath}
234
235 // The collections which are allowed to be used as collateral
236 access(account) var SupportedCollections: {String:Bool}
237
238 // PaymentCut
239 // A struct representing a recipient that must be sent a certain amount
240 // of the payment when a tx is executed.
241 //
242 access(all) struct PaymentCut {
243 // The receiver for the payment.
244 // Note that we do not store an address to find the Vault that this represents,
245 // as the link or resource that we fetch in this way may be manipulated,
246 // so to find the address that a cut goes to you must get this struct and then
247 // call receiver.borrow().owner.address on it.
248 // This can be done efficiently in a script.
249 access(all) let receiver: Capability<&{FungibleToken.Receiver}>
250
251 // The amount of the payment FungibleToken that will be paid to the receiver.
252 access(all) let amount: UFix64
253
254 // initializer
255 //
256 init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) {
257 self.receiver = receiver
258 self.amount = amount
259 }
260 }
261
262 access(all) struct Royalty {
263 // The percentage points that should go to the collection owner
264 // In the event of a loan default
265 access(all) let Rate: UFix64
266 access(all) let Address: Address
267
268 init(rate: UFix64, address: Address) {
269 self.Rate = rate
270 self.Address = address
271 }
272 }
273
274 // ListingDetails
275 // A struct containing a Listing's data.
276 //
277 access(all) struct ListingDetails {
278 // The FlowtyStorefront that the Listing is stored in.
279 // Note that this resource cannot be moved to a different FlowtyStorefront
280 access(all) var flowtyStorefrontID: UInt64
281 // Whether this listing has been funded or not.
282 access(all) var funded: Bool
283 // The Type of the NonFungibleToken.NFT that is being listed.
284 access(all) let nftType: Type
285 // The ID of the NFT within that type.
286 access(all) let nftID: UInt64
287 // The amount of the requested loan.
288 access(all) let amount: UFix64
289 // The interest rate in %, a number between 0 and 1.
290 access(all) let interestRate: UFix64
291 //The term in seconds for this listing.
292 access(all) var term: UFix64
293 // The Type of the FungibleToken that fundings must be made in.
294 access(all) let paymentVaultType: Type
295 // This specifies the division of payment between recipients.
296 access(self) let paymentCuts: [PaymentCut]
297 //The time the funding start at
298 access(all) var listedTime: UFix64
299 // The royalty rate needed as a deposit for this loan to be funded
300 access(all) var royaltyRate: UFix64
301 // The number of seconds this listing is valid for
302 access(all) var expiresAfter: UFix64
303
304 // getPaymentCuts
305 // Returns payment cuts
306 access(all) view fun getPaymentCuts(): [PaymentCut] {
307 return self.paymentCuts
308 }
309
310 access(all) view fun getTotalPayment(): UFix64 {
311 return self.amount * (1.0 + (self.interestRate * Flowty.FundingFee) + self.royaltyRate)
312 }
313
314 // setToFunded
315 // Irreversibly set this listing as funded.
316 //
317 access(contract) fun setToFunded() {
318 self.funded = true
319 }
320
321 // initializer
322 //
323 init (
324 nftType: Type,
325 nftID: UInt64,
326 amount: UFix64,
327 interestRate: UFix64,
328 term: UFix64,
329 paymentVaultType: Type,
330 paymentCuts: [PaymentCut],
331 flowtyStorefrontID: UInt64,
332 expiresAfter: UFix64,
333 royaltyRate: UFix64
334 ) {
335 self.flowtyStorefrontID = flowtyStorefrontID
336 self.funded = false
337 self.nftType = nftType
338 self.nftID = nftID
339 self.amount = amount
340 self.interestRate = interestRate
341 self.term = term
342 self.paymentVaultType = paymentVaultType
343 self.listedTime = getCurrentBlock().timestamp
344 self.expiresAfter = expiresAfter
345 self.royaltyRate = royaltyRate
346
347 assert(paymentCuts.length > 0, message: "Listing must have at least one payment cut recipient")
348 self.paymentCuts = paymentCuts
349
350 // Calculate the total price from the cuts
351 var cutsAmount = 0.0
352 // Perform initial check on capabilities, and calculate payment price from cut amounts.
353 for cut in self.paymentCuts {
354 // make sure we can borrow the receiver
355 cut.receiver.borrow()!
356 // Add the cut amount to the total price
357 cutsAmount = cutsAmount + cut.amount
358 }
359
360 assert(cutsAmount > 0.0, message: "Listing must have non-zero requested amount")
361 }
362 }
363
364 // ListingPublic
365 // An interface providing a useful public interface to a Listing.
366 //
367 access(all) resource interface ListingPublic {
368 // borrowNFT
369 // This will assert in the same way as the NFT standard borrowNFT()
370 // if the NFT is absent, for example if it has been sold via another listing.
371 //
372 access(all) view fun borrowNFT(): &{NonFungibleToken.NFT}?
373
374 // fund
375 // Fund the listing.
376 // This pays the beneficiaries and returns the token to the buyer.
377 //
378 access(all) fun fund(payment: @{FungibleToken.Vault},
379 lenderFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
380 lenderNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>)
381
382 // getDetails
383 //
384 access(all) view fun getDetails(): ListingDetails
385
386 // suspensionTimeRemaining
387 //
388 access(all) view fun suspensionTimeRemaining() : Fix64
389
390 // remainingTimeToFund
391 //
392 access(all) view fun remainingTimeToFund(): Fix64
393
394 // isFundingEnabled
395 //
396 access(all) view fun isFundingEnabled(): Bool
397 }
398
399
400 // Listing
401 // A resource that allows an NFT to be fund for an amount of a given FungibleToken,
402 // and for the proceeds of that payment to be split between several recipients.
403 //
404 access(all) resource Listing: ListingPublic, FlowtyListingCallback.Listing, Burner.Burnable {
405 access(all) event ResourceDestroyed(
406 listingResourceID: UInt64 = self.uuid,
407 flowtyStorefrontID: UInt64 = self.details.flowtyStorefrontID,
408 funded: Bool = self.details.funded,
409 nftID: UInt64 = self.details.nftID,
410 nftType: String = self.details.nftType.identifier,
411 flowtyStorefrontAddress: Address = self.nftPublicCollectionCapability.address
412 )
413
414 // The simple (non-Capability, non-complex) details of the listing
415 access(self) let details: ListingDetails
416
417 // A capability allowing this resource to withdraw the NFT with the given ID from its collection.
418 // This capability allows the resource to withdraw *any* NFT, so you should be careful when giving
419 // such a capability to a resource and always check its code to make sure it will use it in the
420 // way that it claims.
421 access(contract) let nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>
422
423 // A capability allowing this resource to access the owner's NFT public collection
424 access(contract) let nftPublicCollectionCapability: Capability<&{NonFungibleToken.CollectionPublic}>
425
426 // A capability allowing this resource to withdraw `FungibleToken`s from borrower account.
427 // This capability allows loan repayment if there is system downtime, which will prevent NFT losing.
428 // NOTE: This variable cannot be renamed but it can allow any FungibleToken.
429 access(contract) let fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?
430
431 // borrowNFT
432 // This will assert in the same way as the NFT standard borrowNFT()
433 // if the NFT is absent, for example if it has been sold via another listing.
434 //
435 access(all) view fun borrowNFT(): &{NonFungibleToken.NFT}? {
436 if let cap = self.nftProviderCapability.borrow() {
437 if let ref = cap.borrowNFT(self.getDetails().nftID) {
438 if ref.getType() != self.details.nftType || ref.id != self.details.nftID {
439 return nil
440 }
441 return ref
442 }
443 }
444
445 return nil
446 }
447
448 // getDetails
449 // Get the details of the current state of the Listing as a struct.
450 // This avoids having more public variables and getter methods for them, and plays
451 // nicely with scripts (which cannot return resources).
452 //
453 access(all) view fun getDetails(): ListingDetails {
454 return self.details
455 }
456
457 // fund
458 // Fund the listing.
459 // This pays the beneficiaries and move the NFT to the funding resource stored in the marketplace account.
460 //
461 access(all) fun fund(payment: @{FungibleToken.Vault},
462 lenderFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
463 lenderNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>) {
464 pre {
465 self.isFundingEnabled(): "Funding is not enabled or this listing has expired"
466 self.details.funded == false: "listing has already been funded"
467 payment.isInstance(self.details.paymentVaultType): "payment vault is not requested fungible token"
468 payment.balance == self.details.getTotalPayment(): "payment vault does not contain requested amount"
469 self.nftProviderCapability.check(): "nftProviderCapability failed check"
470 self.details.term >= 1_209_600.0: "term must be at least 2 weeks"
471 }
472
473 // Make sure the listing cannot be funded again.
474 self.details.setToFunded()
475
476 // Fetch the token to return to the purchaser.
477 let nft <-self.nftProviderCapability.borrow()!.withdraw(withdrawID: self.details.nftID)
478 let ref = &nft as &{NonFungibleToken.NFT}
479 assert(FlowtyUtils.isSupported(ref), message: "nft type is not supported")
480
481 // Neither receivers nor providers are trustworthy, they must implement the correct
482 // interface but beyond complying with its pre/post conditions they are not gauranteed
483 // to implement the functionality behind the interface in any given way.
484 // Therefore we cannot trust the Collection resource behind the interface,
485 // and we must check the NFT resource it gives us to make sure that it is the correct one.
486 assert(nft.isInstance(self.details.nftType), message: "withdrawn NFT is not of specified type")
487 assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID")
488
489 // Rather than aborting the transaction if any receiver is absent when we try to pay it,
490 // we send the cut to the first valid receiver.
491 // The first receiver should therefore either be the borrower, or an agreed recipient for
492 // any unpaid cuts.
493 var residualReceiver: &{FungibleToken.Receiver}? = nil
494
495 // Pay each beneficiary their amount of the payment.
496 for cut in self.details.getPaymentCuts() {
497 if cut.receiver.check() {
498 let receiver = cut.receiver.borrow()!
499 let paymentCut <- payment.withdraw(amount: cut.amount)
500 receiver.deposit(from: <-paymentCut)
501 if (residualReceiver == nil) {
502 residualReceiver = receiver
503 }
504 }
505 }
506
507 // Funding fee
508 let fundingFeeAmount = self.details.amount * self.details.interestRate * Flowty.FundingFee
509 let fundingFee <- payment.withdraw(amount: fundingFeeAmount)
510 let tokenInfo = FlowtyUtils.getTokenInfo(self.details.paymentVaultType)!
511
512 let flowtyFeeReceiver = Flowty.account.capabilities.get<&{FungibleToken.Receiver}>(tokenInfo.receiverPath)!.borrow()!
513 flowtyFeeReceiver.deposit(from: <-fundingFee)
514
515 // Royalty
516 // Deposit royalty amount
517 let royalty = self.details.royaltyRate
518
519 var royaltyVault: @{FungibleToken.Vault}? <- nil
520 if self.details.royaltyRate > 0.0 {
521 let tmp <- royaltyVault <- payment.withdraw(amount: self.details.amount * royalty)
522 destroy tmp
523 }
524
525 assert(residualReceiver != nil, message: "No valid payment receivers")
526
527 // At this point, if all recievers were active and availabile, then the payment Vault will have
528 // zero tokens left, and this will functionally be a no-op that consumes the empty vault
529 residualReceiver!.deposit(from: <-payment)
530
531 let listingResourceID = self.uuid
532
533 // If the listing is funded, we regard it as completed here.
534 // Otherwise we regard it as completed in the destructor.
535 emit ListingCompleted(
536 listingResourceID: listingResourceID,
537 flowtyStorefrontID: self.details.flowtyStorefrontID,
538 funded: self.details.funded,
539 nftID: self.details.nftID,
540 nftType: self.details.nftType.identifier,
541 flowtyStorefrontAddress: self.nftPublicCollectionCapability.address
542 )
543
544 let repaymentAmount = self.details.amount + self.details.amount * self.details.interestRate
545
546 if let callback = Flowty.borrowCallbackContainer() {
547 callback.handle(stage: FlowtyListingCallback.Stage.Filled, listing: &self as &{FlowtyListingCallback.Listing}, nft: ref)
548 }
549
550 let marketplace = Flowty.borrowMarketplace()
551 marketplace.createFunding(
552 flowtyStorefrontID: self.details.flowtyStorefrontID,
553 listingResourceID: listingResourceID,
554 ownerNFTCollection: self.nftPublicCollectionCapability,
555 lenderNFTCollection: lenderNFTCollection,
556 NFT: <-nft,
557 paymentVaultType: self.details.paymentVaultType,
558 lenderFungibleTokenReceiver: lenderFungibleTokenReceiver,
559 repaymentAmount: repaymentAmount,
560 term: self.details.term,
561 fusdProviderCapability: self.fusdProviderCapability,
562 royaltyVault: <-royaltyVault,
563 listingDetails: self.getDetails()
564 )
565 }
566
567 // suspensionTimeRemaining
568 // The remaining time. This can be negative if is expired
569 access(all) view fun suspensionTimeRemaining() : Fix64 {
570 let listedTime = self.details.listedTime
571 let currentTime = getCurrentBlock().timestamp
572
573 let remaining = Fix64(listedTime+Flowty.SuspendedFundingPeriod) - Fix64(currentTime)
574
575 return remaining
576 }
577
578 // remainingTimeToFund
579 // The time in seconds left until this listing is no longer valid
580 access(all) view fun remainingTimeToFund(): Fix64 {
581 let listedTime = self.details.listedTime
582 let currentTime = getCurrentBlock().timestamp
583 let remaining = Fix64(listedTime + self.details.expiresAfter) - Fix64(currentTime)
584 return remaining
585 }
586
587 // isFundingEnabled
588 access(all) view fun isFundingEnabled(): Bool {
589 let timeRemaining = self.suspensionTimeRemaining()
590 let listingTimeRemaining = self.remainingTimeToFund()
591 return timeRemaining < Fix64(0.0) && listingTimeRemaining > Fix64(0.0)
592 }
593
594 // initializer
595 //
596 init (
597 nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
598 nftPublicCollectionCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
599 fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?,
600 nftType: Type,
601 nftID: UInt64,
602 amount: UFix64,
603 interestRate: UFix64,
604 term: UFix64,
605 paymentVaultType: Type,
606 paymentCuts: [PaymentCut],
607 flowtyStorefrontID: UInt64,
608 expiresAfter: UFix64,
609 royaltyRate: UFix64
610 ) {
611 // Store the sale information
612 self.details = ListingDetails(
613 nftType: nftType,
614 nftID: nftID,
615 amount: amount,
616 interestRate: interestRate,
617 term: term,
618 paymentVaultType: paymentVaultType,
619 paymentCuts: paymentCuts,
620 flowtyStorefrontID: flowtyStorefrontID,
621 expiresAfter: expiresAfter,
622 royaltyRate: royaltyRate
623 )
624
625 // Store the NFT provider
626 self.nftProviderCapability = nftProviderCapability
627
628 self.fusdProviderCapability = fusdProviderCapability
629
630 self.nftPublicCollectionCapability = nftPublicCollectionCapability
631
632 // Check that the provider contains the NFT.
633 // We will check it again when the token is funded.
634 // We cannot move this into a function because initializers cannot call member functions.
635 let provider = self.nftProviderCapability.borrow()!
636
637 // This will precondition assert if the token is not available.
638 let nft = provider.borrowNFT(self.details.nftID) ?? panic("nft not found in provider capability")
639 assert(nft.isInstance(self.details.nftType), message: "token is not of specified type")
640 assert(nft.id == self.details.nftID, message: "token does not have specified ID")
641 }
642
643 access(contract) fun burnCallback() {
644 // We regard the listing as completed here.
645 emit ListingCompleted(
646 listingResourceID: self.uuid,
647 flowtyStorefrontID: self.details.flowtyStorefrontID,
648 funded: self.details.funded,
649 nftID: self.details.nftID,
650 nftType: self.details.nftType.identifier,
651 flowtyStorefrontAddress: self.nftPublicCollectionCapability.address
652 )
653 }
654 }
655
656 // FundingDetails
657 // A struct containing a Fundings's data.
658 //
659 access(all) struct FundingDetails {
660 // The FlowtyStorefront that the Funding is stored in.
661 // Note that this resource cannot be moved to a different FlowtyStorefront
662 access(all) var flowtyStorefrontID: UInt64
663 access(all) var listingResourceID: UInt64
664
665 // Whether this funding has been repaid or not.
666 access(all) var repaid: Bool
667
668 // Whether this funding has been settled or not.
669 access(all) var settled: Bool
670
671 // The Type of the FungibleToken that fundings must be repaid.
672 access(all) let paymentVaultType: Type
673
674 // The amount that must be repaid in the specified FungibleToken.
675 access(all) let repaymentAmount: UFix64
676
677 // the time the funding start at
678 access(all) var startTime: UFix64
679
680 // The length in seconds for this funding
681 access(all) var term: UFix64
682
683 // setToRepaid
684 // Irreversibly set this funding as repaid.
685 //
686 access(contract) fun setToRepaid() {
687 self.repaid = true
688 }
689
690 // setToSettled
691 // Irreversibly set this funding as settled.
692 //
693 access(contract) fun setToSettled() {
694 self.settled = true
695 }
696
697 // initializer
698 //
699 init (
700 flowtyStorefrontID: UInt64,
701 listingResourceID: UInt64,
702 paymentVaultType: Type,
703 repaymentAmount: UFix64,
704 term: UFix64
705 ) {
706 self.flowtyStorefrontID = flowtyStorefrontID
707 self.listingResourceID = listingResourceID
708 self.paymentVaultType = paymentVaultType
709 self.repaid = false
710 self.settled = false
711 self.repaymentAmount = repaymentAmount
712 self.term = term
713 self.startTime = getCurrentBlock().timestamp
714 }
715 }
716
717 // FundingPublic
718 // An interface providing a useful public interface to a Funding.
719 //
720 access(all) resource interface FundingPublic {
721
722 // repay
723 //
724 access(all) fun repay(payment: @{FungibleToken.Vault})
725
726 // getDetails
727 //
728 access(all) view fun getDetails(): FundingDetails
729
730 // get the listing details for this loan
731 //
732 access(all) view fun getListingDetails(): Flowty.ListingDetails
733
734 // timeRemaining
735 //
736 access(all) view fun timeRemaining() : Fix64
737
738 // isFundingExpired
739 //
740 access(all) view fun isFundingExpired(): Bool
741
742 // get the amount stored in a vault for royalty payouts
743 //
744 access(all) view fun getRoyaltyAmount(): UFix64?
745
746 access(all) fun settleFunding()
747 }
748
749 // Funding
750 //
751 access(all) resource Funding: FundingPublic, FlowtyListingCallback.Listing, Burner.Burnable {
752 access(all) event ResourceDestroyed(flowtyStorefrontID: UInt64 = self.details.flowtyStorefrontID, fundingResourceID: UInt64 = self.uuid, listingResourceID: UInt64 = self.details.listingResourceID)
753
754 // The simple (non-Capability, non-complex) details of the listing
755 access(self) let details: FundingDetails
756 access(self) let listingDetails: ListingDetails
757
758 // A capability allowing this resource to access the owner's NFT public collection
759 access(contract) let ownerNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>
760
761 // A capability allowing this resource to access the lender's NFT public collection
762 access(contract) let lenderNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>
763
764 // The receiver for the repayment.
765 access(contract) let lenderFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>
766
767 // NFT escrow
768 access(contract) var NFT: @{NonFungibleToken.NFT}?
769
770 // FUSD Allowance
771 access(contract) let fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?
772
773 // royalty payment vault to be deposited to the specified desination on repayment or default
774 access(contract) var royaltyVault: @{FungibleToken.Vault}?
775
776 // getDetails
777 // Get the details of the current state of the Listing as a struct.
778 // This avoids having more public variables and getter methods for them, and plays
779 // nicely with scripts (which cannot return resources).
780 //
781 access(all) view fun getDetails(): FundingDetails {
782 return self.details
783 }
784
785 access(all) view fun getListingDetails(): ListingDetails {
786 return self.listingDetails
787 }
788
789 access(all) view fun getRoyaltyAmount(): UFix64? {
790 return self.royaltyVault?.balance
791 }
792
793 access(contract) fun borrowNFT(): &{NonFungibleToken.NFT}? {
794 return &self.NFT
795 }
796
797 // repay
798 // Repay the funding.
799 // This pays the lender and returns the NFT to the owner.
800 //
801 access(all) fun repay(payment: @{FungibleToken.Vault}) {
802 pre {
803 self.details.repaid == false: "funding has already been repaid"
804 payment.isInstance(self.details.paymentVaultType): "payment vault is not requested fungible token"
805 payment.balance == self.details.repaymentAmount: "payment vault does not contain requested price"
806 }
807
808 self.details.setToRepaid()
809 let royaltyAmount = self.royaltyVault != nil ? self.royaltyVault?.balance! : 0.0
810
811 let tmp <- self.NFT <- nil
812 let nft <- tmp!
813 let nftID: UInt64 = nft.id
814 let nftType = nft.getType()
815
816 let depositor = Flowty.account.storage.borrow<auth(LostAndFound.Deposit) &LostAndFound.Depositor>(from: LostAndFound.DepositorStoragePath)!
817 let royaltyVault <- self.royaltyVault <- nil
818 if royaltyVault != nil {
819 let vault <-! royaltyVault!
820 vault.deposit(from: <-payment.withdraw(amount: self.details.repaymentAmount))
821 destroy payment
822 assert(vault.balance == self.details.repaymentAmount + royaltyAmount, message: "insufficient balance to send to lender" )
823
824 FlowtyUtils.trySendFungibleTokenVault(vault: <-vault, receiver: self.lenderFungibleTokenReceiver, depositor: depositor)
825 } else {
826 FlowtyUtils.trySendFungibleTokenVault(vault: <-payment, receiver: self.lenderFungibleTokenReceiver, depositor: depositor)
827 destroy royaltyVault
828 }
829
830 if let callback = Flowty.borrowCallbackContainer() {
831 callback.handle(stage: FlowtyListingCallback.Stage.Completed, listing: &self as &{FlowtyListingCallback.Listing}, nft: nil)
832 }
833
834 FlowtyUtils.trySendNFT(nft: <-nft, receiver: self.ownerNFTCollection, depositor: depositor)
835
836 let borrower = self.ownerNFTCollection.address
837 let lender = self.lenderFungibleTokenReceiver.address
838 emit FundingRepaid(
839 fundingResourceID: self.uuid,
840 listingResourceID: self.details.listingResourceID,
841 borrower: borrower,
842 lender: lender,
843 nftID: nftID,
844 nftType: nftType.identifier,
845 repaymentAmount: self.details.repaymentAmount,
846 repaymentAddress: self.fusdProviderCapability?.address
847 )
848 }
849
850 // repay
851 // Repay the funding with borrower permit.
852 // This pays the lender and returns the NFT to the owner using FUSD allowance from borrower account.
853 //
854 access(all) fun repayWithPermit() {
855 pre {
856 self.details.repaid == false: "funding has already been repaid"
857 self.details.settled == false: "funding has already been settled"
858 self.fusdProviderCapability!.check(): "listing is created without FUSD allowance"
859 }
860
861 assert(false, message: "repayWithPermit is disabled temporarily, for more info, please visit: https://x.com/flowty_io")
862
863 self.details.setToRepaid()
864 let royaltyAmount = self.royaltyVault != nil ? self.royaltyVault?.balance! : 0.0
865
866 let tmp <- self.NFT <- nil
867 let nft <- tmp!
868 let nftID = nft.id
869 let nftType = nft.getType()
870
871 let borrowerVault = self.fusdProviderCapability!.borrow()!
872 let payment <- borrowerVault.withdraw(amount: self.details.repaymentAmount)
873
874 if let callback = Flowty.borrowCallbackContainer() {
875 callback.handle(stage: FlowtyListingCallback.Stage.Completed, listing: &self as &{FlowtyListingCallback.Listing}, nft: nil)
876 }
877
878 let depositor = Flowty.account.storage.borrow<auth(LostAndFound.Deposit) &LostAndFound.Depositor>(from: LostAndFound.DepositorStoragePath)!
879 FlowtyUtils.trySendNFT(nft: <-nft, receiver: self.ownerNFTCollection, depositor: depositor)
880
881 let royaltyVault <- self.royaltyVault <- nil
882 let vault <-! royaltyVault!
883 vault.deposit(from: <-payment.withdraw(amount: self.details.repaymentAmount))
884 destroy payment
885 assert(vault.balance == self.details.repaymentAmount + royaltyAmount, message: "insufficient balance to send to lender" )
886
887 FlowtyUtils.trySendFungibleTokenVault(vault: <-vault, receiver: self.lenderFungibleTokenReceiver, depositor: depositor)
888
889 let borrower = self.ownerNFTCollection.address
890 let lender = self.lenderFungibleTokenReceiver.address
891 emit FundingRepaid(
892 fundingResourceID: self.uuid,
893 listingResourceID: self.details.listingResourceID,
894 borrower: borrower,
895 lender: lender,
896 nftID: nftID,
897 nftType: nftType.identifier,
898 repaymentAmount: self.details.repaymentAmount,
899 repaymentAddress: self.fusdProviderCapability?.address
900 )
901 }
902
903 // settleFunding
904 // Settle the different statuses responsible for the repayment and claiming processes.
905 // NFT is moved to the lender, because the borrower hasn't repaid the loan.
906 //
907 access(all) fun settleFunding() {
908 pre {
909 self.isFundingExpired(): "the loan hasn't expired"
910 !self.details.repaid: "funding has already been repaid"
911 !self.details.settled: "funding has already been settled"
912 }
913
914 assert(false, message: "loan settlement is disabled temporarily, for more info, please visit: https://x.com/flowty_io")
915
916 let lender = self.lenderNFTCollection.address
917 let borrower = self.ownerNFTCollection.address
918
919 let repayer = self.fusdProviderCapability?.address
920 let borrowerTokenBalance = repayer != nil ? FlowtyUtils.getTokenBalance(address: repayer!, vaultType: self.details.paymentVaultType) : 0.0
921
922 let ref = &self.NFT as &{NonFungibleToken.NFT}?
923 let royalties = ref!.resolveView(Type<MetadataViews.Royalties>()) as! MetadataViews.Royalties?
924
925 let tmp <- self.NFT <- nil
926 let nft <- tmp!
927 let nftID = nft.id
928 let nftType = nft.getType()
929
930 if let callback = Flowty.borrowCallbackContainer() {
931 callback.handle(stage: FlowtyListingCallback.Stage.Completed, listing: &self as &{FlowtyListingCallback.Listing}, nft: nil)
932 }
933
934 let depositor = Flowty.account.storage.borrow<auth(LostAndFound.Deposit) &LostAndFound.Depositor>(from: LostAndFound.DepositorStoragePath)!
935 if borrowerTokenBalance >= self.details.repaymentAmount && self.fusdProviderCapability?.check() == true {
936 // borrower has funds to repay loan
937 // repay lender
938 // return NFT to owner
939 self.details.setToRepaid()
940
941 let borrowerVault = self.fusdProviderCapability!.borrow()!
942 let payment <- borrowerVault.withdraw(amount: self.details.repaymentAmount)
943
944 FlowtyUtils.trySendNFT(nft: <-nft, receiver: self.ownerNFTCollection, depositor: depositor)
945
946 let repaymentVault <- payment
947 let royaltyVault <- self.royaltyVault <- nil
948 if royaltyVault != nil {
949 repaymentVault.deposit(from: <-royaltyVault!)
950 } else {
951 destroy royaltyVault
952 }
953
954 FlowtyUtils.trySendFungibleTokenVault(vault: <-repaymentVault, receiver: self.lenderFungibleTokenReceiver, depositor: depositor)
955
956 emit FundingRepaid(
957 fundingResourceID: self.uuid,
958 listingResourceID: self.details.listingResourceID,
959 borrower: borrower,
960 lender: lender,
961 nftID: nftID,
962 nftType: nftType.identifier,
963 repaymentAmount: self.details.repaymentAmount,
964 repaymentAddress: self.fusdProviderCapability?.address
965 )
966
967 return
968 }
969
970 // loan defaults; move NFT to lender as payment
971 self.details.setToSettled()
972 assert(nft != nil, message: "NFT is already moved")
973 FlowtyUtils.trySendNFT(nft: <-nft, receiver: self.lenderNFTCollection, depositor: depositor)
974
975 emit FundingSettled(
976 fundingResourceID: self.uuid,
977 listingResourceID: self.details.listingResourceID,
978 borrower: borrower,
979 lender: lender,
980 nftID: nftID,
981 nftType: nftType.identifier,
982 repaymentAmount: self.details.repaymentAmount,
983 repaymentAddress: self.fusdProviderCapability?.address
984 )
985
986 let royaltyVault <- self.royaltyVault <- nil
987 if royaltyVault == nil {
988 destroy royaltyVault
989 return
990 }
991
992 let v <- royaltyVault!
993 let originalBalance = v.balance
994 if v.balance == 0.0 {
995 destroy v
996 return
997 }
998
999 if royalties == nil {
1000 // no defined royalties on this NFT, return is back to the lender
1001 FlowtyUtils.trySendFungibleTokenVault(vault: <-v, receiver: self.lenderFungibleTokenReceiver, depositor: depositor)
1002 return
1003 }
1004
1005 // distribute royalties!
1006 let tokenInfo = FlowtyUtils.getTokenInfo(self.details.paymentVaultType)!
1007 let royaltyCuts = FlowtyUtils.metadataRoyaltiesToRoyaltyCuts(tokenInfo: tokenInfo, mdRoyalties: [royalties!])
1008 if let leftover <- FlowtyUtils.distributeRoyaltiesWithDepositor(royaltyCuts: royaltyCuts, depositor: depositor, vault: <-v) {
1009 FlowtyUtils.trySendFungibleTokenVault(vault: <-leftover, receiver: self.lenderFungibleTokenReceiver, depositor: depositor)
1010 }
1011 }
1012
1013 // timeRemaining
1014 // The remaining time. This can be negative if is expired
1015 access(all) view fun timeRemaining() : Fix64 {
1016 let fundingTerm = self.details.term
1017
1018 let startTime = self.details.startTime
1019 let currentTime = getCurrentBlock().timestamp
1020
1021 let remaining = Fix64(startTime+fundingTerm) - Fix64(currentTime)
1022
1023 return remaining
1024 }
1025
1026 // isFundingExpired
1027 access(all) view fun isFundingExpired(): Bool {
1028 let timeRemaining= self.timeRemaining()
1029 return timeRemaining < Fix64(0.0)
1030 }
1031
1032 // initializer
1033 //
1034 init (
1035 flowtyStorefrontID: UInt64,
1036 listingResourceID: UInt64,
1037 ownerNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
1038 lenderNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
1039 NFT: @{NonFungibleToken.NFT},
1040 paymentVaultType: Type,
1041 repaymentAmount: UFix64,
1042 lenderFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
1043 term: UFix64,
1044 fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?,
1045 royaltyVault: @{FungibleToken.Vault}?,
1046 listingDetails: ListingDetails
1047 ) {
1048 self.ownerNFTCollection = ownerNFTCollection
1049 self.lenderNFTCollection = lenderNFTCollection
1050 self.lenderFungibleTokenReceiver = lenderFungibleTokenReceiver
1051 self.fusdProviderCapability = fusdProviderCapability
1052 self.listingDetails = listingDetails
1053 self.NFT <-NFT
1054 self.royaltyVault <-royaltyVault
1055
1056 // Store the detailed information
1057 self.details = FundingDetails(
1058 flowtyStorefrontID: flowtyStorefrontID,
1059 listingResourceID: listingResourceID,
1060 paymentVaultType: paymentVaultType,
1061 repaymentAmount: repaymentAmount,
1062 term: term
1063 )
1064 }
1065
1066 access(contract) fun burnCallback() {
1067 pre {
1068 self.NFT == nil: "nft is not nil"
1069 self.royaltyVault == nil: "royalty vault if not nil"
1070 }
1071 }
1072 }
1073
1074 // FlowtyMarketplaceManager
1075 // An interface for adding and removing Fundings within a FlowtyMarketplace,
1076 // intended for use by the FlowtyStorefront's own
1077 //
1078 access(all) resource interface FlowtyMarketplaceManager {
1079 // createFunding
1080 // Allows the FlowtyMarketplace owner to create and insert Fundings.
1081 //
1082 access(contract) fun createFunding(
1083 flowtyStorefrontID: UInt64,
1084 listingResourceID: UInt64,
1085 ownerNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
1086 lenderNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
1087 NFT: @{NonFungibleToken.NFT},
1088 paymentVaultType: Type,
1089 lenderFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
1090 repaymentAmount: UFix64,
1091 term: UFix64,
1092 fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?,
1093 royaltyVault: @{FungibleToken.Vault}?,
1094 listingDetails: ListingDetails
1095 ): UInt64
1096 // removeFunding
1097 // Allows the FlowtyMarketplace owner to remove any funding.
1098 //
1099 access(Administrator) fun removeFunding(fundingResourceID: UInt64)
1100
1101 access(Administrator) view fun borrowPrivateFunding(fundingResourceID: UInt64): &Funding?
1102 }
1103
1104 // FlowtyMarketplacePublic
1105 // An interface to allow listing and borrowing Listings, and funding loans via Listings
1106 // in a FlowtyStorefront.
1107 //
1108 access(all) resource interface FlowtyMarketplacePublic {
1109 access(all) view fun getFundingIDs(): [UInt64]
1110 access(all) view fun borrowFunding(fundingResourceID: UInt64): &{FundingPublic}?
1111 }
1112
1113 // FlowtyStorefront
1114 // A resource that allows its owner to manage a list of Listings, and anyone to interact with them
1115 // in order to query their details and fund the loans that they represent.
1116 //
1117 access(all) resource FlowtyMarketplace : FlowtyMarketplaceManager, FlowtyMarketplacePublic, Burner.Burnable {
1118 // The dictionary of Fundings uuids to Funding resources.
1119 access(self) var fundings: @{UInt64: Funding}
1120
1121 // insert
1122 // Create and publish a funding for an NFT.
1123 //
1124 access(contract) fun createFunding(
1125 flowtyStorefrontID: UInt64,
1126 listingResourceID: UInt64,
1127 ownerNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
1128 lenderNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
1129 NFT: @{NonFungibleToken.NFT},
1130 paymentVaultType: Type,
1131 lenderFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
1132 repaymentAmount: UFix64,
1133 term: UFix64,
1134 fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?,
1135 royaltyVault: @{FungibleToken.Vault}?,
1136 listingDetails: ListingDetails
1137 ): UInt64 {
1138 // FundingAvailable event fields
1139 let nftID = NFT.id
1140 let nftType = NFT.getType()
1141
1142 let lenderVaultCap = lenderFungibleTokenReceiver.borrow()!
1143 let lender = lenderVaultCap.owner!.address
1144
1145 let borrowerNFTCollectionCap = ownerNFTCollection.borrow()!
1146 let borrower = borrowerNFTCollectionCap.owner!.address
1147
1148 // Create funding resource
1149 let funding <- create Funding(
1150 flowtyStorefrontID: flowtyStorefrontID,
1151 listingResourceID: listingResourceID,
1152 ownerNFTCollection: ownerNFTCollection,
1153 lenderNFTCollection: lenderNFTCollection,
1154 NFT: <-NFT,
1155 paymentVaultType: paymentVaultType,
1156 repaymentAmount: repaymentAmount,
1157 lenderFungibleTokenReceiver: lenderFungibleTokenReceiver,
1158 term: term,
1159 fusdProviderCapability: fusdProviderCapability,
1160 royaltyVault: <-royaltyVault,
1161 listingDetails: listingDetails
1162 )
1163
1164 let detail = funding.getDetails()
1165 let expiration = detail.term + detail.startTime
1166 // September 3rd 12:00 AM
1167 assert(paymentVaultType.identifier != "A.b19436aae4d94622.FiatToken.Vault" || expiration < 1725321600.0, message: "loans are not able to be funded that expire on or after September 3rd 12:00 AM GMT.")
1168
1169 if let callback = Flowty.borrowCallbackContainer() {
1170 callback.handle(stage: FlowtyListingCallback.Stage.Created, listing: &funding as &{FlowtyListingCallback.Listing}, nft: funding.borrowNFT())
1171 }
1172
1173 let fundingResourceID = funding.uuid
1174
1175 // Add the new Funding to the dictionary.
1176 let oldFunding <- self.fundings[fundingResourceID] <- funding
1177 // Note that oldFunding will always be nil, but we have to handle it.
1178 destroy oldFunding
1179
1180 let enabledAutoRepayment = fusdProviderCapability != nil
1181
1182 emit FundingAvailable(
1183 fundingResourceID: fundingResourceID,
1184 listingResourceID: listingResourceID,
1185 borrower: borrower,
1186 lender: lender,
1187 nftID: nftID,
1188 nftType: nftType.identifier,
1189 repaymentAmount: repaymentAmount,
1190 enabledAutoRepayment: enabledAutoRepayment,
1191 repaymentAddress: fusdProviderCapability?.address
1192 )
1193
1194 return fundingResourceID
1195 }
1196
1197 // removeFunding
1198 // Remove a Funding.
1199 //
1200 access(Administrator) fun removeFunding(fundingResourceID: UInt64) {
1201 let funding <- self.fundings.remove(key: fundingResourceID)
1202 ?? panic("missing Funding")
1203
1204 assert(funding.getDetails().repaid == true || funding.getDetails().settled == true, message: "funding is not repaid or settled")
1205
1206 if let callback = Flowty.borrowCallbackContainer() {
1207 callback.handle(stage: FlowtyListingCallback.Stage.Destroyed, listing: &funding as &{FlowtyListingCallback.Listing}, nft: nil)
1208 }
1209
1210 // This will emit a FundingCompleted event.
1211 Burner.burn(<-funding)
1212 }
1213
1214 // getFundingIDs
1215 // Returns an array of the Funding resource IDs that are in the collection
1216 //
1217 access(all) view fun getFundingIDs(): [UInt64] {
1218 return self.fundings.keys
1219 }
1220
1221 // borrowFunding
1222 // Returns a read-only view of the Funding for the given fundingID if it is contained by this collection.
1223 //
1224 access(all) view fun borrowFunding(fundingResourceID: UInt64): &{FundingPublic}? {
1225 return &self.fundings[fundingResourceID]
1226 }
1227
1228 // borrowPrivateFunding
1229 // Returns a private view of the Funding for the given fundingID if it is contained by this collection.
1230 //
1231 access(Administrator) view fun borrowPrivateFunding(fundingResourceID: UInt64): &Funding? {
1232 return &self.fundings[fundingResourceID]
1233 }
1234
1235 access(contract) fun burnCallback() {
1236 let ids = self.fundings.keys
1237 for id in ids {
1238 let funding <- self.fundings.remove(key: id)!
1239 Burner.burn(<- funding)
1240 }
1241
1242 // Let event consumers know that this marketplace will no longer exist
1243 emit FlowtyMarketplaceDestroyed(flowtyStorefrontResourceID: self.uuid)
1244 }
1245
1246 // constructor
1247 //
1248 init () {
1249 self.fundings <- {}
1250
1251 // Let event consumers know that this storefront exists
1252 emit FlowtyMarketplaceInitialized(flowtyMarketplaceResourceID: self.uuid)
1253 }
1254 }
1255
1256 // FlowtyStorefrontManager
1257 // An interface for adding and removing Listings within a FlowtyStorefront,
1258 // intended for use by the FlowtyStorefront's own
1259 access(all) resource interface FlowtyStorefrontManager {
1260 // createListing
1261 // Allows the FlowtyStorefront owner to create and insert Listings.
1262 //
1263 access(List) fun createListing(
1264 payment: @{FungibleToken.Vault},
1265 nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
1266 nftPublicCollectionCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
1267 fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?,
1268 nftType: Type,
1269 nftID: UInt64,
1270 amount: UFix64,
1271 interestRate: UFix64,
1272 term: UFix64,
1273 paymentVaultType: Type,
1274 paymentCuts: [PaymentCut],
1275 expiresAfter: UFix64
1276 ): UInt64
1277 // removeListing
1278 // Allows the FlowtyStorefront owner to remove any sale listing, accepted or not.
1279 //
1280 access(Cancel) fun removeListing(listingResourceID: UInt64)
1281 }
1282
1283 // FlowtyStorefrontPublic
1284 // An interface to allow listing and borrowing Listings, and funding loans via Listings
1285 // in a FlowtyStorefront.
1286 //
1287 access(all) resource interface FlowtyStorefrontPublic {
1288 access(all) view fun getListingIDs(): [UInt64]
1289 access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? {
1290 post {
1291 result == nil || result!.getType() == Type<@Listing>()
1292 }
1293 }
1294 access(all) fun cleanup(listingResourceID: UInt64)
1295 access(all) view fun getRoyalties(): {String:Flowty.Royalty}
1296 }
1297
1298 // FlowtyStorefront
1299 // A resource that allows its owner to manage a list of Listings, and anyone to interact with them
1300 // in order to query their details and fund the loans that they represent.
1301 //
1302 access(all) resource FlowtyStorefront : FlowtyStorefrontManager, FlowtyStorefrontPublic, Burner.Burnable {
1303 access(all) event ResourceDestroyed(flowtyStorefrontID: UInt64 = self.uuid)
1304
1305 // The dictionary of Listing uuids to Listing resources.
1306 access(self) var listings: @{UInt64: Listing}
1307
1308 // insert
1309 // Create and publish a Listing for an NFT.
1310 //
1311 access(List) fun createListing(
1312 payment: @{FungibleToken.Vault},
1313 nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
1314 nftPublicCollectionCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
1315 fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?,
1316 nftType: Type,
1317 nftID: UInt64,
1318 amount: UFix64,
1319 interestRate: UFix64,
1320 term: UFix64,
1321 paymentVaultType: Type,
1322 paymentCuts: [PaymentCut],
1323 expiresAfter: UFix64
1324 ): UInt64 {
1325 pre {
1326 // We don't allow all tokens to be used as payment. Check that the provided one is supported.
1327 FlowtyUtils.isTokenSupported(type: paymentVaultType): "provided payment type is not supported"
1328 // make sure that the FUSD vault has at least the listing fee
1329 payment.balance == Flowty.ListingFee: "payment vault does not contain requested listing fee amount"
1330 // check that the repayment token type is the same as the payment token if repayment is not nil
1331 fusdProviderCapability == nil || fusdProviderCapability!.check() && fusdProviderCapability!.borrow()!.getType() == paymentVaultType: "repayment vault type and payment vault type do not match"
1332 // There are no listing fees right now so this will ensure that no one attempts to send any
1333 payment.balance == 0.0: "no listing fee required"
1334 // make sure the payment type is the same as paymentVaultType
1335 payment.getType() == paymentVaultType: "payment type and paymentVaultType do not match"
1336 nftProviderCapability.check(): "invalid nft provider"
1337 term >= 1_209_600.0: "term must be at least 2 weeks"
1338 }
1339
1340 let nft = nftProviderCapability.borrow()!.borrowNFT(nftID) ?? panic("nft not found")
1341 assert(nft.getType() == nftType, message: "incorrect nft type")
1342 assert(FlowtyUtils.isSupported(nft), message: "nft type is not supported")
1343
1344 let royaltyRate = FlowtyUtils.getRoyaltyRate(nft)
1345
1346 let listing <- create Listing(
1347 nftProviderCapability: nftProviderCapability,
1348 nftPublicCollectionCapability: nftPublicCollectionCapability,
1349 fusdProviderCapability: fusdProviderCapability,
1350 nftType: nftType,
1351 nftID: nftID,
1352 amount: amount,
1353 interestRate: interestRate,
1354 term: term,
1355 paymentVaultType: paymentVaultType,
1356 paymentCuts: paymentCuts,
1357 flowtyStorefrontID: self.uuid,
1358 expiresAfter: expiresAfter,
1359 royaltyRate: royaltyRate
1360 )
1361
1362 if let callback = Flowty.borrowCallbackContainer() {
1363 callback.handle(stage: FlowtyListingCallback.Stage.Created, listing: &listing as &{FlowtyListingCallback.Listing}, nft: nft)
1364 }
1365
1366 let listingResourceID = listing.uuid
1367 let expiration = listing.getDetails().expiresAfter
1368
1369 // Add the new listing to the dictionary.
1370 let oldListing <- self.listings[listingResourceID] <- listing
1371 // Note that oldListing will always be nil, but we have to handle it.
1372 destroy oldListing
1373
1374 // Listing fee
1375 // let listingFee <- payment.withdraw(amount: Flowty.ListingFee)
1376 // let flowtyFusdReceiver = Flowty.account.borrow<&FUSD.Vault{FungibleToken.Receiver}>(from: Flowty.FusdVaultStoragePath)
1377 // ?? panic("Missing or mis-typed FUSD Reveiver")
1378 // flowtyFusdReceiver.deposit(from: <-listingFee)
1379 destroy payment
1380
1381 let enabledAutoRepayment = fusdProviderCapability != nil
1382
1383 emit ListingAvailable(
1384 flowtyStorefrontAddress: self.owner?.address!,
1385 flowtyStorefrontID: self.uuid,
1386 listingResourceID: listingResourceID,
1387 nftType: nftType.identifier,
1388 nftID: nftID,
1389 amount: amount,
1390 interestRate: interestRate,
1391 term: term,
1392 enabledAutoRepayment: enabledAutoRepayment,
1393 royaltyRate: royaltyRate,
1394 expiresAfter: expiration,
1395 paymentTokenType: paymentVaultType.identifier,
1396 repaymentAddress: fusdProviderCapability?.address
1397 )
1398
1399 return listingResourceID
1400 }
1401
1402 // removeListing
1403 // Remove a Listing that has not yet been funded from the collection and destroy it.
1404 //
1405 access(Cancel) fun removeListing(listingResourceID: UInt64) {
1406 let listing <- self.listings.remove(key: listingResourceID)
1407 ?? panic("missing Listing")
1408
1409 if let callback = Flowty.borrowCallbackContainer() {
1410 callback.handle(stage: FlowtyListingCallback.Stage.Destroyed, listing: &listing as &{FlowtyListingCallback.Listing}, nft: nil)
1411 }
1412
1413 // This will emit a ListingCompleted event.
1414 Burner.burn(<- listing)
1415 }
1416
1417 // getListingIDs
1418 // Returns an array of the Listing resource IDs that are in the collection
1419 //
1420 access(all) view fun getListingIDs(): [UInt64] {
1421 return self.listings.keys
1422 }
1423
1424 // borrowListing
1425 // Returns a read-only view of the Listing for the given listingID if it is contained by this collection.
1426 //
1427 access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? {
1428 return &self.listings[listingResourceID]
1429 }
1430
1431 // cleanup
1432 // Remove an listing *if* it has been funded and expired.
1433 // Anyone can call, but at present it only benefits the account owner to do so.
1434 // Kind purchasers can however call it if they like.
1435 //
1436 access(all) fun cleanup(listingResourceID: UInt64) {
1437 pre {
1438 self.listings[listingResourceID] != nil: "could not find listing with given id"
1439 }
1440
1441 let listing <- self.listings.remove(key: listingResourceID)!
1442 assert(listing.getDetails().funded == true, message: "listing is not funded, only admin can remove")
1443 Burner.burn(<- listing)
1444 }
1445
1446 access(all) view fun getRoyalties(): {String:Flowty.Royalty} {
1447 return Flowty.Royalties
1448 }
1449
1450 // destructor
1451 //
1452 access(contract) fun burnCallback() {
1453 let ids = self.listings.keys
1454 for id in ids {
1455 Burner.burn(<- self.listings.remove(key: id))
1456 }
1457
1458 // Let event consumers know that this storefront will no longer exist
1459 emit FlowtyStorefrontDestroyed(flowtyStorefrontResourceID: self.uuid)
1460 }
1461
1462 // constructor
1463 //
1464 init () {
1465 self.listings <- {}
1466
1467 // Let event consumers know that this storefront exists
1468 emit FlowtyStorefrontInitialized(flowtyStorefrontResourceID: self.uuid)
1469 }
1470 }
1471
1472 // createStorefront
1473 // Make creating a FlowtyStorefront publicly accessible.
1474 //
1475 access(all) fun createStorefront(): @FlowtyStorefront {
1476 return <-create FlowtyStorefront()
1477 }
1478
1479 access(account) fun borrowMarketplace(): &Flowty.FlowtyMarketplace {
1480 return self.account.storage.borrow<&Flowty.FlowtyMarketplace>(from: Flowty.FlowtyMarketplaceStoragePath)!
1481 }
1482
1483 access(all) view fun borrowMarketplacePublic(): &{FlowtyMarketplacePublic} {
1484 let mp = self.account.capabilities.get<&{Flowty.FlowtyMarketplacePublic}>(Flowty.FlowtyMarketplacePublicPath)!.borrow()
1485 ?? panic("marketplace does not exist")
1486 return mp
1487 }
1488
1489 access(all) view fun getRoyaltySafe(nftTypeIdentifier: String): Royalty? {
1490 return Flowty.Royalties[nftTypeIdentifier]
1491 }
1492
1493 access(all) view fun getRoyalty(nftTypeIdentifier: String): Royalty {
1494 return Flowty.Royalties[nftTypeIdentifier]!
1495 }
1496
1497 access(all) view fun getTokenPaths(): {String:PublicPath} {
1498 return self.TokenPaths
1499 }
1500
1501 access(all) fun settleFunding(fundingResourceID: UInt64) {
1502 let marketplace = Flowty.borrowMarketplace()
1503 let funding = marketplace.borrowFunding(fundingResourceID: fundingResourceID)
1504 funding!.settleFunding()
1505 }
1506
1507 // FlowtyAdmin
1508 // Allows the adminitrator to set the amount of fees, set the suspended funding period
1509 //
1510 access(all) resource FlowtyAdmin {
1511 access(Administrator) fun setFees(listingFixedFee: UFix64, fundingPercentageFee: UFix64) {
1512 pre {
1513 // The UFix64 type covers a negative numbers
1514 fundingPercentageFee <= 1.0: "Funding fee should be a percentage"
1515 }
1516
1517 Flowty.ListingFee = listingFixedFee
1518 Flowty.FundingFee = fundingPercentageFee
1519 }
1520
1521 access(Administrator) fun setSuspendedFundingPeriod(period: UFix64) {
1522 Flowty.SuspendedFundingPeriod = period
1523 }
1524
1525 access(Administrator) fun setSupportedCollection(collection: String, state: Bool) {
1526 Flowty.SupportedCollections[collection] = state
1527 emit CollectionSupportChanged(collectionIdentifier: collection, state: state)
1528 }
1529
1530 access(Administrator) fun setCollectionRoyalty(collection: String, royalty: Royalty) {
1531 pre {
1532 royalty.Rate <= 1.0: "Royalty rate must be a percentage"
1533 }
1534
1535 Flowty.Royalties[collection] = royalty
1536 emit RoyaltyAdded(collectionIdentifier: collection, rate: royalty.Rate)
1537 }
1538
1539 access(Administrator) fun registerFungibleTokenPath(vaultType: Type, path: PublicPath) {
1540 Flowty.TokenPaths[vaultType.identifier] = path
1541 }
1542 }
1543
1544 access(contract) fun borrowCallbackContainer(): auth(FlowtyListingCallback.Handle) &FlowtyListingCallback.Container? {
1545 return self.account.storage.borrow<auth(FlowtyListingCallback.Handle) &FlowtyListingCallback.Container>(from: FlowtyListingCallback.ContainerStoragePath)
1546 }
1547
1548 init () {
1549 self.FlowtyStorefrontStoragePath = /storage/FlowtyStorefront
1550 self.FlowtyStorefrontPublicPath = /public/FlowtyStorefront
1551 self.FlowtyMarketplaceStoragePath = /storage/FlowtyMarketplace
1552 self.FlowtyMarketplacePublicPath = /public/FlowtyMarketplace
1553 self.FlowtyAdminStoragePath = /storage/FlowtyAdmin
1554 self.FusdVaultStoragePath = /storage/fusdVault
1555 self.FusdReceiverPublicPath = /public/fusdReceiver
1556 self.FusdBalancePublicPath = /public/fusdBalance
1557
1558 self.ListingFee = 0.0 // Fixed FUSD
1559 self.FundingFee = 0.1 // Percentage of the interest, a number between 0 and 1.
1560 self.SuspendedFundingPeriod = 1.0 // Period in seconds
1561 self.Royalties = {}
1562 self.SupportedCollections = {}
1563 self.TokenPaths = {}
1564
1565 let marketplace <- create FlowtyMarketplace()
1566
1567 self.account.storage.save(<-marketplace, to: self.FlowtyMarketplaceStoragePath)
1568 let mpPubCap = self.account.capabilities.storage.issue<&{Flowty.FlowtyMarketplacePublic}>(Flowty.FlowtyMarketplaceStoragePath)
1569 self.account.capabilities.publish(mpPubCap, at: Flowty.FlowtyMarketplacePublicPath)
1570
1571 // FlowtyAdmin
1572 let flowtyAdmin <- create FlowtyAdmin()
1573 self.account.storage.save(<-flowtyAdmin, to: self.FlowtyAdminStoragePath)
1574
1575 if self.account.storage.borrow<&AnyResource>(from: FlowtyListingCallback.ContainerStoragePath) == nil {
1576 let dnaHandler <- DNAHandler.createHandler()
1577 let listingHandler <- FlowtyListingCallback.createContainer(defaultHandler: <-dnaHandler)
1578 self.account.storage.save(<-listingHandler, to: FlowtyListingCallback.ContainerStoragePath)
1579 }
1580
1581 emit FlowtyInitialized()
1582 }
1583}
1584