Skip to content

Commit

Permalink
add verify actions which extends public client for onchain verificati…
Browse files Browse the repository at this point in the history
…on of counterfactual signatures
  • Loading branch information
coffeexcoin committed Oct 1, 2024
1 parent 3558527 commit 5fd7990
Show file tree
Hide file tree
Showing 13 changed files with 476 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/agw-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
"types": "./dist/types/exports/index.d.ts",
"import": "./dist/esm/exports/index.js",
"require": "./dist/cjs/exports/index.js"
},
"./actions": {
"types": "./dist/types/exports/actions.d.ts",
"import": "./dist/esm/exports/actions.js",
"require": "./dist/cjs/exports/actions.js"
}
},
"files": [
Expand Down
28 changes: 28 additions & 0 deletions packages/agw-client/src/abis/UniversalSigValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const UniversalSignatureValidatorAbi = [
{
inputs: [
{
name: '_signer',
type: 'address',
},
{
name: '_hash',
type: 'bytes32',
},
{
name: '_signature',
type: 'bytes',
},
],
outputs: [
{
type: 'boolean',
},
],
name: 'isValidUniversalSig',
stateMutability: 'nonpayable',
type: 'function',
},
] as const;

export default UniversalSignatureValidatorAbi;
27 changes: 27 additions & 0 deletions packages/agw-client/src/abstractPublicActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
type Account,
type Chain,
type Client,
type PublicActions,
publicActions,
type Transport,
} from 'viem';

import { verifyMessage } from './actions/verifyMessage';
import { verifySiweMessage } from './actions/verifySiweMessage';
import { verifyTypedData } from './actions/verifyTypedData';

export function abstractPublicActions<
transport extends Transport = Transport,
chain extends Chain = Chain,
account extends Account | undefined = Account | undefined,
>(
client: Client<transport, chain, account>,
): PublicActions<transport, chain, account> {
return {
...publicActions(client),
verifyMessage: (args) => verifyMessage(client, args),
verifySiweMessage: (args) => verifySiweMessage(client, args),
verifyTypedData: (args) => verifyTypedData(client, args),
};
}
114 changes: 114 additions & 0 deletions packages/agw-client/src/actions/verifyHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {
bytesToHex,
CallExecutionError,
type CallParameters,
type Chain,
type Client,
encodeFunctionData,
getAddress,
hexToBool,
isAddressEqual,
isErc6492Signature,
isHex,
recoverAddress,
serializeErc6492Signature,
serializeSignature,
type Transport,
} from 'viem';
import {
call,
verifyHash as viemVerifyHash,
type VerifyHashParameters,
type VerifyHashReturnType,
} from 'viem/actions';
import { abstractTestnet } from 'viem/chains';
import { getAction } from 'viem/utils';

import UniversalSignatureValidatorAbi from '../abis/UniversalSigValidator.js';
import { UNIVERSAL_SIGNATURE_VALIDATOR_ADDRESS } from '../constants.js';

const supportedChains: (number | undefined)[] = [abstractTestnet.id];

/**
* Verifies a message hash onchain using ERC-6492.
*
* @param client - Client to use.
* @param parameters - {@link VerifyHashParameters}
* @returns Whether or not the signature is valid. {@link VerifyHashReturnType}
*/
export async function verifyHash<chain extends Chain>(
client: Client<Transport, chain>,
parameters: VerifyHashParameters,
): Promise<VerifyHashReturnType> {
const { address, factory, factoryData, hash, signature, ...rest } =
parameters;

if (!supportedChains.includes(client.chain.id)) {
return await viemVerifyHash(client, parameters);
}

const signatureHex = (() => {
if (isHex(signature)) return signature;
if (typeof signature === 'object' && 'r' in signature && 's' in signature)
return serializeSignature(signature);
return bytesToHex(signature);
})();

const wrappedSignature = await (async () => {
// If no `factory` or `factoryData` is provided, it is assumed that the
// address is not a Smart Account, or the Smart Account is already deployed.
if (!factory && !factoryData) return signatureHex;

// If the signature is already wrapped, return the signature.
if (isErc6492Signature(signatureHex)) return signatureHex;

// If the Smart Account is not deployed, wrap the signature with a 6492 wrapper
// to perform counterfactual validation.
return serializeErc6492Signature({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
address: factory!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
data: factoryData!,
signature: signatureHex,
});
})();

try {
const { data } = await getAction(
client,
call,
'call',
)({
to: UNIVERSAL_SIGNATURE_VALIDATOR_ADDRESS,
data: encodeFunctionData({
abi: UniversalSignatureValidatorAbi,
functionName: 'isValidUniversalSig',
args: [address, hash, wrappedSignature],
}),
...rest,
} as unknown as CallParameters);

const isValid = hexToBool(data ?? '0x0');

return isValid;
} catch (error) {
// Fallback attempt to verify the signature via ECDSA recovery.
try {
const verified = isAddressEqual(
getAddress(address),
await recoverAddress({ hash, signature }),
);
if (verified) return true;
// eslint-disable-next-line no-empty
} catch {}

if (error instanceof CallExecutionError) {
// if the execution fails, the signature was not valid and an internal method inside of the validator reverted
// this can happen for many reasons, for example if signer can not be recovered from the signature
// or if the signature has no valid format
return false;
}

throw error;
}
}
42 changes: 42 additions & 0 deletions packages/agw-client/src/actions/verifyMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { type Chain, type Client, hashMessage, type Transport } from 'viem';
import type {
VerifyMessageParameters,
VerifyMessageReturnType,
} from 'viem/actions';

import { verifyHash } from './verifyHash';

/**
* Verify that a message was signed by the provided address.
*
* Compatible with Smart Contract Accounts & Externally Owned Accounts via [ERC-6492](https://eips.ethereum.org/EIPS/eip-6492).
*
* - Docs {@link https://viem.sh/docs/actions/public/verifyMessage}
*
* @param client - Client to use.
* @param parameters - {@link VerifyMessageParameters}
* @returns Whether or not the signature is valid. {@link VerifyMessageReturnType}
*/
export async function verifyMessage<chain extends Chain>(
client: Client<Transport, chain>,
{
address,
message,
factory,
factoryData,
signature,
...callRequest
}: VerifyMessageParameters,
): Promise<VerifyMessageReturnType> {
const hash = hashMessage(message);
return verifyHash(client, {
address,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
factory: factory!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
factoryData: factoryData!,
hash,
signature,
...callRequest,
});
}
57 changes: 57 additions & 0 deletions packages/agw-client/src/actions/verifySiweMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { type Chain, type Client, hashMessage, type Transport } from 'viem';
import type {
VerifySiweMessageParameters,
VerifySiweMessageReturnType,
} from 'viem/_types/actions/siwe/verifySiweMessage';

import { parseSiweMessage } from '../utils/siwe/parseSiweMessage';
import { validateSiweMessage } from '../utils/siwe/validateSiweMessage';
import { verifyHash } from './verifyHash';

/**
* Verifies [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) formatted message was signed.
*
* Compatible with Smart Contract Accounts & Externally Owned Accounts via [ERC-6492](https://eips.ethereum.org/EIPS/eip-6492).
*
* - Docs {@link https://viem.sh/docs/siwe/actions/verifySiweMessage}
*
* @param client - Client to use.
* @param parameters - {@link VerifySiweMessageParameters}
* @returns Whether or not the signature is valid. {@link VerifySiweMessageReturnType}
*/
export async function verifySiweMessage<chain extends Chain>(
client: Client<Transport, chain>,
parameters: VerifySiweMessageParameters,
): Promise<VerifySiweMessageReturnType> {
const {
address,
domain,
message,
nonce,
scheme,
signature,
time = new Date(),
...callRequest
} = parameters;

const parsed = parseSiweMessage(message);
if (!parsed.address) return false;

const isValid = validateSiweMessage({
address,
domain,
message: parsed,
nonce,
scheme,
time,
});
if (!isValid) return false;

const hash = hashMessage(message);
return verifyHash(client, {
address: parsed.address,
hash,
signature,
...callRequest,
});
}
50 changes: 50 additions & 0 deletions packages/agw-client/src/actions/verifyTypedData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
type Chain,
type Client,
hashTypedData,
type Transport,
type TypedData,
type VerifyTypedDataReturnType,
} from 'viem';
import type { VerifyTypedDataParameters } from 'viem/actions';

import { verifyHash } from './verifyHash';

/**
* Verify that typed data was signed by the provided address.
*
* - Docs {@link https://viem.sh/docs/actions/public/verifyTypedData}
*
* @param client - Client to use.
* @param parameters - {@link VerifyTypedDataParameters}
* @returns Whether or not the signature is valid. {@link VerifyTypedDataReturnType}
*/
export async function verifyTypedData<
const typedData extends TypedData | Record<string, unknown>,
primaryType extends keyof typedData | 'EIP712Domain',
chain extends Chain,
>(
client: Client<Transport, chain>,
parameters: VerifyTypedDataParameters<typedData, primaryType>,
): Promise<VerifyTypedDataReturnType> {
const {
address,
factory,
factoryData,
signature,
message,
primaryType,
types,
domain,
...callRequest
} = parameters as VerifyTypedDataParameters;
const hash = hashTypedData({ message, primaryType, types, domain });
return verifyHash(client, {
address,
factory: factory!,
factoryData: factoryData!,
hash,
signature,
...callRequest,
});
}
4 changes: 4 additions & 0 deletions packages/agw-client/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ const VALIDATOR_ADDRESS = '0xC894DE2894e2F84C0C2944FDcce9490eC22A92b6';
const CONTRACT_DEPLOYER_ADDRESS =
'0x0000000000000000000000000000000000008006' as const;

const UNIVERSAL_SIGNATURE_VALIDATOR_ADDRESS =
'0x4d98aa5724ef4f638d326Eac4Ab032C67B08ac65' as const;

const INSUFFICIENT_BALANCE_SELECTOR = '0xe7931438' as const;

export {
BATCH_CALLER_ADDRESS,
CONTRACT_DEPLOYER_ADDRESS,
INSUFFICIENT_BALANCE_SELECTOR,
SMART_ACCOUNT_FACTORY_ADDRESS,
UNIVERSAL_SIGNATURE_VALIDATOR_ADDRESS,
VALIDATOR_ADDRESS,
};
Loading

0 comments on commit 5fd7990

Please sign in to comment.