Macerus NFT Security Overview
Transparency is a key part of the Macerus project, and the system architecture is no different. Through a comprehensive review of the system and the security measures in place, we hope to show that the Macerus team has made their best-effort to protect the assets of our users. As always, feedback and questions are welcome — please reach out if you have any concerns! Before diving into the blockchain-specific pieces and workflows, let’s start with a high-level overview of the system in general.
There are a number of pieces here, most of which aren’t unlike a typical web application. The main difference is in the blockchain components, which we are using in place of a traditional back-end with data storage, along with InterPlanetary File System (IPFS).
app.macerus.com: This is the main Macerus application, which is built with React and Typescript. Users are able to purchase/open loot boxes, view their items, go on adventures, and level up their character — all from this interface. These actions are possible by leveraging the Ethers library to call on-chain functions, and signing them via integration with MetaMask.
api.macerus.com: The Macerus API is an ASP.NET Core application running in AWS. This application exposes an API that allows loot to be generated based on information stored in the on-chain contracts; generated loot data is stored in IPFS. The loot generation engine is built on top of a system that has been developed over several years and includes item enchantments, socketing, crafting, as well as many other robust RPG features. We hope to incorporate more of these features in the Macerus project over time.
Blockchain/Contracts: The on-chain components of the Macerus project, written in Solidity. These contracts are deployed via Hardhat and use OpenZepplin to allow for upgradability. Administration of the upgradable contracts is transferred to a Gnosis Safe multi-signature wallet to ensure that any change to the contracts is approved by multiple members of the Macerus committee in OpenZepplin Defender. Defender is also used to execute some on-chain functions in scenarios where we want to ensure actions are approved by committee (adding a new loot box set, for example). Finally, we use ChainLink as an oracle to communicate with the off-chain loot generation server.
Content: Content management is done in good ol’ Excel! We’ve built a converter to take the content from Excel and turn it into code that the generation engine can understand. We also setup a CircleCI job so that this happens automatically when we build/deploy the server. The content library is the only part of the system that is not open-source — this was done so that we could keep some of the rarest items and lore a secret for the community to uncover. We will publish statistics on the drop rates of our loot tables and data about loot box sets and their supply is always available via the contract.
IPFS: We use IPFS (and pin via Pinata) for storing any metadata or images generated by the engine. These IPFS links are then embedded into the items, loot boxes and explorations at mint time so that minted tokens will always retain their content. The metadata is also formatted in a way that is compatible with OpenSea, which is helpful for users that may want to buy/sell their tokens on the marketplace.
When designing the system, the team had some key objectives in mind from a security perspective:
- Users should feel confident that the Macerus team cannot take their tokens, or otherwise change the contracts on-chain to devalue the tokens in user wallets (commonly referred to as ‘rug-pulling’).
- All actions against a loot box, item or exploration should be initiated from a user wallet via the integration with MetaMask. The server should never directly trigger a change to loot or loot boxes. Further, loot boxes, items and explorations should only be actionable by their rightful owner.
- Users should never open a loot box or exploration and not get items. In the event of an error, user should retain their loot boxes unless they are successfully granted the items inside.
We initially deploy our upgradable contracts by using the private key of a shared team wallet. We are not aware of a method that would allow us to complete the initial deployment with a multi-signature wallet. If you know of a better way to handle this — please let us know!
After initial deployment, we immediately transfer the administrative privileges of our contracts to a multi-signature wallet powered by Gnosis Safe. The safe is set up so that the Macerus team members act as the committee for signing off on actions taken by the safe.
- At the time of writing, we require that two-thirds of the committee approve an action before it will be taken by the safe.
- The safe is where MacerusLootbox transfers the proceeds of loot box sales. The safe is also used to purchase and fill our ChainlinkWrapper with the LINK needed to make oracle calls.
- We will update this document to include the on-chain address of the safe when the Macerus project is deployed to mainnet.
All of the Macerus contracts are upgradable, with the exception of the ChainlinkWrapper contract. This will allow the team to fix bugs and introduce new features without needing to start from scratch. We’ve integrated our safe with OpenZepplin Defender, which gives us the ability to propose changes to the contracts and have them be reviewed by the committee. If two-thirds of the committee approve the change, it will be deployed and the contract will be upgraded.
In addition to these protections, we’ve made every effort to lockdown functions on the contracts themselves. In some cases, this means setting up functions so that they may only be called by the address of the safe. This is especially helpful for actions we would not want members of the team to take unilaterally, such as making a new loot box set available for purchase. With Defender, these actions can be taken by the safe once they have appropriate sign-off.
Other contract functions are restricted such that they can only be called by specific wallets or other contracts. For example, the mint function on MacerusLoot is restricted so it may only be called by the MacerusLootbox contract (for more on this, read below!). Additionally, there are functions on both MacerusLootbox and MacerusExploration that may only be called by wallets that belong to the server. These allow-lists may only be changed with approval from the committee.
The contract and deployment code (and nearly everything else) will be made open-source, and we welcome any suggestions for improvement from the community.
Loot (the reason we’re all here)
The team behind the Macerus project loves loot. Grinding, finding, crafting, burning, hunting down that last piece of set armor — we love it all! With some of the boring stuff out of the way, we can finally jump into how loot is generated and how makes it from a loot box and back into a user wallet.
Originally intended for use in a tactical RPG, the Macerus loot generation engine is extremely robust and has been refined over a number of years. Built for that use case, it wasn’t exactly designed with the blockchain in mind. Our goal was to take that engine and build something around it so it could fit in a decentralized application.
The number one priority when designing this part of the system was to ensure that users retain their loot boxes until the generated items have landed in their wallets. Admittedly, this has been a challenging task when trying to integrate with an off-chain loot generation server (we’ll explain why below!). We also have tried to balance being transparent with keeping some secrecy around the lore and items so that players can still get the thrill of uncovering new parts of the Macerus world.
All actions described below can be taken through the Macerus app and signed with MetaMask.
Purchasing a loot box
Users purchase loot boxes by sending the correct token amount to the MacerusLootbox contract, along with the loot box set they wish to purchase a box from. We’ve been developing with low-gas chains in mind, so Polygon, Fantom, and Avalanche are top of mind — We’ll assume Polygon chain using MATIC for the purpose of this document. The contract ensures that the user has sent enough MATIC and also that there is enough supply for them to purchase a box. If these conditions are met, a loot box token is minted to the user wallet, embedding some metadata about what sort of loot should be generated when the box is opened. Loot box sets are released by the team periodically, which describe the boxes available for purchase — cost, total supply, stat requirements to open, and a description of what users might find inside!
Opening a loot box
Users can open a loot box they own by making an ‘open’ request to the MacerusLootbox contract, typically done through the Macerus app. After ensuring that the caller is the rightful owner of the loot box, and that the player meets the stat requirements for opening, the contract kicks-off the loot generation process. End-to-end:
- User initiates an ‘open’ action
- The contract then generates a “requestId”, a hash of the lootboxId and the timestamp of the current block. This requestId is then stored on-chain with some loot box specific information.
- The requestId is sent as part of a query parameter to a Chainlink oracle, which in turn sends the request off-chain to the Macerus server.
- The server then uses that requestId to query the contract directly, retrieving the loot box information for that request. If no such requestId is found, then no loot is generated.
- If it is found, the server uses the loot box information to generate loot. This loot can contain any number of items or exploration tokens, based on the drop table for the loot box being opened.
- The server then makes a call to the contract to store the resulting item and exploration metadata for that specific requestId. This metadata will be used when a user claims the generated items for their loot box.
- With the item metadata stored, the user can initiate a “Collect” action, which will look up the stored metadata based on the lootboxId and the requestId.
- This metadata is used to mint both MacerusLoot and MacerusExploration tokens to the owner’s wallet. If successfully minted, the MacerusLootbox token will then be burned. Since the item minting and loot box burning happen in the same transaction, a failure will leave the loot box in the owner’s possession.
It’s worth explicitly noting an obvious, but key security measure: only the owner of a loot box can open it or trigger any sort of loot generation to occur. Combined with the flow outlined above, this makes it impossible for another user to influence loot generation for another user. Even if an enterprising user wanted to try and open another user’s loot box for them, they would need to get the requestId just right and call the API — but the server won’t generate loot because that requestId won’t exist on chain when the server queries for it.
To eliminate the possibility that someone could inspect their loot before collecting it and then decide to sell the box, loot box and exploration tokens are not able to be transferred if there are items waiting to be claimed.
Why not just return the item metadata to the Chainlink oracle call?
Good question! We could do this, if we built our own oracle node that could handle multi-part responses. At this time, standard Chainlink callbacks are restricted to a 32-byte payload, which is simply not enough for us to return rich item metadata. This is an option for us to explore in the future, but the current design feels like a reasonable middle ground, for now.
Items and Explorations
MacerusLoot tokens are the items that users receive when opening a box. In general, these items can be burned by their owners to earn experience and level up. Levelling up allows players to spend stat points, which are important because some loot boxes require that their owners have a high strength or a high intelligence to open them!
MacerusExploration tokens are a special type of loot that a player might find when opening a box! These tokens provide opportunities for players to learn more about the world of Macerus by exploring ruins and mountains or by battling with the many strange creatures that call Macerus home. Some of these explorations will provide players with their choice of adventure, should they meet the requirements! Exploration tokens follow the same pattern outlined above for loot boxes, the server will generate loot based on a completed quest and then store the rewards on the contract for their rightful owner to claim.
When the server generates loot, it also pins the metadata and corresponding images to IPFS via Pinata. These IPFS links are part of what gets embedded into the MacerusLoot and MacerusExploration tokens at the time of minting. Additionally, this metadata is created in a way that is compatible with both the Macerus app and OpenSea, so loot still retains its value (and its awesome stats!) even if the Macerus servers are destroyed by The Collector.
Thoughts, questions or concerns about the Macerus system? We’d love to chat more about it — feel free to reach out:
- Email: MacerusRPG@gmail.com
- Twitter: https://twitter.com/MacerusRPG
We will continue to post development updates on our social channels, and soon will allow access to our wiki where you can check out the lore in more detail! For more on the systems of the Macerus project and their interactions, check out the whitepaper: here