Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/wise-owners-reach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'ox': patch
---

Aligned configurable account owner and threshold validation with TIP-1061.
46 changes: 40 additions & 6 deletions src/tempo/MultisigConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,21 +169,21 @@ describe('assert / validate', () => {
expect(MultisigConfig.validate({ threshold: 1, owners: [] })).toBe(false)
})

test('accepts 50 owners', () => {
const owners = Array.from({ length: 50 }, (_, i) => ({
test('accepts 48 owners', () => {
const owners = Array.from({ length: 48 }, (_, i) => ({
owner: `0x${(i + 1).toString(16).padStart(40, '0')}` as `0x${string}`,
weight: 1,
}))
expect(
MultisigConfig.validate({
threshold: MultisigConfig.maxThreshold,
threshold: MultisigConfig.maxSignatures,
owners,
}),
).toBe(true)
})

test('too many owners', () => {
const owners = Array.from({ length: 51 }, (_, i) => ({
const owners = Array.from({ length: 49 }, (_, i) => ({
owner: `0x${(i + 1).toString(16).padStart(40, '0')}` as `0x${string}`,
weight: 1,
}))
Expand All @@ -199,11 +199,29 @@ describe('assert / validate', () => {
).toBe(false)
})

test('threshold exceeds protocol maximum', () => {
test('accepts a uint8 threshold above the signature limit', () => {
expect(
MultisigConfig.validate({
threshold: 9,
owners: [{ owner: owner1, weight: 9 }],
}),
).toBe(true)
})

test('accepts the maximum uint8 threshold', () => {
expect(
MultisigConfig.validate({
threshold: MultisigConfig.maxThreshold,
owners: [{ owner: owner1, weight: MultisigConfig.maxThreshold }],
}),
).toBe(true)
})

test('threshold exceeds uint8 maximum', () => {
expect(
MultisigConfig.validate({
threshold: MultisigConfig.maxThreshold + 1,
owners: [{ owner: owner1, weight: MultisigConfig.maxThreshold + 1 }],
owners: [{ owner: owner1, weight: MultisigConfig.maxThreshold }],
}),
).toBe(false)
})
Expand All @@ -226,6 +244,22 @@ describe('assert / validate', () => {
).toBe(false)
})

test('threshold cannot require more than eight owners', () => {
const owners = Array.from({ length: 9 }, (_, i) => ({
owner: `0x${(i + 1).toString(16).padStart(40, '0')}` as `0x${string}`,
weight: 1,
}))
expect(MultisigConfig.validate({ threshold: 9, owners })).toBe(false)
})

test('threshold can be reached by eight of more than eight owners', () => {
const owners = Array.from({ length: 9 }, (_, i) => ({
owner: `0x${(i + 1).toString(16).padStart(40, '0')}` as `0x${string}`,
weight: i === 0 ? 2 : 1,
}))
expect(MultisigConfig.validate({ threshold: 9, owners })).toBe(true)
})

test('total weight exceeds u8 max', () => {
expect(
MultisigConfig.validate({
Expand Down
23 changes: 18 additions & 5 deletions src/tempo/MultisigConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import * as Hex from '../core/Hex.js'
import type { Compute, OneOf } from '../core/internal/types.js'

/** Maximum number of owners allowed in a native multisig config. */
export const maxOwners = 50
export const maxOwners = 48

/** Maximum threshold accepted by a native multisig config. */
export const maxThreshold = 8
export const maxThreshold = 0xff

/** Maximum number of owner approvals in a native multisig signature. */
export const maxSignatures = maxThreshold
export const maxSignatures = 8

/**
* Maximum number of native multisig signatures in one nested authorization
Expand Down Expand Up @@ -72,7 +72,8 @@ export type Tuple = readonly [
* Mirrors the Tempo `InitMultisig::validate` rules: owners non-empty and
* `<= maxOwners`, strictly ascending unique nonzero owner addresses, nonzero
* integer owner weights, integer `threshold` between `1` and `maxThreshold`,
* total weight `<= 255` (u8 max), and `threshold <= total weight`.
* total weight `<= 255` (u8 max), and a threshold reachable by at most
* `maxSignatures` owners.
*
* @example
* ```ts twoslash
Expand Down Expand Up @@ -105,6 +106,7 @@ export function assert<numberType = number>(config: Config<numberType>): void {
throw new InvalidConfigError({ reason: 'threshold exceeds max threshold' })

let totalWeight = 0
const weights: number[] = []
let previous: bigint | undefined
for (const owner of owners) {
if (!Address.validate(owner.owner) || Hex.toBigInt(owner.owner) === 0n)
Expand All @@ -123,7 +125,9 @@ export function assert<numberType = number>(config: Config<numberType>): void {
})
previous = current

totalWeight += Number(owner.weight)
const weight = Number(owner.weight)
totalWeight += weight
weights.push(weight)
}

if (totalWeight > 0xff)
Expand All @@ -134,6 +138,15 @@ export function assert<numberType = number>(config: Config<numberType>): void {
throw new InvalidConfigError({
reason: 'threshold exceeds total owner weight',
})

const reachableWeight = weights
.sort((a, b) => b - a)
.slice(0, maxSignatures)
.reduce((sum, weight) => sum + weight, 0)
if (Number(threshold) > reachableWeight)
throw new InvalidConfigError({
reason: `threshold exceeds weight reachable by ${maxSignatures} owner signatures`,
})
}

export declare namespace assert {
Expand Down
Loading