Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions docs/specs/AUTH_BRIDGE_INTEGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Auth-Bridge Oracle Integration Guide

This guide explains how to wire the Auth-Bridge hook + oracle for a new pool.

## Contracts

- Hook: `AuthBridgeDopplerHook`
- Oracle: `AuthBridgeOracle`
- Interface: `IAuthBridgeOracle`

The hook is a thin orchestrator. All signature verification, nonce tracking,
deadline checks, executor binding, and signer allowlist enforcement live in the oracle.

## Initialization Flow

1. Deploy the hook and oracle:

```solidity
AuthBridgeDopplerHook hook = new AuthBridgeDopplerHook(address(initializer));
AuthBridgeOracle oracle = new AuthBridgeOracle(address(hook));
```

2. Enable the hook on the initializer:

```solidity
initializer.setDopplerHookState(
[address(hook)],
[ON_INITIALIZATION_FLAG | ON_SWAP_FLAG]
);
```

3. Encode oracle init data (single immutable signer):

```solidity
AuthBridgeOracleInitData memory oracleInit = AuthBridgeOracleInitData({
platformSigner: platformSigner
});

AuthBridgeInitData memory hookInit = AuthBridgeInitData({
oracle: address(oracle),
oracleData: abi.encode(oracleInit)
});
```

4. Pass `hookInit` during pool creation:

```solidity
InitData memory initData = InitData({
// ...other fields...
dopplerHook: address(hook),
onInitializationDopplerHookCalldata: abi.encode(hookInit),
graduationDopplerHookCalldata: new bytes(0)
});
```

## Swap Authorization

Swappers must include `hookData` that ABI-decodes to:

```solidity
struct AuthBridgeData {
address user;
address executor; // 0 = any
uint64 deadline;
uint64 nonce;
bytes userSig;
bytes platformSig;
}
```

The oracle verifies:

- EIP-712 signature over `AuthSwap` (same digest for user + platform)
- Deadline not expired
- Executor binding (if non-zero)
- Sequential nonce per user per pool
- Platform signer allowlist
- EIP-1271 support for contract wallets

## Oracle Immutability

The oracle and its single `platformSigner` are set once per pool during `onInitialization`
and cannot be changed. Attempts to re-initialize will revert.

## Signature Generation (SDK Guide)

The oracle verifies an EIP-712 signature over the `AuthSwap` struct. The **same digest**
is signed by both the user and the platform signer.

### Typed Data

- **Domain**
- `name`: `"AuthBridgeDopplerHook"`
- `version`: `"1"`
- `chainId`: current chain ID
- `verifyingContract`: `AuthBridgeOracle` address (per pool)

- **Primary Type**: `AuthSwap`

```solidity
AuthSwap(
address user,
address executor,
bytes32 poolId,
bool zeroForOne,
int256 amountSpecified,
uint160 sqrtPriceLimitX96,
uint64 nonce,
uint64 deadline
)
```

### Example (TypeScript + ethers v6)

```ts
import { ethers } from "ethers";

const domain = {
name: "AuthBridgeDopplerHook",
version: "1",
chainId,
verifyingContract: authBridgeOracleAddress,
};

const types = {
AuthSwap: [
{ name: "user", type: "address" },
{ name: "executor", type: "address" },
{ name: "poolId", type: "bytes32" },
{ name: "zeroForOne", type: "bool" },
{ name: "amountSpecified", type: "int256" },
{ name: "sqrtPriceLimitX96", type: "uint160" },
{ name: "nonce", type: "uint64" },
{ name: "deadline", type: "uint64" },
],
};

const value = {
user,
executor, // 0x000...000 to allow any executor
poolId, // bytes32
zeroForOne,
amountSpecified, // int256
sqrtPriceLimitX96,
nonce,
deadline, // unix seconds
};

const userSig = await userWallet.signTypedData(domain, types, value);
const platformSig = await platformWallet.signTypedData(domain, types, value);

// Hook data
const hookData = ethers.AbiCoder.defaultAbiCoder().encode(
[
"tuple(address user,address executor,uint64 deadline,uint64 nonce,bytes userSig,bytes platformSig)"
],
[{ user, executor, deadline, nonce, userSig, platformSig }]
);
```

### Notes

- `nonce` is sequential per `(poolId, user)` and is stored in the oracle.
- `deadline` is enforced by the oracle. Use short TTLs.
- `executor` binding is optional; set to zero address to allow any sender.
46 changes: 46 additions & 0 deletions docs/specs/AUTH_BRIDGE_ORACLE_INTERFACE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Auth-Bridge Oracle Interface (Minimal)

This describes the on-chain interface expected by the hook.

## Structs

```solidity
struct AuthSwap {
address user;
address executor;
bytes32 poolId;
bool zeroForOne;
int256 amountSpecified;
uint160 sqrtPriceLimitX96;
uint64 nonce;
uint64 deadline;
}
```

## Interface

```solidity
interface IAuthBridgeOracle {
function initialize(PoolId poolId, address asset, bytes calldata data) external;

function isAuthorized(
AuthSwap calldata swap,
address sender,
bytes calldata userSig,
bytes calldata platformSig
) external returns (bool);
}
```

### Semantics

- `initialize` is called once per pool by the hook during `onInitialization`.
- `isAuthorized` returns `true` only if:
- signatures are valid (user + platform)
- nonce is expected
- deadline is not expired
- executor binding matches (if provided)
- platform signer matches the single immutable signer for the pool

Oracle is responsible for all replay protection and signature verification logic.
The platform signer is configured once per pool during `initialize` and cannot be changed.
122 changes: 122 additions & 0 deletions src/dopplerHooks/AuthBridgeDopplerHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;

import { IPoolManager } from "@v4-core/interfaces/IPoolManager.sol";
import { BalanceDelta } from "@v4-core/types/BalanceDelta.sol";
import { Currency } from "@v4-core/types/Currency.sol";
import { PoolId, PoolIdLibrary } from "@v4-core/types/PoolId.sol";
import { PoolKey } from "@v4-core/types/PoolKey.sol";
import { BaseDopplerHook } from "src/base/BaseDopplerHook.sol";
import { IAuthBridgeOracle, AuthSwap } from "src/interfaces/IAuthBridgeOracle.sol";

// ============ Errors ============

/// @notice Thrown when hookData is missing or cannot be decoded
error AuthBridge_MissingHookData();

/// @notice Thrown when the executor does not match the expected executor
error AuthBridge_InvalidOracle(address oracle);
error AuthBridge_OracleAlreadySet(PoolId poolId);
error AuthBridge_OracleNotSet(PoolId poolId);
error AuthBridge_Unauthorized();

// ============ Structs ============

/// @notice Data passed in hookData for swap authorization
struct AuthBridgeData {
address user; // the user identity being authorized (EOA for P1)
address executor; // optional: required swap executor (0 = allow any executor)
uint64 deadline; // unix seconds timestamp
uint64 nonce; // expected nonce for (poolId, user)
bytes userSig; // ECDSA sig over EIP-712 digest
bytes platformSig; // ECDSA sig over EIP-712 digest
}

/// @notice Data passed during pool initialization
struct AuthBridgeInitData {
address oracle;
bytes oracleData;
}

/**
* @title Auth-Bridge Doppler Hook
* @author Whetstone Research
* @custom:security-contact security@whetstone.cc
* @notice Doppler Hook that gates swaps using two-party EIP-712 signature authorization.
* Each swap requires both a user signature and a platform signature over the same digest.
* @dev Auth logic (nonces, signatures, deadlines) lives in the oracle.
*/
contract AuthBridgeDopplerHook is BaseDopplerHook {
using PoolIdLibrary for PoolKey;

// ============ State ============

/// @notice Oracle per pool (set once at initialization)
mapping(PoolId poolId => address oracle) public poolOracle;

// ============ Constructor ============

/**
* @param initializer Address of the DopplerHookInitializer contract
*/
constructor(address initializer) BaseDopplerHook(initializer) { }

// ============ Initialization ============

/// @inheritdoc BaseDopplerHook
function _onInitialization(address asset, PoolKey calldata key, bytes calldata data) internal override {
PoolId poolId = key.toId();
if (poolOracle[poolId] != address(0)) revert AuthBridge_OracleAlreadySet(poolId);

AuthBridgeInitData memory initData = abi.decode(data, (AuthBridgeInitData));
if (initData.oracle == address(0)) revert AuthBridge_InvalidOracle(initData.oracle);

poolOracle[poolId] = initData.oracle;
IAuthBridgeOracle(initData.oracle).initialize(poolId, asset, initData.oracleData);
}

// ============ Swap Validation ============

/// @inheritdoc BaseDopplerHook
function _onSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
BalanceDelta,
bytes calldata data
) internal override returns (Currency, int128) {
// 1. Decode AuthBridgeData from hookData
if (data.length == 0) {
revert AuthBridge_MissingHookData();
}

AuthBridgeData memory authData = abi.decode(data, (AuthBridgeData));

PoolId poolId = key.toId();
address oracle = poolOracle[poolId];
if (oracle == address(0)) revert AuthBridge_OracleNotSet(poolId);

AuthSwap memory swapData = AuthSwap({
user: authData.user,
executor: authData.executor,
poolId: PoolId.unwrap(poolId),
zeroForOne: params.zeroForOne,
amountSpecified: params.amountSpecified,
sqrtPriceLimitX96: params.sqrtPriceLimitX96,
nonce: authData.nonce,
deadline: authData.deadline
});

bool authorized = IAuthBridgeOracle(oracle).isAuthorized(
swapData,
sender,
authData.userSig,
authData.platformSig
);

if (!authorized) revert AuthBridge_Unauthorized();

return (Currency.wrap(address(0)), 0);
}

}
Loading
Loading