In error handling, assert and require statements revert the state of the contract to the initial state and then the transaction passes or fails depending on the code. I'm trying to understand how this happens internally?
2 Answers
Of course, this situation would only happen when calling a function which does change the state, seeing as view
functions require no gas/dont modify the state.
So, let's say you as msg.sender
call a function within a deployed contract. Now, if that function is designed to modify the state, the EVM will evaluate the subsequent data change and require permission for you to validate this transaction - pay for the gas fees, essentially - and modify storage slots (the state). Once you have paid, the transaction goes through and the state(s) is changed.
In this instance, whilst the EVM is evaluating the subsequent data change, if an assertion fails, you will be thrown an error before any authorisation can be made. As such, these statements 'revert' the contract to its initial state in memory before storage slots are modified and miners are required to validate any of these 'transactions'.
-
suppose that the assert/require statement which would eventually fail the transaction comes somewhere in the middle of the contract and prior to that some state variables have already been changed and gas has been used. Are those state changes also reverted back their initial state? Commented Dec 10, 2022 at 21:45
-
permanent 'state changes' as you mention are made only when a transaction is mined. To be mined, gas must have been paid. If gas has been paid, any code contained within the function responsible for state change has been fully executed. So, unless the assertion is omitted by something like a try/catch condition; yes, those state changes are reverted. Commented Dec 10, 2022 at 22:17
this is implemented in core/vm/evm.go
file in case you want to check.
first you will see this line:
snapshot := evm.StateDB.Snapshot()
this is how the EVM marks the start of all the state changes (add/remove balance, etc) your contract is doing.
then it calls your contract:
ret, err = evm.interpreter.Run(contract, input, false)
and this is how the exceptions are handled when your contract ends:
// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in homestead this also counts for code storage gas errors.
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
gas = 0
}
// TODO: consider clearing up unused snapshots:
//} else {
// evm.StateDB.DiscardSnapshot(snapshot)
}
your contract call may call another 20 contracts internally and if the last contract returns an error (the err
variable) then it the functionRevertToSnapshot()
will be called and all state changes will be reverted back as nothing happened. The snapshot is basically a transaction log, all changes to balances are tracked using a "journal", for example this is how it works (in file core/state/state_object.go)
func (s *stateObject) SetBalance(amount *big.Int) {
s.db.journal.append(balanceChange{
account: &s.address,
prev: new(big.Int).Set(s.data.Balance),
})
s.setBalance(amount) }
as you can see, before it changes the balance, the value is stored in a journal. This is how it knows how to revert all the changes to the beginning of your transaction in case an error happens. RevertToSnapshot()
function will sequentially rollback all the changes
Now if you have a good eye, you might note that your reminding gas is cleared to 0:
gas = 0
this is actually gas "robbery" because you will have some remaining gas there, but it is hardcoded to disappear in case your transaction reverts. I don't know why your reminding gas is taken, but this is how it works, so be careful when you specify too much gas, it will disappear on any REVERT