diff --git a/client-sdk/hardhat-3-plugin/client.mdx b/client-sdk/hardhat-3-plugin/client.mdx
new file mode 100644
index 0000000..e5b855f
--- /dev/null
+++ b/client-sdk/hardhat-3-plugin/client.mdx
@@ -0,0 +1,101 @@
+---
+title: Client
+description: "Create and connect a CofheClient inside a Hardhat 3 test"
+---
+
+The plugin extends every Hardhat 3 connection with a `cofhe` namespace. There are three ways to create and connect a `CofheClient` from a test, in increasing order of control.
+
+## Batteries included (recommended)
+
+`cofhe.createClientWithBatteries(walletClient?)` is the one-call setup. It:
+
+1. Creates a CoFHE config with `environment: 'hardhat'`.
+2. Creates a `CofheClient`.
+3. Connects it using the wallet client you pass (or the first wallet client on the connection if you don't).
+4. Generates and signs a self permit so the client can immediately decrypt encrypted values.
+
+```typescript
+import { describe, it } from 'node:test';
+import { network } from 'hardhat';
+
+describe('My FHE contract', async () => {
+ const { viem, cofhe } = await network.connect();
+ const [walletClient] = await viem.getWalletClients();
+
+ it('decrypts a value', async () => {
+ const client = await cofhe.createClientWithBatteries(walletClient);
+ client.connected; // true
+ });
+});
+```
+
+Or call it with no arguments to use the first wallet client on the connection:
+
+```typescript
+const client = await cofhe.createClientWithBatteries();
+```
+
+
+Because the self permit is created up-front, encrypt / decrypt / decryptForView / decryptForTx all work immediately without any additional setup.
+
+
+## Manual setup
+
+For more control — custom config options, multiple signers, adjusting `encryptDelay` — set up the client step by step.
+
+
+
+
+
+```typescript
+const config = await cofhe.createConfig();
+// With overrides:
+const config = await cofhe.createConfig({ mocks: { encryptDelay: 0 } });
+```
+
+`cofhe.createConfig` wraps `createCofheConfig` from `@cofhe/sdk/node` with two Hardhat-3-specific additions:
+
+- Sets `environment: 'hardhat'` automatically.
+- Defaults `mocks.encryptDelay` to `0` so tests run without artificial wait times.
+
+
+
+
+
+```typescript
+const client = cofhe.createClient(config);
+```
+
+The client is **not connected yet** — call `client.connect(...)` before using it.
+
+
+
+
+
+```typescript
+const publicClient = await viem.getPublicClient();
+const [walletClient] = await viem.getWalletClients();
+
+await client.connect(publicClient, walletClient);
+```
+
+
+
+
+
+## API summary
+
+| Method | Returns | Description |
+| --- | --- | --- |
+| `cofhe.createConfig(overrides?)` | `Promise` | CoFHE config pre-wired for the Hardhat mock environment (`environment: 'hardhat'`, `mocks.encryptDelay: 0`). |
+| `cofhe.createClient(config)` | `CofheClient` | Unconnected client. Call `client.connect(publicClient, walletClient)` before use. |
+| `cofhe.createClientWithBatteries(walletClient?)` | `Promise` | Fully configured + connected + self-permit signed. Defaults to the first wallet client on the connection. |
+
+## Using the client
+
+Once connected, the client works identically to the standard SDK client. See:
+
+- [Encrypting Inputs](/client-sdk/guides/encrypting-inputs)
+- [Decrypt to View](/client-sdk/guides/decrypt-to-view)
+- [Decrypt to Transact](/client-sdk/guides/decrypt-to-tx)
+- [Permits](/client-sdk/guides/permits)
diff --git a/client-sdk/hardhat-3-plugin/getting-started.mdx b/client-sdk/hardhat-3-plugin/getting-started.mdx
new file mode 100644
index 0000000..6a96c78
--- /dev/null
+++ b/client-sdk/hardhat-3-plugin/getting-started.mdx
@@ -0,0 +1,140 @@
+---
+title: Getting Started
+description: "Set up @cofhe/hardhat-3-plugin for local FHE contract development under Hardhat 3"
+---
+
+`@cofhe/hardhat-3-plugin` is the Hardhat 3 counterpart to [`@cofhe/hardhat-plugin`](/client-sdk/hardhat-plugin/getting-started). It deploys the CoFHE mock contracts to an in-process Hardhat network on every `network.connect()` call and exposes a `cofhe` namespace on the connection object — ready to use immediately, no boilerplate required.
+
+
+Use this plugin if you've adopted the [Hardhat 3 plugin/hook model](https://hardhat.org). For Hardhat v2 projects, use [`@cofhe/hardhat-plugin`](/client-sdk/hardhat-plugin/getting-started) instead.
+
+
+## What the plugin provides
+
+- **Auto-deploys mocks** on every `network.connect()` — `MockTaskManager`, `MockACL`, `MockZkVerifier`, `MockThresholdNetwork`, and `TestBed`.
+- **`conn.cofhe` namespace** on the Hardhat 3 connection object (sits alongside `conn.viem` from `@nomicfoundation/hardhat-viem`).
+- **`cofhe.createClientWithBatteries()`** — one-call SDK client setup with a pre-signed self permit.
+- **Mock helpers** — Viem contract descriptors for every mock, plaintext inspection (`getPlaintext` / `expectPlaintext`), and logging control.
+
+## Installation
+
+
+
+
+
+
+
+```bash npm
+npm install @cofhe/hardhat-3-plugin @cofhe/sdk @fhenixprotocol/cofhe-contracts
+```
+
+```bash pnpm
+pnpm add @cofhe/hardhat-3-plugin @cofhe/sdk @fhenixprotocol/cofhe-contracts
+```
+
+```bash yarn
+yarn add @cofhe/hardhat-3-plugin @cofhe/sdk @fhenixprotocol/cofhe-contracts
+```
+
+
+
+
+
+
+
+```typescript hardhat.config.ts
+import { defineConfig } from 'hardhat/config';
+import cofhePlugin from '@cofhe/hardhat-3-plugin';
+import hardhatViem from '@nomicfoundation/hardhat-viem';
+import hardhatNodeTestRunner from '@nomicfoundation/hardhat-node-test-runner';
+
+export default defineConfig({
+ plugins: [cofhePlugin, hardhatViem, hardhatNodeTestRunner],
+});
+```
+
+
+
+
+
+## Configuration
+
+The plugin adds an optional `cofhe` key to your Hardhat 3 config. All values shown below are their defaults:
+
+```typescript hardhat.config.ts
+export default defineConfig({
+ plugins: [cofhePlugin, hardhatViem, hardhatNodeTestRunner],
+
+ cofhe: {
+ gasWarning: true, // warn that mock gas costs differ from live FHE
+ logMocks: true, // enable event-based logging in mock contracts
+ mocksDeployVerbosity: 'v', // '' silent | 'v' summary | 'vv' full per-contract
+ },
+});
+```
+
+| Option | Type | Default | Description |
+| --- | --- | --- | --- |
+| `gasWarning` | `boolean` | `true` | Print a one-line warning after mock deployment reminding that mock gas costs differ from live FHE. |
+| `logMocks` | `boolean` | `true` | Enable event-based logging inside mock contracts. |
+| `mocksDeployVerbosity` | `'' \| 'v' \| 'vv'` | `'v'` | How much output to print while deploying mocks. `''` silent, `'v'` one summary line per mock, `'vv'` full per-contract deployment log. |
+
+## How auto-deployment works
+
+Every call to `network.connect()` automatically:
+
+1. Deploys all CoFHE mock contracts to the fresh in-process EVM.
+2. Attaches a `cofhe` namespace to the returned connection object.
+
+Because `network.connect()` is awaitable at the top level of an `async describe`, you can set everything up without lifecycle hooks:
+
+```typescript
+import { describe, it } from 'node:test';
+import { network } from 'hardhat';
+
+describe('My FHE contract', async () => {
+ const { viem, cofhe } = await network.connect();
+ const publicClient = await viem.getPublicClient();
+ const [walletClient] = await viem.getWalletClients();
+
+ it('encrypts and decrypts a value', async () => {
+ const client = await cofhe.createClientWithBatteries(walletClient);
+ // ... test logic
+ });
+});
+```
+
+
+**Why `async describe`?** Hardhat 3's `node:test` runner supports top-level `await` inside the describe callback. This lets you resolve the connection (and deploy mocks) exactly once per test file without needing a `before()` hook.
+
+
+## Mock contracts deployed
+
+| Contract | Address | Description |
+| --- | --- | --- |
+| `MockTaskManager` | `0xeA30c4B8b44078Bbf8a6ef5b9f1eC1626C7848D9` | Coordinates FHE operations and ACL |
+| `MockACL` | dynamic | Access Control List — address resolved from TaskManager |
+| `MockZkVerifier` | `0x0000000000000000000000000000000000005001` | Verifies ZK proofs for encrypted inputs |
+| `MockThresholdNetwork` | `0x0000000000000000000000000000000000005002` | Simulates the threshold decryption network |
+| `TestBed` | `0x0000000000000000000000000000000000005003` | Utility contract for storing and retrieving encrypted values in tests |
+
+Fixed-address contracts are deployed via `hardhat_setCode`, so they are always at the same address regardless of deployment order. `MockACL` is deployed as a normal contract (so its EIP-712 domain constructor runs correctly) and its address is registered in `MockTaskManager`.
+
+## Differences from `@cofhe/hardhat-plugin`
+
+| Concern | `@cofhe/hardhat-plugin` (v2) | `@cofhe/hardhat-3-plugin` |
+| --- | --- | --- |
+| Entry point | `hre.cofhe` (global) | `conn.cofhe` (per `network.connect()` call) |
+| Lifecycle | Pre-task hook (`npx hardhat test` / `node`) | `network.connect()` returns a fresh deployment |
+| Test runner expectation | Mocha-style (`before`, `it`) | `node:test` with `async describe` |
+| Mock compilation for custom-error decoding | Overrides `TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS` | Runs `hre.solidity.build()` during the `hre.created` hook |
+| Bytecode sourcing | `hre.artifacts.readArtifact()` | `hre.artifacts.readArtifact()` (same — for variable-address mocks like `MockACL`) |
+
+The Solidity surface (mock contracts, plaintext semantics, `FHE.verifyDecryptResult` acceptance) is **identical** between the two plugins; the differences are all in how Hardhat hosts the plugin.
+
+## Next steps
+
+- [Client](/client-sdk/hardhat-3-plugin/client) — `conn.cofhe.createConfig`, `createClient`, `createClientWithBatteries`.
+- [Mock Contracts](/client-sdk/hardhat-3-plugin/mock-contracts) — Viem contract descriptors and plaintext inspection helpers.
+- [Logging](/client-sdk/hardhat-3-plugin/logging) — `enableLogs`, `disableLogs`, `withLogs`.
+- [Testing](/client-sdk/hardhat-3-plugin/testing) — end-to-end test patterns.
diff --git a/client-sdk/hardhat-3-plugin/logging.mdx b/client-sdk/hardhat-3-plugin/logging.mdx
new file mode 100644
index 0000000..f76c16a
--- /dev/null
+++ b/client-sdk/hardhat-3-plugin/logging.mdx
@@ -0,0 +1,42 @@
+---
+title: Logging
+description: "Inspect FHE operations in your Hardhat 3 test output"
+---
+
+The mock contracts emit structured logs for every FHE operation. This makes it easy to see what your contracts are doing under the hood during tests.
+
+## `withLogs(name, fn)` — recommended
+
+Wraps a block of code with logging enabled and prints a labeled box around the output. The `name` appears as the header so you can identify which call produced which operations.
+
+```typescript
+await cofhe.mocks.withLogs('counter.increment()', async () => {
+ await walletClient.writeContract({
+ ...counterContract,
+ functionName: 'increment',
+ });
+});
+```
+
+`withLogs` enables logging before the closure runs and disables it after, so only operations from within that block appear in the output. The previous on/off state is restored when it returns.
+
+## `enableLogs()` / `disableLogs()` — manual
+
+For finer-grained control, you can enable and disable logging manually:
+
+```typescript
+await cofhe.mocks.enableLogs();
+
+await walletClient.writeContract({
+ ...counterContract,
+ functionName: 'increment',
+});
+
+await cofhe.mocks.disableLogs();
+```
+
+## Default behavior
+
+Logging is controlled by the `cofhe.logMocks` config option (see [Getting Started → Configuration](/client-sdk/hardhat-3-plugin/getting-started#configuration)). When the option is `true` (the default), mock-contract log events are emitted; when `false`, they're suppressed even if you call `enableLogs()`.
+
+`withLogs`, `enableLogs`, and `disableLogs` only flip the runtime gate at the mock-contract level — they do **not** override the config.
diff --git a/client-sdk/hardhat-3-plugin/mock-contracts.mdx b/client-sdk/hardhat-3-plugin/mock-contracts.mdx
new file mode 100644
index 0000000..113b807
--- /dev/null
+++ b/client-sdk/hardhat-3-plugin/mock-contracts.mdx
@@ -0,0 +1,73 @@
+---
+title: Mock Contracts
+description: "Inspect plaintext and call mock CoFHE contracts in Hardhat 3 tests"
+---
+
+`conn.cofhe.mocks` exposes Viem contract descriptors for every deployed mock contract and helpers for reading the on-chain plaintext that the mock task manager stores. None of this requires the SDK client — the mocks are reachable from any Viem `publicClient` / `walletClient`.
+
+## Contract descriptors
+
+Each mock is exposed as a synchronous `{ address, abi }` object — spread it directly into Viem's `readContract` / `writeContract`:
+
+```typescript
+cofhe.mocks.MockTaskManager; // { address: '0x...', abi: [...] }
+cofhe.mocks.MockACL; // { address: '0x...', abi: [...] }
+cofhe.mocks.MockZkVerifier; // { address: '0x...', abi: [...] }
+cofhe.mocks.MockThresholdNetwork; // { address: '0x...', abi: [...] }
+cofhe.mocks.TestBed; // { address: '0x...', abi: [...] }
+```
+
+### Calling a mock directly
+
+```typescript
+const ctHash = await publicClient.readContract({
+ ...cofhe.mocks.TestBed,
+ functionName: 'numberHash',
+});
+
+await walletClient.writeContract({
+ ...cofhe.mocks.MockTaskManager,
+ functionName: 'setSecurityZones',
+ args: [0, 1],
+});
+```
+
+This is the lowest-friction way to assert mock state without going through the SDK.
+
+## Reading plaintext values
+
+Because `MockTaskManager` stores plaintext values on-chain, you can read the underlying plaintext of any encrypted handle directly in tests — no permit needed.
+
+### `getPlaintext(ctHash)`
+
+Returns the plaintext `bigint` for a given ciphertext hash. Accepts either a `bigint` or a hex `string`.
+
+```typescript
+const plaintext = await cofhe.mocks.getPlaintext(ctHash);
+```
+
+### `expectPlaintext(ctHash, expected)`
+
+Assertion shorthand — throws if the on-chain plaintext doesn't match `expected`:
+
+```typescript
+await cofhe.mocks.expectPlaintext(ctHash, 42n);
+```
+
+
+`getPlaintext` and `expectPlaintext` only work on the in-process Hardhat network where `MockTaskManager.mockStorage` exists. They will throw on real CoFHE networks.
+
+
+## Re-deploying mocks mid-test
+
+Normally you don't need this — mocks are deployed automatically on every `network.connect()`. For advanced scenarios where you want to reset mock state inside a single test:
+
+```typescript
+await cofhe.mocks.deployMocks({ deployTestBed: true, silent: true });
+```
+
+| Option | Type | Default | Description |
+| --- | --- | --- | --- |
+| `deployTestBed` | `boolean` | `true` | Whether to deploy the `TestBed` utility contract. |
+| `gasWarning` | `boolean` | inherits from config | Print the gas-warning line after deployment. |
+| `silent` | `boolean` | `false` | Suppress all deployment output (overrides `mocksDeployVerbosity`). |
diff --git a/client-sdk/hardhat-3-plugin/testing.mdx b/client-sdk/hardhat-3-plugin/testing.mdx
new file mode 100644
index 0000000..26b3b24
--- /dev/null
+++ b/client-sdk/hardhat-3-plugin/testing.mdx
@@ -0,0 +1,139 @@
+---
+title: Testing
+description: "Canonical test-writing patterns for FHE contracts under Hardhat 3"
+---
+
+This page shows the load-bearing patterns for writing FHE contract tests under Hardhat 3 with `@cofhe/hardhat-3-plugin`. Hardhat v2 counterpart: [Hardhat Plugin → Testing](/client-sdk/hardhat-plugin/testing).
+
+## Skeleton
+
+```typescript test/Counter.test.ts
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+import { network } from 'hardhat';
+import { Encryptable, FheTypes } from '@cofhe/sdk';
+
+describe('Encrypted counter', async () => {
+ const { viem, cofhe } = await network.connect();
+ const publicClient = await viem.getPublicClient();
+ const [walletClient] = await viem.getWalletClients();
+
+ it('encrypts, stores, and decrypts a uint32', async () => {
+ const client = await cofhe.createClientWithBatteries(walletClient);
+
+ // 1. Encrypt
+ const [enc] = await client
+ .encryptInputs([Encryptable.uint32(42n)])
+ .execute();
+
+ // 2. Store on-chain via TestBed
+ await walletClient.writeContract({
+ ...cofhe.mocks.TestBed,
+ functionName: 'setNumber',
+ args: [{
+ ctHash: enc.ctHash,
+ securityZone: enc.securityZone,
+ utype: enc.utype,
+ signature: enc.signature,
+ }],
+ });
+
+ const ctHash = (await publicClient.readContract({
+ ...cofhe.mocks.TestBed,
+ functionName: 'numberHash',
+ })) as `0x${string}`;
+
+ // 3. Decrypt via view (off-chain, instant)
+ const decryptedView = await client
+ .decryptForView(ctHash, FheTypes.Uint32)
+ .execute();
+ assert.equal(decryptedView, 42n);
+
+ // 4. Alternatively, verify plaintext without decryption
+ await cofhe.mocks.expectPlaintext(ctHash, 42n);
+ });
+});
+```
+
+That's the load-bearing shape. Everything below is what to add when the contract gets non-trivial.
+
+## Rules
+
+### 1. `async describe` runs `network.connect()` once per file
+
+Hardhat 3's `node:test` runner supports top-level `await` inside the describe callback. Resolve the connection (and deploy mocks) at the top of the describe — every `it` inside the same block shares the same fresh deployment.
+
+```typescript
+describe('My contract', async () => {
+ const { viem, cofhe } = await network.connect();
+ // ...
+});
+```
+
+If you want a **completely fresh** mock deployment between tests, call `await cofhe.mocks.deployMocks()` inside a `beforeEach`.
+
+### 2. Use `cofhe.createClientWithBatteries()` for most tests
+
+It handles config + connect + self-permit in one call. The self permit lets `decryptForView` work immediately:
+
+```typescript
+const client = await cofhe.createClientWithBatteries(walletClient);
+const value = await client.decryptForView(ctHash, FheTypes.Uint32).execute();
+```
+
+Drop down to `cofhe.createConfig` / `cofhe.createClient` only when you need to override defaults (e.g. a non-zero `mocks.encryptDelay`).
+
+### 3. Assert plaintext with `expectPlaintext` whenever possible
+
+```typescript
+await cofhe.mocks.expectPlaintext(ctHash, 42n);
+```
+
+Faster than `decryptForView` and needs no permit. Reserve the SDK path for tests where the SDK behavior itself is under test.
+
+### 4. Call mock contracts directly through their descriptors
+
+For state setup or assertions that don't need the SDK, spread the descriptor into Viem:
+
+```typescript
+await walletClient.writeContract({
+ ...cofhe.mocks.MockTaskManager,
+ functionName: 'setSecurityZones',
+ args: [0, 1],
+});
+```
+
+### 5. Wrap noisy code in `withLogs(name, fn)` for debugging
+
+When a test is misbehaving, scope mock logging to the suspicious block:
+
+```typescript
+await cofhe.mocks.withLogs('counter.increment()', async () => {
+ await walletClient.writeContract({
+ ...counterContract,
+ functionName: 'increment',
+ });
+});
+```
+
+## Common pitfalls
+
+
+
+`network.connect()` returns a **fresh** deployment each call. If you `await network.connect()` once and share the result across `describe` blocks, those blocks share mock state — which is sometimes what you want and sometimes not. When in doubt, call `network.connect()` per `describe`.
+
+
+
+Tests pass on the first op, then a second op reverts with `ACLNotAllowed` because the contract itself isn't on the ACL. Toggle logging (`await cofhe.mocks.enableLogs()`) to see the missing grant — every op prints a line showing whether `allowThis` / `allow` was called.
+
+
+
+The client is bound to whichever `walletClient` you passed to `createClientWithBatteries`. Calling the contract from a *different* wallet client (e.g. via `vm.prank`-style impersonation) will fail the ZK-verifier signature check — the input was signed for the original wallet, not the new caller.
+
+
+
+## Related
+
+- [Hardhat 3 Plugin → Getting Started](/client-sdk/hardhat-3-plugin/getting-started) — install + config.
+- [Hardhat 3 Plugin → Mock Contracts](/client-sdk/hardhat-3-plugin/mock-contracts) — descriptors + plaintext inspection.
+- [Hardhat Plugin (v2) → Testing](/client-sdk/hardhat-plugin/testing) — the same patterns under Hardhat v2.
diff --git a/docs.json b/docs.json
index 5c8b34d..9aa162f 100644
--- a/docs.json
+++ b/docs.json
@@ -103,6 +103,16 @@
"client-sdk/hardhat-plugin/testing"
]
},
+ {
+ "group": "Hardhat 3 Plugin",
+ "pages": [
+ "client-sdk/hardhat-3-plugin/getting-started",
+ "client-sdk/hardhat-3-plugin/client",
+ "client-sdk/hardhat-3-plugin/mock-contracts",
+ "client-sdk/hardhat-3-plugin/logging",
+ "client-sdk/hardhat-3-plugin/testing"
+ ]
+ },
{
"group": "Foundry Plugin",
"pages": [