Skip to content

Commit

Permalink
prank->impersonate, add throw tests, respond to PR comments
Browse files Browse the repository at this point in the history
  • Loading branch information
rahul-kothari committed Aug 17, 2023
1 parent b18abbe commit 4cb3ebf
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 45 deletions.
85 changes: 47 additions & 38 deletions docs/docs/dev_docs/testing/cheat_codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: Cheat Codes

## Introduction

Most of the time, simply testing your smart contract isn't enough. To manipulate the state of the Aztec blockchain, as well as test for specific reverts and events, the sandbox is shipped with a set of cheatcodes.
To help with testing, the sandbox is shipped with a set of cheatcodes.

Cheatcodes allow you to change the time of the L2 block, load certain state or more easily manipulate L1 instead of having to write dedicated RPC calls to anvil.

Expand All @@ -17,9 +17,6 @@ If you aren't familiar with [Anvil](https://book.getfoundry.sh/anvil/), we recom
The guide will cover how to manipulate the state of the:
- L1 blockchain;
- Aztec network.
### Why is this useful?

<!-- Contextualise why a dapp developer needs this. What use cases / products / features does this unlock? Any real world examples? -->

### Dependencies

Expand Down Expand Up @@ -56,7 +53,7 @@ public async mine(numberOfBlocks = 1): Promise<void>
// Set the timestamp for the next block on L1.
public async setNextBlockTimestamp(timestamp: number): Promise<void>
// Dumps the current L1 chain state to a given file. Can be used with `loadChainState()` to first create a forked version of mainnet on anvil (to for example interact with uniswap), save the chain state to a file, and later load it to the sandbox' anvil instance.
// Dumps the current L1 chain state to a given file.
public async dumpChainState(fileName: string): Promise<void>
// Loads the L1 chain state from a file. You may use `dumpChainState()` to save the state of the L1 chain to a file and later load it.
Expand All @@ -72,10 +69,10 @@ public async store(contract: EthAddress, slot: bigint, value: bigint): Promise<v
public keccak256(baseSlot: bigint, key: bigint): bigint
// Let you send transactions on L1 impersonating an externally owned or contract, without knowing the private key.
public async startPrank(who: EthAddress): Promise<void>
public async startImpersonating(who: EthAddress): Promise<void>
// Stop impersonating an account on L1 that you are currently impersonating.
public async stopPrank(who: EthAddress): Promise<void>
public async stopImpersonating(who: EthAddress): Promise<void>
// Set the bytecode for a L1 contract
public async etch(contract: EthAddress, bytecode: `0x${string}`): Promise<void>
Expand Down Expand Up @@ -159,7 +156,8 @@ Remember that timestamp can only be set in the future and not in the past.

#### Example
```js
await cc.l1.setNextBlockTimestamp(1692183270) // Set next block timestamp to 16 Aug 2023 10:54:30 GMT
// // Set next block timestamp to 16 Aug 2023 10:54:30 GMT
await cc.l1.setNextBlockTimestamp(1692183270)
// next transaction you will do will have the timestamp as 1692183270
```

Expand All @@ -172,7 +170,8 @@ public async dumpChainState(fileName: string): Promise<void>

#### Description
Dumps the current L1 chain state to a file.
Returns a hex string representing the complete state of the chain. Can be re-imported into a fresh/restarted instance of Anvil to reattain the same state.
Stores a hex string representing the complete state of the chain in a file with the provided path. Can be re-imported into a fresh/restarted instance of Anvil to reattain the same state.
When combined with `loadChainState()` cheatcode, it can be let you easily import the current state of mainnet into the Anvil instance of the sandbox, to use Uniswap for example.

#### Example
```js
Expand All @@ -187,8 +186,8 @@ public async loadChainState(fileName: string): Promise<void>
```

#### Description
Loads the L1 chain state from a file.
When given a hex string previously returned by `cc.l1.dumpChainState()`, merges the contents into the current chain state. Will overwrite any colliding accounts/storage slots.
Loads the L1 chain state from a file which contains a hex string representing an L1 state.
When given a file previously written to by `cc.l1.dumpChainState()`, it merges the contents into the current chain state. Will overwrite any colliding accounts/storage slots.

#### Example
```js
Expand All @@ -211,7 +210,8 @@ Loads the value at a storage slot of a L1 contract.
/// uint256 private leet = 1337; // slot 0
/// }
const value = await cc.l1.load(EthAddress.from(leetContractAddress), BigInt(0));
const leetContractAddress = EthAddress.fromString('0x1234...');
const value = await cc.l1.load(leetContractAddress, BigInt(0));
console.log(value); // 1337
```

Expand All @@ -231,8 +231,9 @@ Stores the value in storage slot on a L1 contract.
/// uint256 private leet = 1337; // slot 0
/// }
await cc.l1.store(EthAddress.from(leetContractAddress), BigInt(0), BigInt(1000));
const value = await cc.l1.load(EthAddress.from(leetContractAddress), BigInt(0));
const leetContractAddress = EthAddress.fromString('0x1234...');
await cc.l1.store(leetContractAddress, BigInt(0), BigInt(1000));
const value = await cc.l1.load(leetContractAddress, BigInt(0));
console.log(value); // 1000
```

Expand Down Expand Up @@ -260,36 +261,36 @@ const slot = cc.l1.keccak256(1n, address);
await cc.l1.store(contractAddress, slot, 100n);
```

### startPrank
### startImpersonating

#### Function Signature
```js
public async startPrank(who: EthAddress): Promise<void>
public async startImpersonating(who: EthAddress): Promise<void>
```

#### Description
Start impersonating an L1 account.
Sets msg.sender for all subsequent calls until stopPrank is called.
This allows you to use this address as a sender.

#### Example
```js
await cc.l1.startPrank(EthAddress.fromString(address));
await cc.l1.startImpersonating(EthAddress.fromString(address));
```

### stopPrank
### stopImpersonating

#### Function Signature
```js
public async stopPrank(who: EthAddress): Promise<void>
public async stopImpersonating(who: EthAddress): Promise<void>
```

#### Description
Stop impersonating an L1 account.
Stops an active prank started by startPrank, resetting msg.sender to the values before startPrank was called.
Stops an active impersonation started by startImpersonating.

#### Example
```js
await cc.l1.stopPrank(EthAddress.fromString(address))
await cc.l1.stopImpersonating(EthAddress.fromString(address))
```

### getBytecode
Expand Down Expand Up @@ -377,73 +378,81 @@ await cc.l2.warp(newTimestamp);
// any noir contract calls that make use of current timestamp and is executed in the next rollup block will now read `newTimestamp`
```

### loadPublic
### computeSlotInMap

#### Function Signature
```js
public async loadPublic(who: AztecAddress, slot: Fr | bigint): Promise<Fr>
public computeSlotInMap(baseSlot: Fr | bigint, key: Fr | bigint): Fr
```

#### Description
Loads the value stored at the given slot in the public storage of the given contract.
Compute storage slot for a map key.
The baseSlot is specified in the noir contract.

#### Example
#### Example
```
/// struct Storage {
/// current_value: PublicState<Field, FIELD_SERIALISED_LEN>,
/// // highlight-next-line:PublicState
/// balances: Map<PublicState<Field, FIELD_SERIALISED_LEN>>,
/// }
///
/// impl Storage {
/// fn init() -> Self {
/// Storage {
/// current_value: PublicState::new(1, FieldSerialisationMethods),
/// balances: Map::new(1, |slot| PublicState::new(slot, FieldSerialisationMethods)),
/// }
/// }
/// }
///
/// contract Hello {
///
/// contract Token {
/// ...
/// }
const value = await cc.l2.loadPublic(contract, 1n) // current_value is stored in slot 1
const slot = cc.l2.computeSlotInMap(1n, key)
```

### computeSlotInMap
### loadPublic

#### Function Signature
```js
public computeSlotInMap(baseSlot: Fr | bigint, key: Fr | bigint): Fr
public async loadPublic(who: AztecAddress, slot: Fr | bigint): Promise<Fr>
```

#### Description
Compute storage slot for a map key.
The baseSlot is specified in the noir contract.
Loads the value stored at the given slot in the public storage of the given contract.

#### Example
Note: One Field element occupies a storage slot. So structs with multiple field elements won't fit in a single slot. So using loadPublic would only load a part of the struct (depending on the size of the attributes within it).

#### Example
```
/// struct Storage {
/// balances: Map<EasyPrivateUint>,
/// // highlight-next-line:PublicState
/// balances: Map<PublicState<Field, FIELD_SERIALISED_LEN>>,
/// }
///
/// impl Storage {
/// fn init() -> Self {
/// Storage {
/// balances: Map::new(1, |slot| EasyPrivateUint::new(slot)),
/// balances: Map::new(1, |slot| PublicState::new(slot, FieldSerialisationMethods)),
/// }
/// }
/// }
///
///
/// contract Token {
/// ...
/// }
const address = AztecAddress.fromString("0x123...")
const slot = cc.l2.computeSlotInMap(1n, key)
const value = await cc.l2.loadPublic(address, slot);
```
## Participate

Keep up with the latest discussion and join the conversation in the [Aztec forum](https://discourse.aztec.network).

You can also use the above link to request for more cheatcodes.

import Disclaimer from "../misc/common/\_disclaimer.mdx";
import Disclaimer from "../../misc/common/\_disclaimer.mdx";
<Disclaimer/>
22 changes: 19 additions & 3 deletions yarn-project/end-to-end/src/e2e_cheat_codes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ describe('e2e_cheat_codes', () => {
expect(await cc.l1.timestamp()).toBe(timestamp + increment);
});

it('setNextBlockTimestamp to a past timestamp throws', async () => {
const timestamp = await cc.l1.timestamp();
const pastTimestamp = timestamp - 1000;
await expect(async () => await cc.l1.setNextBlockTimestamp(pastTimestamp)).rejects.toThrow(
`Error setting next block timestamp: Timestamp error: ${pastTimestamp} is lower than or equal to previous block's timestamp`,
);
});

it('load a value at a particular storage slot', async () => {
// check that storage slot 0 is empty as expected
const res = await cc.l1.load(EthAddress.ZERO, 0n);
Expand Down Expand Up @@ -106,7 +114,7 @@ describe('e2e_cheat_codes', () => {
const beforeBalance = await publicClient.getBalance({ address: randomAddress });

// impersonate random address
await cc.l1.startPrank(EthAddress.fromString(randomAddress));
await cc.l1.startImpersonating(EthAddress.fromString(randomAddress));
// send funds from random address
const amountToSend = parseEther('0.1');
const txHash = await walletClient.sendTransaction({
Expand All @@ -119,7 +127,7 @@ describe('e2e_cheat_codes', () => {
expect(await publicClient.getBalance({ address: randomAddress })).toBe(beforeBalance - amountToSend - feePaid);

// stop impersonating
await cc.l1.stopPrank(EthAddress.fromString(randomAddress));
await cc.l1.stopImpersonating(EthAddress.fromString(randomAddress));

// making calls from random address should not be successful
try {
Expand All @@ -135,7 +143,7 @@ describe('e2e_cheat_codes', () => {
}
});

it('can modify L1 block time', async () => {
it('can modify L2 block time', async () => {
// deploy lending contract
const tx = LendingContract.deploy(aztecRpcServer).send();
await tx.isMined({ interval: 0.1 });
Expand Down Expand Up @@ -165,5 +173,13 @@ describe('e2e_cheat_codes', () => {
expect(Number(await rollup.read.lastBlockTs())).toEqual(newTimestamp);
expect;
}, 50_000);

it('should throw if setting L2 block time to a past timestamp', async () => {
const timestamp = await cc.l1.timestamp();
const pastTimestamp = timestamp - 1000;
await expect(async () => await cc.l2.warp(pastTimestamp)).rejects.toThrow(
`Error setting next block timestamp: Timestamp error: ${pastTimestamp} is lower than or equal to previous block's timestamp`,
);
});
});
});
8 changes: 4 additions & 4 deletions yarn-project/end-to-end/src/fixtures/cheat_codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,19 +170,19 @@ export class L1CheatCodes {
* Send transactions impersonating an externally owned account or contract.
* @param who - The address to impersonate
*/
public async startPrank(who: EthAddress): Promise<void> {
public async startImpersonating(who: EthAddress): Promise<void> {
const res = await this.rpcCall('anvil_impersonateAccount', [who.toString()]);
if (res.error) throw new Error(`Error pranking ${who}: ${res.error.message}`);
if (res.error) throw new Error(`Error impersonating ${who}: ${res.error.message}`);
this.logger(`Impersonating ${who}`);
}

/**
* Stop impersonating an account that you are currently impersonating.
* @param who - The address to stop impersonating
*/
public async stopPrank(who: EthAddress): Promise<void> {
public async stopImpersonating(who: EthAddress): Promise<void> {
const res = await this.rpcCall('anvil_stopImpersonatingAccount', [who.toString()]);
if (res.error) throw new Error(`Error pranking ${who}: ${res.error.message}`);
if (res.error) throw new Error(`Error when stopping the impersonation of ${who}: ${res.error.message}`);
this.logger(`Stopped impersonating ${who}`);
}

Expand Down

0 comments on commit 4cb3ebf

Please sign in to comment.