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.
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.
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.
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:
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.
Pros:
Cons:
A large contract can be split to multiple libraries, and then use a single contract to call those libraries.
Pros:
Cons:
57Blocks is a blockchain innovation lab helping companies turn blockchain ideas into reality. If your company is working with smart contracts and want feedback or help, give us a shout at [email protected]
This article was written by Roy Xie, engineering lead at 57Blocks