The decentralized world is growing exponentially in terms of user adoption. Companies are building hundreds of innovative dApps every year on Ethereum to fulfill various market demands. At the core of these dApps we have EVM compatible smart contracts, primarily written in Solidity. While learning the syntax and method of solidity is not difficult, but to build a scalable, accommodative and secure contract, one has to understand proper design patterns.
To understand is to perceive patterns
- Isaiah Berlin
One of the major differences between a noob coder & a pro developer is the understanding of appropriate design patterns. These patterns are tried and tested solutions for common problems, and can become your most powerful tool.
Compared to other verticals in software development, programming robust smart contracts is a sophisticated task. The distributed state machine runs your deployed program 24/7, giving access to the entire world, this is a double-edged sword. To make sure your business logic runs in a secure, reliable & deterministic way, one has to follow the best design practices.
On a platform, where the smallest bug can cost you millions of dollars, would you dare to risk it?
Though smart contracts are easily accessible over the network by anyone, that may not be the desired objective of a program. There may be certain functions that should be utilized only by certain user groups.
Failing to properly handle that, may lead to unpredictable use-cases.
Some objectives that authorization patterns can achieve:
I’ve identified 4 types of authorization patterns.
Concept:
Before execution of a function logic, there are certain conditions the caller must fulfill. These conditions can be related to the caller’s identity, input parameters, contract state etc. The function gets executed once all the requirements are met.
In case the requirements aren’t met, the EVM handles the error by reverting the state, making no changes for the function call. If a similar restriction is needed across multiple functions, a guard check can be utilized with modifiers. Also, if you don’t want variables and functions to be available publicly, then protect their state data by opting for private visibility.
Code Sample:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title An incremental time-bound donation receiver
*/
contract AccessRestriction {
/**
* @dev public variables in global, visible to everyone
*/
address public treasury = msg.sender;
uint256 public creationTime = block.timestamp;
uint256 public minimumDonation;
/**
* @dev private visibility of winner address
*/
address private winner;
/**********Modifier Blocks********/
/**
* @dev check if donation period has started
*/
modifier onlyBefore(uint256 _time) {
require(block.timestamp < _time);
_;
}
/**
* @dev check if donation period has ended
*/
modifier onlyAfter(uint256 _time) {
require(block.timestamp > _time);
_;
}
modifier isHigherDonation() {
require(msg.value > minimumDonation, "Please send higher amount");
winner = msg.sender;
minimumDonation = msg.value;
_;
}
function sendDonation()
external
payable
onlyBefore(creationTime + 1 weeks)
isHigherDonation
{
payable(treasury).transfer(msg.value);
}
function revealHighestDonor()
external
view
onlyAfter(creationTime + 1 weeks)
returns (address)
{
return winner;
}
}
Concept:
There are situations when a transaction or function call might require the acknowledgement of multiple users. So if the pool of authorizers is of size X and we require a subset of Y no. of authorizations(where X≥Y), to execute the transaction. This is a useful approach where multisignature based payment transactions are involved.The challenge in this approach is that the members in the authorization group have to be predetermined and the minimum number of authorizers must be available during the time of execution for signing. Also in case of a key is compromised/lost, that authorizers account becomes useless.
Code Sample:
A simple implementation for multi authorization that I found interesting was written by Christian Lundkvist. Checkout his SimpleMultisig implementation here.
Relatively most secure, audited reference and implementation can be found in the Gnosis safe repo.
It’s advisable not to use any multi-auth code without audit for such pattern implementations.
Concept:
A deployed contract can have dedicated role-based operation & access, like in any traditional system. The roles allocation and administration can be done by enforcing ownership & role based access control rules. Role based access can help design feature specific modifiers that allow the dedicated group only.
Code Sample:
The standard implementation for this pattern is available in the OpenZeppelin library.
Concept:
Unlike pre-authorized groups and role-based members, there can be situations where the authorization party is unknown for a given situation. Dynamic binding of an address isn't done by default, they need to be authorized in the first transaction/contract deployment, as typically the access control lists are predefined.
For such a situation, we can generate an off-chain secret from the client side for the dedicated function purpose. This secret will be hashed(SHA-256) and registered on the contract. An operational binding will be made against the secret, so that any user who sends the transaction along with the secret as parameter, will be authorized. These secrets can be shared with a standard key-exchange protocol with the intended addresses.
Hashlocks are useful for payment state channels, atomic swaps escrows. Also, the secrets are best for one transaction as verification on-chain will have it revealed.
Code Sample:
This is a simple secret registration that can be used along with any hashlocks. The secret is passed down to the contract for revelation and reference. For time lock events, the revelation block and expiration block can be compared.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;
contract SecretRegistry {
mapping(bytes32 => uint256) private secretToBlock;
event SecretRevealed(bytes32 indexed secrethash, bytes32 secret);
/**
* @dev Register the secret that's been used for validation
*/
function registerSecret(bytes32 secret) public returns (bool) {
bytes32 secrethash = sha256(abi.encodePacked(secret));
if (secretToBlock[secrethash] > 0) {
return false;
}
secretToBlock[secrethash] = block.number;
emit SecretRevealed(secrethash, secret);
return true;
}
function getRevealedSecretBlockHeight(bytes32 secrethash) public view returns (uint256) {
return secretToBlock[secrethash];
}
}
On-chain authorization through roles and ownership management has become a necessity for contract-based asset management. Also, be extremely careful with off-chain secrets and the way you share them. Try using one of these patterns in your next dApp.
You can find the sample code snippets in this repo.
Oh look, Obi-wan has something to say
In the next part of this series, I’ll be talking about “Control Patterns”.
References Used: