What stood out as new was the gas=0 use case, which wasn’t included in the rationale you pulled from the EIP.
It’s the first item in the rationale:
Not sure what to say here. My objection was not the fact of the EIP being declined, more that the EIP was declined for opaque reasons (apparently, due to backroom discussions) – rather than given in the EIP thread, where concerns or misunderstandings could have been discussed+addressed.
I spent a bit of time thinking about this, and from my perspective, I think both are reasonable. Unlike CALL, you can’t get a failure for a reason outside of your control, since you can check how much balance you have before issuing the opcode. The only thing I can think of is users may want to do this to avoid calling SELFBALANCE.
Overall, I would consider exceptional halting as slightly safer for users, but as you said, pushing 1 or 0 to stack is more consistent with the implementation for CALL.
Another, third option, would be to send min(selfbalance, <specified value>). Not sure how useful it is, but it allows you to send full balance with pay(uint256_max).
Safer in the sense that it’s harder to screw up - there is no way to “forget” to handle the invalid input case, the EVM handles it for you.
Also I don’t really think the amount of gas consumed is an issue. Generally if the argument to PAY is higher than SELFBALANCE, it indicates a user bug (same as stack underflow or other invalid opcode). I think that’s the point I was trying to make about “can’t get a failure for a reason outside of your control”. Like even with the exceptional-halting scheme, you can avoid the exceptional halt with DUP1<value> SELFBALANCE LT PUSH2 [join] JUMPI PAY [join] JUMPDEST.
I still think that consistency with CALL of the 1/0 scheme (and let’s face it, consistency with most of the rest of the EVM) is a pretty strong argument. I’ll let others chime in here.
I would go for the CALL scheme also, as you mentioned these exceptional halts are easy to prevent. In CALL if you try to send more than current balance, it exceptionally halts (consumes all gas in current frame) and puts 0 on the stack of the caller. This is consistent, and I like it.
The EIP reads:
Does addr exist or is val zero?
* If yes to either, zero;
* Otherwise, GAS_NEW_ACCOUNT.
This reads to me as if the case that val is nonzero always GAS_NEW_ACCOUNT must be charged which seems wrong. It should only charge GAS_NEW_ACCOUNT if value is nonzero and the addr is non-existent. NVM, I now understand the word “either”
Also brings the question for the val=0 case:
Is this allowed in STATICCALL? (I’d argue: just ban the entire opcode in static context)
Does this perform the warm/cold account check? (should not do this and not charge for it)
Does this put the target in warm (should not do this)
I like this EIP a lot because this is extremely good UX in the case a smart contract developer wants to send ETH to an account without having to worry about any code which might be executed on that account. Also for starting smart contract developers it does not make much sense that you have to worry about executing code if you want to send someone ETH. This is like the “safe way” to transfer ETH. I like this a lot, this should have been introduced to EVM much earlier.
Without PAY it is not possible to reach this behavior (well, techncially there is: CREATE a contract with X value and then selfdestruct X to pay A, but this is extremely expensive and will likely also not work at some fork in the future) because all CALL types will execute code, and in order to succesfully transfer ETH you must succeed the call frame.
Hey all, I just realized a super nice feature of this opcode, and this is to mark addresses as warm without running any code on them (PAY the address-to-warm 0!).
Exceptionally halts if addr has any of the high 12 bytes set to a non-zero value (i.e. it does not contain a 20-byte address).
I am curious about the order of operations between charging the gas and exceptional halting when address has non-zero high 12 bytes.
One of the components of the gas cost is if the to_address is warm or not and if the address has high bytes set to non-zero values, performing this check does not make sense. Would it more sense to provide more clarification on this?
The halting because of ASE should come first for performance reasons.
The cost wouldn’t matter as an exceptional halt consumes all available gas.
If the cache kept 32-byte addresses it wouldn’t matter because the only way to access them currently is via opcodes that only revert when they see them, so warm/cold state is not observable.