stopAndReturnStateDiff
Signature
enum AccountAccessKind {
Call,
DelegateCall,
CallCode,
StaticCall,
Create,
SelfDestruct,
Resume,
Balance,
Extcodesize,
Extcodehash,
Extcodecopy
}
struct ChainInfo {
uint256 forkId;
uint256 chainId;
}
struct AccountAccess {
ChainInfo chainInfo;
AccountAccessKind kind;
address account;
address accessor;
bool initialized;
uint256 oldBalance;
uint256 newBalance;
bytes deployedCode;
uint256 value;
bytes data;
bool reverted;
StorageAccess[] storageAccesses;
uint64 depth;
}
struct StorageAccess {
address account;
bytes32 slot;
bool isWrite;
bytes32 previousValue;
bytes32 newValue;
bool reverted;
}
function stopAndReturnStateDiff() external returns (AccountAccess[] memory accesses);
Description
Retrieves state changes recorded after a call to startStateDiffRecording
. This function will consume the recorded state diffs when called and disable state diff recording. One may call startStateDiffRecording
to resume recording.
There are two types of state change records; account accesses and storage accesses represented as AccountAccess
and StorageAccess
.
Account state changes (AccountAccess
) are recorded at the start of a new EVM context; i.e. induced by the various CREATE, CALL and SELFDESTRUCT operations.
An AccountAccess
record contain storage accesses, represented as StorageAccess
, that occurred before it was preempted via sub-calls or create operations.
The ordering of AccountAccess
records reflect the EVM execution order of their associated operations. An AccountAccess
is created whenever an EVM context is created or resumed.
If a sub-context is created, a Resume
AccountAccess
is recorded to indicate that a previous AccountAccess
that was pre-empted has been resumed.
AccountAccessKind
The kind of account access that determines the account
that was accessed. This is typically designated by the EVM operation that initiated the account’s execution context.
If kind is Call
, DelegateCall
, StaticCall
or CallCode
, then the account
is the callee.
If kind is Create, then the account is the newly created account.
If kind is SelfDestruct, then the account is the selfdestruct recipient.
If kind is a Resume, then account represents an execution context that had resumed.
Call
- The account was calledDelegateCall
- The account was called via delegate callCallCode
- The account was called via callcodeStaticCall
- The account was called via staticcallCreate
- The account was createdSelfDestruct
- The account was selfdestructedResume
- Indicates that a previously pre-emptyed account access was resumedBalance
- The account’s codesize was readExtcodesize
- The account’s codesize was readExtcodehash
- The account’s codehash was readExtcodecopy
- The account’s code was copied
AccountAccess
chainInfo
- The chain and fork the accessed occurred.kind
- The kind of account access. This determines how to interpret theAccountAccess
account
- The account that was accessed. It’s the account created forAccountAccessKind.Create
. In the case of anAccountAccessKind.SelfDestruct
, it’s the selfdestruct recipient. For all other types ofAccountAccessKind
, it’s the account of the current EVM context.accessor
- What accessedaccount
. That is either the account creator, caller or the account being selfdestructed.initialized
- If the account was initialized or empty prior to the access. An account is considered initialized if it has code, a non-zero nonce, or a non-zero balance.oldBalance
: The previous balance of the accessedaccount
.newBalance
- The potential new balance of the accessed account. That is, all balance changes are recorded here, even if reverts occurred.deployedCode
- Code of theaccount
deployed in the case ofAccountAccessKind.Create
. This field is empty For all other account access kinds.value
- The value passed along with the account access.data
- Input data provided (i.e.msg.data
) in the case of aCREATE
orCALL
type access.reverted
- If this access reverted in either the current or parent context.storageAccesses
- An ordered list of storage accesses made while the account access is non-preemptive.depth
- Call depth traversed during the recording of state differences.
StorageAccess
The storage accesses made during an AccountAccess
. StorageAccess
cannot exist without an associated AccountAccess
. This means that when state diffs begins on the given context, storage accesses made during that context are not recorded as the context (but not its sub-contexts) isn’t recorded.
StorageAccess
contains the following fields:
account
- A account whose storage was accessedslot
- The slot that was accessedisWrite
- If the access was a writepreviousValue
- The value of the slot prior to this storage accessnewValue
- The value of the slot after this storage accessreverted
- If this access was reverted
Resumed AccountAccess
This type of AccountAccess is generated when a sub-context returns to its parent context. It retains the same values as the original context, including accessor
, account
, initialized
, storageAccesses
, and reverted
.
The following control flow table illustrate how Resume AccountAccesses are recorded.
Step in Contract A’s alpha() | Step in Contract B’s beta() | AccountAccess Records State |
---|---|---|
Call A.alpha() | [A.call] | |
Access state | [A.call[A.access]] | |
Call B.beta() | B.beta() begins | [A.call[A.access], B.call] |
(Execution Paused) | Access state | [A.call[A.access], B.call[B.access]] |
Return | ||
Resume execution | (Return to A.alpha()) | [A.call[A.access], B.call[B.access]] |
Access state | [ A.call[A.access], B.call[B.access], A.resume[A.access’] ] |
ℹ️ Note
A Resumed AccountAccess is created only if storage accesses occurred after a context was resumed.
Example: Recording storage state changes during a CREATE operation
contract Contract {
uint256 internal _reserved;
uint256 public data;
constructor(uint _data) payable { data = _data; }
}
vm.startStateDiffRecording();
Contract contract = new Contract{value: 1 ether}(100);
Vm.AccountAccess[] memory records = vm.stopAndReturnStateDiff();
assertEq(records.length, 1);
assertEq(records[0].kind, Vm.AccountAccessKind.Create);
assertEq(records[0].account, address(contract));
assertEq(records[0].accessor, address(this));
assertEq(records[0].initialized, true);
assertEq(records[0].oldBalance, 0);
assertEq(records[0].newBalance, 1 ether);
assertEq(records[0].deployedCode, address(contract).code);
assertEq(records[0].value, 1 ether);
assertEq(records[0].data, abi.encodePacked(type(Contract).creationCode, (uint(100))));
assertEq(records[0].reverted, false);
assertEq(records[0].storageAccesses.length, 1);
assertEq(records[0].storageAccesses[0].account, address(contract));
assertEq(records[0].storageAccesses[0].slot, bytes32(uint256(1)));
assertEq(records[0].storageAccesses[0].isWrite, true);
assertEq(records[0].storageAccesses[0].previousValue, bytes32(uint(0)));
assertEq(records[0].storageAccesses[0].newValue, bytes32(uint(100)));
assertEq(records[0].storageAccesses[0].reverted, false);
Note that there are no Resume
account accesses in this example.
Example: Resumed Account Access
contract Foo {
Bar b;
uint256 public val;
constructor(Bar _b) { b = _b; }
function run() external {
val = val + 1;
b.run();
val = val + 1;
}
}
contract Bar {
function run() external {}
}
Bar bar = new Bar();
Foo foo = new Foo(bar);
vm.startStateDiffRecording();
foo.run();
Vm.AccountAccess[] memory records = vm.stopAndReturnStateDiff();
assertEq(records.length, 3);
Vm.AccountAccess memory fooCall = records[0];
assertEq(fooCall.kind, Vm.AccountAccessKind.Call);
assertEq(fooCall.account, address(foo));
assertEq(fooCall.accessor, address(this));
// foo.val increment
assertEq(fooCall.storageAccesses.length, 2);
assertEq(fooCall.storageAccesses[0].isWrite, false);
assertEq(fooCall.storageAccesses[1].isWrite, true);
assertEq(fooCall.storageAccesses[1].oldValue, bytes32(uint(0)));
assertEq(fooCall.storageAccesses[1].newValue, bytes32(uint(1)));
// bar.run CALL
Vm.AccountAccess memory barCall = records[1];
assertEq(barCall.kind, Vm.AccountAccessKind.Call);
assertEq(barCall.account, address(bar));
assertEq(barCall.accessor, address(foo));
// foo.run RESUME
Vm.AccountAccess memory fooResume = records[2];
assertEq(fooResume.kind, Vm.AccountAccessKind.Resume);
// foo.val increment
assertEq(fooResume.storageAccesses.length, 2);
assertEq(fooResume.storageAccesses[0].isWrite, false);
assertEq(fooResume.storageAccesses[1].isWrite, true);
assertEq(fooResume.storageAccesses[1].oldValue, bytes32(uint(1)));
assertEq(fooResume.storageAccesses[1].newValue, bytes32(uint(2)));