Design: Push vs Pull Pattern in EVM
4 jul 2024
7 minutes
Beyond mastering Solidity syntax and the ability to write smart contracts, it is imperative for blockchain developers and auditors to thoroughly understand the architectural implications of smart contracts. This encompasses an in-depth analysis of gas consumption, operational efficiency, and the correct functionality in alignment with the Ethereum Virtual Machine (EVM) capabilities and constraints.
This article aims to assist blockchain professionals in enhancing their understanding of a crucial aspect of the Ethereum Virtual Machine (EVM): transaction gas consumption and the phenomenon of transactions running out of gas. This topic is directly correlated with the architecture of smart contracts and the design of their functions. The core focus of the article will be the exploration and analysis of two distinct Solidity design patterns: the push and pull patterns.
Before delving deeply into the main topic, it is essential to provide some background knowledge to ensure a comprehensive understanding.
Contents
Turing Machine and the EVM
Smart Contract Archetectures
Push Function Architecture
Pull Function Architecture
Turing Machine and the EVM
Turing Machine
A Turing machine is a theoretical construct in computation capable of simulating any algorithm, establishing it as a standard for computational universality. The principle of Turing completeness originates from the seminal work of Alan Turing, a British mathematician and logician. In 1936, Turing proposed the concept of an abstract computing device, now known as the Turing machine.
A Turing-complete machine encompasses all fundamental operations necessary for universal computation. It can manipulate diverse data types, including lists, words, and numbers. The machine supports iterative processes through loops and provides conditional operations like “if-else” statements. Additionally, it includes mechanisms for memory access and storage, enabling it to perform any algorithmic computation
Turing Completeness
‘Turing-completeness’ is a term defined by Alan Turing that describes the capability of certain computing machines to perform any computational task that can be executed by a computer.
Smart contracts are directly influenced by the quality of the Ethereum Virtual Machine (EVM), owing to its Turing completeness. This characteristic enables the execution of complex smart contracts that perform flexible and dynamic operations.
The EVM as a Turing Complete Machine and Gas
As readers can infer, the Ethereum Virtual Machine (EVM) is Turing complete, enabling developers to create intricate decentralized applications (dApps) with sophisticated algorithms.
In relation to the EVM, a pivotal distinction from most Turing-complete machines is the concept of gas. When a user initiates a transaction, they are required to cover the transaction’s gas consumption. Simple transactions consume less gas compared to complex ones, which is crucial for maintaining network stability and preventing abuse of network resources.
The concept of gas is pivotal for understanding the main topic of this article: the push vs pull pattern and how to design smart contract architectures. Unlike most Turing machines, which can theoretically run forever, executing infinite loops without constraints, the Ethereum Virtual Machine (EVM) introduces the concept of gas. Gas prevents infinite loops, among others, by requiring users to pay for transaction execution, with transactions and blocks having a maximum gas limit. Consequently, large transactions such as infinite loops are not feasible within the EVM, as they would exceed the maximum gas limit and fail due to running out of gas.
It is crucial to note that transactions can run out of gas not only due to infinite loops but also because of large transactions that are not necessarily infinite.
Smart Contract Archetectures
When creating smart contracts, developers and auditors must consider the implications mentioned earlier, particularly regarding gas usage and transactions running out of gas. An improper architectural design could result in a denial of service scenario for a protocol, where critical transactions consistently fail due to excessive gas consumption.
Image source: https://www.tokenmetrics.com/blog/crypto-airdrops
Push Function Architecture
The push architecture for a smart contract function involves executing an action for a user without requiring direct interaction from the user with the smart contract. This can be illustrated using the earlier example of airdrops.
Consider the following smart contract:
This smart contract implements an airdrop architecture, where users can be whitelisted to become eligible for the airdrop. The whitelistUser function does not include any access control restrictions, as it is intended for illustrative purposes. It is evident that eligibleUsers is the array to which new users are added to receive the airdrop. This array can grow significantly, depending on the number of participants in the airdrop.
The claimPush function is utilized to transfer the airdropAmount to each user eligible for the airdrop. For instance, if there are 10,000 eligible users, this function will execute 10,000 transfers of the airdropAmount. This exemplifies Push Architecture, where none of the 10,000 users need to manually interact with the contract to receive the airdrop. Instead, a single caller can initiate the token transfers on their behalf.
This push architecture can lead to several significant issues. As outlined throughout the article, if the gas consumed during a transaction exceeds the block gas limit, the transaction will fail due to running out of gas. The claimPush function serves as a clear example: it involves a growing array that cannot be reduced in size and is likely to expand to the extent that it consumes more gas than the limit allows. Consequently, transactions will be unable to execute, resulting in a denial of service scenario.
This example scenario is straightforward, yet the same issue can manifest in larger and more complex protocols. For instance, when a for loop is employed to calculate significant parameters or execute critical functions, it poses a potential risk. If the for loop iterates over a sufficiently large array or if the computation within each iteration consumes enough gas that, in aggregate, exceeds the gas limit, it can lead to a denial of service for the protocol.
Another critical point to address is the use of push architecture for transferring ether across an array. Even with just 2 addresses in the array, such an approach can lead to failures. The primary issue here isn’t necessarily excessive gas consumption, although that remains a concern for larger arrays. Instead, the issue relates to intentional reverts.
It’s important to note that when ether is transferred to a smart contract, the receive() function is automatically triggered. Within this receive() function, any logic can be implemented. Consider the following scenario:
The same smart contract is implemented but for the transfer of Ether.
The same smart contract is implemented but for the transfer of Ether.
One of the eligible users is a smart contract that implements a receive() function with a revert(); inside it.
The length of the array of eligible users is 10.
A transaction with just 10 addresses should typically be short enough to avoid running out of gas. However, if a revert is triggered during the transfer to the contract, the entire transaction will revert. This outcome means that no users will receive the airdrop.
Intentionally triggering a revert inside a receive() function isn’t necessary, but implementing complex logic — such as a gas-consuming loop — can inadvertently lead to a revert. In either case, protocol developers or owners have the responsibility to prevent such denial-of-service scenarios. This underscores why push architecture should be avoided.
Pull Function Architecture
Instead of utilizing a push architecture, which can result in transactions running out of gas and lead to denial of service for the protocol, an alternative approach should be employed. In this new architecture, each user is responsible for executing their own claim transaction without depending on a large array. This approach is known as pull architecture.
In the code snippet below, the array has been replaced with a mapping. This mapping indicates whether a user is whitelisted or not. The claimPull function then verifies if the user is whitelisted and, if so, transfers the tokens.
With this architecture, thousands of users can be whitelisted, and thousands of airdrop transactions can be distributed. Each user will be responsible for executing their own airdrop claim transaction.
This pull architecture also addresses the previously mentioned scenario involving ether transfers. If a user implements a revert() inside a receive() function, the rest of the users can still receive their ether, preventing any denial of service.
About Zokyo
Zokyo (“augment” in Japanese) keeps pace with your in-house development team and provides blockchain security, design, and development talent to startups and enterprise organizations as needed. As a go-to web3 security, development, and investment partner working with some of the most progressive companies since 2019, we are highly experienced in tackling some of the most challenging problems with an entrepreneurial spirit.
With immediate access to in-demand skills ranging from security auditing, cryptography, white-hat hacking, mathematical specifications of network design, UI/UX design, QA, and full-stack engineering, we help legendary companies accelerate time to market and achieve their goals on time and on budget. Our clients demand and deserve best-in-class security and engineering support. As such, we at Zokyo are committed, passionate and proud to build a more secure Web3 future.