diff --git a/packages/delegation-core/README.md b/packages/delegation-core/README.md index e706a987..6f8ea0ad 100644 --- a/packages/delegation-core/README.md +++ b/packages/delegation-core/README.md @@ -240,6 +240,31 @@ import { ROOT_AUTHORITY } from '@metamask/delegation-core'; console.log(ROOT_AUTHORITY); // Root authority identifier ``` +#### `hashDelegation(delegation)` + +Computes a hash for a given delegation object. + +**Parameters:** +- `delegation: DelegationStruct` - The delegation object to hash + +**Returns:** `Hex` - The hash of the delegation + +**Example:** +```typescript +import { hashDelegation } from '@metamask/delegation-core'; + +const delegation = { + delegate: '0x742d35Cc6634C0532925a3b8d40EC49b0e8BaA5e', + delegator: '0x...', + authority: '0x...', + caveats: [], + salt: 0n, + signature: '0x...' +}; + +const hash = hashDelegation(delegation); +``` + ## Type Definitions ### Core Types @@ -247,16 +272,16 @@ console.log(ROOT_AUTHORITY); // Root authority identifier ```typescript export type Hex = `0x${string}`; -export type Delegation = { +export type DelegationStruct = { delegate: string; delegator: string; authority: string; - caveats: Caveat[]; + caveats: CaveatStruct[]; salt: bigint; signature: string; }; -export type Caveat = { +export type CaveatStruct = { enforcer: string; terms: string; }; diff --git a/packages/delegation-core/package.json b/packages/delegation-core/package.json index 0a35ac5b..0025b65a 100644 --- a/packages/delegation-core/package.json +++ b/packages/delegation-core/package.json @@ -71,6 +71,7 @@ }, "dependencies": { "@metamask/abi-utils": "^3.0.0", - "@metamask/utils": "^11.4.0" + "@metamask/utils": "^11.4.0", + "@noble/hashes": "^1.8.0" } } diff --git a/packages/delegation-core/src/delegation.ts b/packages/delegation-core/src/delegation.ts index 7e44f40f..95d73ee7 100644 --- a/packages/delegation-core/src/delegation.ts +++ b/packages/delegation-core/src/delegation.ts @@ -1,5 +1,6 @@ -import { encodeSingle, decodeSingle } from '@metamask/abi-utils'; -import { type BytesLike } from '@metamask/utils'; +import { encode, encodeSingle, decodeSingle } from '@metamask/abi-utils'; +import { hexToBytes, type BytesLike } from '@metamask/utils'; +import { keccak_256 as keccak256 } from '@noble/hashes/sha3'; import { bytesLikeToBytes, @@ -9,7 +10,12 @@ import { type EncodingOptions, type ResultValue, } from './returns'; -import type { Delegation, Hex } from './types'; +import type { CaveatStruct, DelegationStruct, Hex } from './types'; + +/** + * When designated as the delegate address in a delegation, this allows any beneficiary to redeem the delegation. + */ +export const ANY_BENEFICIARY = '0x0000000000000000000000000000000000000a11'; /** * To be used on a delegation as the root authority. @@ -17,6 +23,22 @@ import type { Delegation, Hex } from './types'; export const ROOT_AUTHORITY = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; +/** + * The typehash for a delegation, used when generating a delegation hash. + * + * keccak('Delegation(address delegate,address delegator,bytes32 authority,Caveat[] caveats,uint256 salt)Caveat(address enforcer,bytes terms)') + */ +export const DELEGATION_TYPEHASH = + '0x88c1d2ecf185adf710588203a5f263f0ff61be0d33da39792cde19ba9aa4331e'; + +/** + * The typehash for a caveat, used when generating a caveat hash. + * + * keccak('Caveat(address enforcer,bytes terms)') + */ +export const CAVEAT_TYPEHASH = + '0x80ad7e1b04ee6d994a125f4714ca0720908bd80ed16063ec8aee4b88e9253e2d'; + /** * The ABI types for an array of delegations. */ @@ -30,11 +52,11 @@ const DELEGATION_ARRAY_ABI_TYPES = * @returns The encoded delegations as a hex string (default) or Uint8Array. */ export function encodeDelegations( - delegations: Delegation[], + delegations: DelegationStruct[], options?: EncodingOptions<'hex'>, ): Hex; export function encodeDelegations( - delegations: Delegation[], + delegations: DelegationStruct[], options: EncodingOptions<'bytes'>, ): Uint8Array; /** @@ -44,7 +66,7 @@ export function encodeDelegations( * @returns The encoded delegations as a hex string (default) or Uint8Array. */ export function encodeDelegations( - delegations: Delegation[], + delegations: DelegationStruct[], options: EncodingOptions = defaultOptions, ): Hex | Uint8Array { let result: Uint8Array; @@ -83,7 +105,7 @@ export function encodeDelegations( const delegationFromDecodedDelegation = ( decodedDelegation: DecodedDelegation, convertFn: (value: BytesLike) => TEncoding, -): Delegation => { +): DelegationStruct => { const [delegate, delegator, authority, caveats, salt, signature] = decodedDelegation; @@ -124,11 +146,11 @@ type DecodedDelegation = [ export function decodeDelegations( encoded: BytesLike, options?: EncodingOptions<'hex'>, -): Delegation[]; +): DelegationStruct[]; export function decodeDelegations( encoded: BytesLike, options: EncodingOptions<'bytes'>, -): Delegation[]; +): DelegationStruct[]; /** * Decodes an encoded permission context back into an array of delegations. * @param encoded - The encoded delegations as a hex string or Uint8Array. @@ -138,7 +160,7 @@ export function decodeDelegations( export function decodeDelegations( encoded: BytesLike, options: EncodingOptions = defaultOptions, -): Delegation[] | Delegation[] { +): DelegationStruct[] | DelegationStruct[] { // it's possible to short circuit for empty delegations, but due to the // complexity of the input type, and the relative infrequency of empty delegations, // it's not worthwhile. @@ -158,3 +180,91 @@ export function decodeDelegations( delegationFromDecodedDelegation(struct, bytesLikeToHex), ); } + +/** + * Calculates the hash of a delegation for signing purposes. + * The hash is computed by encoding the delegation parameters with the delegation typehash + * and then applying keccak256 hashing. + * + * @param delegation - The delegation to hash. + * @param options - Encoding options. Defaults to { out: 'hex' }. + * @returns The keccak256 hash of the encoded delegation as a hex string (default) or Uint8Array. + */ +export function hashDelegation( + delegation: DelegationStruct, + options?: EncodingOptions<'hex'>, +): Hex; +export function hashDelegation( + delegation: DelegationStruct, + options: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Calculates the hash of a delegation for signing purposes. + * The hash is computed by encoding the delegation parameters with the delegation typehash + * and then applying keccak256 hashing. + * + * @param delegation - The delegation to hash. + * @param options - Encoding options. Defaults to { out: 'hex' }. + * @returns The keccak256 hash of the encoded delegation as a hex string (default) or Uint8Array. + */ +export function hashDelegation( + delegation: DelegationStruct, + options: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const encoded = encode( + ['bytes32', 'address', 'address', 'bytes32', 'bytes32', 'uint256'], + [ + DELEGATION_TYPEHASH, + delegation.delegate, + delegation.delegator, + delegation.authority, + getCaveatsArrayHash(delegation.caveats), + delegation.salt, + ], + ); + const hash = keccak256(encoded); + return prepareResult(hash, options); +} + +/** + * Calculates the hash of an array of caveats. The caveats are individually abi + * encoded and hashed, and concatenated. The resulting byte array is then + * hashed to produce the CaveatsArrayHash. + * + * @param caveats - The array of caveats to hash. + * @returns The keccak256 hash of the encoded caveat array. + */ +function getCaveatsArrayHash(caveats: CaveatStruct[]): Uint8Array { + const byteLength = 32 * caveats.length; + const encoded = new Uint8Array(byteLength); + + for (let i = 0; i < caveats.length; i++) { + const caveat = caveats[i]; + if (!caveat) { + throw new Error(`Caveat was undefined at index ${i}`); + } + const caveatHash = getCaveatHash(caveat); + encoded.set(caveatHash, i * 32); + } + + return keccak256(encoded); +} + +/** + * Calculates the hash of a single caveat. + * @param caveat - The caveat to hash. + * @returns The keccak256 hash of the encoded caveat. + */ +function getCaveatHash(caveat: CaveatStruct): Uint8Array { + const termsBytes = + typeof caveat.terms === 'string' ? hexToBytes(caveat.terms) : caveat.terms; + + const termsHash = keccak256(termsBytes); + + const encoded = encode( + ['bytes32', 'address', 'bytes32'], + [CAVEAT_TYPEHASH, caveat.enforcer, termsHash], + ); + const hash = keccak256(encoded); + return hash; +} diff --git a/packages/delegation-core/src/index.ts b/packages/delegation-core/src/index.ts index d2051b1c..594788e1 100644 --- a/packages/delegation-core/src/index.ts +++ b/packages/delegation-core/src/index.ts @@ -1,4 +1,8 @@ -export type { Hex, Delegation, Caveat } from './types'; +export type { + Hex, + DelegationStruct as Delegation, + CaveatStruct as Caveat, +} from './types'; export { createValueLteTerms, @@ -12,5 +16,9 @@ export { export { encodeDelegations, decodeDelegations, + hashDelegation, ROOT_AUTHORITY, + ANY_BENEFICIARY, + DELEGATION_TYPEHASH, + CAVEAT_TYPEHASH, } from './delegation'; diff --git a/packages/delegation-core/src/types.ts b/packages/delegation-core/src/types.ts index 68cb9879..730945e6 100644 --- a/packages/delegation-core/src/types.ts +++ b/packages/delegation-core/src/types.ts @@ -9,7 +9,7 @@ export type { Hex } from '@metamask/utils'; * @property terms - The terms or conditions of the caveat encoded as hex data. * @property args - Additional arguments required by the caveat enforcer, encoded as hex data. */ -export type Caveat = { +export type CaveatStruct = { enforcer: TBytes; terms: TBytes; args: TBytes; @@ -25,11 +25,11 @@ export type Caveat = { * @property salt - A unique value to prevent replay attacks and ensure uniqueness of the delegation. * @property signature - The cryptographic signature validating this delegation. */ -export type Delegation = { +export type DelegationStruct = { delegate: TBytes; delegator: TBytes; authority: TBytes; - caveats: Caveat[]; + caveats: CaveatStruct[]; salt: bigint; signature: TBytes; }; diff --git a/packages/delegation-core/test/delegation.test.ts b/packages/delegation-core/test/delegation.test.ts index a436a03f..f1018d2d 100644 --- a/packages/delegation-core/test/delegation.test.ts +++ b/packages/delegation-core/test/delegation.test.ts @@ -1,11 +1,13 @@ +import { bytesToHex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; import { encodeDelegations, decodeDelegations, ROOT_AUTHORITY, + hashDelegation, } from '../src/delegation'; -import type { Delegation, Caveat } from '../src/types'; +import type { DelegationStruct, CaveatStruct } from '../src/types'; describe('delegation', () => { describe('ROOT_AUTHORITY', () => { @@ -26,7 +28,7 @@ describe('delegation', () => { }); it('encodes single delegation with no caveats', () => { - const delegation: Delegation = { + const delegation: DelegationStruct = { delegate: '0x1234567890123456789012345678901234567890', delegator: '0xABCDEF0123456789012345678901234567890123', authority: ROOT_AUTHORITY, @@ -43,13 +45,13 @@ describe('delegation', () => { }); it('encodes single delegation with caveats', () => { - const caveat: Caveat = { + const caveat: CaveatStruct = { enforcer: '0x9999999999999999999999999999999999999999', terms: '0xdeadbeef', args: '0xcafebabe', }; - const delegation: Delegation = { + const delegation: DelegationStruct = { delegate: '0x1234567890123456789012345678901234567890', delegator: '0xABCDEF0123456789012345678901234567890123', authority: ROOT_AUTHORITY, @@ -66,7 +68,7 @@ describe('delegation', () => { }); it('encodes multiple delegations', () => { - const delegation1: Delegation = { + const delegation1: DelegationStruct = { delegate: '0x1111111111111111111111111111111111111111', delegator: '0x2222222222222222222222222222222222222222', authority: ROOT_AUTHORITY, @@ -75,7 +77,7 @@ describe('delegation', () => { signature: '0xaaa', }; - const delegation2: Delegation = { + const delegation2: DelegationStruct = { delegate: '0x3333333333333333333333333333333333333333', delegator: '0x4444444444444444444444444444444444444444', authority: @@ -93,19 +95,19 @@ describe('delegation', () => { }); it('encodes delegation with multiple caveats', () => { - const caveat1: Caveat = { + const caveat1: CaveatStruct = { enforcer: '0x1111111111111111111111111111111111111111', terms: '0xdeadbeef', args: '0xcafebabe', }; - const caveat2: Caveat = { + const caveat2: CaveatStruct = { enforcer: '0x2222222222222222222222222222222222222222', terms: '0xfeedface', args: '0xdeadc0de', }; - const delegation: Delegation = { + const delegation: DelegationStruct = { delegate: '0x1234567890123456789012345678901234567890', delegator: '0xABCDEF0123456789012345678901234567890123', authority: ROOT_AUTHORITY, @@ -124,7 +126,7 @@ describe('delegation', () => { it('handles delegation with custom authority', () => { const customAuthority = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd'; - const delegation: Delegation = { + const delegation: DelegationStruct = { delegate: '0x1234567890123456789012345678901234567890', delegator: '0xABCDEF0123456789012345678901234567890123', authority: customAuthority, @@ -141,7 +143,7 @@ describe('delegation', () => { }); it('handles delegation with large salt', () => { - const delegation: Delegation = { + const delegation: DelegationStruct = { delegate: '0x1234567890123456789012345678901234567890', delegator: '0xABCDEF0123456789012345678901234567890123', authority: ROOT_AUTHORITY, @@ -158,7 +160,7 @@ describe('delegation', () => { }); it('handles delegation with empty signature', () => { - const delegation: Delegation = { + const delegation: DelegationStruct = { delegate: '0x1234567890123456789012345678901234567890', delegator: '0xABCDEF0123456789012345678901234567890123', authority: ROOT_AUTHORITY, @@ -175,7 +177,7 @@ describe('delegation', () => { }); it('produces consistent output for same input', () => { - const delegation: Delegation = { + const delegation: DelegationStruct = { delegate: '0x1234567890123456789012345678901234567890', delegator: '0xABCDEF0123456789012345678901234567890123', authority: ROOT_AUTHORITY, @@ -191,7 +193,7 @@ describe('delegation', () => { }); it('handles mixed case addresses correctly', () => { - const delegation: Delegation = { + const delegation: DelegationStruct = { delegate: '0x1234567890123456789012345678901234567890', // test case handling delegator: '0xAbCdEf0123456789012345678901234567890123', // mixed case authority: ROOT_AUTHORITY, @@ -214,13 +216,13 @@ describe('delegation', () => { }); it('encodes as specified return type', () => { - const caveat: Caveat = { + const caveat: CaveatStruct = { enforcer: '0x9999999999999999999999999999999999999999', terms: '0xdeadbeef', args: '0xcafebabe', }; - const delegation: Delegation = { + const delegation: DelegationStruct = { delegate: '0x1234567890123456789012345678901234567890', delegator: '0xABCDEF0123456789012345678901234567890123', authority: ROOT_AUTHORITY, @@ -260,7 +262,7 @@ describe('delegation', () => { }); it('decodes single delegation with no caveats', () => { - const delegation: Delegation = { + const delegation: DelegationStruct = { delegate: '0x1234567890123456789012345678901234567890', delegator: '0xABCDEF0123456789012345678901234567890123', authority: ROOT_AUTHORITY, @@ -448,7 +450,7 @@ describe('delegation', () => { describe('encodeDelegations and decodeDelegations, round trip', () => { it('encode then decode produces same result', () => { - const originalDelegation: Delegation = { + const originalDelegation: DelegationStruct = { delegate: '0x1234567890123456789012345678901234567890', delegator: '0xABCDEF0123456789012345678901234567890123', authority: ROOT_AUTHORITY, @@ -485,7 +487,7 @@ describe('delegation', () => { }); it('encode then decode produces same result when encoding as bytes', () => { - const originalDelegation: Delegation = { + const originalDelegation: DelegationStruct = { delegate: '0x1234567890123456789012345678901234567890', delegator: '0xABCDEF0123456789012345678901234567890123', authority: ROOT_AUTHORITY, @@ -522,7 +524,7 @@ describe('delegation', () => { }); it('handles multiple complex delegations', () => { - const originalDelegations: Delegation[] = [ + const originalDelegations: DelegationStruct[] = [ { delegate: '0x1111111111111111111111111111111111111111', delegator: '0x2222222222222222222222222222222222222222', @@ -565,4 +567,564 @@ describe('delegation', () => { expect(decoded).toStrictEqual(originalDelegations); }); }); + + describe('hashDelegation', () => { + it('returns the correct hash for a delegation with no caveats', () => { + const delegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + caveats: [], + salt: 0x42n, + signature: '0x789abc', + }; + + const hash = hashDelegation(delegation); + + expect(hash).toMatchInlineSnapshot( + `"0xcaeb6434dc99e3098ff79ea1458a16b3e53b380f211cdb359628f9302d9c41b4"`, + ); + }); + + it('returns the correct hash for a delegation with a single caveat', () => { + const delegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [ + { + enforcer: '0x9999999999999999999999999999999999999999', + terms: '0xdeadbeef', + args: '0xcafebabe', + }, + ], + salt: 0x42n, + signature: '0x789abc', + }; + + const hash = hashDelegation(delegation); + + expect(hash).toMatchInlineSnapshot( + `"0x2c67f5e651bf654552c047f3bdc935996eb8867fea62274ebd6643522a34ea65"`, + ); + }); + + it('returns the correct hash for a delegation with multiple caveats', () => { + const delegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [ + { + enforcer: '0x1111111111111111111111111111111111111111', + terms: '0xdeadbeef', + args: '0xcafebabe', + }, + { + enforcer: '0x2222222222222222222222222222222222222222', + terms: '0xfeedface', + args: '0xdeadc0de', + }, + ], + salt: 0x123n, + signature: '0x456789', + }; + + const hash = hashDelegation(delegation); + + expect(hash).toMatchInlineSnapshot( + `"0x71a41e757d3aba596d483d0414a1681961573e8024238c4e72b19c194627fbdb"`, + ); + }); + + it('returns the correct hash for a delegation with custom authority', () => { + const customAuthority = + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd'; + const delegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: customAuthority, + caveats: [], + salt: 0x999n, + signature: '0xffffff', + }; + + const hash = hashDelegation(delegation); + + expect(hash).toMatchInlineSnapshot( + `"0xeb8ff35e32030bee23e16167e98536ccf4dffb79558d0fb76b73628fc70805cd"`, + ); + }); + + it('returns the correct hash for a delegation with zero salt', () => { + const delegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [], + salt: 0x0n, + signature: '0x123456', + }; + + const hash = hashDelegation(delegation); + + expect(hash).toMatchInlineSnapshot( + `"0xa05839f6864d332530b2d1a4043668777940e609931ae9c09d19b866a1408a6b"`, + ); + }); + + it('returns the correct hash for a delegation with maximum salt', () => { + const delegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [], + salt: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn, + signature: '0x123456', + }; + + const hash = hashDelegation(delegation); + + expect(hash).toMatchInlineSnapshot( + `"0xbbc3b60ea68fa14fa170a207435a4ae96cf3adc8ab488d7bfa57e9c8da67f9af"`, + ); + }); + + it('produces consistent hashes for the same delegation', () => { + const delegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [ + { + enforcer: '0x9999999999999999999999999999999999999999', + terms: '0xdeadbeef', + args: '0xcafebabe', + }, + ], + salt: 0x42n, + signature: '0x789abc', + }; + + const hash1 = hashDelegation(delegation); + const hash2 = hashDelegation(delegation); + + expect(hash1).toEqual(hash2); + }); + + it('produces different hashes for different delegates', () => { + const baseDelegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [], + salt: 0x42n, + signature: '0x789abc', + }; + + const altDelegation: DelegationStruct = { + ...baseDelegation, + delegate: '0x9876543210987654321098765432109876543210', + }; + + const hash1 = hashDelegation(baseDelegation); + const hash2 = hashDelegation(altDelegation); + + expect(hash1).not.toEqual(hash2); + }); + + it('produces different hashes for different delegators', () => { + const baseDelegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [], + salt: 0x42n, + signature: '0x789abc', + }; + + const altDelegation: DelegationStruct = { + ...baseDelegation, + delegator: '0x9876543210987654321098765432109876543210', + }; + + const hash1 = hashDelegation(baseDelegation); + const hash2 = hashDelegation(altDelegation); + + expect(hash1).not.toEqual(hash2); + }); + + it('produces different hashes for different authorities', () => { + const baseDelegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [], + salt: 0x42n, + signature: '0x789abc', + }; + + const altDelegation: DelegationStruct = { + ...baseDelegation, + authority: + '0x1111111111111111111111111111111111111111111111111111111111111111', + }; + + const hash1 = hashDelegation(baseDelegation); + const hash2 = hashDelegation(altDelegation); + + expect(hash1).not.toEqual(hash2); + }); + + it('produces different hashes for different salts', () => { + const baseDelegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [], + salt: 0x42n, + signature: '0x789abc', + }; + + const altDelegation: DelegationStruct = { + ...baseDelegation, + salt: 0x43n, + }; + + const hash1 = hashDelegation(baseDelegation); + const hash2 = hashDelegation(altDelegation); + + expect(hash1).not.toEqual(hash2); + }); + + it('produces different hashes for different caveat enforcers', () => { + const baseDelegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [ + { + enforcer: '0x1111111111111111111111111111111111111111', + terms: '0xdeadbeef', + args: '0xcafebabe', + }, + ], + salt: 0x42n, + signature: '0x789abc', + }; + + const altDelegation: DelegationStruct = { + ...baseDelegation, + caveats: [ + { + enforcer: '0x2222222222222222222222222222222222222222', + terms: '0xdeadbeef', + args: '0xcafebabe', + }, + ], + }; + + const hash1 = hashDelegation(baseDelegation); + const hash2 = hashDelegation(altDelegation); + + expect(hash1).not.toEqual(hash2); + }); + + it('produces different hashes for different caveat terms', () => { + const baseDelegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [ + { + enforcer: '0x1111111111111111111111111111111111111111', + terms: '0xdeadbeef', + args: '0xcafebabe', + }, + ], + salt: 0x42n, + signature: '0x789abc', + }; + + const altDelegation: DelegationStruct = { + ...baseDelegation, + caveats: [ + { + enforcer: '0x1111111111111111111111111111111111111111', + terms: '0xfeedface', + args: '0xcafebabe', + }, + ], + }; + + const hash1 = hashDelegation(baseDelegation); + const hash2 = hashDelegation(altDelegation); + + expect(hash1).not.toEqual(hash2); + }); + + it('produces the same hash regardless of caveat args (args are not part of hash)', () => { + const baseDelegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [ + { + enforcer: '0x1111111111111111111111111111111111111111', + terms: '0xdeadbeef', + args: '0xcafebabe', + }, + ], + salt: 0x42n, + signature: '0x789abc', + }; + + const altDelegation: DelegationStruct = { + ...baseDelegation, + caveats: [ + { + enforcer: '0x1111111111111111111111111111111111111111', + terms: '0xdeadbeef', + args: '0xdeadc0de', // Different args + }, + ], + }; + + const hash1 = hashDelegation(baseDelegation); + const hash2 = hashDelegation(altDelegation); + + // Args should not affect the hash according to the implementation + expect(hash1).toEqual(hash2); + }); + + it('produces the same hash regardless of signature (signature is not part of hash)', () => { + const baseDelegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [], + salt: 0x42n, + signature: '0x789abc', + }; + + const altDelegation: DelegationStruct = { + ...baseDelegation, + signature: '0xffffff', // Different signature + }; + + const hash1 = hashDelegation(baseDelegation); + const hash2 = hashDelegation(altDelegation); + + // Signature should not affect the hash according to the implementation + expect(hash1).toEqual(hash2); + }); + + it('produces different hashes for different caveat order', () => { + const baseDelegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [ + { + enforcer: '0x1111111111111111111111111111111111111111', + terms: '0xdeadbeef', + args: '0xcafebabe', + }, + { + enforcer: '0x2222222222222222222222222222222222222222', + terms: '0xfeedface', + args: '0xdeadc0de', + }, + ], + salt: 0x42n, + signature: '0x789abc', + }; + + const altDelegation: DelegationStruct = { + ...baseDelegation, + caveats: [ + { + enforcer: '0x2222222222222222222222222222222222222222', + terms: '0xfeedface', + args: '0xdeadc0de', + }, + { + enforcer: '0x1111111111111111111111111111111111111111', + terms: '0xdeadbeef', + args: '0xcafebabe', + }, + ], + }; + + const hash1 = hashDelegation(baseDelegation); + const hash2 = hashDelegation(altDelegation); + + expect(hash1).not.toEqual(hash2); + }); + + it('handles delegations with empty caveat terms', () => { + const delegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [ + { + enforcer: '0x1111111111111111111111111111111111111111', + terms: '0x', + args: '0xcafebabe', + }, + ], + salt: 0x42n, + signature: '0x789abc', + }; + + const hash = hashDelegation(delegation); + + expect(hash).toMatchInlineSnapshot( + `"0xeb607828d330ff18bb9e0cffb3941c53c289fdf9ab412f7bc0fe3c09dfd2878c"`, + ); + }); + + it('returns a hex string by default', () => { + const delegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [], + salt: 0x42n, + signature: '0x789abc', + }; + + const hash = hashDelegation(delegation); + + expect(hash).toMatchInlineSnapshot( + `"0xcaeb6434dc99e3098ff79ea1458a16b3e53b380f211cdb359628f9302d9c41b4"`, + ); + }); + + it('handles mixed case addresses consistently', () => { + const delegation1: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [ + { + enforcer: '0xaaBBccDDeeFF1122334455667788990011223344', + terms: '0xdead', + args: '0xbeef', + }, + ], + salt: 0x1n, + signature: '0x789', + }; + + const delegation2: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xabcdef0123456789012345678901234567890123', // lowercase + authority: ROOT_AUTHORITY, + caveats: [ + { + enforcer: '0xAABBCCDDEEFF1122334455667788990011223344', // uppercase + terms: '0xdead', + args: '0xbeef', + }, + ], + salt: 0x1n, + signature: '0x789', + }; + + const hash1 = hashDelegation(delegation1); + const hash2 = hashDelegation(delegation2); + + // Addresses should be treated the same regardless of case + expect(hash1).toEqual(hash2); + }); + + it('returns bytes when specified in options', () => { + const delegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [], + salt: 0x42n, + signature: '0x789abc', + }; + + const hash = hashDelegation(delegation, { out: 'bytes' }); + + expect(hash).toBeInstanceOf(Uint8Array); + expect(hash).toMatchInlineSnapshot(` + Uint8Array [ + 202, + 235, + 100, + 52, + 220, + 153, + 227, + 9, + 143, + 247, + 158, + 161, + 69, + 138, + 22, + 179, + 229, + 59, + 56, + 15, + 33, + 28, + 219, + 53, + 150, + 40, + 249, + 48, + 45, + 156, + 65, + 180, + ] + `); + }); + + it('returns hex when explicitly specified in options', () => { + const delegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [], + salt: 0x42n, + signature: '0x789abc', + }; + + const hash = hashDelegation(delegation, { out: 'hex' }); + + expect(hash).toMatchInlineSnapshot( + `"0xcaeb6434dc99e3098ff79ea1458a16b3e53b380f211cdb359628f9302d9c41b4"`, + ); + }); + + it('produces same result in different formats', () => { + const delegation: DelegationStruct = { + delegate: '0x1234567890123456789012345678901234567890', + delegator: '0xABCDEF0123456789012345678901234567890123', + authority: ROOT_AUTHORITY, + caveats: [], + salt: 0x42n, + signature: '0x789abc', + }; + + const hashHex = hashDelegation(delegation, { out: 'hex' }); + const hashBytes = hashDelegation(delegation, { out: 'bytes' }); + + const hashFromBytes = bytesToHex(hashBytes); + + expect(hashHex).toBe(hashFromBytes); + }); + }); }); diff --git a/packages/delegation-toolkit/src/DelegationFramework/DelegationManager/constants.ts b/packages/delegation-toolkit/src/DelegationFramework/DelegationManager/constants.ts index 58a4deb5..13522f1f 100644 --- a/packages/delegation-toolkit/src/DelegationFramework/DelegationManager/constants.ts +++ b/packages/delegation-toolkit/src/DelegationFramework/DelegationManager/constants.ts @@ -1,4 +1,4 @@ -export { ROOT_AUTHORITY, ANY_DELEGATE } from '../../constants'; +export { ROOT_AUTHORITY, ANY_BENEFICIARY } from '../../delegation'; export const NAME = 'DelegationManager'; export const VERSION = '1.3.0'; diff --git a/packages/delegation-toolkit/src/DelegationFramework/HybridDeleGator/constants.ts b/packages/delegation-toolkit/src/DelegationFramework/HybridDeleGator/constants.ts index 066a07b7..aa24c103 100644 --- a/packages/delegation-toolkit/src/DelegationFramework/HybridDeleGator/constants.ts +++ b/packages/delegation-toolkit/src/DelegationFramework/HybridDeleGator/constants.ts @@ -1,4 +1,4 @@ -export { ROOT_AUTHORITY, ANY_DELEGATE } from '../../constants'; +export { ROOT_AUTHORITY, ANY_BENEFICIARY } from '../../delegation'; export const NAME = 'HybridDeleGator'; export const VERSION = '1.3.0'; diff --git a/packages/delegation-toolkit/src/constants.ts b/packages/delegation-toolkit/src/constants.ts index bec33ef7..0c851425 100644 --- a/packages/delegation-toolkit/src/constants.ts +++ b/packages/delegation-toolkit/src/constants.ts @@ -7,14 +7,3 @@ export enum Implementation { Hybrid = 'Hybrid', Stateless7702 = 'Stateless7702', } - -/** - * To be used on a delegation as the root authority. - */ -export const ROOT_AUTHORITY = - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; - -/** - * To be used on a delegation as the any delegate. - */ -export const ANY_DELEGATE = '0x0000000000000000000000000000000000000a11'; diff --git a/packages/delegation-toolkit/src/delegation.ts b/packages/delegation-toolkit/src/delegation.ts index a2d53e69..1d99aca3 100644 --- a/packages/delegation-toolkit/src/delegation.ts +++ b/packages/delegation-toolkit/src/delegation.ts @@ -1,16 +1,13 @@ import { encodeDelegations as encodeDelegationsCore, decodeDelegations as decodeDelegationsCore, + hashDelegation, + ANY_BENEFICIARY, + DELEGATION_TYPEHASH, + CAVEAT_TYPEHASH, + ROOT_AUTHORITY, } from '@metamask/delegation-core'; -import { - encodeAbiParameters, - parseAbiParameters, - keccak256, - hashMessage, - toBytes, - toHex, - getAddress, -} from 'viem'; +import { hashMessage, toBytes, toHex, getAddress } from 'viem'; import type { TypedData, AbiParameter, @@ -23,13 +20,16 @@ import type { } from 'viem'; import { type Caveats, resolveCaveats } from './caveatBuilder'; -import { - CAVEAT_ABI_TYPE_COMPONENTS, - getCaveatArrayPacketHash, -} from './caveats'; -import { ROOT_AUTHORITY } from './constants'; +import { CAVEAT_ABI_TYPE_COMPONENTS } from './caveats'; import type { Delegation } from './types'; +export { + ANY_BENEFICIARY, + DELEGATION_TYPEHASH, + CAVEAT_TYPEHASH, + ROOT_AUTHORITY, +}; + /** * The ABI type components of a Delegation. */ @@ -141,21 +141,6 @@ export const decodePermissionContexts = (encoded: Hex[]): Delegation[][] => { return delegationChains; }; -/** - * To be used in the allowList field of a gas delegation so as not to restrict who can redeem the gas delegation. - */ -export const ANY_BENEFICIARY = '0x0000000000000000000000000000000000000a11'; - -/** - * To be used when generating a delegation hash to be signed - * NOTE: signature is omitted from the Delegation typehash - */ -export const DELEGATION_TYPEHASH = keccak256( - toHex( - 'Delegation(address delegate,address delegator,bytes32 authority,Caveat[] caveats,uint256 salt)Caveat(address enforcer,bytes terms)', - ), -); - /** * TypedData to be used when signing a Delegation. Delegation value for `signature` and Caveat values for `args` are omitted as they cannot be known at signing time. */ @@ -200,19 +185,7 @@ export const prepDelegationHashForPasskeySign = (delegationHash: Hex) => { export const getDelegationHashOffchain = (input: Delegation): Hex => { const delegationStruct = toDelegationStruct(input); - const encoded = encodeAbiParameters( - parseAbiParameters('bytes32, address, address, bytes32, bytes32, uint'), - [ - DELEGATION_TYPEHASH, - delegationStruct.delegate, - delegationStruct.delegator, - delegationStruct.authority, - getCaveatArrayPacketHash(delegationStruct.caveats), - delegationStruct.salt, - ], - ); - - return keccak256(encoded); + return hashDelegation(delegationStruct); }; type BaseCreateDelegationOptions = { diff --git a/packages/delegation-toolkit/src/index.ts b/packages/delegation-toolkit/src/index.ts index de032d93..eec7062a 100644 --- a/packages/delegation-toolkit/src/index.ts +++ b/packages/delegation-toolkit/src/index.ts @@ -1,10 +1,11 @@ export { toMetaMaskSmartAccount } from './toMetaMaskSmartAccount'; export { - ANY_BENEFICIARY, createDelegation, createOpenDelegation, signDelegation, + ROOT_AUTHORITY, + ANY_BENEFICIARY, } from './delegation'; export type { @@ -35,7 +36,7 @@ export { getDeleGatorEnvironment, } from './delegatorEnvironment'; -export { Implementation, ROOT_AUTHORITY, ANY_DELEGATE } from './constants'; +export { Implementation } from './constants'; export { createExecution } from './executions'; diff --git a/packages/delegation-toolkit/test/delegation.test.ts b/packages/delegation-toolkit/test/delegation.test.ts index a829782b..824b9a9f 100644 --- a/packages/delegation-toolkit/test/delegation.test.ts +++ b/packages/delegation-toolkit/test/delegation.test.ts @@ -2,9 +2,9 @@ import { expect } from 'chai'; import { stub } from 'sinon'; import { resolveCaveats } from '../src/caveatBuilder'; -import { ROOT_AUTHORITY } from '../src/constants'; import { type DelegationStruct, + ROOT_AUTHORITY, toDelegationStruct, createDelegation, createOpenDelegation, diff --git a/packages/delegation-toolkit/test/experimental/delegationStorageTestData.ts b/packages/delegation-toolkit/test/experimental/delegationStorageTestData.ts index 735f0258..8df454e7 100644 --- a/packages/delegation-toolkit/test/experimental/delegationStorageTestData.ts +++ b/packages/delegation-toolkit/test/experimental/delegationStorageTestData.ts @@ -1,5 +1,7 @@ -import { ROOT_AUTHORITY } from '../../src/constants'; -import { getDelegationHashOffchain } from '../../src/delegation'; +import { + ROOT_AUTHORITY, + getDelegationHashOffchain, +} from '../../src/delegation'; import type { Delegation } from '../../src/types'; export const TEST_ACCOUNT = '0x0E9bBA6e2D962645c5FB1064d86cc6dE6050739C'; diff --git a/yarn.lock b/yarn.lock index 4d87ac6d..55bf2ad7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1260,6 +1260,7 @@ __metadata: "@metamask/abi-utils": "npm:^3.0.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/utils": "npm:^11.4.0" + "@noble/hashes": "npm:^1.8.0" "@types/node": "npm:^20.10.6" tsup: "npm:^7.2.0" typescript: "npm:5.0.4" @@ -1503,6 +1504,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:^1.8.0": + version: 1.8.0 + resolution: "@noble/hashes@npm:1.8.0" + checksum: 10/474b7f56bc6fb2d5b3a42132561e221b0ea4f91e590f4655312ca13667840896b34195e2b53b7f097ec080a1fdd3b58d902c2a8d0fbdf51d2e238b53808a177e + languageName: node + linkType: hard + "@noble/secp256k1@npm:1.7.1, @noble/secp256k1@npm:~1.7.0": version: 1.7.1 resolution: "@noble/secp256k1@npm:1.7.1"