The rise of smart contract—autonomous applications running on blockchains—has led to a growing number of threats, necessitating sophisticated program analysis. However, smart contracts, which transact valuable tokens and cryptocurrencies, are compiled to very low-level bytecode. This bytecode is the ultimate semantics and means of enforcement of the contract.
We present the Gigahorse toolchain. At its core is a reverse compiler (i.e., a decompiler) that decompiles smart contracts from Ethereum Virtual Machine (EVM) bytecode into a high- level 3-address code representation.
In a few hours, an attacker will claim the prize for the first Consensys Diligence Ethereum hacking challenge. Here’s how they’ll do it, why nobody else can perform the same attack (any longer), and why the attacker has to wait…
The challenge consisted of a smart contract submitted to the mainnet, without sources. The contract is meant to be decoded, attacked, and drained of its minimal funds. The draining account will then get an off-contract bounty.
At this point in time, an attacker has not just entered the house but also locked the door behind them, so nobody else can enter. (Which is also why we stopped looking into the challenge and are are instead writing this text.) But, interestingly, the attacker has to wait until the Constantinople rollout enables the CREATE2 opcode, for the second step of the attack to take place!
As it turns out, the challenge requires solving two sub-problems: first, gaining ownership of the contract, in order to enable a delegatecallto a contract that the attacker controls, and, second, circumventing checks over the bytecode of the contract getting delegatecall-ed: the contract cannot contain instructions create, call, callcode, delegatecall, staticcall, selfdestruct. Let’s look at both sub-problems in detail, and see how they are solved.
Challenge Problem 1
In the decompiled code, one can notice that there are two arrays, with guessed names array_0 and owners. The latter is used to check whether the caller has the required privileges to perform the final part of the attack. Although there are no setters for owners, one can still pollute the data stored in it, as all arrays are stored in the same address space. The length of the first array in the deployed contract was set to maxint: a size that allows overflow, so that an attacker can write anywhere in storage.
Per standard convention for (dynamic) storage arrays, their lengths are stored in storage locations 0 and 1, while their contents are stored at storage locations keccak256(0) and keccak256(1), respectively. One can therefore compute the offset of the contents of owners and of the length of owners (as well as that of array_0) relative to the start of the contents of array_0, as can be seen in the following “attacker’s” code:
Since the challenge contract allows overflow of the array_0 contents area, these offsets let us write into owners, and also change the length of owners. In fact, the attacker did not stop there! They also set the length of array_0 to 0, so that no future attacker can employ the same overflow.
function attack() public {
address attackerAddress = address(...);
address victim = address(0x68Cb...);
(uint contentOffset, uint lengthOffset0, uint lengthOffset1)
= offsets();
bool success;
bytes memory result;
// set address I control as one of the owners
victim.call(abi.encodeWithSelector(
bytes4(0x4214352d), uint(attackerAddress), contentOffset)
);
// set length of array 0 to 0 (no more out of bounds)
victim.call(abi.encodeWithSelector(
bytes4(0x4214352d), uint(0), lengthOffset0)
);
// set length of array 1 to 1 (make attacker the only owner)
victim.call(abi.encodeWithSelector(
bytes4(0x4214352d), uint(1), lengthOffset1)
);
}
The contract registered as owner (attackerAddress) can be any that the attacker controls. Now the attacker has both entered and secured the door! But the more serious challenge is still up ahead.
Challenge Problem 2
The second part of the challenge is the actual draining of the contract’s funds. This involves creating yet another attacker contract that will simply drain the contract’s balance. If one checks function 0x2918435f of the challenge contract, the code calls delegatecall on an attacker-supplied address parameter, effectively handing it full control of the account. There is a small twist to this plot however. The delegatecall is preceded by checks of all the bytecodes of the called contract, to ensure that they never match values 0xf0, 0xf1, 0xf2, 0xf4, 0xfa, or 0xff. This precludes use of instructions create, call, callcode, delegatecall, staticcall, and selfdestruct.
Currently (Feb. 27), these are the only instructions that can be used to drain a contract from its funds. In a few hours, however, a new bytecode instruction (create2) will be available and it can also move funds! Hence the attacker now only needs to pass the address to a smart contract implementing something similar to this:
contract BountyClaimer {
function() external {
uint res;
assembly {
let res := create2(balance(address), 0, 1, 0)
}
}
}
A minor challenge is that byte value 0xff arises commonly in Solidity compilation, so the attacker has to use roundabout ways to compute some values, but this is little more than a nuisance.
We would like to salute the clever attacker that will be executing this attack within the next few hours.
Trivial Exploits of Bad Randomness In Ethereum, and How To Do On-Chain Randomness (Reasonably) Well
Ethereum has been used as a platform for a variety of applications of financial interest. Several of these have a need for randomness — e.g., to implement a lottery, a competitive game, or crypto-collectibles. Unfortunately, writing a random number generator on a public blockchain is hard: computation needs to be deterministic, so that it can be replayed in a decentralized way, and all data that can serve as sources of randomness are also available to an attacker. Several exploits of bad randomness have been discussed exhaustively in the past. Next, we discuss near-trivial exploits of bad randomness, as well as ways to obtain true randomness in Ethereum.
We begin by showing how easy it often is to exploit bad randomness without complex machinery, such as being a miner or reproducing the attacked contract’s internal state. The key idea is to use information leaks inside a transaction to determine whether the outcome of a random trial favors the attacker: an intra-transaction information leak. This is, to our knowledge, a new flavor of attack. Even though it shares most elements of past attacks on randomness, it generalizes to more contracts and is more easily exploitable.
Before we discuss the interesting aspects of intra-transaction information leaks, a bit of background is useful.
Ethereum Randomness Practices and Threat Model
Much has been written on the topic of random number generation in Ethereum smart contracts. The Ethereum Yellow Paper itself suggests “[approximating randomness with] pseudo-random numbers by utilising data which is generally unknowable at the time of transacting. Such data might include the block’s hash, the blocks’s timestamp, and the block’s beneficiary address. In order to make it hard for malicious miners to control those values, one should use the BLOCKHASH operation in order to use hashes of the previous 256 blocks as pseudo-random numbers.”
More recent excellent advice on anti-practices and hands-on demonstrations of good practices have helped raise the bar of random number generation in smart contracts, as have several high-profile contracts (e.g., CryptoKitties — more on that later), serving as prototypes. For instance, it is now well understood that the current block number (or contents, or gas price, or gas limit, or difficulty, or timestamp, or miner address) is not a source of randomness. These quantities can be read by any other transaction within the same mined block. Even worse, they can be manipulated if the attacker is also a miner.
Ethereum miners predict the future by inventing it. Furthermore, Ethereum, the distributed “world computer”, is much slower than a physical computer. Therefore, a miner can actively choose to invent a future (i.e., mine a block) whose “random” properties will yield a favorable outcome. In one extreme case, a miner can precompute several alternative “next blocks”, pick the one that favors him/her, and then invest in making this block the next one (e.g., by dedicating more compute power to mine more subsequent blocks).
Therefore, the current understanding of the threat model to pseudo-randomness focuses on the scenario where the attacker is a miner. Thorough, well-considered discussions often recommend avoiding randomness “[that uses] a blockhash, timestamp, or other miner-defined value.” A common guideline is that “BLOCKHASH can only be safely used for a random number if the total amount of value resting on the quality of that randomness is lower than what a miner earns by mining a single block.” (As we discuss at the end, this guideline can be both too conservative and too lax. The expected value of all bets in a single block should be used instead of the “total amount of value”.)
Even though the usual threat model considers the case of a miner, most of the block-related pseudo-random properties can be exploited a lot more easily. The interesting block-related properties of the EVM are (in Solidity syntax) block.coinbase, block.difficulty, block.gaslimit, block.number, block.timestamp, and blockhash. For all these, an attacker can get the same information as the victim contract by just having a transaction in the same block. (The blockhash value is only defined for the previous 256 blocks, the rest of the quantities are only defined for the current block. In both cases, all current-block transactions receive the same values for these quantities.) In this way, an attacker can replay the randomness computation of the attacked contract before deciding whether to take a random bet. Effectively the pattern becomes:
if (replicatedVictimConditionOutcome() == favorable)
victim.tryMyLuck();
Possible? Yes. Easy? Not quite.
Although the attack just described seems trivial, in practice it requires sophistication. A typical generator of randomness in a contract is often not merely blockhash(block.number-1) or some other such block-relative quantity. Instead, a common pattern mixes a seed value with block-relative quantities — for instance:
This does not make the contract less vulnerable, in principle. There is no secret in the blockchain, so even a private _seed variable can be read. But in practice this can make the attack significantly harder. A contract with several users and intense activity will see its private seed modified often enough to be much less predictable. The attacker either needs (again) to be a miner, or needs to somehow coordinate receiving non-stale external information before the attack transaction. A very interesting illustration of both kinds of attacks (both as a miner and as a transaction with external information) shows how they are possible but not before admitting: “So much for a simple solution.”
It’s Easier to Ask For Forgiveness Than to Get Permission
Yet, there is a very simple, non-miner attack that has guaranteed success, even with fast-changing private seeds. The transactional model of Ethereum computation together with the public nature of all stored information make exploitation of bad random number generators near-trivial.
The general pattern is simple. All a contract needs to do to be vulnerable is to finalize in a single transaction (typically before the end of a public call) an outcome that possibly favors the attacker. (This outcome may be determined through any technique producing entropy, including hashing of past blocks, reading the current block number, etc.) The attacker simply executes code such as:
In other words, the attacker can choose to commit a transaction only when the outcome of a “random” trial is favorable, and abort otherwise. The only cost in the latter case is minor: the gas spent to execute the transaction. The attack works even if there is value transfer in the tryMyLuck() trial: if the transaction aborts, its effects are reverted.
In this transaction-revert-and-retry approach, the attacker turns the code of the victim contract against itself! There is no need to emulate the victim’s randomness calculation, only to check if the result is favorable. This is information that’s typically publicly accessible, or easy for the attacker to leak out of the victim (e.g., via gas computations, as we will discuss later).
Practical Examples
There are several examples of (already with past techniques) vulnerable contracts that can be attacked more easily in the way we describe. For a vivid illustration, consider the (defunct?) CryptoPuppies Dapp. CryptoPuppies attempted to build on the CryptoKitties code base and add “rarity assessments for puppies determined by the average between initial CryptoPuppy attributes (Strength + Agility + Intelligence + Speed) / 4”. The code for the contract, however, adds (to the otherwise solid CryptoKitties contract code) a bad random number generator, combining a seed and block properties (including block.blockhash(block.number-1), block.coinbase, block.difficulty). Furthermore, the result is readily queryable: anyone can read the attributes of a generated puppy. It is trivial for an attacker to try to breed a puppy with the desired attributes and to abort the transaction if the result is not favorable.
In other cases of vulnerable contracts, an attacker can determine a favorable outcome of a battle between dragons and knights, create pets only when they have desired features, set the damage inflicted by heros or monsters, win a coin toss, and more.
(All contract examples are collected via analysis queries on the bytecode of the entire contents of the blockchain and inspected in source or via our alpha-version decompiler at contract-library.com.)
Hiding State Does Little To Help
The benefit of the attack pattern that cancels the transaction based on outcome is that the outcome of an Ethereum computation is easy to ascertain. In most cases, the vulnerable contract exposes publicly the outcome of a “random” trial. Even when not (i.e., when the outcome of the trial is kept in private storage only) it is easy to have an intra-transaction information leak. Perhaps the most powerful technique for leaking information (regarding what a computation did) is by measuring the gas consumption of different execution paths. Given the widely different gas costs of distinct instructions, this technique is often a reliable way of determining randomness outcomes.
For illustration, consider a rudimentary vulnerable contract:
The contract performs an extra store in the case of a winning outcome. The attacker can trivially exploit this to leak information about the outcome, before the transaction even completes:
contract Attacker {
function test() public payable {
Victim v = Victim(address(<address of victim>));
v.draw.value(msg.value)(block.number); // or any guess
require (gasleft() < 253000); // or any number that will
// distinguish an extra store
// relative to the original gas
}
}
So, What Can One Do? The Blockhash Minus-256 Problem
We saw some of the pitfalls of bad randomness on Ethereum, but what can one do to produce truly random numbers? A standard recommendation is to go off-chain and employ external sources. These are typically either an outside “oracle” service (e.g., Oraclize), or hashed inputs by multiple users with competitive interests. Both solutions have their drawbacks: the former relies on external trust, while the latter is only applicable in specific usage scenarios and may require as much care as designing nearly any cryptographic protocol. Furthermore, the issue with randomness on Ethereum is not the entropy of the bits — after all, there are excellent sources of entropy on the blockchain, yet they are predictable. Therefore, in principle, even external solutions may be vulnerable to transaction-revert-and-retry attacks, if they have not been carefully coded.
Although off-chain solutions have great merit, an interesting question is what one can do to produce random numbers entirely on-chain. There are certainly limitations to such randomness, but it is also quite possible, under strict qualifications. The best recommendation is to use the blockhash of a “future” block, i.e., a block not-yet-known at the time a bet is placed. For instance, a good protocol (formulating a random trial as a “bet”) is the following:
accept a bet, with payment, register the block number of the bet transaction
in a later transaction, compute the blockhash of the earlier-registered block number, and use it to determine the success of the bet.
The key to the approach is that the hash used for randomness is not known at bet placement time, yet cannot change on future trials. The approach still has limitations in the randomness it can yield, because of miners, who can predict the future (at a cost). We analyze these limitations in the next section, where we collect all randomness qualifications in a single place. Before that, however, we need to consider another caveat of the approach. As mentioned earlier, the blockhash function is only defined for the previous 256 blocks. (In the non-immediate future, EIP-210 aims to change this.) Therefore, if the second step of the above protocol is performed too late (>256 blocks later) or too early (in the same transaction as the first step), the result (zero) of blockhash will be known to an attacker.
Therefore, any protocol using blockhash of “future” blocks needs to integrate extra assumptions. The most practical ones seem to be:
the bettor has to not only place the bet but also invoke the contract in a future transaction (within the next 256 blocks) to determine the outcome
if the bettor is too late (or too early) the outcome should favor the contract, not a potential attacker.
Some smart contracts have attempted to circumvent the need for the second step with solutions that may be acceptable in context. A good example is the randomizer in the CryptoKitties GeneScience contract. (This contract seems to have no publicly available source code, unlike the CryptoKitties front-end contract, so we examine its decompiled intermediate-language version.) In function mixGenes, one can see code of the form:
That is, if the block number of the bet is older than 256 blocks back (i.e., blockhash returns zero) the current block number’s high bits are merged with the older block’s lower bits, possibly with 256 subtracted, so as to produce a block number within the 256 most recent, whose blockhash is taken.
Such code can be well exploited with the transaction-revert-and-retry approach. The benefit of hashing an unknown-at-betting-time block is lost, instead sampling a predictable quantity, whose outcome may vary upon a retry. However, retries will yield different values only every 256 blocks — once the high bits of the block number change. In the specific context of the application (where other players can breed the same crypto-kitty) this risk is probably acceptable.
Putting it All Together
Based on the above, let us consider an end-to-end recommendation for purely-on-chain randomness. Computing the blockhash of a “future” block is a pattern that can yield truly unknown bits to the current transaction, but is still vulnerable to miners: a miner can place a bet, then mine more than one version of the “future” block. Therefore, for safe use of blockhash, the expected value of the random trial for an attacker should be lower than the reward of mining a block: an attacker should never benefit from throwing away non-winning blocks. Note that this expected value may be much lower than the total stakes riding on the randomness. For instance, a bet awarding 1000 ETH with probability 1/1000 is still only worth 1ETH to an attacker. Such randomness could, therefore, be quite practical for many applications.
However, in computing the expected value of a random trial it is important to remember that bets are compounding. If a single block contains N bets (e.g., in N independent transactions, which could be by the same attacker), each for 1000ETH, and each with 1/1000 probability, the expected value of the block for the attacker is N ETH. This reasoning can be used to bound the maximum number of bets accepted in the same transaction. Unfortunately, a single contract cannot know what other bets are taken by other contracts’ transactions in a single block, and an attacker could well be targeting multiple contracts to compound bets. Therefore, the estimate will be either approximate, or too conservative, yielding very low expected values per bet. Even worse, a badly-coded contract can incentivize attackers to violate the randomness of an unrelated contract, at least temporarily. The attacker/miner has an incentive in exploiting the badly-coded, vulnerable contract, and an extra opportunity to also take bets against a contract that wouldn’t be profitable on its own. (The attacker may not be able to exploit the weaker contract more, e.g., because it has limits in the bets per block, but can fit in more transactions in the same block.) Still, such an attack is only valid until the badly-coded contract is depleted.
A back-of-the-envelope calculation of pessimistic values with the current block mining reward (3ETH) and block gas limit (8 million) suggests that an expected value of an individual bet at under 3.75E-7 ETH-per-unit-of-gas is safe for steady-state use, even if temporarily vulnerable (until depletion of other contracts). For instance, a transaction consuming 100,000 gas should result in bets with expected return at most 0.0375 ETH. (If the block was filled with such transactions, it would still be unprofitable for an attacker-miner to throw it away.) This is currently around 50x the gas cost of such a transaction, so the bet value is not unrealistically low for real applications. Again, this does not limit the payoff of the bet but the expected return. The successful bet could result in 1M ETH, but if this only happens with probability 1/27,000,000, the expected bet value is under 0.0375 ETH.
More generally, such reasoning motivates an interesting practice that we have not seen adopted so far: to make bets consume gas proportionately to their expected value. For instance, a bet with a high expected value, e.g., of 2 ETH, should be perfectly possible but should require gas nearly equal to the block gas limit (i.e., the caller should know to supply the gas and the bet contract should consume it via extra computation), so that virtually no other transactions can be part of the same block.
[Standard caveat: all analysis assumes an attacker is incentivized only to maximize his/her profit in ETH (or tokens) based on smart contract execution. There may be attack models not considered, although most conventional attacks (e.g., double spending through chain reorg) don’t seem to benefit from throwing away a block. However, notably, the assumption does not apply to an attacker willing to lose ETH to perpetrate an attack (e.g., in order to cause damages to the victim, or to disrupt the ecosystem in order to manipulate ETH exchange rates, or …). Such attack conditions are a topic for a different post, but much of Ethereum is vulnerable to such attacks.]
To summarize, our recommendation for on-chain random number generation is to follow a pattern such as:
Accept a bet, with payment, register the block number of the bet transaction.
The bettor has to not only place the bet but also invoke the contract in a future transaction (within the next 256 blocks). The contract will compute the blockhash of the earlier-registered block number, and use it to determine the success of the bet.
If the bettor is too late (or too early) the outcome should favor the contract, not a potential attacker.
The expected value of the random trial for all bets in a single block should be lower than the reward for mining a block. (You should convince yourself that this calculation works in your favor.)
This approach has the disadvantages of a delay until a bet outcome is revealed, of requiring a second transaction, and of placing severe limits on the expected value of the bet. It is, however, otherwise the only known quasi-acceptable technique for purely-on-chain randomness.
Ethereum is a distributed blockchain platform, serving as an ecosystem for smart contracts: full-fledged inter- communicating programs that capture the transaction logic of an account. Unlike programs in mainstream languages, a gas limit restricts the execution of an Ethereum smart contract: execution proceeds as long as gas is available. Thus, gas is a valuable resource that can be manipulated by an attacker to provoke unwanted behavior in a victim’s smart contract (e.g., wasting or blocking funds of said victim). Gas-focused vulnerabilities exploit undesired behavior when a contract (directly or through other interacting contracts) runs out of gas. Such vulnerabilities are among the hardest for programmers to protect against, as out-of-gas behavior may be uncommon in non-attack scenarios and reasoning about it is far from trivial.
In this paper, we classify and identify gas-focused vulnerabilities, and present MadMax: a static program analysis technique to automatically detect gas-focused vulnerabilities with very high confidence. Our approach combines a control-flow-analysis-based decompiler and declarative program-structure queries. The combined analysis captures high-level domain-specific concepts (such as łdynamic data structure storagež and łsafely resumable loopsž) and achieves high precision and scalability. MadMax analyzes the entirety of smart contracts in the Ethereum blockchain in just 10 hours (with decompilation timeouts in 8% of the cases) and flags contracts with a (highly volatile) monetary value of over $2.8B as vulnerable. Manual inspection of a sample of flagged contracts shows that 81% of the sampled warnings do indeed lead to vulnerabilities, which we report on in our experiment.
1 INTRODUCTION
Ethereum is a decentralized blockchain platform that can execute arbitrarily-expressive compu- tational smart contracts. Developers typically write smart contracts in a high-level language that a compiler translates into immutable low-level EVM bytecode for a persistent distributed virtual machine. Smart contracts handle transactions in Ether, a cryptocurrency with a current market
capitalization in the tens of billions of dollars. Smart contracts (as opposed to non-computational łwalletsž) hold a considerable portion of the total Ether available in circulation, which makes them ripe targets for attackers. Hence, developers and auditors have a strong incentive to make extensive use of various tools and programming techniques that minimize the risk of their contract being
attacked. Analysis and verification of smart contracts is, therefore, a high-value task, possibly more so
than in any other programming setting. The combination of monetary value and public availability makes the early detection of vulnerabilities a task of paramount importance. (Detection may occur after contract deployment. Despite the code immutability, which prevents bug fixes, discovering a vulnerability before an attacker may exploit it could enable a trusted party to move vulnerable funds to safety.)
A broad family of contract vulnerabilities concerns out-of-gas behavior. Gas is the fuel of com- putation in Ethereum. Due to the massively replicated execution platform, wasting the resources of others is prevented by charging users for running a contract. Each executed instruction costs gas, which is traded with the Ether cryptocurrency. Since a user pays gas upfront, a transaction’s computation may exceed its allotted amount of gas. As a consequence, the Ethereum Virtual Machine (EVM) raises an out-of-gas exception and aborts the transaction. A contract that does not correctly handle the possible abortion of a transaction, is at risk for a gas-focused vulnerability. Typically, a vulnerable smart contract will be blocked forever due to the incorrect handling of out-of-gas conditions: re-executing the contract’s function will fail to make progress, re-yielding out-of-gas exceptions, indefinitely. Thus, a contract is susceptible to, effectively, denial-of-service attacks, locking its balance away.
In this work, we present MadMax1: a static program analysis framework for detecting gas-focused vulnerabilities in smart contracts. MadMax is a static analysis pipeline consisting of a decompiler (from low-level EVM bytecode to a structured intermediate language) and a logic-based analysis specification producing a high-level program model. MadMax is highly efficient and effective: it analyzes the whole Ethereum blockchain in 10 hours and reports numerous vulnerable contracts holding a total value exceeding $2.8B, with high precision, as determined from a random sample.
MadMax is unique in the landscape of smart contract analyzers and verifiers. (Section 7 contains a more detailed treatment of related work.) It is an approach employing cutting-edge static program analysis techniques (e.g., data-flow analysis together with context-sensitive flow analysis and memory layout modeling for data structures), whereas past analyzers have primarily focused on symbolic execution or full-fledged verification for functional correctness. As MadMax demonstrates, static program analysis offers a unique combination of advantages: very high scalability, universal applicability, and high coverage of potential vulnerabilities.
We speculate that past approaches have not employed static analysis techniques due to three main reasons: a) the belief that the thoroughness of static analysis is unnecessary for smart contracts since they are small in size; b) the possibility that static analysis, although thorough, can yield a high number of false positivesÐfull-fledged, less automated verification techniques may be necessary; and c) the difficulty of applying static analysis techniques uniformly, at a low level: decompiling the low-level EVM bytecode into a manageable representation is a non-trivial challenge.
MadMax addresses or disproves these objections. It provides an effective decompilation substrate for analyzing low-level EVM bytecode. MadMax exhibits high precision, due to the sophisticated modeling of the gas-focused concepts it examines. Finally, our study of the Ethereum blockchain (and the subsequent application of MadMax to it) reveals that smart contracts can significantly benefit from static analysis. Figure 1 gives an early indication, by plotting smart contract size against the Ether held. We can see that relatively complex contracts (measured in the number of basic blocks) contain most of the Ether. Hence, the potential risk compounds for sophisticated smart contracts because complex contracts are harder to get right. This observation strongly supports the use of static program analysis, which scales well to relatively complex programs.
The main contributions of our work are:
● Validation: We validate the approach for all 6.3 million contracts deployed on the entire blockchain. To our knowledge, no other work in the smart contract security literature has performed program analysis on such a number of contracts. Our analysis does not require source code to run, nor external input, and at the same time is highly scalable. The analysis reports vulnerabilities for contracts holding a total value of over $2.8B. Even though it is uncertain whether most vulnerabilities are real and how easily exploitable they might be, manual inspection of a small sample reveals over 80% precision and the existence of specific issues, which we detail.
● A decompiler from EVM bytecode to structured low-level IR: We propose the use of static program analysis directly on the EVM bytecode. Analyzing EVM bytecode is challenging due to the stack-based low-level nature of the EVM with minimal control-flow structures.
● The identification of gas-focused vulnerabilities: The semantics of limited, gas-based execution on top of smart contracts handling monetary transactions introduces a new class of vulnerabilities that does not occur in other programming language paradigms. We identify out-of-gas vulnerabilities thoroughly and explain their essence.
● Abstractions for high-level data-structures and program constructs: We construct high-level abstractions for EVM bytecode for bridging the gap between the low-level EVM and the high-level vulnerabilities. We express analysis concepts that include safely resumable loops, data structures whose size increases in repeat invocations of public functions, and recognition of nested dynamic structures in low-level memory.
Have you ever wondered how secure your smart contracts are? In the Wild West of blockchain technology, ensuring their safety and reliability is paramount. Let’s explore the world of smart contract auditing and discover why it’s a game-changer for blockchain applications.
What Are Smart Contracts?
Definition and Basic Concepts
So, what’s a smart contract, anyway? Think of it as a self-executing contract in which the terms between buyer and seller are directly written into lines of code. They reside on a blockchain, ensuring transparency and immutability.
Importance in Blockchain Technology
Smart contracts are the lifeblood of decentralized applications (dApps). They automate agreements, reduce the need for intermediaries, and make transactions more efficient. But great power comes great responsibility—if not adequately secured, they can be a hacker’s playground.
The Need for Auditing Smart Contracts
Common Vulnerabilities in Smart Contracts
You might be surprised how many smart contracts have vulnerabilities lurking beneath the surface. From reentrancy attacks to integer overflows, the list of potential pitfalls is long and winding. Learn More.
Consequences of Unsecured Smart Contracts
An unsecured smart contract is like leaving your front door wide open. Hackers can exploit vulnerabilities to steal funds, manipulate data, or even shut down entire platforms. Remember the Curve Finance of 2023? It resulted in a loss of $70 million! Learn more.
Smart Contract Audit | Process
Cost and Schedule Proposal
The audit process starts with estimating the cost and timeline based on the smart contract’s complexity and scope. The assessment is aligned with the project’s deadlines and budget for a smooth process from start to finish.
Audit Commencement
After the terms are agreed upon, auditors analyze the contract thoroughly and communicate regularly with the development team for continuous feedback and adjustments to ensure optimal outcomes.
Preliminary Findings Delivery
During the audit, a preliminary report categorizes identified vulnerabilities by risk level: Critical, High, Medium, Low, or Advisory. The development team is engaged in a discussion to clarify the issues and understand the required steps for resolution.
Issue Resolution Process
After the preliminary findings are delivered, the development team fixes the identified vulnerabilities. Auditors provide guidance to ensure that the issues are correctly addressed according to the security recommendations offered.
Final Review and Report
Once the issues are resolved, auditors conduct a final review to verify that all vulnerabilities have been adequately mitigated. They then issue a comprehensive final report documenting the process, the findings, and the remediation efforts.
Smart Contract Audit | Methodology
A thorough, smart contract audit requires a blend of technical expertise and collaborative review. The process typically involves multiple senior security researchers, alongside cryptography or financial modeling specialists, to address each project’s unique complexity. Their hands-on, multi-phase approach—paired with advanced automated tools—ensures code security and optimization while considering integrations with external protocols like oracles and AMMs. Learn more.
Team Composition
A successful smart contract audit is conducted by at least two senior security researchers alongside cryptography or financial modeling specialists, carefully selected to match the complexity and nature of the smart contracts being analyzed.
Meticulous Code Review
The audit process involves a thorough, line-by-line review of the entire codebase. Both auditors thoroughly examine every contract within the audit scope, ensuring a deep understanding of the code and forming a mental model of its interactions and assumptions. This hands-on approach is critical to identifying potential vulnerabilities.
Critical Strategies in Smart Contract Auditing
Two-Phase Review Auditing:
Phase A: The auditors focus on the contract’s intended functionality and legitimate use cases, gaining a complete understanding of the contract’s expected behavior.
Phase B: The auditors adopt an adversarial mindset, actively attempting to exploit weaknesses by abusing the system’s flexibility to subvert its security assumptions.
Collaborative Challenges
The two senior auditors continuously challenge each other’s findings throughout the audit. This back-and-forth ensures no stone is left unturned. By explaining complex code elements, they push each other to uncover potential blind spots or overlooked vulnerabilities.
Multi-Level Thinking
Auditors analyze the code at the level of individual functions and consider how different parts of the system interact. This approach helps identify complex attack vectors that could arise from unexpected combinations of contract components.
Use of Advanced Tools
Automated tools also play a critical role. Projects are uploaded to automated analysis systems, including static analysis, AI-driven testing, and fuzzing tools. Auditors manually review the output from over 70 algorithms, supplemented by custom tests they create to explore possible issues further.
Gas Efficiency and Integrations
Beyond security, auditors also identify inefficiencies in gas usage and provide optimization recommendations. Additionally, we thoroughly examine external integrations with protocols like AMMs, lending platforms, and oracles to ensure they function as expected and align with their specifications.
Choosing a Smart Contract Auditor
Qualifications to Look For
Auditors possess varying levels of expertise. Look for professionals with a strong blockchain security and cryptography background and a track record of successful audits.
Questions to Ask Potential Auditors
Don’t hesitate to ask direct questions when choosing an auditor. Understanding their process and tools is essential, as is ensuring they stay updated on the latest security trends. Key questions include:
What specific projects have they audited before?
Are those projects similar in complexity or structure to yours?
For example, if your project involves a liquidity pool, selecting an auditor with extensive experience in similar environments can provide deeper insights into potential vulnerabilities. Familiarity with the same functions or libraries your contract uses allows the auditor to identify issues faster and offer more targeted recommendations for improvement.
Check References and Post-Audit Security
When selecting an auditor, it’s crucial to assess their experience and check for references and testimonials from past clients. Positive feedback from reputable projects can be a strong indicator of their reliability. Additionally, it’s wise to research whether their audited projects have maintained security post-audit. Websites like Rekt News Leaderboard provide valuable insights into projects that have been hacked after their audits. If a project repeatedly appears on these lists after an audit, it could signal issues with the thoroughness of the auditor’s work or missed vulnerabilities. Always cross-check testimonials with such resources to ensure the auditors can deliver long-term security, not just pass initial checks.
Auditing Smart Contracts | Best Practices
Provide Clear Documentation
Ensure you supply the auditors with concise but comprehensive documentation. This should include both high-level project overviews and detailed code explanations. The goal is to align the auditors’ understanding of the project’s intent with its technical implementation.
Consistent Naming and Comments
Use consistent naming conventions and comments throughout your code. Well-documented code can significantly reduce auditors’ time interpreting complex logic and help them focus on identifying vulnerabilities.
Establish a Communication Channel
Maintain an open line of communication between your team and the auditors. Whether it’s a walkthrough of your code or real-time questions during the audit, responsiveness is key to keeping the process efficient and focused.
Ensure Your Project Is Ready
Before the audit begins, compile your project without errors and thoroughly test it. This allows auditors to concentrate on complex security concerns rather than debugging fundamental functionality issues. Deploying your code on a testnet and testing it against edge cases can save valuable time.
Recognize the Scope of an Audit
Do not substitute audits for thorough testing or assume you will find all bugs. Use audits to identify security vulnerabilities, especially in adversarial environments. Functional correctness issues may not be within the auditor’s purview unless clearly communicated.
The Future of Smart Contract Auditing
Emerging Technologies
Artificial intelligence (AI) and machine learning (ML) will transform smart contract auditing by automating vulnerability detection and improving accuracy. These technologies enable advanced static analysis, pattern recognition, and anomaly detection, allowing auditors to identify potential risks more efficiently and precisely.
Regulatory Considerations
Regulatory compliance is becoming increasingly crucial in smart contract auditing as governments establish more explicit frameworks for blockchain technology. In the European Union, the Markets in Crypto-Assets Regulation (MiCA), introduced by the European Securities and Markets Authority (ESMA), is a significant step toward regulating digital assets. MiCA aims to ensure transparency, consumer protection, and market integrity across the EU. As this regulation takes effect, auditors will need to ensure that smart contracts comply with security standards and regulatory requirements like those outlined in MiCA. This includes ensuring that smart contracts meet criteria for transparency, risk management, and governance, making compliance a critical part of the auditing process.
Auditing Smart Contracts | Conclusion
Auditing smart contracts isn’t just a checkbox—it’s a necessity. As blockchain technology continues to reshape industries, ensuring the security and reliability of smart contracts will be more critical than ever. So, are your smart contracts up to the task?
Auditing Smart Contracts | FAQs
Q1: How often should you audit smart contracts?
A: Ideally, before any major release or after significant code changes. Regular audits help maintain security over time.
Q2: Can automated tools replace human auditors?
A: Not entirely. While they can catch many issues, a human auditor’s nuanced understanding is irreplaceable.
Q3: How much does a smart contract audit cost?
A: Costs vary based on the complexity of the contract and the auditor’s expertise. It’s an investment in security.
Q4: What is a reentrancy attack?
A: A reentrancy attack is a common vulnerability where an attacker repeatedly calls a function before the previous execution is completed, potentially draining funds. Learn More.
Q5: Should you audit all smart contracts?
A: Even though auditing is not mandatory, you should strongly consider it to prevent security breaches and build user trust.