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.
This would make sense (from an implementation/optimization perspective), but we also dont do this for CALL instructions where you CALL into address(this) and send nonzero value: you still pay the GAS_CALL_VALUE. It also does not make sense to PAY the current address (this can be checked in the contract to avoid “wasting” gas on this PAY).
Yes, otherwise it would have been a security issue since call needs to create an execution context and actually perform the call. I mean, what if address(this) had a fallback function?
In regard to pay, I would still recommend considering a different cost for a no-op situation.
That gas parameter is unrelated to creating an execution context. It is only charged when there is non-zero value transfer.
The cost scheme should match CALL. If an ether transfer log was added, the operation would not be a no-op. In fact it’s still likely to be recorded as an “internal transaction” in several systems. I also don’t think this case should be bikeshedded because it should never happen. Additional gas branching is not free. The system is simpler if it doesn’t check that case. The developer should check that case if they are concerned about it.
One concern that I have with PAY is that it is enshrining a behavior that makes formal verification of contracts more difficult. Currently force sending ether with self destruct is niche and something we can either increase the cost of substantially or remove altogether (eventually). If we lean into PAY we will not have the this ability.
The issue PAY poses is that it is impossible for contracts to accurately track its balance and there will be downstream effects of this. I think it would be better for us to go towards a system that is more amenable to FV, rather than less.
Since PAY is not adding new behavior that isn’t possible today, we don’t need to stop pursuing just due to the above. But I want to make sure we are explicitly acknowledging that we are making FV harder in some cases, but that the benefits outweigh the cost.