Pawn your illiquid or underperforming NFTs, save thousands in tax money or simply get some liquidity during tough times.
🛠 This project requires NodeJS v16.x or above. See: Node installation instructions
🛠 This project requires flow-cli v0.49.0 or above. See: Flow CLI installation instructions
git clone https://github.com/Build-Squad/nft-pawnshop.gitcd nft-pawnshop
npm installRun the emulator with:
flow emulator --storage-limit=false --contracts --coverage-reporting -vand create a few accounts that will be used for demonstrating the basic flows.
flow keys generate --output=json > peter.json
flow keys generate --output=json > emulator-admin.json
flow keys generate --output=json > john-buyer.json
# Create an account for each of the three keys above, in the same order.
flow accounts create --keyMake sure to update the private keys in the flow.json file.
Deploy the contracts with:
flow project deploy --network=emulatorPerform the necessary setup for the NFTPawnshop contract, and the admin account:
flow transactions send ./cadence/transactions/setup_storefront.cdc --network=emulator --signer=emulator-admin
flow transactions send ./cadence/transactions/setup_nft_catalog.cdc --network=emulator --signer=emulator-admin
flow transactions send ./cadence/transactions/transfer_flow_tokens.cdc 0x045a1763c93006ca 1500.0 --network=emulator --signer=emulator-account
flow transactions send ./cadence/transactions/fund_admin_vault.cdc 1000.0 --network=emulator --signer=emulator-admin
node cadence/transactions/add_admin_collection.mjsWith a proper setup, we should get:
flow scripts execute ./cadence/scripts/get_admin_balance.cdc --network=emulator
Result: 1000.00000000
flow scripts execute ./cadence/scripts/get_collection_names.cdc --network=emulator
Result: ["ExampleNFT"]
flow scripts execute ./cadence/scripts/get_sale_price.cdc --network=emulator
Result: 15.00000000Let's setup the account of peter, to hold some NFTs from the ExampleNFT collection, which is included on the NFTCatalog.
flow transactions send ./cadence/transactions/transfer_flow_tokens.cdc 0xe03daebed8ca0615 1500.0 --network=emulator --signer=emulator-account
node cadence/transactions/setup_collection.mjs "ExampleNFT" peter
flow transactions send ./cadence/transactions/setup_account_to_receive_royalty.cdc '/storage/flowTokenVault' --network=emulator --signer=emulator-account
flow transactions send ./cadence/transactions/mint_nft.cdc 0xe03daebed8ca0615 'My Example NFT' 'My Example NFT Description' 'https://www.example-nft.com/thumbnails/0' '[0.05]' '["Tribute to Creator!"]' '[0xf8d6e0586b0a20c7]' --network=emulator --signer=emulator-account
flow transactions send ./cadence/transactions/mint_nft.cdc 0xe03daebed8ca0615 'My Example NFT #2' 'My Example NFT Description #2' 'https://www.example-nft.com/thumbnails/2' '[0.02]' '["Tribute to Creator!"]' '[0xf8d6e0586b0a20c7]' --network=emulator --signer=emulator-accountThe above account, should now contain 2 NFTs, which the holder can pawn to the contract:
flow scripts execute ./cadence/scripts/get_collection_ids.cdc 0xe03daebed8ca0615 --network=emulator
Result: [0, 1]To pawn NFTs for a specific collection, run:
flow transactions send ./cadence/transactions/setup_pledge_collection.cdc --network=emulator --signer=peter
flow transactions send ./cadence/transactions/pawn_nfts.cdc "ExampleNFT" '[1]' --network=emulator --signer=peter
flow transactions send ./cadence/transactions/pawn_nfts.cdc "ExampleNFT" '[0]' --network=emulator --signer=peterNote that multiple NFTs for a specific collection can be pawned with a single transaction also, by
passing '[0, 1]'. For demo purposes, we made it in two transactions.
Let's observe the side effects of these transactions:
flow scripts execute ./cadence/scripts/get_admin_balance.cdc --network=emulator
# The admin account currently pays 15 FLOW tokens for each NFT, that's why its
# balance was reduced by 30 FLOW tokens (2 * 15 = 30). In the real world, each
# NFT should be properly valuated by the owner of the pawnshop.
Result: 970.00000000
flow accounts get 0xe03daebed8ca0615
# The 30 FLOW tokens were deposited to the account of `peter`
Address 0xe03daebed8ca0615
Balance 1530.00000000flow scripts execute ./cadence/scripts/get_collection_ids.cdc 0xe03daebed8ca0615 --network=emulator
# After pawning the 2 NFTs from the `ExampleNFT` collection, the account of `peter`
# has no NFTs in its collection, which is expected.
Result: []
flow scripts execute ./cadence/scripts/get_admin_collection.cdc "ExampleNFT" --network=emulator
# This implies that the 2 NFTs, were deposited to the `ExampleNFT` collection of the
# admin account.
Result: [1, 0]As with real-world pawn shops, the account of peter receives a pledge, which allows
the debitor to redeem the pawned NFTs, within the period of a year, by paying the sale
price.
flow scripts execute ./cadence/scripts/get_pledge_collection_info.cdc 0xe03daebed8ca0615 --network=emulatorResult: [
A.045a1763c93006ca.NFTPawnshop.PledgeInfo(
id: 56,
debitor: 0xe03daebed8ca0615,
expiry: 1704892702.00000000,
pawns: A.045a1763c93006ca.NFTPawnshop.NFTPawnInfo(
collectionIdentifier: "ExampleNFT",
nftIDs: [0],
salePrice: 15.00000000
)
),
A.045a1763c93006ca.NFTPawnshop.PledgeInfo(
id: 58,
debitor: 0xe03daebed8ca0615,
expiry: 1704892698.00000000,
pawns: A.045a1763c93006ca.NFTPawnshop.NFTPawnInfo(
collectionIdentifier: "ExampleNFT",
nftIDs: [1],
salePrice: 15.00000000
)
)
]The above exists as a resource (NFTPawnshop.PledgeCollection) in the account storage of peter.
However, the same info can be found in the contract:
flow scripts execute ./cadence/scripts/get_admin_pledges.cdc --network=emulatorResult: {
58: A.045a1763c93006ca.NFTPawnshop.PledgeInfo(
id: 58,
debitor: 0xe03daebed8ca0615,
expiry: 1704892698.00000000,
pawns: A.045a1763c93006ca.NFTPawnshop.NFTPawnInfo(
collectionIdentifier: "ExampleNFT",
nftIDs: [1],
salePrice: 15.00000000
)
),
56: A.045a1763c93006ca.NFTPawnshop.PledgeInfo(
id: 56,
debitor: 0xe03daebed8ca0615,
expiry: 1704892702.00000000,
pawns: A.045a1763c93006ca.NFTPawnshop.NFTPawnInfo(
collectionIdentifier: "ExampleNFT",
nftIDs: [0],
salePrice: 15.00000000
)
)
}Let's see how peter can redeem each of the pawned NFTs:
flow transactions send ./cadence/transactions/redeem_pledge.cdc "ExampleNFT" 56 --network=emulator --signer=peter
flow transactions send ./cadence/transactions/redeem_pledge.cdc "ExampleNFT" 58 --network=emulator --signer=peterLet's observe the side effects of this transaction:
flow scripts execute ./cadence/scripts/get_admin_balance.cdc --network=emulator
# The admin account received 30 FLOW tokens
Result: 1000.00000000
flow accounts get 0xe03daebed8ca0615
# The account of `peter` paid 30 FLOW tokens
Address 0xe03daebed8ca0615
Balance 1500.00000000
flow scripts execute ./cadence/scripts/get_collection_ids.cdc 0xe03daebed8ca0615 --network=emulator
# The `ExampleNFT` collection of `peter` regained possession of the 2 pawned NFTs
Result: [0, 1]
flow scripts execute ./cadence/scripts/get_admin_collection.cdc "ExampleNFT" --network=emulator
# The `ExampleNFT` collection of `emulator-admin` is now empty.
Result: []Upon successful redemption, the contract should have no pledges now:
flow scripts execute ./cadence/scripts/get_admin_pledges.cdc --network=emulator
Result: {}This was the basic flow where an account holder pawned some NFTs, received some FLOW tokens and was able to redeem them, during the grace period, without needing any permission from the smart contract or its admin.
Now let's see what would happen in the scenario where an account holder was not able to redeem during the grace period.
The default grace period is 1 year, so let's reduce it to just a second, for our demo. This doesn't affect already existing pledges. We also changed the sale price to 10 FLOW tokens, this also doesn't affect already existing pledges.
flow transactions send ./cadence/transactions/update_default_expiry.cdc 1.0 --network=emulator --signer=emulator-admin
flow transactions send ./cadence/transactions/update_sale_price.cdc 10.0 --network=emulator --signer=emulator-adminThis time, peter pawns just one NFT:
flow transactions send ./cadence/transactions/pawn_nfts.cdc "ExampleNFT" '[1]' --network=emulator --signer=peterflow scripts execute ./cadence/scripts/get_pledge_collection_info.cdc 0xe03daebed8ca0615 --network=emulator
flow transactions send ./cadence/transactions/redeem_pledge.cdc "ExampleNFT" 62 --network=emulator --signer=peter
error: panic: The reedem period has expired!
--> 045a1763c93006ca.NFTPawnshop:109:16The redeem was unsuccessful. This means that the admin account can only now transfer the proceeds, meaning the expired pledges.
flow transactions send ./cadence/transactions/admin_transfer_proceeds.cdc "ExampleNFT" 0x045a1763c93006ca --network=emulator --signer=emulator-adminTo verify that it worked:
flow scripts execute ./cadence/scripts/get_collection_ids.cdc 0x045a1763c93006ca --network=emulator
Result: [1]The NFTPawnshop contract, and the account where it is deployed, has NFTStorefrontV2 installed,
meaning it can sell the proceeds:
flow transactions send ./cadence/transactions/sell_item_via_catalog.cdc "ExampleNFT" 1 7.5 "NFT Pawnshop Marketplace" 0.0 1704124654 '[]' --network=emulator --signer=emulator-adminLet's view the created listing and its details:
flow scripts execute ./cadence/scripts/get_storefront_ids.cdc 0x045a1763c93006ca --network=emulator
Result: [63]
flow scripts execute ./cadence/scripts/get_listing_details.cdc 0x045a1763c93006ca 63 --network=emulator
Result: A.045a1763c93006ca.NFTStorefrontV2.ListingDetails(
storefrontID: 37,
purchased: false,
nftType: Type<A.f8d6e0586b0a20c7.ExampleNFT.NFT>(),
nftUUID: 45,
nftID: 1,
salePaymentVaultType: Type<A.0ae53cb6e3f42a79.FlowToken.Vault>(),
salePrice: 7.50000000,
saleCuts: [
A.045a1763c93006ca.NFTStorefrontV2.SaleCut(
receiver: Capability<&AnyResource{A.ee82856bf20e2aa6.FungibleToken.Receiver}>(address: 0xf8d6e0586b0a20c7, path: /public/GenericFTReceiver),
amount: 0.15000000
),
A.045a1763c93006ca.NFTStorefrontV2.SaleCut(
receiver: Capability<&AnyResource{A.ee82856bf20e2aa6.FungibleToken.Receiver}>(address: 0x045a1763c93006ca, path: /public/flowTokenReceiver),
amount: 7.35000000
)
],
customID: "NFT Pawnshop Marketplace",
commissionAmount: 0.00000000,
expiry: 1704124654
)We can buy this listing with the john-buyer account:
# First we need to setup the account's collection.
node cadence/transactions/setup_collection.mjs "ExampleNFT" john-buyer
flow transactions send ./cadence/transactions/transfer_flow_tokens.cdc 0x120e725050340cab 1500.0 --network=emulator --signer=emulator-account
flow transactions send ./cadence/transactions/buy_item_via_catalog.cdc "ExampleNFT" 63 0x045a1763c93006ca 0x045a1763c93006ca --network=emulator --signer=john-buyerWe can verify the purchase with:
flow scripts execute ./cadence/scripts/get_collection_ids.cdc 0x120e725050340cab --network=emulator
Result: [1]While at it, let's cleanup the purchased listing from the Storefront:
flow transactions send ./cadence/transactions/cleanup_purchased_listings.cdc 0x045a1763c93006ca 63 --network=emulator --signer=emulator-admin
flow scripts execute ./cadence/scripts/get_storefront_ids.cdc 0x045a1763c93006ca --network=emulator
Result: []This was the basic flow from the side of the pawn shop's admin, showing how to utilise the expired pledges.
From the perspective of the debitor (account holder with a Pledge resource), the only accessible method is the redeemNFT, which accepts the collection identifier, the NonFungibleToken.Receiver capability and a FungibleToken.Vault containing the necessary fees.
pub resource Pledge: PledgePublic, PledgePrivate {
access(contract) let debitor: Address
access(contract) let expiry: UFix64
access(contract) let pawns: NFTPawnInfo
...
}The debitor, expiry and pawns fields can only be accessed by the contract itself, not even the
resource owner. To verify, run:
flow transactions send ./cadence/transactions/user_rug_pull.cdc "ExampleNFT" 50 --network=emulator --signer=peter
...
error: cannot access `expiry`: field has contract access
--> 8778a81dba69f9b99095b68eb0568f932c7d0c1adaf1d3ed21867d8547bc95e3:17:8
|
17 | pledge.expiry = 1735825572.0
| ^^^^^^^^^^^^^
error: cannot assign to `expiry`: field has contract access
--> 8778a81dba69f9b99095b68eb0568f932c7d0c1adaf1d3ed21867d8547bc95e3:17:15
|
17 | pledge.expiry = 1735825572.0
| ^^^^^^ consider making it publicly settable with `pub(set)`
error: cannot assign to constant member: `expiry`
--> 8778a81dba69f9b99095b68eb0568f932c7d0c1adaf1d3ed21867d8547bc95e3:17:15
|
17 | pledge.expiry = 1735825572.0
...From the perspective of the admin account, where the contract is deployed, the pawned NFTs and their respective collections, are also accessible only to the contract itself, and not the admin account.
access(contract) let collections: @{String: NonFungibleToken.Collection}The admin account, which has an Admin resource in the account storage, can only call the
transferProceeds method, which also accepts a NonFungibleToken.Receiver capability. This method
does not touch pledges within the grace period:
if (pledgeInfo.expiry > getCurrentBlock().timestamp) {
continue
}To verify, run:
flow transactions send ./cadence/transactions/admin_rug_pull.cdc "ExampleNFT" 0xac69e3c69589639e --network=emulator --signer=emulator-admin
error: cannot access `collections`: field has contract access
--> ee977550c874e35dbff1f341c0b2497e5688482eb18bfe456e6d0bc7c8a4095c:18:27
|
18 | let collection = (&NFTPawnshop.collections[identifier] as auth &NonFungibleToken.Collection?)!By interacting with the Emulator when:
- Deploying smart contracts,
- Executing scripts,
- Sending transactions
we are able to get the code coverage for our NFTPawnshop smart contract. Head over to http://localhost:8080/emulator/codeCoverage and search for the A.045a1763c93006ca.NFTPawnshop key in the resulting JSON response. It should look something like this:
That is quite a lot of code that we managed to cover with our demo!
This part is still a work-in-progress! More to come soon 🙏
