Closed nlordell closed 1 year ago
For a bit more context:
We currently build access lists for our settlements 🎉. We mostly do this as a small gas optimization (as having access lists provides a small gas bonus to not using them).
However, one issue with generated access lists is that they will stop generating them once it encounters a revert. This means that, in our settlement, when we execute the following line to send Ether to a smart contract:
payable(transfer.account).transfer(transfer.amount);
The call will revert, and the access list generation will stop! This leads us to a bit of a chicken-and-egg problem in the access list generation feature were:
The solution is to first:
eth_createAccessList
call for generating the full access list for a settlementI hope this helps clarify things.
Sorry for the missing context, I'll take another stab at it. There are a few moving parts here:
First of all Solidity provides a transfer
function for payable address
-es. This will (emphasis my own):
send given amount of Wei to Address, reverts on failure, forwards 2300 gas stipend, not adjustable
This means that the Solidity compiler will generate a CALL
opcode instruction with a fixed, small amount of gas. Note that CALL
s always execute code, in the case of transfer
the calldata
was empty - and Solidity has special fallback
and receive
functions for handling these kinds of CALL
s. The reason for this is mostly for security, in that it does allows the contracts to do any expensive work (like change blockchain state), but allows smart contract wallets (like the Safe) to receive Ether - it is a wallet after all! SC wallets designed their fallback
and/or receive
functions to work within these gas constraints (2300
gas).
The Safe, and many SC wallets for that matter, are typically implemented as "proxy" contract. They are just very small and simple contracts that forward calls to an "implementation" contract. This allows for things like upgrading your SC wallet (by changing the implementation contract address) as well as reduced deployment fees (you deploy a tiny contract that is a handful of bytes instead of KBs of the implementation contract). The above gas stipend of 2300
worked fine as the Safe would so something like:
SLOAD implementationAddress ;; 800 gas
DELEGATECALL ;; 700 gas
... ;; Roughly 800 gas left for the remaining work, which was enough
However, the Berlin hardfork introduced EIP-2929 which changed the gas costs for storage access. Specifically, the cost for a storage access changed in pretty fundamental ways. They added a concept of "cold" and "warm" access. "Cold" access is what you pay for accessing blockchain state (balance, code, storage, etc.) for the first time within a transaction which is much more expensive, while "warm" access is what you pay for subsequent accesses and is much cheaper. This means that the Safe's handling of receiving Ether if it was "cold" would now became problematic, since:
SLOAD implementationAddress ;; 2100 gas
DELEGATECALL ;; 2600 gas
... ;; -2400 gas... Oops!
(Note that DELEGATECALL
is a special variant of CALL
which basically means "run this other SC's code as if it were in my address - the op-code that underpins the "proxy" pattern).
This means that the built-in transfer
function in Solidity no longer provided a large enough stipend for many SC wallets (at least the ones that implemented this proxy pattern which was very popular) to be able to receive Ether (even if they did no "real" work as was the case with the Safe).
Note that sending Ether to a "warm" Safe (i.e. sending Ether the second time within a transaction for example) is not an issue, since the gas costs become:
SLOAD implementationAddress ;; 100 gas
DELEGATECALL ;; 100 gas
... ;; 2100 gas left 🎉
Since this would cause a lot of issues for existing contracts, the Ethereum developers also introduced EIP-2930 which allows transactions to optionally specify an optional "access list" - a list of storage to "warm" up that you pay upfront. This internal calls to use the cheaper "warm" variant by paying for the cold access before the transaction starts.
So, for the settlement contract to be able to send Ether to a Safe as part of a trade, we need to make sure to create an access list for the Safe's "receive Ether" call - so that we only pay for "warm" storage accesses. This means that:
eth_generateAccessList({ to: smart_contract_order_owner, value: 1, data: [] })
Would generate the access list required to "warm" of the storage needed for a smart contract order owner to receive Ether as part of a "buy Ether order".
PS: Sorry for open/closing the issue - was a misclick.
This PR captures the work required for implementing support for EIP-2929/2930 access lists. This is required in order to support transferring ETH to SC wallets.
This is needed because of the gas increase in the recent Berlin hardfork which caused Solidity
transfer
to not use enough gas when sending ETH to a contract.We should also consider increasing the gas amount for the
transfer
in the smart contracts.