Building on Ethereum: Part 4 — Writing Contracts
Learn more about writing smart contracts on Ethereum.
Join the DZone community and get the full member experience.Join For Free
In this series, I'm discussing the phases of a project encompassing a non-trivial set of Ethereum smart contracts and the React/Redux application that communicates with them.
The project, called In-App Pro Shop, aims to help Ethereum developers easily support in-app purchases. It was written over the last half of 2018 as a way of learning about the Ethereum development ecosystem.
This project revealed many aspects of the power and constraints of Ethereum and its programming language, Solidity. I hope to pass as much of that on to you as possible in this series.
There are plenty of ways to go about structuring the smart contracts for your Ethereum blockchain project. I'll describe how In-App Pro Shop's evolved, but the key bit of wisdom I took away from the experience is: think small.
When I began this project, I added plenty of data structures and methods that seemed like they'd eventually be useful. Honestly, I was just thinking ahead.
WRONG! Bad developer, no biscuit.
There is a very real limitation on the size of your contracts; the block gas limit. Though this amount changes slowly over time and may vary between the mainnet and testnets, just remember that a transaction will not succeed if it requires more gas than the current block gas limit. Since deploying a contract is achieved via a transaction, and the more data to be written to the blockchain, the more gas is required, it follows that there is a hard upper limit on the size of any given contract.
Your contracts will grow bit by bit, and one day, you'll go to deploy and find that you can't, due to the block gas limit. Now, all you can do is cast off ballast. Your options are to remove unused functions, structures, etc., optimize necessary ones, or move them to another contract, which is no light undertaking for a number of reasons.
So, as your project evolves, write only what you need, just when it's needed — no sooner.
Solidity has inheritable classes (called contracts) and interfaces similar to object-oriented languages, like Java. If, like me, you are (or have been) an OOP developer, you're probably already thinking about how you'll split your contracts into actors with clearly defined roles, responsibilities, and collaboration patterns, perhaps dusting off your copy of Gang of Four. If so, I hate to bust your bubble, but the unique environment of the blockchain is going to work against you here.
For one thing, communication between contracts takes more gas than method calls within a single contract. Also, every contract in your system increases the overall attack surface. Maybe you assume that contract A will be the only caller to contract B because that's the way you designed it, but hackers will hit every public method of every contract you deploy, looking for a vulnerability. So, there is a not-so-gentle pressure to keep it all in a single contract if at all possible.
Why then, you might ask, even have inheritable classes and interfaces if you're not going to create an architecture composed of actors that implement some sort of polymorphism?
Well, you will... after a fashion.
You'll use libraries like Open Zeppelin to imbue your contracts with functionality, like role-based access, or to implement the ERC-721 interface for non-fungible tokens. To take advantage of all this goodness, you will usually inherit from their contracts. In doing so, you'll be relying on the "many eyes" principle of open source. Open Zeppelin's code for these core capabilities has been reviewed, tested, and probed by a great many developers, and so, it is likely much more robust than an implementation you might develop in support of your application, which itself has a completely different goal.
But within a small application, there's very little need to create interfaces and contracts for the purpose of implementing a traditional OOP architecture. Again, size, gas, and attack surface preclude this sort of thinking.
Stratification of Concerns
Whether you're an OOP developer or not, at this point, you may be thinking you could put all your code in one contract, import and extend the utility contracts you need, and be done with it. You'd be absolutely correct.
But there's still a good argument for building a contract hierarchy. You may not be implementing a classic OOP separation of concerns, but you can still stratify them.
For the In-App Pro Shop, I placed all the data structures in a base contract then layered on contracts for handling maintenance of each part: shops, categories, items, tokens. The final contract at the tip of the chain dealt with withdrawal of shop and franchise balances. When the contracts are compiled, only that final contract needs to be deployed to the blockchain. Its bytecode contains the whole inheritance chain, including the specific Open Zeppelin contracts extended by the base.
Why is this better than putting it all in one source file?
- You still get the benefit of a single deployable contract
- Shorter files are easier to navigate and comprehend in any language
- All the data structures that the intermediate contracts use is in one place
- You can quickly isolate and test the functionality you're working on at any given moment
For much of the project, my hierarchy looked like this:
Ah, these were simpler times. You can browse the contracts at this point in their development here.
Note that Solidity contracts support multiple inheritance, so our base contract was able to extend everything we need from a utility standpoint, and that's inherited by all our other contracts. Beyond the base contract containing all the data structures, the subsequent contracts were ordered in such a way that they would inherit any system functionality they might need access to. For instance, Items rely on SKUs, SKU Types, and Shops, and so, it might need access not only to those data structures but possibly to their related methods.
Just for clarification, SKUs are descriptions of things to be sold, and Items are minted ERC-721 tokens representing instances of those SKUs. SKU Types are categories for grouping SKUs. In the front-end Shop maintenance application, it was easier and more user friendly to call SKUs "Items" and SKU Types "Categories."
The Inevitable Bifurcation Event
Even when you've followed all the above advice, if your project is ambitious enough, you'll hit that aforementioned hard ceiling and will no longer be able to deploy your contract.
It happened to me, and it was a sad, sad day.
You'll optimize. You'll toss out every line of code you can afford to lose. You'll hurriedly look up the current block gas limit on the mainnet and testnets and hope that you can bump it up on your local blockchain in your truffle.js (networks.development.gas):
But eventually, you'll have no other choice but to split your hierarchy into separate lineages, terminating in two or more deployable contracts that must talk to each other. That means they'll need interfaces describing them. And they'll be more vulnerable to hacking, so a whole new can of worms is opened there.
In the case of In-App Pro Shop, I split the contract into two lineages called
ProShopline contains the code associated with minting Items and withdrawing shop and franchise balances.
StockRoomline contains the code associated with maintaining Shops, SKU Types, and SKUs.
You can browse the revised contracts here.
Even though the bifurcation event was not a happy day, I'm still not certain that I'd advise starting out with multiple lineages. I'd say put a premium on contract size and safety. Only split when absolutely necessary — at least when you're starting out. Once you've got some experience under your belt, understand all the security concerns, and find yourself presented with a system you're certain will encompass multiple contract lineages, then by all means, do what you gotta do.
In Part 3, we went through the project dependencies, setup, and running unit tests. Today, we've additionally discussed the size and structure constraints that guide our Solidity development. Let's take a look at what's involved in writing the actual contracts.
Initially, as described above, there was only one base contract,
ProShopBase.sol, which contained data structure and variable definitions. The next contract in the line was one which implemented role-based access control,
But after the great bifurcation event, the project was left with two contract lineages and, thus, two base contracts, ProShopBase.sol and StockroomBase.sol. Since both lineages required access control features, these base contracts now extend AccessControl.sol. Aside from the Open Zeppelin contracts, this is the ultimate base contract of our system.
This contract demonstrates several key features of Solidity, so lets have a look.
Version Pragma and Inheritance
First, note the
pragma line at the top. Solidity is a fast-evolving language, and so, all contracts written in it should include this line, which defines the version of the compiler that's required. If a prior or incompatible future version of the compiler is presented this code, it will fail to compile.
Next, we import the Open Zeppelin RBAC contract, which provides us with some handy functions for implementing role-based access control. The
AccessControl contract definition then explicitly extends the RBAC contract with the
We declare a public constructor, which immediately makes use of an inherited RBAC contract feature,
msg.sender variable is always present upon execution of a contract's public methods and could represent a wallet address or the address of another smart contract. If you look back at the truffle.js file, under
networks.development.from, you'll see an Ethereum address. When you deploy the contract, that's the address that shows up in
Our constructor makes a couple of calls to the
addRole method, giving the instantiating address the roles of Franchise Owner and System Admin.
How do we make use of those roles?
Glad you asked. The Franchise and Shop Owner don't come into play until we get into some of the other contracts, but we can see the System Admin role implemented in this particular contract.
First, notice the
onlySysAdmin modifier declaration. This is essentially a function block that calls the inherited
checkRole method to find out if the invoker has the System Admin role. But after that call, there is a strange-looking statement:
_;. In order to understand what that underscore keyword does, you need to know how this modifier's code gets called in the first place.
Look at the definition for the
pause function. After the
public modifier and before the code block surrounded by curly braces, there are two further modifiers:
onlySysAdmin modifier's code isn't invoked like a regular method. Rather, it is called as a guard condition. When we call the
pause method, the Ethereum Virtual Machine (EVM) will first call the code in
whenNotPaused. The intention is fairly obvious; we only want the System Admin to be able to execute the code in this method, and then only when the contract is not already paused. We didn't have to add parentheses to the modifiers here, but that's only because there were no arguments to pass. Modifiers can accept arguments, and in that case, they look like a regular method call when used.
Now that you know how the modifier code gets called, what's up with that underscore statement? It basically means "carry on, all good here." If there's a problem, we don't want to execute that line — how do we achieve that?
The next line of code after a
require statement is only reached if the expression within the
require resolves to true. And if the modifier doesn't execute a
_; line, then the code it's guarding doesn't get executed. In the case of
onlySysAdmin, we call
checkRole, which in turn has a
require statement that fails if the address we pass doesn't have the specified role. You can also see
require used in our
whenNotPaused modifiers, which evaluate the state of the
When something happens on the blockchain, clients out in the world can be notified about it by way of events. In the
unpause methods, note the
emit statements. Above these methods, you'll see the
event declarations, which, in the case of
ContractUnpaused, take no arguments. Soon, we'll see an example of event with arguments.
Checks, Effects, Interactions
Let's jump into a slightly more complicated bit of code that deals with actual Ether. As code goes, this is rather simple, but there are plenty of things that can go wrong in a smart contract and hackers are continually uncovering more. Therefore, when handling currency, we need to be extremely careful about it and not be fooled by apparent simplicity.
In the following excerpt from the ProShop.sol contract, we're allowing the Shop Owner to withdraw their Shop's balance, and emitting an event when that happens.
When sales are made (in
ItemFactory, not shown here), the contract receives the incoming Ether in its
balance property. The onus is on us to keep track of how that accumulating value should be split between the Franchise Owner and all the Shop Owners.
For the purposes of understanding how we allow the Shop Owner to make a withdrawal, know that for each sale, the Shop's share is added to the value in the contract's
shopBalances array, at the index for the Shop's unique ID.
withdrawShopBalance method takes a Shop ID as its single argument and is declared with modifiers that identify it as being callable only from external sources (not from within the contract itself), when the contract is not paused, and only if the caller's address is the owner of the given Shop.
The first thing this method does is retrieve the value in
shopBalances for the Shop represented by
_shopId. As a side note, it is idiomatic to name Solidity method arguments using a leading underscore, to distinguish them from local or contract state variables.
Next, we need to be certain that the Shop has a balance, and that the contract actually has that amount available. If we've managed the accounting properly, the contract should always have exactly the sum of the Franchise's balance and those of all the Shops. But as a precaution, we still need to check for the expected amount being available because hackers.
Since we have the Shop balance in a local variable, we now set the value in the
shopBalances array to 0. Then, we transfer the amount to the Shop Owner's address by calling
msg.sender.transfer with the amount we retrieved from the array before zeroing it.
Finally, we emit a
ShopBalanceWithdrawn event, passing the Shop ID and the amount, so listening clients will be aware that the transaction has taken place.
Reflecting on this code, you might think we should do the transfer before zeroing the account balance. But that
msg.sender address could be another contract, which could respond by calling back into our contract before returning. It would appear that the Shop still had the same balance, and we would happily transfer it. The process would repeat until the entire contract balance was drained. This is called the "re-entrancy" vulnerability.
Consequently, we should follow the check-effects-interactions pattern whenever we might be interacting with another contract. Perform all required sanity checks first, followed by any state-changing effects, and finally any interactions with external contracts. This protects us, in this case, from re-entrancy attacks, and we can rest comfortably because if the transfer fails, the method will revert and none of the state changes will take place, nor will an event be emitted.
(Not the) Conclusion
This article ran a little long, but there is still so much more to learn about writing Ethereum smart contracts with Solidity. The main takeaways here are:
- The block gas limit puts a hard ceiling on the size of your contracts, so keep them as small as possible, implementing functionality only when it's needed, and not speculatively.
- Every contract in your system increases its attack surface, so keep it all in one contract if you can.
- Use inheritance to stratify the functionality of your contract. Each subclass (subcontract?) should be ordered such that any internal functionality it requires exists in one of its superclasses (supercontracts?).
- The compiled bytecode of the final contract at the tip of an inheritance chain contains all the inherited code, and can be deployed by itself.
- Smart contracts often deal with actual currency, so be careful. Keep abreast of the known vulnerabilities, and follow the patterns and best practices adopted by the community.
- The Open Zeppelin project offers a great set of contracts to help you implement common patterns safely, so familiarize yourself with their offerings before rebuilding the wheel in a potentially more vulnerable way.
Published at DZone with permission of Cliff Hall, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.