Sooner or later, most engineering teams will run into similar issues when trying to build complex and interactive smart contracts. At 57Blocks, we’ve built multiple products for the crypto space and wanted to our learnings to help the overall ecosystem flourish. Below, we cover Ethereum smart contract programming patterns and public resources that have helped us develop our products. If you’re an old hand, perhaps you’ll find our discussion of novel application of the Proxy Delegate pattern useful.
Patterns are architecture design and programming best practices. Understanding patterns not only helps us understand how to deal with a problem but also why we need to do it in a given way and what problem the pattern is designed to deal with. It helps us to better understand the platform we are working on.
The following patterns described on https://fravoll.github.io/solidity-patterns/ cover most of the cases in day to day development.
- Guard Check: Ensure that the behavior of a smart contract and its input parameters are as expected.
- State Machine: Enable a contract to go through different states with different corresponding functionality exposed.
- Oracle: Gain access to data stored outside of the blockchain.
- Randomness: Generate a random number of a predefined interval in the deterministic environment of a blockchain.
- Access Restriction: Restrict the access to contract functionality according to suitable criteria.
- Checks Effects Interactions: Reduce the attack surface for malicious contracts trying to hijack control flow after an external call.
- Secure Ether Transfer: Secure transfer of ether from a contract to another address.
- Pull over Push: Shift the risk associated with transferring ether to the user.
- Emergency Stop: Add an option to disable critical contract functionality in case of an emergency.
- Proxy Delegate: Introduce the possibility to upgrade smart contracts without breaking any dependencies.
- Eternal Storage: Keep contract storage after a smart contract upgrade.
- String Equality Comparison: Check for the equality of two provided strings in a way that minimizes average gas consumption for a large number of different inputs.
- Tight Variable Packing: Optimize gas consumption when storing or loading statically-sized variables.
- Memory Array Building: Aggregate and retrieve data from contract storage in a gas efficient way.
Using Proxy Delegates to Update Smart Contracts
The Proxy Delegate pattern is designed to solve smart contract upgradeability issue. However we found another usage of this pattern while developing Tokenpad. Using this pattern, we were able to save gas when many copies of the same contracts need to be deployed on the blockchain.
Tokenpad is a platform syndicating ICO investments. Syndicate leads can create pools to collect funds for ICOs. A lead deploys a smart contract each time he or she wants to create a syndicate pool, and the smart contract contains all code for the pool to work. In the first version of Tokenpad, the cost to create a pool was around 4 million gas, which at a gas price of 5 gwei would be 0.02 ETH. This is the cost after we have moved all code to libraries and the smart contract the leader deploys only delegate the calls to the library. It is still high considering the gas price can go up to 20 gwei, 30 gwei, or even more.
With the Proxy Delegate pattern, we instead deploy a single instance of the syndicate pool smart contract to the blockchain together with the other stuff we need to deploy to support the product. When a leader wants to create a syndicate pool, we create an instance of proxy and point it to the only instance we deployed already.
Using the Proxy Delegate pattern reduced our gas costs by over 60% to around 1.5 million!
Understanding these patterns are a good start to developing efficient and secure smart contracts, but we don’t need to start from scratch to implement all those patterns, we can leverage frameworks and libraries to help us accelerate development and also make fewer errors.
2. Frameworks & Libraries
Framework and libraries are well tested and reusable implementations of known patterns and best practices. In the section we introduce some frameworks and libraries we used in our project and what patterns they provide and the problems they may have not solved.
OpenZeppelin is a library for secure smart contract development. It provides implementations of standards like ERC20 and ERC721 which you can deploy as-is or extend to suit your needs, as well as Solidity components to build custom contracts and more complex decentralized systems. Checkout the Github page: https://github.com/OpenZeppelin/openzeppelin-solidity
OpenZepplin provides a few contracts/libraries that solve common problems. The following are some most common and useful features the framework provides used in our project.
math/SafeMath.sol — Protect from overflow and underflow. Never use default arithmetic operators. Arithmetic operators do not fail when there is overflow or underflow, which may be exploitable.
ownership/Ownable — Save the contract creator address as owner, and check if a calling address is owner or not with modifier onlyOwner. The ownership can be transferred to other addresses. This is an application of the Access Restriction Pattern.
Ownable implementation has a critical design flaw we need to be aware of: when we call transferOwnership to transfer the ownership of the contract to another address, if that address is not a valid address or is not an address with known private key, we lose control of the contract forever. A better way to handle the ownership transfer is not to transfer the ownership immediately when transferOwnership finishes, but allow current owner to remain owner until the new owner claims ownership. This way, we can make sure the contract is always owned by a valid address and avoid transferring ownership to an address that is out of our control.
lifecycle/Pausable — Provides an emergency stop mechanism. Stops the contract from running in emergency situations to reduce damages. This is an application of the Emergency Stop Pattern.
If we found a bug in the production smart contract, we can pause those contracts to avoid a third-party exploitation of that critical issue before we can fix them.
access/rbac/RBAC — Role based access control. An application of the Access Restriction Pattern. RBAC is a more flexible way to manage access controls than Ownable. We can have multiple admins, whereas Ownable only allows a single owner to have control over the contract.
token/ERC20 — The files here provide a set of contracts implementing common ERC20 token features. We can easily build an ERC20 token with these base contracts by using multi-inheritance.
zos-lib provides a library to develop, deploy and operate upgradeable smart contracts on Ethereum and every other EVM and eWASM-powered blockchain. Checkout the github page: https://github.com/zeppelinos/zos
upgradeability/Proxy — Proxy implementation. Delegate a call to another contract with low level delegatecall.
This contract implements the Proxy Delegate pattern and fixed a critical problem the pattern does not solve. In the proxy delegate pattern article, there are 2 storages variable declared in the proxy contract which can easily be overwritten by the storage variables defined by the contracts called using delegatecall, resulting in data corruption. This implementation uses a technique called unstructured storage to avoid storage variables been overwritten accidentally.
migrations/Initializable — Provide initialization besides constructor. Used in conjunction with Proxy to initialize the contract since constructors are not called in this case.
3. Choosing Between Contracts and Libraries
The Ethereum blockchain has a block gas limit, if a contract’s size is too large, the gas required to deploy the contract can exceed that limit, causing the contract deployment to fail. To successfully deploy large and complex contracts, we need to split the contract and deploy with multiple transactions. There are 2 ways to split the contract:
Split a contract into multiple contracts
A large contract can be split to multiple smaller contracts. You can then pass these contract addresses to a tertiary calling contract when deploying to wire them together.
- Contract can be small enough to be deployed
- It’s possible to update just one part of the code to upgrade the whole application or to fix bugs. This is done by deploying a new version of one of the contracts then updating the reference in the calling contract.
- The message sender will be the calling contract not the address that initiates the transaction, which can be complex to manage
Split a contract into multiple libraries
A large contract can be split to multiple libraries, and then use a single contract to call those libraries.
- Libraries can be as small as possible to be deployed
- Libraries can be easily reused across multiple contracts, and only need to be deployed once
- No message sender issue, library’s code is executed in the context of the contract
- Libraries can be upgraded individually
- Each time a new version of a library is deployed, any libraries or contracts that rely on that library will also need to be deployed again since libraries are statically linked at the time of deployment