A Solidity authorization and permission system
If you have used Cooltopia, you will know that you only need to authorize your account once via a signed message. After that, you will no longer see any wallet popups asking for permissions. You don't have to pay for any transactions and the whole system runs on Polygon, while you are connected to Ethereum.
It all feels a little counterintuitive and confusing, but the end result is a system that allows users to freely interact with Cooltopia without interruptions or worries about spending too much on transactions.
So how did we achieve this?
Ethereum and Polygon
In designing Cooltopia we wanted to make it really simple for new NFT/crypto users to interact without the need to buy MATIC and swap chains on Metamask. Explaining how to fund a wallet with Eth is hard enough and for first-time users, it's scary.
Luckily Polygon is EVM-compatible.
Among other things, this means we can write Solidity code designed for Ethereum and it will also run on Polygon. This opens a lot of doors
Your address on Ethereum is also your address on Polygon. Of course, the tokens held on Ethereum are not mirrored on Polygon.
Think of your address as a house and that house exists in two somewhat parallel universes. Same street and town address and both houses start empty. As you buy furniture in the Ethereum universe it gets added to your Ethereum house, but not your Polygon house and vice versa.
The beauty of having a shared address and EVM compatibility is that you can sign a data packet on Ethereum and pass that signature to Polygon and it is still valid.
When you authorize on Cooltopia you are connected to Ethereum and sign the following data:
Note: Always take note of anything you sign with your wallet. Signing unexpected data could result in a wallet hack. For this reason, we make all our signature requests human-readable and not hashed data
Once the data is signed by you, the signature is passed to our Polygon contracts where we run a few security checks to ensure the data is unique and actually signed by you.
We never store any signatures :)
There are a few points to note here:
- msgHash: We rebuild the expected message to ensure the signature matches
- _isValidSignature(): The msgHash is checked against your address and the signature to verify that you have requested this connection
If you would like to learn more about signature verifications have a look here:
Verifying Solidity Signatures
Processing everything on-chain is great, but not always practical and sometimes you might want to do something a little…
Let’s look at why we require you to connect in this way.
Access Control is exactly as its name suggests. A system to control who has access to certain features and functions. You can think of it as a system to grant users things like moderator and admin permissions.
Access Control - OpenZeppelin Docs
Access control-that is, "who is allowed to do this thing"-is incredibly important in the world of smart contracts. The…
Our contracts use three main roles:
- ADMIN_ROLE: Used to make changes to game settings for the purposes of balancing and maintenance
- CONTRACT_ROLE: Refers to contracts within our system. Sometimes they need to fire off functions within other system contracts and this role allows for that
- GAME_ROLE: Used for requests coming directly from users playing the game
Possibly a little confusing without an example so let's look at how the ItemFactory plugs into the system and what happens when a user buys a Pet Chest.
Below is an ItemFactory.sol contract that connects to the SystemChecker.sol contract via the HSystemChecker.sol
Notice that ItemFactory.sol line #17 passes
systemCheckerAddress as a parameter to
HSystemChecker which it extends.
Looking at HSystemChecker.sol line #14 you can see we have connected to SystemChecker via an interface.
Note HSystemChecker.sol does not extend SystemChecker.sol. This is because we need to access the data that exists within the contract and not just the structure.
After all this connecting, ItemFactory can use the modifiers
isUser . With access to these modifiers and the ability to access the data stored in SystemChecker.sol, we are now ready to use the roles that were detailed above and let users buy Pet Chest.
Buying a Pet Chest
Before we follow the flow of buying a chest, a quick recap.
- Connected to Ethereum
- Authorized on Cooltopia
- Sat on the shop with MILK and ready to buy
- ItemFactory.sol extends HSystemChecker.sol
- HSystemChecker.sol has access to SystemChecker.sol
- ItemFactory.sol can access SystemChecker.sol data via modifiers
Time to buy
A user requests to buy a Pet Chest by interacting with the web UI. The request is passed to our system which in turn fires off the transaction on Polygon (did I mention we pay for all the gas? :))
The transaction hits the
buyBox() function and the
onlyRole(GAME_ROLE) checks that the transaction call originated from the Cool Cats’ system.
From there the function starts to run and then we hit the
_mint() function. If you examine that function you will notice that it fires off the modifier
isUser which checks that the user minting the Pet Chest is an authorized user.
If an unauthorized user tried to buy a Pet Chest, it would revert. Otherwise, all is well and the user mints a Pet Chest.
Wait, what about the burning of MILK?
This is where the power of the Cooltopia authorization comes into play. When you authorized you hopefully took the time to read the notification:
You can think of authorizing as logging into a traditional game.
While you are logged into the game and playing, the game doesn't keep asking you to give permission for the action you just asked it to perform. Instead, it does as you requested (given that you have all the appropriate requirements — gold, items, etc). Thereby helping you to stay immersed.
Nice and streamlined for the user, but how do we achieve this?
Note: Cooltopia can only update Cooltopia related tokens. Granting authorization does not allow access to any other tokens in your wallet
When a user buys a Pet Chest the function
treasury.burn() is called from within
buyBox() on the ItemFactory.sol contract. As you can see on line #28 in the above code snippet,
burn() uses the modifier
When we deployed ItemFactory.sol we granted it the
CONTRACT_ROLE which allows it to call
treasury.burn() . If you were to call
burn() from an address that didn't have the
CONTRACT_ROLE , your transaction would revert.
_milkChild.gameBurn() and this is where the users' MILK is burned.
gameBurn() can only be called by a contract with the
TREASURY_ROLE . Given to the Treasury.sol contract upon deployment.
gameBurn() is where the anti-popup magic happens.
Remember that logged-in users have already agreed that Cooltopia can update their tokens as part of the gaming experience. With this permission, we transact on the users' behalf — simple yet powerful.
This prevents the user from being shown a permissions popup every time a token needs to be interacted with.
Every token that a user earns in Cooltopia, goes straight into their wallets and if a user wishes to deauthorize (log out) of the game, they can:
In doing so, Cooltopia will have no ability to update their tokens any longer and the user still retains everything, free to sell and trade on the open market in places like Opensea.
Extending the System
Not only does this system allow for a nice user experience, it also makes contract upgrading extremely easy (with some caveats).
Contracts that produce tokens or store time-sensitive info can not be upgraded without the migration, things like the ItemFactory and MilkChild.
However, all other contracts can easily be decoupled from the system by revoking their
CONTRACT_ROLE and ties to dependants. An upgraded contract can then be slotted in.
The simple use of roles allows anyone to create a contract for Cooltopia and as long as it passes security and balancing audits (internal and public), it can be bolted on and ready to use.
The demo code snippets are taken from the live contracts and those are linked below each snippet.
I haven't explained all the features, there are a number of security features and structures that I won't detail.
As with all my dev blogs, use the code at your own risk.
Cool Cats - Cool Hub
The official resource for all things Cool Cats, Cool Pets, and the world of Cooltopia, updated regularly by the Cool…
ERC 1155 - OpenZeppelin Docs
implements the mandatory interface, as well as the optional extension IERC1155MetadataURI , by relying on the…
ERC 20 - OpenZeppelin Docs
For an overview of ERC20 tokens and a walk through on how to create a token contract read our ERC20 guide. There are a…
ERC 721 - OpenZeppelin Docs
For a walk through on how to create an ERC721 token read our ERC721 guide. The EIP specifies four interfaces…
Where to find me
I can normally be found in the Cool Cats discord channel
Or on Twitter