In this article, we are going to go deep into the architecture and design of the smart contracts of the LooksRare NFT marketplace. We will be cloning(and re-writing some parts for the purpose of gas-optimisation and simplification) and deploying the contracts to Goerli testnet. Our clone will be called - 🥁 - BooksRare! If you make it to the end of this article, you will know every last detail of how to build a complex NFT marketplace for production. Its going to be a long article, but I promise it will be worth the effort! Let's go!
Before we get into the nitty-gritty details, let's take a moment to look back at what LooksRare is and how it came to be-
What is LooksRare?
In the Web3.0 world, every centralised VC funded protocol has an corresponding de-centralised, community driven equivalent...just like Uniswap was vampire attacked by Sushiswap in August of '20, in January of '22, OpenSea got vampire attacked by its newly launched de-centralised competitor - an NFT marketplace called LooksRare.
LooksRare protocol's incentives were lucrative enough to get the whole NFT Youtube/Twitter-verse talking about it, generating a massive buzz and sending the trading volumes soaring within days of launch. Here is what the trading volumes looked like on LooksRare compared to OpenSea days after launch -
While the active user count on OpenSea far outnumbers that on LooksRare, the trading volume data - which is arguably what LooksRare developers optimised for - has been tilting quite consistently in favour of LooksRare to this day, although there have been allegations of wash trading that exploits the incentive system.
In summary, LooksRare NFT marketplace
- Attracted initial users from OpenSea by airdropping LOOKS tokens to them and making them trade assets on LooksRare
- Rewards its users by distributing LOOKS tokens based on trade volume
- Makes LOOKS hodlers & liquidity providers stake their tokens in exchange for rewards, locking up liquidity and driving up the token value
- Rewards the stakers by distributing transaction fees collected from trading activity based on their staked amount
Key Features
...
In this article we are going to understand how each of these features are implemented.
Let's Begin!
Architecture
Click here for high res version of the above diagram
As you can see, we have divided the set of contracts in 4 broad categories: A(Managers), B(Transfer Handlers), C(Exchange), and D(Token)
The order of deployment of these contracts is A → B → C → D
Lets understand how we have categorised these contracts
Category A: Managers - Manage currencies, trading logic and royalties
There are 3 types of managers used by the exchange - currency manager, execution manager and royalty fee manager. These are convenient 'middlemen' that allow us to change various parameters (such as allowed currencies and trading strategies) without the added risks of using upgradable contracts. This way, the BooksRare exchange remains flexible and can add or remove features if needed.
Category B: Transfer Handlers - Handle the actual transfer of tokens
These contracts handle the transferring of the tokens after a trade is executed.
Category C: Exchange - The heart of the protocol which users interact with
There is only one contract in this category - the exchange contract - which is the heart of the protocol
Category D: Token - Manages the ERC-20 token incentives for BooksRare
This set of contracts is responsible for handling the minting and distribution of tokens. It manages staking and compounding of the ERC-20 token of the protocol called BOOKS. Token rewards are given on the basis of staking (BOOKS or Uniswap LP Token) or that of traded volume. It also manages the sharing of fees charged by the protocol on the basis of stake of BOOKS token.
Now that we understand how the smart contracts are arranged, let's dive into the functionality and implementation of each contract
Smart Contract Breakdown
Every contract might have dependencies on contracts that are deployed before it. This means that we will be re-writing the contracts in the order that we will deploy them.
We are going to be using Solidity compiler 0.8.6 for BooksRare, although the LooksRare protocol originally uses 0.8.0. One big advantage is that it allows us to use the modern way of reverting transactions using error codes, which is far more gas efficient as it doesn't require us to store long strings in the contracts describing the errors.
We'll be explaining the most important storage items and functions in this breakdown.
A1. CurrencyManager.sol
Let's start with the contract that allows us to manage the currencies that are allowed to be used to trade NFTs.
Storage Items
- _whitelistedCurrencies: This is an AddressSet augmented with EnumerableSet library, which allows us to count the number of currencies and check whether our set contains a given currency (for our view functions), which is something we cannot do with a usual Solidity array of addresses
State Altering Functions
- addCurrency
- Add currency to _whitelistedCurrencies
- Can be called only by owner
- Calling frequency is low
- removeCurrency
- Remove currency from _whitelistedCurrencies
- Can be called only by owner
- Calling frequency is low
A2. ExecutionManager.sol
This script is going to manage our 'strategies' for trades executed on the exchange. A 'strategy' is our abstraction for different ways a token or project can be traded. For e.g. sell order matched with a buy order on a single token can be called a 'standard' execution strategy. We can add more such strategies like a Dutch or English auction, and the execution of these strategies is managed by the implementation smart contract. The manager manages the strategies that are valid for our protocol.
Storage Items
- _whitelistedStrategies: Similar to the currency manager, this is an AddressSet augmented with EnumerableSet library, which allows us to count the number of strategies and check whether our set contains a given strategy
State Altering Functions
- addStrategy
- Add strategy to _whitelistedStrategies
- Can be called only by owner
- Calling frequency is low
- removeStrategy
- Remove strategy from _whitelistedStrategies
- Can be called only by owner
- Calling frequency is low
L1. (Library) OrderTypes.sol
This library creates the structs for the orders which comprise the core of our protocol. A 'maker' is an account that creates an order, while a 'taker' is an account that accepts the order on the makers terms for an order to be completed. A 'bid' is an order where the taker is the one who owns an NFT and the maker intends to buy it, whereas an 'ask' is an order where the maker originally owns an NFT and asks for a price from the taker.
Functions
- hash: Takes a maker order, concatenates it(in two parts, to avoid the 'stack too deep' error when a function has more than 12 arguments) and hashes it to store in the contract that consumes this library
A3. StrategyStandardSaleForFixedPrice.sol
A strategy that matches a maker and taker order pair at a fixed price. It has no state altering functions, only view functions that return whether or not a given pair of maker and taker orders can be matched
View Functions Used by Other Contracts
- canExecuteTakerBid
- Tells whether a given pair of taker and maker orders can be successfully matched
- Called by the Exchange Contract
- Calling frequency is high, since this function is the backbone of the protocol functionality
- canExecuteTakerAsk
- Same as above except for when the maker order is a bid
A4. StrategyAnyItemFromCollectionForFixedPrice.sol
A strategy with which a taker can purchase any item for a given collection for a fixed bid price. Maker ask orders are not possible with this strategy(since normally the maker would not own every item in a collection). Again, it has no state altering functions, only view functions that return whether or not a given pair of maker and taker orders can be matched
View Functions Used by Other Contracts
-
canExecuteTakerAsk
- Tells whether a given pair of taker and maker orders can be successfully matched
- Called by the Exchange Contract
- Calling frequency is high, since this function is the backbone of the protocol functionality
Now, since we are writing the same functions multiple times in each strategy contract, let's abstract out the strategy interface (and use it to interact with any strategy implementation in the exchange contract)
Interface 1. IExecutionStrategy.sol
Functions
- canExecuteTakerAsk
- canExecuteTakerBid
- viewProtocolFee
A5. StrategyPrivateSale.sol
Using the same interface above, lets make one final strategy...this is for when the maker intends to sell his NFT to a particular buyer only, this buyers address is passed in the 'params' field of the order struct. We do a abi.decode on the params field to recover the intended buyer's address. On the JS side(front end or testing), we can use the ethers library's defaultAbiEncoder to encode the address with the function ethers.utils.defaultAbiCoder.encode()
A6. RoyaltyFeeRegistry.sol
This contract maintains a registry for royalties specifically set for BooksRare exchange by the owner of a ERC 721/1155 contract. If this amount is set by the owner, then ERC2981 royalty amount is ignored, since it shows direct and specific intent. There is a limit on how high the royalties can be (we'll set it at 9.5%).
Storage items
- Map of FeeInfo: A map of a struct that contains setter, receiver and fee amount for each collection
State Altering Functions
- updateRoyaltyFeeLimit
- Updates the limit
- Called only by owner, very rarely since we dont expect to change the limit too often
- updateRoyaltyInfoForCollection
- Updates the royalty info for a particular collection
- Called only by the owner, expected to be called rarely ... You might be wondering, why should the owner have the ability to change the royalty of a random collection? You're right, he won't, actually we'll transfer the ownership of the contract to RoyaltyFeeSetter.sol (which we will write next), which will serve as the entry point to access RoyaltyFeeRegistry contract.
View Functions used by other contracts
- royaltyInfo
- Gets the amount of royalty to be paid out and the receiver on a particular collection, will be used when royalties will be calculated on transfer of funds
- Will be called by RoyaltyFeeManager with a high frequency since it forms the core of the protocol
A7. RoyaltyFeeSetter.sol
This contract allows us to change the royalty info for any collection. It can be done by 3 entities: owner of a collection, setter of a collection (from the FeeInfo struct in RoyaltyFeeRegistry.sol) and the owner of this contract. The order of preference is as follows: contract owner → collection setter → collection owner (only if the contract is not ERC2981 - explicit royalties). This is because if a setter is set explicitly in the registry, it shows direct intent of setting the manipulator of royalties for the collection.
State Altering Functions
- updateRoyaltyInfoForCollection
- updateRoyaltyInfoForCollectionIfSetter
- updateRoyaltyInfoForCollectionIfOwner
- Cannot run if the contract supports ERC2981. Otherwise the 2981 royalty takes precedent
View functions
- checkForCollectionSetter
- Checks if the collection has a setter
- Logic: Returns setter if explicitly set → else checks for 2981 support, if so then returns 0 address → else checks if the contract has an owner, if so then returns owner → else returns 0 address
A8. RoyaltyFeeManager.sol
Interfaces with the registry to get the royalty information for the Exchange contract(that we will write in a short while). Exchange can use registry directly but Managers are our category of contracts that talk to the exchange, all other complexity is hidden behind the managers - hence the RoyaltyFeeManager.
View Function
- calculateRoyaltyFeeAndGetRecipient
- Asks Registry if the collection has a royalty set. If not checks if contract supports 2981 royalties and returns the amount and receiver of royalty
[🚧 This article is a work in progress 🚧]
Top comments (0)