Build in Public | How our NFT Smart Contracts Work

For the Swoops project, we knew that as well as conforming to the 721 standard, we needed a clear structural vision for our contracts. We needed our contracts to be open for extension as new requirements arise, while keeping them as simple as possible for readability and maintainability. One subject we knew would have evolving requirements was how players would be acquiring tokens (players). For example, leading up to the launch of the game we knew that we wanted the ability to do pre-release drops, but the logic that dictates these drops can and should be managed separately from the NFT standard.

Mission number one for Swoops, and really any NFT project is to prevent changes to the deployed 721. As project owners, we put our work into the world and point to the address that acts as the golden source for ownership. Therefore, it is your job as the project owner to rally your token holders to the contract address that holders can trust as the authority. If changes need to be made to a 721 and a new version needs to be published, you need a way of communicating to the current token owners that a new contract you are publishing is the one that gives their tokens utility, and a migration plan needs to be executed.

Separation of Concerns

We decided to lean heavily on separation of concerns and treat the 721 as a building block. If you give the 721 no responsibilities outside of the 721 standard (transfer tokens and verify owners), you drastically reduce the probability that it will require any changes. We just have to create simple mechanisms to allow the 721 to be used as a building block.

Since the 721’s responsibilities have been covered, we can now form another contract to handle the concerns of our business logic. In Swoops contracts today, business logic pertains to the flows surrounding how people mint- for example, managing and verifying against an allowlist. The “minting contract” as we call it, performs all the checks to be sure the current user can mint, then calls the 721 to perform the actual minting.

The 721’s ownership structure is extended from OpenZeppelin’s Ownable. We added an additional state variable to store a second address, _provisionedContract, that is also allowed to invoke functions on the 721- in our case, the mint function.

/// @notice Modifier. Throws if the caller is not the contract owner or
///         the provisioned calling contract.
modifier onlyOwnerOrProvisionedContract() {
    require(
        msg.sender == _provisionedContract || msg.sender == owner(),
        "Not provisioned contract or owner"
    );
    _;
}

function safeMint(address to) external onlyOwnerOrProvisionedContract {
    …
}

Now we can provision any contract we like to have the ability to call functions on the 721 marked with our new modifier. Now our 721 can serve as a building block

If the business logic contract that was previously provisioned needs to change, this isn’t a big deal at all. We only need to protect the 721 contract’s address- the contracts that sit in front of it are a different story. Usually, business logic contracts are free to change since it will be just your app’s frontend pointing to them. You can always redeploy your app and use the new address instead of the old. Through setters on the 721, you can set the new contract address for the business logic contract, and have a brand new flow.

Upgradeability

The question of going with upgradeable contracts is also valid. At Swoops, we decided against going upgradeable for our contracts for the time being. Going fully upgradeable at this stage didn’t make sense for us because aside from the 721, the addresses of our contracts don’t need to be fixed. But the 721 has been given so little responsibility, it’s extremely unlikely to change- so we’d be taking on a complexity burden as an insurance policy that we didn’t feel was worth it. With community buy-in, a later migration to something upgradeable could be on the table, so we felt good about deferring upgradeability for the time being in favour of keeping things as simple as possible.

Keeping things simple is tried and true. There is plenty of room to take more sophisticated approaches, and down the road we may find we need to. But in the meantime by keeping our initial iteration as simple as possible and investing SOLID principles, we were able to move incredibly quickly while leaving ourselves room to adapt to new use cases.

Subscribe to Swoops

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe