EIP-5920: PAY opcode

The zero address should not have a nonzero balance.

It already does. And adding a single special case for reversion just replaces one class of potential bugs with another. If an address gets set to zero and is unchangeable, then any function that calls PAY on that address will always revert and effectively be uncallable. If that function needs to be called as some necessary phase of the contract, then your contract is now permanently frozen and the ETH is inaccessible.

E.g. imagine a lottery contract where people can set their payout address when they buy a ticket. A function called startNewRound() checks if the last round is over, PAYs out ETH to the winner, and starts the next round anew. If the winner sets their payout address to 0, then startNewRound() will always revert when attempting to make the payout, and the lottery is now frozen with no future rounds possible.

It makes PAY a lot easier to reason about if it succeeds no matter what, as long as you pass in a valid address and ETH amount. If you want your contract to prevent payouts to the zero address, just add in some custom check like require(addr != address(0)). This is semantically equivalent to how people already make payouts with call(), transfer(), and send(), and there is no reason to change it for just this single opcode. Even less so, in fact, since the point of PAY is pretty much to bypass any checks and force an address to receive ETH.

That makes sense. What’s currently written probably makes more sense then.

burning for payments to address(0) is still a bad idea for a few reasons:

  1. when a contract PAYs to some address, it might operate under the assumption that the balance of that address will increase. E.g. the contract PAYs some amount ether to some receiving address, then at a later point checks that the receiving address’s balance has increased by that amount. If the receiver is address(0), then that check will fail, even though the payment was made.
  2. the existing CALL opcode does not burn ETH when the recipient is address(0); changing the semantics of ETH transfers for this new opcode will be confusing
  3. it’s pointless; the only reason I can think of for someone to burn ETH is executing a malicious attack using the situation described in (1)

Again, it’s best for no exceptions to be made for the zero address.

Why would that possibly need to be done?

The call still fails.

The call still fails.

Just checked on Goerli, and CALL does not fail when the receiver is address(0). Here’s my tx sending 0.01 ETH there from a contract:
https://goerli.etherscan.io/tx/0x785a1484424779a040b3c6213e01f162b43c5fc4c5d9780d8ed117d332b595b4
And it increased the balance of address(0) from “11,090.383…” to “11,090.393…”, so no burn. So to bring the opcodes semantically in-line would require not giving address(0) any special treatment.

Why would that possibly need to be done?

Dunno, but someone might still end up doing it. Like, if there’s a bunch of complicated conditional code that decides whether or not to make a payment, so the dev decides that the simplest way to check whether or not the payment was made is just to query the receiver’s balance. My point stands that a burn is pointless, and (hypothetically at least) could lead to bugs.

Never mind then. It seems that the special case is not needed.

1 Like

Since this EIP is now CFId, I would like a clarfication if this new “burn rule” for the zero address still holds. It seems that there is confusion if it is ok to transfer eth to the zero address, and this is ok, and it behaves just as normal. But this EIP introduces a rule to burn the eth…? Is this still desired, or is this rule going to be removed (I have no strong opinion here).

I also think that the gas section is not clear. From current EIP:

Gas pricing

The gas pricing is that of a CALL with a positive msg.value, but without any memory expansion costs or “gas sent with call” costs, with a gas reduction of 500 to compensate for the reduced amount of computation.

If we review CALL regarding the relevant gas costs for PAY: the CALL gas price currently depends on: do we transfer value (9000), is it warm or cold (100 for warm, 2600 for cold), and do we create a new account (25000).

I do not see how this has a gas reduction of 500 in any situation here, to reach the base gas costs of the PAY opcode of 3000.

At this point I do not understand how to implement the gas costs :cry:

Clearly I forgot to fix the pricing. The cost of the PAY opcode should 100 for warm addresses, 2600 for cold addresses, and 25000 for new accounts, and should have a base gas cost of 9000.

I clearly forgot to push quite a few changes. Fixed.

1 Like

Given that EIP 6780 is included in Cancun, and it preserves the unconditional send aspect of SELFDESTRUCT. It seems that the functionality of this EIP can be implemented as a contract with low gas overhead.

While this is true, it creates unnecessary work for the EVM. Additionally, ‘sending ether’ is such a common task for contracts, and the behavior of selfdestruct is still very much TBD.

1 Like

I think EOF needs PAY for security if it won’t have zero-gas CALL.

It’s useful security-wise to be able to block re-entry on an ether payment. This is one of the reasons that the callvalue stipend is not enough gas to SSTORE or call with value. While re-entry can be protected with TLOAD /TSTORE , simple ether payments are the main reason I still use less than GAS for the gas parameter of CALL . Especially with eip-7702 you do not want to grant control to the recipient just to pay them. They might even be an account unknown to you, as is the case for COINBASE payments.

PAY is in the spirit of EOF because it doesn’t have a gas parameter. It doesn’t enable anything not already possible, because it is still possible to pay an account without its permission via COINBASE priority fees.

The workaround is to EXTCALL the value into a legacy contract and CALL with 0 gas. The overhead is 9000 gas. But PAY is strictly better because there is only one value transfer.

This aspect is still preserved by COINBASE priority fees. It is impossible to reject these fees and you aren’t notified.

@Pandapip1 Please update the opcode so it does not conflict with one of the ones currently used by EOF.

I proposed this EIP for inclusion into Prague. But client teams blocked it without any discussion. I think @shemnon put together a piece summarizing each client team’s position on all of the Prague EIPs, but I can’t find it offhand.

1 Like

What we were lacking was the rationale for why it was needed, what hole it filled. It wasn’t until last night that the gas=0 for CALL pattern was shared, and how gas introspection broken that. So, that was new information material to the opcodes utility.

Last year we were by default resistant to new additions that didn’t fill holes due to pectra scheduling pressure.

We added it in the call this morning to the universally nebulous “post eof-devnet-1” bucket because we don’t have much clarity on when Fusaka will ship. Once we get devnet-1 shipped we can determine where it lands. This bucket includes Osaka proposed EIPs as well as EIPs not proposed for Osaka and likely targeting Amsterdam.

If inclusion in Osaka is a goal, then this request should also be following the EIP-7723 process, and you or @wjmelements should post a PR to the osaka meta (EIP-7607) and attend the next two ACDE calls where my understanding is we will set and freeze Osaka scope. Be aware that the vibe is that all new EIPs proposed for Fusaka not already known is “defer to Amsterdam” at this point, and that advocacy will need to overcome this position. The door is not closed yet, at least not officially.

1 Like
1 Like

Opcode update, also mentioning EXTCALL and priority fees: Update EIP-5920: Update PAY Opcode, Motivation, and Rationale by wjmelements ¡ Pull Request #9389 ¡ ethereum/EIPs ¡ GitHub

1 Like

Sharing my presentation for this week’s ACDE here:

Copying the address halting behavior from EXTCALL (EIP-7069):

And that was brought up where? What is the point of requiring EIP authors to post to ethereum magicians if these objections won’t actually be discussed in ethereum magicians. By the way, the rationale has already been covered in this thread; and is also right there in the EIP:

Currently, to send ether to an address requires you to call into that address, which transfers execution context to that address, which creates several issues:

  • First of all, it opens a reentrancy attack vector, as the recipient can call back into the sender. More generally, the recipient can unilaterally execute arbitrary state changes, limited only by the gas stipend, which is not desirable from the point of view of the sender.
  • Secondly, it opens a DoS vector. Contracts which want to send ether must be cognizant of the possibility that the recipient will run out of gas or revert.
  • Finally, the CALL opcode is needlessly expensive for simple ether transfers, as it requires the memory and stack to be expanded, the recipient’s full data including code and memory to be loaded, and finally needs to execute a call, which might do other unintentional operations. Having a dedicated opcode for ether transfers solves all of these issues, and would be a useful addition to the EVM.

It seems like every other ACD call, client teams wonder why engagement from application and compiler teams is low. This is why – in order to advocate for changes you need to attend hours of calls (and most of the content is irrelevant to outsiders), and in the end, the changes you advocate for can and will be rejected by an opaque, backroom process with little-to-no feedback.

I was trying to point out how it fits into the current EOF plan—what specific gap it addresses. The reasons you mentioned are more general benefits, not tied directly to EOF. What stood out as new was the gas=0 use case, which wasn’t included in the rationale you pulled from the EIP.

When it comes to proposing new opcodes, I often bring up TSTORE/TLOAD as a great example. The person championing it wasn’t a developer from a client team, yet they still managed to provide implementations across all four clients, along with solid reference tests (including pre-EEST tests). Client developers are usually stretched thin, so showing up with working implementations and strong tests is often the best way to get them to seriously evaluate an EIP on its own merits. From what I’ve seen, the one time this approach was used, it got included in the fork after the current one under consideration.