Impact Study of EIP-3074
Date: May 19, 2021
________________
Abstract
Dedaub was commissioned by the Ethereum Foundation to perform an audit/study of the impact of Ethereum Improvement Proposal (EIP) 3074 (AUTH and AUTHCALL) on existing contracts.
In order to appraise the impact of the proposed change, we performed extensive queries over the source code and bytecode of deployed contracts, inspected code manually, examined past transactions/balances/approvals, and informally interviewed developers.
Executive Summary
The extent of the impact is not straightforward to ascertain. There are many affected contracts, estimated at 1.85% of unique (i.e., contracts with the same bytecode are counted once) active deployed contracts. Many of these contracts handle substantial sums and interact with an Automated Market Maker (AMM), such as Uniswap or Balancer. The result is that the contract is being threatened with flash-loan or other pool-tilting attacks.
However, our considered opinion after this study is that a) the impact will be significantly limited with appropriate awareness of the upcoming change; b) the vulnerable code patterns are already exploitable by miners and flashbots, and such exploits will become more threatening in the near future. As a result, we believe that the impact of EIP-3074 is certainly manageable and perhaps a net positive in the overall security of the Ethereum blockchain ecosystem. We recommend reading the section titled “Opinion” at the end of this report for more detail and documentation of our (subjective) opinion. The objective, numeric findings of the study are listed in detail in the “Experimental Findings and Study” section.
Setting and Background
The focus of the study is to examine to which extent existing contracts are adversely impacted by the changes introduced by EIP-3074. The most significant impact of EIP-3074 (in its current “strong” form) on past contracts is the inability to reliably distinguish the msg.sender (in Solidity) of a transaction. In particular EIP-3074 enables programmatically setting the transaction’s msg.sender, thus rendering obsolete common checking patterns such as “msg.sender == tx.origin”. This pattern is often used to ensure that a contract’s caller is an Externally Owned Account (EOA) and not a smart contract, and, thus, cannot have manipulated on-chain quantities atomically without the rest of the environment (i.e., real-world actors) getting a chance to correct or punish such manipulation. As the study confirms, protection against flash-loans in contracts that interact with AMMs is the primary use of such patterns.
Experimental Findings and Study
Task 1: Source Queries
Retrieve all contracts with msg.sender == tx.origin in their published source.
As the first quick experiment, we queried our database (behind contract-library.com) for contracts with common combination patterns between tx.origin and msg.sender in their source. This means that there are two sources of incompleteness in this query: a contract may not have source, or a contract may be checking tx.origin against msg.sender but without using this exact source code pattern. On the other hand, this query is over a fairly complete set of contracts, virtually all contracts ever deployed (with very minor exceptions). Notably, the numbers concern accounts, i.e., contracts with the same code are counted as many times as deployed. (This aspect will be different in our next experiments.)
select hex(a.address)
from address a
join source_code s on a.md5_bytecode = s.md5_bytecode
where s.code like '%require(msg.sender == tx.origin)%'
and a.network = 'Ethereum';
874 rows
select hex(a.address)
from address a
join source_code s on a.md5_bytecode = s.md5_bytecode
where s.code like '%msg.sender == tx.origin%'
and a.network = 'Ethereum';
2312 rows
Clearly, the number of contracts that employ the pattern is substantial.
Task 2: Bytecode Queries
Retrieve all contracts with msg.sender == tx.origin checks regardless of whether the contract has published source and of whether the contract has this exact code pattern or merely an equivalent one (e.g., getting msg.sender through an internal function, which is common).
In order to reduce the time overhead of our queries and not have our dataset be dominated by old and unused contracts, we limit our dataset to contracts that have transacted recently (within 200K blocks from block number 12374455). This is effectively all contracts that saw activity in the past month. We also disregard duplicates: contracts with the same bytecode are counted only once, regardless of how many times they are deployed. We end up with 34,962 unique contracts. For comparison, the total unique bytecodes in the DB of contract-library.com (up until the same block) are 398,275. By limiting our attention to contracts that transacted in the past month, we reduce our workload by over 10x, allowing more targeted exploration and quick experimentation with the most relevant contracts.
After decompiling the contracts using the gigahorse decompiler, we ran a simple analysis for the detection of comparisons between msg.sender and tx.origin. Because of the way our query is written (to enable completeness of the results) this will consider both “msg.sender == tx.origin” and “msg.sender != tx.origin”, as well as any other data-flow combination.
Through our pipeline we are able to successfully analyze 99.8% of all contracts in our dataset (with 41 decompilation timeouts) finding that the comparison between msg.sender and tx.origin is present in 1.85% of them (648 unique contracts).
Sanity checking of static analysis completeness:
The analysis detecting the pattern of this task will be used as a building block for the queries of the next tasks. Because of this, it is crucial to evaluate its completeness. To do so we reran the 2nd database query of task 1 (the one that returned 2312 rows), this time limiting its results to contracts transacted in the last 200K blocks. This query of our source database returned 387 unique contracts.
The sanity check is how many of the 387 are not in the 648 flagged by the bytecode-level analysis. The sanity check returned 59 contracts, i.e., the analysis would seem to have 85% recall. We sampled the first few: in most cases the guard is not present in the deployed contract but its deployers had uploaded the source of several files on etherscan and some other file had the guard. We had to sample 8 (of the missing 59) files before we found the first actual false negative for the static analysis. This leads to the conclusion that the 387 number is greatly inflated, therefore the static analysis at the bytecode level misses very few actual combinations of tx.origin and msg.sender.
Conclusion: the static analysis of bytecode is solid, finding at least ~98% of real guards that combine msg.sender and tx.origin and twice as many as a source-level query.
We now have a solid base for continuing with deeper analysis combinations. For the tasks that follow, we report results over the 648 contracts (1.85% of all active contracts of the past 200K blocks) returned by the analysis of this task.
Task 3: Revert for EOA Caller
Retrieve all contracts that do (effectively) require(msg.sender == tx.origin), i.e., revert (directly or soon thereafter) when the above check pattern fails.
In the next experiment, we examine more closely (yet still with automated analysis) the results of the previous step. We detect instances of the comparisons between msg.sender and tx.origin produced for Task 2 where the result of the condition flows to a conditional jump that can control whether the program will reach a REVERT/THROW statement or not.
Our query is flexible enough to recognize simple guards such as “require(msg.sender == tx.origin)” but also more complex patterns such as “require(isApproved(msg.sender) || msg.sender == tx.origin)” while being agnostic to the exact shape of the checking code.
The results of our query show that such a pattern is present in 94.44% (612 unique contracts) of the contracts returned by Task 2.
Task 4: Automatic Classification: AMM Interactions
Retrieve all contracts that have msg.sender == tx.origin that guards an interaction with an AMM (Uniswap, Balancer). This indicates that the pattern is used as flash loan protection.
We next use static analysis to determine the extent to which a tx.origin+msg.sender guard pattern clearly protects AMM interactions. This will necessarily be an under-estimate! The code semantics could be arbitrarily complex. We capture AMM interactions that are discernible in the same contracts and that can clearly be affected by price manipulation--e.g., swaps.
We provide two static analysis variants:
1. The first, optimized for completeness, detects programs that have at least one instance of the condition that combines msg.sender and tx.origin and at least one external call with a function signature matching the Uniswap and Balancer APIs we model, even if our analysis cannot detect a way for them to be part of the same transaction execution.
2. For the second variant, we optimize for precision, detecting instances of the conditions that combine msg.sender and tx.origin produced in Task 2, where the condition can be followed by an external call with a signature matching the Uniswap and Balancer APIs we model.
The API calls we consider are the following:
* Uniswap/Sushiswap:
* swapExactTokensForTokens(uint256,uint256,address[],address,uint256)
* swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256,uint256,address[],address,uint256)
* swapExactTokensForTokens(uint256,uint256,address[],address,uint256,bool)
* swapTokensForExactTokens(uint256,uint256,address[],address,uint256)
* swapTokensForExactETH(uint256,uint256,address[],address,uint256)
* swapTokensForExactETH(uint256,uint256,address[],addres