Skip to content

Commit

Permalink
feat: add support for counterfactual contract calls (#2405)
Browse files Browse the repository at this point in the history
* feat: add support for counterfactual contract calls

* chore: update comment

* docs

* chore: tweaks

* feat: read contract

* feat: getEip712Domain

* chore: changeset

* docs

* chore: exports

* chore: ci

* fix: types
  • Loading branch information
jxom authored Jun 14, 2024
1 parent 7503ce2 commit fc8919f
Show file tree
Hide file tree
Showing 17 changed files with 575 additions and 22 deletions.
7 changes: 7 additions & 0 deletions .changeset/poor-cats-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"viem": minor
---

Added `factory` & `factoryData` parameters to `call` & `readContract` to enable "deployless counterfactual calls" (calling a function on a contract which has not been deployed).

This is particularly useful for the use case of calling functions on [ERC-4337 Smart Accounts](https://eips.ethereum.org/EIPS/eip-4337) that have not been deployed yet.
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
{
"name": "viem (esm)",
"path": "./src/_esm/index.js",
"limit": "59.1 kB",
"limit": "59.6 kB",
"import": "*"
},
{
Expand Down Expand Up @@ -137,7 +137,7 @@
{
"name": "viem/actions",
"path": "./src/_esm/ens/index.js",
"limit": "45 kB",
"limit": "46.5 kB",
"import": "*"
},
{
Expand Down Expand Up @@ -167,25 +167,25 @@
{
"name": "viem/ens",
"path": "./src/_esm/ens/index.js",
"limit": "45 kB",
"limit": "46.5 kB",
"import": "*"
},
{
"name": "viem/ens (tree-shaking)",
"path": "./src/_esm/ens/index.js",
"limit": "19 kB",
"limit": "22.3 kB",
"import": "{ getEnsAvatar }"
},
{
"name": "viem/siwe",
"path": "./src/_esm/siwe/index.js",
"limit": "28 kB",
"limit": "30.3 kB",
"import": "*"
},
{
"name": "viem/siwe (tree-shaking)",
"path": "./src/_esm/siwe/index.js",
"limit": "27 kB",
"limit": "29.5 kB",
"import": "{ verifySiweMessage }"
}
]
Expand Down
95 changes: 95 additions & 0 deletions site/pages/docs/actions/public/call.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,67 @@ export const publicClient = createPublicClient({

:::

### Deployless Counterfactual Calls

It is possible to call a function on a contract that has not been deployed yet. For instance, we may want
to call a function on an [ERC-4337 Smart Account](https://eips.ethereum.org/EIPS/eip-4337) contract which has not been deployed.

Viem utilizes a **Deployless Counterfactual Call** pattern to:
1. "temporarily deploy" a contract (e.g. a Smart Account) with a provided [Deployment Factory Contract](https://docs.alchemy.com/docs/create2-an-alternative-to-deriving-contract-addresses#create2-contract-factory) address ([`factory`](#factory-optional)) with deployment arguments ([`factoryData`](#factorydata-optional)),
2. Call the function on the "temporarily deployed" contract ([`to`](#to-optional)).

The example below demonstrates how we can utilize this pattern to call the `entryPoint` function on an [ERC-4337 Smart Account](https://eips.ethereum.org/EIPS/eip-4337) which has not been deployed:

:::code-group

```ts twoslash [example.ts]
import { encodeFunctionData, parseAbi } from 'viem'
import { account, publicClient } from './config'

const data = await publicClient.call({
// Address of the contract deployer (e.g. Smart Account Factory).
factory: '0xE8Df82fA4E10e6A12a9Dab552bceA2acd26De9bb',

// Function to execute on the factory to deploy the contract.
factoryData: encodeFunctionData({
abi: parseAbi(['function createAccount(address owner, uint256 salt)']),
functionName: 'createAccount',
args: [account, 0n],
}),

// Function to call on the contract (e.g. Smart Account contract).
data: encodeFunctionData({
abi: parseAbi(['function entryPoint() view returns (address)']),
functionName: 'entryPoint'
}),

// Address of the contract.
to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
})
```

```ts twoslash [config.ts] filename="config.ts"
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'

export const account = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266'

export const publicClient = createPublicClient({
chain: mainnet,
transport: http()
})
```

:::

:::note
This example utilizes the [SimpleAccountFactory](https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/samples/SimpleAccountFactory.sol).
:::

:::tip
The **Deployless Counterfactual Call** pattern (and `factory` + `factoryData` parameters) is also accessible via the [`readContract`](/docs/contract/readContract#deployless-counterfactual-reads) & [Contract Instance](http://localhost:5173/docs/contract/getContract) APIs.
:::

## Returns

`0x${string}`
Expand Down Expand Up @@ -150,6 +211,40 @@ const data = await publicClient.call({
})
```

### factory (optional)

- **Type:**

Contract deployment factory address (ie. Create2 factory, Smart Account factory, etc).

```ts twoslash
// [!include ~/snippets/publicClient.ts]
// ---cut---
const data = await publicClient.call({
account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
factory: '0x0000000000ffe8b47b3e2130213b802212439497', // [!code focus]
factoryData: '0xdeadbeef',
to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
})
```

### factoryData (optional)

- **Type:**

Calldata to execute on the factory to deploy the contract.

```ts twoslash
// [!include ~/snippets/publicClient.ts]
// ---cut---
const data = await publicClient.call({
account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
factory: '0x0000000000ffe8b47b3e2130213b802212439497',
factoryData: '0xdeadbeef', // [!code focus]
to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
})
```

### gas (optional)

- **Type:** `bigint`
Expand Down
69 changes: 69 additions & 0 deletions site/pages/docs/actions/public/getEip712Domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,43 @@ export const publicClient = createPublicClient({

:::

### Counterfactual Call

It is possible to read the EIP-712 domain on a contract that **has not been deployed** by providing deployment factory (`factory` + `factoryData`) parameters:

:::code-group

```ts twoslash [example.ts]
import { factory, publicClient } from './config'

const { domain, extensions, fields } = await publicClient.getEip712Domain({
address: '0x57ba3ec8df619d4d243ce439551cce713bb17411',
factory: factory.address,
factoryData: encodeFunctionData({
abi: factory.abi,
functionName: 'createAccount',
args: ['0x0000000000000000000000000000000000000000', 0n]
}),
})
```

```ts [client.ts] filename="config.ts"
import { createPublicClient, http, parseAbi } from 'viem'
import { mainnet } from 'viem/chains'

export const factory = {
address: '0xE8Df82fA4E10e6A12a9Dab552bceA2acd26De9bb',
abi: parseAbi(['function createAccount(address owner, uint256 salt)']),
} as const

export const publicClient = createPublicClient({
chain: mainnet,
transport: http()
})
```

:::

## Returns

`GetEip712DomainReturnType`
Expand All @@ -50,4 +87,36 @@ The address of the contract to read the EIP-712 domain from.
const result = await publicClient.getEip712Domain({
address: '0x57ba3ec8df619d4d243ce439551cce713bb17411', // [!code focus]
})
```

### factory (optional)

- **Type:**

Contract deployment factory address (ie. Create2 factory, Smart Account factory, etc).

```ts twoslash
// [!include ~/snippets/publicClient.ts]
// ---cut---
const result = await publicClient.getEip712Domain({
address: '0x57ba3ec8df619d4d243ce439551cce713bb17411',
factory: '0xE8Df82fA4E10e6A12a9Dab552bceA2acd26De9bb', // [!code focus]
factoryData: '0xdeadbeef',
})
```

### factoryData (optional)

- **Type:**

Calldata to execute on the factory to deploy the contract.

```ts twoslash
// [!include ~/snippets/publicClient.ts]
// ---cut---
const result = await publicClient.getEip712Domain({
address: '0x57ba3ec8df619d4d243ce439551cce713bb17411',
factory: '0xE8Df82fA4E10e6A12a9Dab552bceA2acd26De9bb',
factoryData: '0xdeadbeef', // [!code focus]
})
```
88 changes: 88 additions & 0 deletions site/pages/docs/contract/readContract.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,62 @@ export const publicClient = createPublicClient({

:::

### Deployless Counterfactual Reads

It is possible to call a function on a contract that has not been deployed yet. For instance, we may want
to call a function on an [ERC-4337 Smart Account](https://eips.ethereum.org/EIPS/eip-4337) contract which has not been deployed.

Viem utilizes a **Deployless Counterfactual Read** pattern to:
1. "temporarily deploy" a contract (e.g. a Smart Account) with a provided [Deployment Factory Contract](https://docs.alchemy.com/docs/create2-an-alternative-to-deriving-contract-addresses#create2-contract-factory) address ([`factory`](#factory-optional)) with deployment arguments ([`factoryData`](#factorydata-optional)),
2. Call the function on the "temporarily deployed" contract ([`address`](#address)).

The example below demonstrates how we can utilize this pattern to call the `entryPoint` function on an [ERC-4337 Smart Account](https://eips.ethereum.org/EIPS/eip-4337) which has not been deployed:

:::code-group

```ts twoslash [example.ts]
import { encodeFunctionData, parseAbi } from 'viem'
import { account, publicClient } from './config'

const data = await publicClient.readContract({
// Address of the Smart Account deployer (factory).
factory: '0xE8Df82fA4E10e6A12a9Dab552bceA2acd26De9bb',

// Function to execute on the factory to deploy the Smart Account.
factoryData: encodeFunctionData({
abi: parseAbi(['function createAccount(address owner, uint256 salt)']),
functionName: 'createAccount',
args: [account, 0n],
}),

// Function to call on the Smart Account.
abi: account.abi,
address: account.address,
functionName: 'entryPoint',
})
```

```ts twoslash [config.ts] filename="config.ts"
import { createPublicClient, http, parseAbi } from 'viem'
import { mainnet } from 'viem/chains'

export const account = {
address: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
abi: parseAbi(['function entryPoint() view returns (address)'])
} as const

export const publicClient = createPublicClient({
chain: mainnet,
transport: http()
})
```

:::

:::note
This example utilizes the [SimpleAccountFactory](https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/samples/SimpleAccountFactory.sol).
:::

## Return Value

The response from the contract. Type is inferred.
Expand Down Expand Up @@ -213,6 +269,38 @@ const data = await publicClient.readContract({
})
```

### factory (optional)

- **Type:**

Contract deployment factory address (ie. Create2 factory, Smart Account factory, etc).

```ts twoslash
const data = await publicClient.readContract({
address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2',
abi: wagmiAbi,
functionName: 'totalSupply',
factory: '0x0000000000ffe8b47b3e2130213b802212439497', // [!code focus]
factoryData: '0xdeadbeef',
})
```

### factoryData (optional)

- **Type:**

Calldata to execute on the factory to deploy the contract.

```ts twoslash
const data = await publicClient.readContract({
address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2',
abi: wagmiAbi,
functionName: 'totalSupply',
factory: '0x0000000000ffe8b47b3e2130213b802212439497',
factoryData: '0xdeadbeef', // [!code focus]
})
```

### stateOverride (optional)

- **Type:** [`StateOverride`](/docs/glossary/types#stateoverride)
Expand Down
Loading

0 comments on commit fc8919f

Please sign in to comment.