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
.
- Here we list our analysis on contracts might stop working with EIP-1884.
- This is not to blame the developers (as they generally followed best practices) but to raise awareness of the issue so developers can redeploy or take other measures.
- In our analysis we have focused on recently executed contracts, it is therefore not complete
- Below we list contracts that get called by other contracts with a fixed amount of gas. The most common case for calls with a fixed amount of gas is Solidity's
.transfer()
function, but Solidity also allows to add.gas(X)
to calls to only passX
amount of gas. And there are several other sources. The receiving contracts currently successfully execute but would run out-of-gas due to the higher gas costs of EIP-1884. - Frequency of affected transactions:
- ~0.15 before block 8476979
- roughly 0.093 between blocks 8476979 and 8572236
- 0.172 between blocks 8572241 and 8784933
- Mostly these transactions are related to the first two contracts listed below.
- For more analysis see: https://github.com/holiman/eip-1884-security
- For issues regarding ugradability and OpenZeppelin Proxies, see: https://forum.openzeppelin.com/t/openzeppelin-upgradeable-contracts-affected-by-istanbul-hardfork/1616
- Please contact @HRitzdorf on Telegram or Twitter in case of questions or comments
- Called primarily through internal transactions, > 2,8 million message calls
- Fallback function:
function() public payable {
require(reserveType[msg.sender] != ReserveType.NONE);
EtherReceival(msg.sender, msg.value);
}
- Contains one SLOAD, currently has 357 gas left, would break with EIP 1884
- Second affected kyber.network contract: 0x65897adcba42dcca5dd162c647b1cc3e31238490, same reason, Example TX
- 4,263 ETH, over 70,000 internal transactions
- Fallback function:
function () public payable {
require(total() + msg.value <= limit);
}
- Triggers 2 SLOADS and 1 BALANCE
- Currently has 1342 gas left, would break with EIP 1884
- 403 transactions, 47k in Wrapped ETH
- https://etherscan.io/tx/0x74e4523f44c5d4352c9c0c330bb1aaac433562116a2b32a28c1879c3761406a5
- Calls 0x448a5065aebb8e423f0896e6c5d525c040f59af3/SaiTub/Maker with a fixed gas amount of 190000. Currently, 18260 gas left after execution, but due to 35 SLOADs this would break with EIP 1884.
- Token contracts of the on-chain game 0xUniverse will be affected. These are four tokens, the address above is from the Shadows (CORP3) token. They implement a proxy-pattern.
- Fallback function checks the caller against some storage variable, in case of a mismatch it performs a delegatecall based on another storage location Source, Example TX
- Currently, has 820 gas left in fallback function, would break with EIP 1884.
- Eternity (CORP1) Token of 0xUniverse, 250 transactions
- Forge (CORP0) Token of this contract, Forge Token, Example TX
- Dominion (CORP2) Token, Dominion Token, Example TX
- Recently deployed, August 12th, 200 Transactions since
// Gas stipends for acceptRelayedCall, preRelayedCall and postRelayedCall
uint256 constant private acceptRelayedCallMaxGas = 50000;
uint256 constant private preRelayedCallMaxGas = 100000;
uint256 constant private postRelayedCallMaxGas = 100000;
- Example Issue: Used to call 0x557f91c8ea60aaf2c33b9be3a80ac691103515f4, currently consumes 33538 of 50000 gas, but performs 96 SLOADs and hence would break in the future, Example TX
- Recently deployed, August 19th 2019
- If a storage variable is not null, forwards the call using delegatecall to that stored address, Source
- Part of fairly complex transactions, Example
- Deployed by a factory at 0x74f1a37a2324857c262e9762e84db4b08273b6ae, which has deployed 28 contracts
- Identical copies include 0x39617bda08eb2bfc7c97fced5a793f2b0e90a05b, 0x9e38c3109fff25b81034a6ca51d913890b2277b1, 0x1d7953a2eb550f924f01e66a0d7b3bb4541f5152, 0xae55e7dc34607c6e56c381effb748e817d3de8d5, 0x3a512157be01104bb4ce7ff113b353fe04877063
- 2 SLOADs, Current gas consumption 1315 gas, would break, however, could be fixed as the same storage location is SLOADed twice
- For such proxy-related issues also see: https://forum.openzeppelin.com/t/openzeppelin-upgradeable-contracts-affected-by-istanbul-hardfork/1616
- Deployed once per Aragon organisation (rougly 600)
- Relevant Code:
function isDepositable() public view returns (bool) {
return DEPOSITABLE_POSITION.getStorageBool();
}
event ProxyDeposit(address sender, uint256 value);
function () external payable {
// send / transfer
if (gasleft() < FWD_GAS_LIMIT) {
require(msg.value > 0 && msg.data.length == 0);
require(isDepositable());
emit ProxyDeposit(msg.sender, msg.value);
} else { // all calls except for send or transfer
address target = implementation();
delegatedFwd(target, msg.data);
}
}
}
- The current execution of the fallback function in case of a send/transfer consumes 1759 gas. It contains one
SLOAD
due toisDepositable()
. Therefore, it will break with EIP-1884. - Example TX
- Issue at Aragon
- This contract calls the
trade
function of the very popular TokenStore contract with 100,000 gas - Source - Contract had over 2,000 transactions at time of writing
- For certain trades the 100,000 gas will still be sufficient after the hard fork, but for certain trades the 100,000 gas won't be sufficient where they currently are
- Example transaction that would fail in the future:
- Action 6 in parity trace: 100,000 gas provided, 95,728 gas used, 13 SLOADs, hence will break
- Action 8 in parity trace: 95,728 gas provided (all the remaining gas), 95,728 gas used, 13 SLOADs, hence will break even if 100,000 gas would be provided
- This is an example where the sender contract should be redeployed
- MonsterBit is a collectible game on Ethereum
- The affected contract had been the entry point of over 36,000 transactions at the time of writing
- Problematic fallback function can have different number of SLOADs: (Source)
def _fallback() payable: # default function
if saleAuctionAddress != caller:
if siringAuctionAddress != caller:
if unknowne4c73abeAddress != caller:
require caller == unknowna6531d34Address
- Any invocation with three or more SLOADs will break. Current cost is 964 gas in case of three SLOADs.
- Example TX: One call has 3 and another has 4 SLOADs
- 488 transactions and 11.8 ETH
- Problematic fallback function only emits a single event: (Source)
def _fallback() payable: # default function
log Transfer(
address from=call.value,
address to=caller,
uint256 value=owner)
- Performs single
SLOAD
forowner
- Current cost 2135 gas, would break after hard fork
- Example TX
- PickFlix is a Weekly Fantasy Movie Game
- Affected Contracts have an expensive event inside their fallback function: (Source)
def _fallback() payable: # default function
log Received(
address user=call.value,
uint256 ethers=eth.balance(this.address),
uint256 tokens=caller)
- It is an example where the gas cost increase of
BALANCE
is the problem - Current cost: 2149 gas, and hence would break after hard fork
- Example TXs: 1, 2, 3
- Totle combines multiple Decentralized Exchange Platforms (e.g. KyberSwap)
- When the
TotlePrimary
contract executes theperformSwap
function, it might execute theminimumRateFailed
function, which internally callsgetDecimals
on the target token contract. As part of this call,getDecimals
only passes 5000 gas:
let success := call(
5000, // Amount of gas
token, // Address to call
0, // ether to send
- If this the token is Augur's REP token, then the Delegator will be called. This call currently consumes 4511 gas and successfully returns 18. However, this call also contains three invocations of SLOAD, so it will cost 6311 after the activation of the Istanbul hard fork. Hence, it will break after the hard fork.
- As certain tokens do not implement the
decimals
function the failing call (due to out-of-gas) will be interpreted as0
decimals. As the decimals are used to calculate token quantities, incorrect token quantities will be computed. Therefore, users would be as the security check for the minimum rate could be circumvented. - Issue at Totle
- Example TX trading 0.06978 Ether into 1.497 Reputation (REP) tokens
- This is related to the issue above
- The RelayHub forwards a fixed amount of 100,000 gas to this contract when calling
preRelayedCall(bytes)
- Note that the RelayHub also calls
0x96b7f5fe925aa47cbb2cfabe0e050539fd5c8912
with other fixed gas functions, e.g.acceptRelayedCall(address,address,bytes,uint256,uint256,uint256,uint256,bytes,uint256)
with 50,000 gas, but these appear to be fine for now - At the time of writing, the contract gets called regularly
- The contract call consumes 98,910-98,912 gas but 5 SLOADs are executed, hence it would break in the future
- Example TXs: 0xee520a18a3d60c4871401d78e5b5bff80aad733433fe73458ee1ae7bdf80af85, 0x9934d8475f52eb82cad684d3ea6eb658e0c3bb1dcbf68d5ec3f3ffbb43493176, 0x6ebd70af9c0597d5924ab0ce8bfba2d78f55990e04ac607a092e669c1cc69f11
- Involved in complex transactions
- Currently holds ~6.8 ETH
- Fallback function is called with 2300 gas (Source):
def _fallback() payable: # default function
if stor3 != tx.origin:
if stor4 != tx.origin:
require tx.origin == stor5
- Currently consumes 1363 in case the
tx.origin
isstor5
(==0xdead3b4bdb1c7160196c41fd1bab168f71486123
). Hence, it would break with EIP-1884. - Example TXs (note that the TXs fail, but that is unrelated of this contract): 1, 2, 3
- The BZxProxy is part of BZx
- According to the documentation it provides the entry point to the bzx system, which enables trading and lending
- As part of more complex trading transactions (Example) it sometimes withdraws WETH. This causes the ETH to be sent using
.transfer()
to the BZxProxy - The execution of the fallback function currently consumes 2135 but would break after Istanbul as the proxy does 2 storage lookups, hence triggering 2 SLOADs
- Therefore, transactions that include such WETH withdrawals or similar calls would break with EIP-1884