Peer-to-Peer NFT Swapping on the Flow Blockchain
A technical overview of the new Swap smart contract from evaluate.xyz
The Flow blockchain has emerged as the premier ecosystem for highly collectible, trading card-like NFTs. While you won’t likely see the Beeples of the world releasing high concept art on Flow (although that may be changing soon!), collectors of many other stripes are starting to call the Flow blockchain home—wherever there are collectors, there is a healthy market for swapping.
However, until recently, there wasn’t an easy, trustworthy platform for peer-to-peer NFT trading on Flow. The handful of existing marketplaces only offer the ability to sell collectibles for currency, but many collectors aren’t looking to cash out or spend tons more money. Just like kids trading Pokemon cards, many Flow collectors today simply want to refine their collection by swapping with their peers.
At evaluate.xyz, we decided to fill this technological gap with our new Swap platform, consisting of an airtight smart contract as well as a clean, drag-and-drop trading UI that randomly matches similar collectors looking to trade real-time. In this blog post, we’ll walk through the Swap contract and some example transactions, going through some of the security features and outlining how to integrate with the contract from a third-party contract or application.
Contract structure
The initiating user (i.e. the one who hosts the swap proposal object in their account storage) is known as the “left user,” and the responding user is labelled the “right user” in order to differentiate between the two, even though there is effectively no difference between the two users on either side of a swap.
If they’re a first-time swapper, the left user creates a SwapCollection object and saves it to their account storage. This object allows the left user to create and manage various proposals to trade with various other users. Given that the contract requires a partner address and all token IDs to be specified when creating a swap proposal, trades must generally be negotiated off-chain beforehand (potentially using evaluate's swap client!).
Once both parties have agreed to the trade details, the left user calls the createProposal method on their SwapCollection to create a SwapProposal object specific to the trade in question. The SwapProposal object initialization function requires a UserOffer object for both the left and right users, a UserCapabilities object from the left user, and an expiration time. The UserOffer objects specify user addresses and exactly which tokens are to be traded by each user, while the UserCapabilities object contains capabilities from the left user’s account to provide and receive the tokens in the trade.
Once the SwapProposal has been created, the right user then gets a reference to the left user’s SwapCollectionPublic capability, which allows any user to get human-readable versions of swap proposals and user offers. After verifying that all details of the proposal are correct, the right user then calls the executeProposal method on the left user’s SwapCollection object by passing their own UserCapabilities object with all necessary token providers and receivers, and the proposed tokens are finally swapped!
Security measures
As with any well-designed smart contract, evaluate’s Swap contract has a host of security measures baked into its architecture, but here we’ll outline a few of the most important features to know about as a user or third-party integration developer.
Due to the fact that blockchain operations are meant to be trustless by nature, our contract never has direct custody of any tokens or even the capabilities used to access them. The left user’s SwapCollection, which is stored in the left user’s account storage, holds the left user’s token provider and receiver capabilities and only ever calls the withdraw function inside the executeProposal function. Similarly, the right user’s token provider capabilities are passed directly to the executeProposal function where they are immediately used and discarded. As the final step of proposal execution, the SwapProposal object is deleted from the left user’s SwapCollection object, removing any possibility of future use of those capabilities.
Relatedly, all address and ownership verification occurs in the SwapProposal initialization function and directly in the executeProposal function, meaning that it’s impossible to bypass those checks, unlike the code in helper functions such as createProposal.
On the manual side of security, each struct involved in a SwapProposal has a built-in getReadable function that outputs human-readable objects specifying exactly which NFT IDs and contract addresses are involved in the trade. This feature, combined with the two-transaction execution flow, allows both traders to double-check that everything looks good on-chain after the SwapProposal has been created but before it has expired or been executed. If the left user spots a mistake, they can call the deleteProposal method on their SwapCollection. If the right user perhaps spots an intentionally misleading contract address for an inauthentic NFT collection, they can simply not execute the trade.
Finally, while the executeProposal method on any user’s SwapCollection is technically public, each SwapProposal only accepts token capabilities from the two addresses explicitly specified in the proposal. This extra safeguard adds a bit of future-proofing against any as of yet unknown Flow blockchain exploits that might try to cheat one user out of their tokens.
Third-party integration
While evaluate’s Swap client application is a robust product with a sleek user interface, we encourage others to build on top of our smart contract to add other useful features and contribute to the Flow NFT ecosystem. We designed the application with live, randomly matched peer-to-peer trading in mind (like chatroulette without the nudity), but there are plenty of other potential uses of the Swap contract, such as NFT gifting, in-game trading, or NFT crafting (exchanging NFTs that represent components for a NFT item).
To see how our client app uses the contract, see this proposal creation transaction and this proposal execution transaction.
Beyond the built-in functions already outlined above, the contract provides two events to allow for analytics and real-time monitoring. The ProposalCreated event is emitted at the end of the SwapProposal initialization function, and the ProposalExecuted function is emitted as the final step of the execute function of each SwapProposal. Just like the unavoidable security checks, these events are baked into the internal functions of the SwapProposal object, so they will always be emitted even if the trade occurs through third-party code.
Thank you for reading! Please reach out with any feedback or if you have a Swap integration we might be able to showcase. We look forward to seeing what you build!