Smart Contract Security Part 2 — Cross Function State Dependency
Author: Benjamin Chai
Overview of Blockchain Infrastructure — States
Decentralized code executing blockchains have provided the base level infrastructure for a variety of decentralized applications, spawning a new generation of Web 3.0 applications. Web 3.0 applications operate on-chain through smart contracts — blockchain transactions containing executable code that can be used to create automated programs. Despite their utility, smart contracts contain a variety of vulnerabilities due to the configurations of blockchain virtual machines and their limitations in executing code.
Blockchains are limited in scalability in throughput both in terms of transactional and computational capability. Given smart contracts are transactions containing lines of executable codes, smart contract transactions must be processed in blocks, just like normal transactions. Blocks are added on a rolling basis on an algorithmically determined semi-regular time frame. Given the limited data storage on a blockchain, each individual process is not recorded explicitly within each block. Rather, a cryptographic hash of processes is recorded as the state of the blockchain. Likewise, within the smart contract, data is only stored if it is designated as a state variable — meaning it is stored on chain as opposed to dynamically stored. A smart contract process is only observed as completed on-chain if a corresponding state change has occurred with a corresponding valid block.
Given the limited scalability of smart contracts and the high cost of deployment, smart contracts typically contain multiple functions to support decentralized applications operations. Each individual function may have multiple processes prior to a state change. In a previous article, the vulnerabilities of reentrancy were discussed for individual function processes prior to state changes. However, reentrancy is not the only vulnerability associated with function calls and state changes. Another notable vulnerability associated with function calls and state changes is cross function state dependency.
Cross Function State Dependency Vulnerabilities
Cross function state dependency are vulnerabilities in which two or more functions rely on the same contract state. Cross function state dependency vulnerabilities are an example of race condition bugs — bugs that arise when a system’s behavior is dependent on a sequence of timing or operations in order to produce the expected results. Reentrancy attacks can be considered race condition bugs, given the attack occurs from an unexpected reentrance into the target contract, disrupting control flow. Reentrancy attacks are similar to cross function state dependency attacks in that expected control flows are disrupted by contract calls. However, unlike reentrancy attacks, the attack is not necessarily based on an external contract fallback function, but rather two functions on a targeted contract that rely on the same state.
When two functions rely on the same contract state, a vulnerability may exist in that it is possible to call two functions simultaneously. When two functions are called simultaneously externally, both processes may execute prior to expected state transitions, producing undesired results. Anytime a state is sequentially dependent and multiple functions rely on said state, simultaneous external calls of functions may be coordinated in an attack to exploit the un-updated state change. If the state is not updated for each call, a vulnerability exists. For greater clarity, an example of cross state dependency can be observed below.
Within this code, there are two functions that share the same state — transfer and withdrawlBalance. Both functions allow a user to remove funds from the smart contract and send them to an external address. Both functions are also dependent on the state of userBalances or the balance of the user. Although both functions update the state of the balance accordingly with the amount withdrawn, such updates occur at the end of the function, meaning it is the last process. Thus, an attacker may simultaneously call both functions allowing an additional withdrawal prior to a balance update. Although the amount withdrawn may not exceed the balance, an attacker is able to withdraw any amount up to double his balance, effectively “double spending” his balance.
Mitigating Cross Function State Dependency Vulnerabilities
Cross function state dependency is a complex vulnerability without an easy fix. Given the high cost of data storage on smart contracts, the inconvenient reality is that most smart contracts have limited state storage and must rely on one state for multiple functions. For example, the balance on a smart contract may be used for a variety of functions: withdrawals, transfers, buying, etc. As such, it is difficult to implement a different state for each function. Likewise, cross state dependency may even extend into multiple interconnected smart contracts. While simple fixes may not exist, there are solutions.
There are several solutions to cross chain state dependencies. The first is to identify functions with potential external calls and to ensure that all internal processes occur prior to potential external function calls — most notably state changes. Another solution is to implement mutual exclusion, allowing a smart contract to effectively lock a state, with proper access control. However, mutual exclusion becomes much more difficult to implement when multiple contracts are involved. Ultimately, a variety of solutions exist, but require precision to implement. In any case, it is imperative for any smart contract owner to audit their smart contracts and be wary of any vulnerabilities, including cross function state dependencies.