I ran into an EVM edge case. Is it correct that extcodesize(addr)
is not zero, after calling selfdestruct
on addr
in the same transaction?
This behavior seems strange and I could not find any documentation on it.
assemblyevmyul
I ran into an EVM edge case. Is it correct that extcodesize(addr)
is not zero, after calling selfdestruct
on addr
in the same transaction?
This behavior seems strange and I could not find any documentation on it.
You've misunderstood the steps. But close!
Consensus is easily misunderstood.
The fundamental problem first solved by Bitcoin was how to reach eventual consensus among nodes that do not know or necessarily trust each other. Add money to the environment and it becomes an adversarial environment. This is in no way like the situation in a typical cluster of computers that do know each other and do trust each other.
We get a hint about how to solve it from transaction logs employed by databases. If one has a replayable log with all the inputs in the right order, then one can reconstruct a database. It's not important to have "the state" if you have the inputs that created that state.
There is a non-obvious challenge to deal with. Given physics, it is not possible for all nodes to learn about all transactions at the same time or even in the same order. It will (probably) never be possible to make everything as fast or faster than everything else.
In a trustful environment, that could be resolved with accurate, trustworthy timestamps that would help everyone enumerate the "correct" transaction order. In an adversarial environment with no authority, that doesn't work because no one's clock is considered more trustworthy than anyone else's.
So, how to order the transactions?
The mining process collapses that ambiguity. The "winning" miner earns the privilege of setting a de facto network transaction order, for one block. Importantly, this is not even an attempt to resolve the temporal order of transactions. In fact, gasPrice, is a way to queue jump. One can probably incentivize a miner to include a transaction sooner by attaching a high bid. Conversely, one can save on transaction fees by being patient.
Nothing happens anywhere until nodes receive news of transactions included in blocks. Transactions in blocks are well-ordered. The blocks themselves are well-ordered. So, the chain of blocks is a well-ordered set of all transactions that have happened.
Each full node, not just the miners, processes transactions completely, reaching their own conclusions about state changes. This is very much like replaying a transaction log because everyone agrees on the inputs and the order of those inputs. The functions are deterministic (strict requirement) so there can be no disagreement between well-functioning nodes at the same block height.
Blocks have time-stamps but transactions don't. Block time is the minimal resolution, and all that can be said is that all transactions in a block were mined at that block time, in the order of inclusion. Each transaction executed in the context left by the previous transaction.
Miners have non-trivial privilege. Working together they can censor transactions. They can play with timestamps and transaction order, if there is selfish benefit doing so. This can be important in contract design when factors such as "deadline" exist.
In any case, the consensus resolves transaction order. Nodes figure out the state for themselves.
Hope it helps.
p.s. There is a lot more going on in mining but I wanted to focus on your question.
Nothing is impossible but if the CALL
in your example actually got removed, it would be a bug in the compiler.
The only case I'm aware of where the compiler (I mean specifically the code generator) might skip emitting code is if you have an internal function that never gets called. The function as a whole is removed though, not individual instructions.
As for the optimizer (which can modify already emitted code), it does have a set of annotations for builtins, one of which is canBeRemoved
but only the ones with no side effects get this annotation - CALL
is definitely not one of them.
You can check if the instruction actually gets removed by inspecting the --asm
output (see Analysing the Compiler Output). It's a direct rendering of the bytecode in a human-readable assembly form so if the instruction is there, it must be present in your compiled contract as well.
The revert() is not executed, meaning the call succeeded.
That's unfortunately not always true. There's an important gotcha that you need to be aware of when making a low-level call (from Units and Globally Available Variables > Members of Address Types):
Due to the fact that the EVM considers a call to a non-existing contract to always succeed, Solidity includes an extra check using the
extcodesize
opcode when performing external calls. This ensures that the contract that is about to be called either actually exists (it contains code) or an exception is raised.The low-level calls which operate on addresses rather than contract instances (i.e.
.call()
,.delegatecall()
,.staticcall()
,.send()
and.transfer()
) do not include this check, which makes them cheaper in terms of gas but also less safe.
Are you 100% sure that a contract actually exists at the target address? You said the tokens got transferred so I guess it does but it might be worth double-checking. You can add EXTCODESIZE
to your code to verify that.
Best Answer
I was able to reproduce your issue with the following code on Remix (You can check the different behavior between calling
extcodesize
in the same tx or in a subsequent one)The reason for that behavior can be explained by looking at the geth implementation of selfdestruct.
This is actually a logical behavior, as you have to take the possibility of reverts into account, plus you cannot leave another contract in an unstable state in the middle of a transaction if you decide to self-destruct. Until the state is committed, the self-destructed contract is still fully accessible.