Pak Burn.art Contract Design Overview 🔥
Designing ASH to be secure and extendable
One Manifold’s first public projects was Pak’s Burn.art. Burn.art takes NFTs and burns them in exchange for a new fungible ERC20 token called ASH.
These smart contracts were audited by CertiK
When designing Burn.art, we split the smart contracts into two primary components:
An NFT/ERC20 Burn Controller, which is responsible for burning NFTs and issuing ASH tokens.
The Exchange Rate Contract, which controls the conversion rate for any given NFT. Since all NFT burns are gated by this contract, it also provides an elegant way to disable burning if necessary.
Below, we’ll discuss both of these components, and the design decisions behind them.
ASH.sol - The NFT/ERC20 Burn Controller
This smart contract is responsible for the logic to ingest an NFT, look up the appropriate ASH conversion rate, burn the NFT and give the new ASH tokens to the burner. When designing the NFT/ERC20 Burn Controller, we wanted to ensure that it was secure, yet extendible enough to support any NFT spec in the future. We also wanted to avoid using an upgradeable proxy, in order to solidify the underlying ERC20s as immutable.
Contract Extendability - Supporting any NFT spec
One of the key innovations of ASH.sol was the ability to specify the transfer signature per token spec, allowing it to support ERC721, ERC1155 and even CryptoPunks (although, who is really going to burn a punk).
Here’s how it works:
For any given spec, the contract owner can specify the bytes4 transfer function. So, for ERC721 it’s 0x23B872DD, and for ERC1155 it’s 0xF242432A. The only requirement is that the first two parameters of any transfer function are the from address and to address.
function setTransferFunction(string calldata spec, bytes4 transferFunction)
When an NFT is burnt for ASH, the caller specifies the spec, and passes in an array of calldata parameters it wants to use for the transfer function. In the case of ERC721’s, it is just the token id. For ERC1155’s, it is the token id, amount and bytes data.
burnToken(address tokenContract, uint256 calldata args, string calldata spec, address receiver)
When an NFT is burnt, the Controller gets the NFT conversion rate from the Rate Engine (more on that later), then it does a contract call on the NFT’s contract with the preconfigured transfer function and the parameters given.
By having a way to dynamically add function signatures and input parameters, ASH supports any NFT spec in the future, and any NFTs that came before ERC721 and ERC1155.
You might be wondering, “if you can call any contract, and any function signature, isn’t this a security vulnerability waiting to happen?” Well, there are two ways that prevent bad actors from minting infinite ASH.
1. Access control around setTransferFunction and setRateEngine
Only the contract owner (in this case Pak) has the ability to add/override token specs and change the Rate Engine. So, it would be impossible for an unauthorized party to add a malicious spec that emulated transfer behavior, or bypass the checks which exist on the Rate Engine.
2. NFT Contract Verification via the Rate Engine
Even with access control around a given spec, a bad actor could create an NFT that behaved poorly, causing an infinite mint loophole. Here is a proof of hack we performed on a beta version of a burn contract (which was written by another party). Simply put, you can’t just trust any contract’s underlying behavior simply on the basis that the implement an interface. So our solution centered around creating a gate within the contract that determined the conversion rate from an NFT to ASH.
ASHRateEngineUpgradeable.sol - The Conversion Rate Logic
This is the contract that determines how much ASH someone will receive if they burn an NFT. It is deployed behind an upgradeable proxy, so the logic can be upgraded at any time to change the conversion curve, add more NFT specs, and anything else that would impact the conversion rate.
The first implementation of the Rate Engine has two curves, one for Pak NFTs and one for all other NFTs. As mentioned above, accepting any NFT is dangerous, as a bad actor could simply deploy an ‘NFT’ smart contract that looks like a standard ERC721/1155 by implementing its interface, but behave badly in the transfer function. So this implementation takes in a whitelist of NFT contract addresses.
The rate engine also has an owner gated function which can disable the computation of the NFT to ASH rate. Since the Burn Controller is dependent on the Rate Engine to determine the amount of ASH to reward, disabling the Rate Engine also disables the ability to burn.
You might note that the getRate function takes in the total token supply in order to compute the conversion rate. This allows for any individual to map out the conversion curve, and in fact, we use it on the burn.art site to estimate the amount of ASH an individual will receive for burning an NFT. The ability to specify the token supply is not a security hole though, as when used by the Controller, the controller itself already knows the total ASH supply and is the one supplying that parameter.
ERC721/1155 Receivers - Contracts to make conversion easy
To make it easier for the Burn.art frontend and collectors to burn NFTs without needing to understand the nuances of the ASH smart contracts, we created receivers for ERC1155 and ERC721 tokens. All you need to do is send the NFT to the appropriate receiver smart contract, and it will take care of all the underlying logic to burn.
Designing a secure yet extendable smart contract requires a lot of care. Without extensive testing beforehand (our smart contract has 100% code coverage), the extensible nature of your smart contract could expose it to a hack that could destroy your economy in a second. When interacting with external contracts, you always have to consider what would happen if those contracts behave in ways outside the norm (e.g. doing a no-op in the transfer function). At Manifold, we take an adversarial viewpoint when writing test cases, and always ensure that we have full coverage at the smart contract layer. These contracts are permanent, so better be safe than sorry!