Safer Smart Contracts (in Solidity) via Micro-Patterns - 1
It’s very hard to program smart contracts correctly: The virtual machine they run on is powerful yet odd in many ways, and the programming languages are new and focus on power rather than safety. Failure to code correctly in all respects can cost you or your company boatloads of actual real world money as smart contracts are high-value targets and attackers are extremely motivated.
This is the first post of a short series to discuss finding safe programming micro-patterns for Solidity coding to (hopefully) create more correct - and thus, less exploitable, smart contracts. Two methods of doing this via (partially) automated tools will be described in the sequel.
précis:
- part 1 ☜ you are here
- Why is it hard to program smart contracts correctly?
- Any examples of common coding problems leading to exploits?
- part 2 - to be written
- Research into micro-patterns in actual Solidity code, with a catalog of 18 such micro-patterns found, and measurements of adoption, Micro-Patterns in Solidity Code (Ruschioni, Shuttleworth, Neykova, Re, Destefanis) (EASE 2025)
- part 3 - to be written
- A proposal to use the Starlasu system, developed by Strumenta, to build a catalog of micro-patterns in use by Solidity-language smart contracts, and then present them to programmers in a visualization browser that identifies where the micro-patterns, in their code, are used or missing.
But first - this post will describe the conditions in which smart contracts operate, and a couple of pretty common flaws - out of many! - that bite you in that environment.
Solidity - one of the high level languages for programming smart contracts in, and the one most commonly used - has the following characteristics:
- Fully Turing complete,
- High-level over an unusual virtual machine (considering typical modern architectures):
- Stack machine over a 256-bit wide storage with 256-bit wide addresses
- That storage can only be accessed by 256-bit wide load/store instructions so packing data structures is not optional, it’s required
- Each virtual machine operation has a specific cost, charged to the user
- And accessing memory can be relatively expensive to relatively very expensive
- Completely deterministic operation
- Stack machine over a 256-bit wide storage with 256-bit wide addresses
- Operates on blockchains which means the code is immutable and forever,
- Data held by the contract is mutable and can only ever be accessed by the contract,
- Everything done by the contract is visible (on the blockchain): inputs and outputs. The contract’s bytecode is visible to everyone, so is its state. (Most frequently the contract’s source code is also available.) Everything about the contract and its operations can be analyzed by anyone at their leisure,
- The expected way things operate is that contracts - holding persistent state - are not only called by other contracts (of course) frequently make callbacks to them - and those other contracts are owned by (and written by!) other entities, and thus may be malicious.
Put these together with major fact about how smart contracts are used:
- They handle a lot of actual real-world money and/or actual real-world assets (e.g., real estate, concert tickets, shares in utility company operations),
- To a first (and second) approximation anyone can interact with the contracts in any way at all, while remaining anonymous.
And you have a situation not just ready for hackers to manipulate it, but in some ways that encourages hackers to manipulate it.
And boy, do they ever1.
There are lots of ways exploitable flaws can be programmed into smart contracts, including your typical edge cases and off-by-ones, plus plenty that are specific to common smart contract use cases. But here I’ll highlight two particular issues which show up frequently in reports of major hacks that cause loss of much money: Failure to protect against contract reentrancy, and the classic arithmetic overflow.
-
Smart contracts can call each other - that’s the way anything really gets done. And frequently it is a conversation that takes place between two contracts; that is, a called contract calls back to the calling contract. For example, a common, and potentially lucrative, DeFi operation is to get a “flash loan” - a loan of some tokens to you that you use and return within the same transaction. Your contract calls the loaning contract and it calls back to you with the tokens you’ve borrowed so you can do what you want with them - then, when you return from the callback method it closes out the loan and finally returns back to your original call.
But in that callback routine you can do what you want - and you might call again into the loaning contract, perhaps asking for another loan. That’s a reentrant call to the loaning contract. And if, as happens with alarming frequency, the loaning contract was not programmed to protect against such a reentrant call - it will be executing against its own modified state - the state that reflects that a loan is already in progress. And that can be (sometimes) exploited. E.g., in the “Fei Rari” hack (2022) the attacker got a flash loan and within a callback from the loaning contract called back into it and withdrew the collateral it had supplied to get the loan. Now it had hold of loaned tokens without collateral and was able to leverage that. (Other bugs also were involved).
In Solidity you need explicit code, and coding practices, to protect against reentrancy. There are several acceptable ways to do it, but no standard way (as yet).
-
You might think that with 256-bit arithmetic you would never have to worry about integer overflow, or underflow, but you would be wrong. Attackers can deliberately craft transactions with very very large positive or negative values, creating overflows that lead to contracts making incorrect decisions, and incorrect transfers of value.
On the Ethereum blockchain, the “BeautyChain” contract (with its “Beauty Ecosystem Coin”,
BEC) was exploited (CVE here) by passing in, to a specific public method designed to do multiple transfers in one call, a “value” argument of0x8000...0000, that is, the largest 64-bit signed negative value. There it was multiplied by two - overflowing to result in0. Which then passed various validity checks. A classic overflow bug, leading to invalid transfers of a large amount of coin. Full description is in this excellent post “New batchOverflow Bug in Multiple ERC20 Smart Contracts (CVE-2018–10299)” which points out that the same flaw was found “more than a dozen” similar contracts on Ethereum. (Possibly due to programmer’s reusing “proven tested” code from someone else’s contract or contract library.)
So how can smart contracts be made safer, so that you could actually feel confident entrusted your money to one? That’s the subject of the next posts …
-
https://blog.solidityscan.com/ is an excellent blog for this sort of thing, but only one of many sources of such issues. See also rekt, web3 is going just great, Web3Sec, and plenty of others … ↩︎