EIP-5920: PAY opcode

I support this EIP because:

  1. Reentrancy attacks are very frequent and dangerous, reducing attack surface for them is great
  2. It is consistent with ERC20 tokens, most of which not providing any ability to react to balance changes. It’s common (and costly) practice to wrap ether into WETH and transfer it this way.
  3. Gas optimizations are nice, especially for smart contract wallets. This would also stack nicely with eip-3651
  4. I don’t see that much use cases in having contracts being able to fully control their ETH balance in post-SELFDESTRUCT EVM
  5. Creating opcode that would guarantee succesfull transfer through all hardforks would resolve confusion about different ways to send ether that exist today
1 Like

I’d like to raise a concern for this OPCODE, and ask the authors solicit the advice of Solidity devs and some sampling of security auditors before this proceeds to be eligible for inclusion in Cancun.

Using the PAY opcode would enable bypassing the fallback functions of smart contracts, and the default coding of non-payable functions is to revert on function calls unexpectedly sending Ether to a smart contract. This subverts that protection. It is possible for a smart contract to configure itself so that no valid invocation results in Ether being deposited in the account. The motivation section of the EIP speaks to stoping reentrancy attacks, but a smart contract could configure their fallback function to accept payment and exit if they desired that ability. Enabling the PAY opcode prevents contracts from rejecting all payments.

One alternative to preserve this behavior would be to allow PAY to only work when sending value to accounts with no code in their account, but that isn’t much different from the existing CALL mechanisms.

Another concern is the gas schedule, it is not in harmony with the warm account/cold account pattern established in EIP-2929 (which it cites) and instead proposes a different handling. If it proceeds I would ask the gas be set up as a premium (100 gas or so) on top of the warm/cold cost, and that it interacts with the warm/cold lists like the CALL and SSTORE series opcodes do. Specifically, i think adopting the gas pattern used in CALL would be the best pricing strategy and present the most maintainable code for clients.

It’s already possible to do this with SELFDESTRUCT and coinbase payments, as mentioned in the motivation. This introduces no new security concerns.

See SWC-132 - Smart Contract Weakness Classification (SWC)

As mentioned in the specification, the EIP-2929 costs are applied.

1 Like

As mentioned in the specification, the EIP-2929 costs are applied.

New account costs are not being applied (25K when paying to an address with an empty account)

Are they not? Oops then, I’ll fix that.

This is a good point but I would argue that fallbacks are potentially dangerous and should be deprecated. However, this should definitely be noted as a big caveat in the documentation for the new PAY opcode.

What reason would someone have to burn ETH?

Beats me. Now that I think about it, it would probably make more sense for the call to fail, to avoid potential bug-related losses.

It’s probably best not to make any kind of special exemption for the zero address, that’s how all the call opcodes already operate

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