Security considerations for EIP-1884
Background
EIP 1884 is set to be implemented into the upcoming Ethereum 'Istanbul' hard fork. It
- increases the cost of opcode
SLOAD
from200
to800
gas - increases the cost of
BALANCE
andEXTCODEHASH
from400
to700
gas - adds a new opcode
SELFBALANCE
with cost5
.
The reasoning is that due to the increase in state size, and thus the added IO overhead for fetching tries from disk, the opcodes SLOAD
, BALANCE
and EXTCODEHASH
have become disproportionally 'cheap', for the amount of work that a node has to perform. Having badly 'tuned' gas cost versus the underlying computational cost of an operation is a problem which can cause various problems, and pave the way for attacks such as the so called 'Shanghai attacks' as seen in late 2017.
Potential problems
In general, repricing opcodes can always break contracts that explicitly rely on assumptions of gas cost being constant. However, this has been considered bad practice for a long time, especially so since certain opcodes historically already have been repriced in Tangerine Whistle, where SLOAD
was repriced from 50
to 200
.
However, there is one case which could potentially become more problematic; default
functions.
Default functions
A default
function is a method of a contract that handles calls without any data -- they are there to handle transfers of ether that does not explicitly invoke any method at all. They are typically used to create an event using a LOG
operation, so external systems can detect the event, and e.g. register that a transfer was made.
A regular transfer of ether to a contract always gives the receiver at minimum 2300
as gas stipend
. This number is meant to allow the recipient to issue an event, but is not sufficient to perform state changes (such as making another transfer, or updating a storage slot).
EIP 1884 and default functions
One potential problem of EIP-1884 is that default
functions might start to fail on 2300
gas, e.g. for the following reasons:
- Limited wallets: A contract only allows payments if
balance(self)
is below a certain limit - Designated senders: A contract only allows payments from a set of pre-approved senders
- Disabled wallets: A contract only allows payments if a certain variable (slot) is set to
true
.
Now, if a default
function ceases to work with 2300
gas, this is not always a very serious problem. For example, if the caller is a so called EOA
(Externally Owner Account - meaning end user), the caller can simply make sure to send a bit more than 21000
gas in the transaction. But the problem can arise if, for example
- The
target
has designated sender, - The
senders
are smart contracts, which are programmed to only ever usetransfer
with no extra gas.
In that case, the flow of ether from the senders
to the target
would be broken in a way that is not 'fixable' unless other mechanisms can be used to handle the situation (e.g. replacing the senders).
Investigation
I reached out to the EthSecurity community to help assess this situation. Some notes:
- Contracts that does not have a
payable
default function would not be affected, - Contracts whose default function would not be executable today on
2300
gas would not be affected e.g. contracts that do SLOAD or transfer ether indefault
would already be 'broken'
Neville Grech, of Contract Library, performed an analysis of decompiled mainnet contracts. The analysis covers about 95% of all contracts on mainnet (500K unique bytecodes), and lists those that could potentially be affected.
- The list is available here
Hubert Ritzdorf, of ChainSecurity, performed an analysis of recent transactions. The analysis is based on investigating actual transactions on mainnet, and seeing which of those would have failed if SLOAD
had cost 800
instead of 200
. Partial results are here.
Chain Security analysis
See this gist, with the following comment:
The first two occur very frequently, the others are less frequent. We listed the final one even though it would still work the EIP as we are not sure how these gas values are currently being determined for such "deep" transactions. We wanted to raise awareness of potential issues.
Kyber Network
function() public payable {
require(reserveType[msg.sender] != ReserveType.NONE);
EtherReceival(msg.sender, msg.value);
}
- KyberNetwork meets several of the criterias,
- Implements the "Designated senders" pattern,
- Called primarily through other contracts, which rely on
transfer
(this limited to2300
gas)
We reached out to KyberNetwork, and although it is obviously a chore to do, this can be solved:
technically the market maker can just deploy new reserve contract
CappedVault
function total() public view returns(uint) {
return getBalance() + withdrawn;
}
function () public payable {
require(total() + msg.value <= limit);
}
In this context, withdrawn
is a storage slot
, and so is limit
.
- CappedVault, with over
4K ether
and70K
internal transactions, meet the criteria:- Implements the "Limited" pattern
- Two
SLOAD
and oneBALANCE
Implementation note:
- This contract is programmed to 'break' exactly like this, in case the total of ether passed through the contract exceeds
33333 ether
. That is, regardless of how muchether
is currently in the vault, it will cease to acceptether
after33K
has passed through it.- **This indicates that there already must be mechanisms to handle the case when
default
cease functioning. **
- **This indicates that there already must be mechanisms to handle the case when
- The
limit
is a storageslot
, but could have been implemented as a compile-time constant, reducing oneSLOAD
. - The
balance(self)
could, after Istanbul, be rewritten asSELFBALANCE
In essence, it currently uses:
200 (sload limit) +200 (sload withdrawn) +400 (balance) = 800 gas
into, post-EIP-1884:
5 (selfbalance) + 800 (sload withdrawn) = 805 gas
.