Skip to content

Commit 6afd57b

Browse files
authored
fix(protocol-devtools): treat explicitly-empty ULN config values as NIL sentinels (#1944)
1 parent 128b697 commit 6afd57b

50 files changed

Lines changed: 1434 additions & 205 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@layerzerolabs/devtools-move": patch
3+
---
4+
5+
Reject an omitted `requiredDVNs` in `buildConfig` with a clear error. `requiredDVNs` is now
6+
optional on the shared `Uln302UlnUserConfig` type, but this encoder maps an empty required set
7+
to the NIL sentinel (pin "no required DVNs") and cannot express "inherit the on-chain default".
8+
Defaulting an omitted value to `[]` would silently pin the least-secure shape, so it now throws
9+
instead — callers must pass the required DVNs explicitly, or `[]` to pin "no required DVNs".
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@layerzerolabs/ua-devtools-evm-hardhat": patch
3+
---
4+
5+
Generate ULN configs (both the ULN302 send/receive and the Read library generators) that
6+
round-trip the new NIL-sentinel semantics: a field inheriting the on-chain default is
7+
OMITTED (for both `requiredDVNs` and `optionalDVNs`, which now behave identically) rather
8+
than emitted as an explicit empty value that would pin zero/none on re-apply. Pinned-none
9+
configs continue to emit `[]`/`0n`.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@layerzerolabs/metadata-tools": minor
3+
---
4+
5+
`generateConnectionsConfig` now treats a pathway with no optional DVNs as an explicit
6+
"no optional DVNs" (pinned via the NIL sentinel) instead of a value that inherits the
7+
on-chain default.
8+
9+
The emitted config still carries `optionalDVNs: []`, but under the new ULN302 sentinel
10+
semantics that empty array now pins "no optional DVNs" on-chain rather than falling back
11+
to the chain default. This is deliberate: the metadata config is the primary way a config
12+
is consumed, and an empty optional-DVN set should be visible in the file rather than
13+
silently inheriting the default.
14+
15+
Re-wiring a pathway that previously inherited the on-chain default will now pin its
16+
optional-DVN set explicitly. If that default carried optional DVNs (a non-zero threshold),
17+
pinning an empty set drops them — this is intended. The goal is that a team's verification
18+
config is exactly what their config file says, not something that can change underneath them
19+
when a LayerZero-controlled default is updated. An empty optional-DVN set means "no optional
20+
DVNs"; teams that want an optional quorum should list those DVNs explicitly. Required DVNs
21+
are unaffected by this change.

.changeset/solana-bigint-to-bn.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@layerzerolabs/devtools-solana": minor
3+
---
4+
5+
Add `bigIntToBN` helper (and `Bignum` type) for converting a `bigint` to the `BN` type
6+
the Solana program instruction builders expect, preserving full precision for `u64`
7+
values that overflow a JS number.

.changeset/uln-nil-sentinels.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@layerzerolabs/protocol-devtools": major
3+
"@layerzerolabs/protocol-devtools-evm": major
4+
"@layerzerolabs/protocol-devtools-solana": major
5+
---
6+
7+
Treat explicitly-empty ULN302 config values as NIL sentinels instead of defaults
8+
9+
This lets a team pin a literal "none"/"zero" so their security configuration is exactly
10+
what their config file says, rather than silently inheriting a default that LayerZero
11+
controls. Being able to opt out of defaults is the point: a pinned config cannot change
12+
underneath a team when a default is updated.
13+
14+
When serializing an OApp ULN302 / Read config, `requiredDVNs` and `optionalDVNs` now
15+
behave identically — omitted, explicitly-empty, and concrete each map to a distinct
16+
on-chain meaning:
17+
18+
- Omitting a DVN field (leaving it `undefined`) inherits the on-chain default.
19+
- An explicitly-empty array (`[]`) pins "no DVNs" via `NIL_DVN_COUNT` (`0xff`).
20+
- A concrete array pins those DVNs.
21+
- Likewise `confirmations: 0n` now serializes to `NIL_CONFIRMATIONS`
22+
(`type(uint64).max`), while omitting it inherits the default.
23+
24+
To make `requiredDVNs` express "inherit" the same way `optionalDVNs` already could, it
25+
is now OPTIONAL on `Uln302UlnUserConfig` and `UlnReadUlnUserConfig` (previously
26+
mandatory). This removes the need for any count override — the count is always derived
27+
from the array, so the three serializers (EVM ULN302, EVM Read, Solana ULN302) share a
28+
single `resolveDVNCount` helper.
29+
30+
The read types `Uln302UlnConfig`/`UlnReadUlnConfig` carry `optionalDVNCount` (and
31+
`UlnReadUlnConfig` also `requiredDVNCount`) so the stored sentinel round-trips through
32+
the configuration diff, and the on-chain read path normalizes rather than re-applying
33+
the empty→NIL mapping, keeping `hasAppUlnConfig` idempotent on both paths. The
34+
library-wide DEFAULT config continues to serialize literal values (it rejects NIL
35+
sentinels on-chain). On Solana, `confirmations` is now encoded as a `BN` so the `u64`
36+
NIL sentinel survives without precision loss.
37+
38+
MIGRATION:
39+
40+
- If you wrote `confirmations: 0`, `requiredDVNs: []`, or `optionalDVNs: []` expecting
41+
the config to inherit the protocol default, OMIT the field instead. An explicit empty
42+
value now pins literal zero/none — for `confirmations` this means zero block
43+
confirmations, and for `requiredDVNs` it means no required DVNs, both
44+
security-relevant. Re-wiring an existing OApp whose config used these empty values
45+
will emit a `setConfig` that flips it from inherit to pinned.
46+
- The read types `Uln302UlnConfig` (gains `optionalDVNCount`) and `UlnReadUlnConfig`
47+
(gains `requiredDVNCount` and `optionalDVNCount`) have new required fields. Any code
48+
that hand-constructs one of these (e.g. mocking an SDK read) must supply the new
49+
fields.
50+
- `requiredDVNs` is no longer required on the user config. Code that always set it
51+
keeps working unchanged; you may now omit it to inherit the on-chain default.

examples/lzapp-migration/layerzero.config.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,16 @@ const config: OAppOmniGraphHardhat = {
4242
ulnConfig: {
4343
confirmations: BigInt(15),
4444
requiredDVNs: ['0x8eebf8b423b73bfca51a1db4b7354aa0bfca9193'], // LayerZero Labs DVN
45-
optionalDVNs: [],
46-
optionalDVNThreshold: 0,
45+
// optionalDVNs omitted → inherit the default. To pin "no optional DVNs"
46+
// explicitly, set `optionalDVNs: []` (it now serializes to the NIL sentinel).
4747
},
4848
},
4949
receiveConfig: {
5050
ulnConfig: {
5151
confirmations: BigInt(32),
5252
requiredDVNs: ['0x8eebf8b423b73bfca51a1db4b7354aa0bfca9193'],
53-
optionalDVNs: [],
54-
optionalDVNThreshold: 0,
53+
// optionalDVNs omitted → inherit the default. To pin "no optional DVNs"
54+
// explicitly, set `optionalDVNs: []` (it now serializes to the NIL sentinel).
5555
},
5656
},
5757
},
@@ -85,11 +85,9 @@ const config: OAppOmniGraphHardhat = {
8585
requiredDVNs: [
8686
'4VDjp6XQaxoZf5RGwiPU9NR1EXSZn2TP4ATMmiSzLfhb', // LayerZero
8787
],
88-
// The address of the DVNs you will pay to verify a sent message on the source chain ).
89-
// The destination tx will wait until the configured threshold of `optionalDVNs` verify a message.
90-
optionalDVNs: [],
91-
// The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified.
92-
optionalDVNThreshold: 0,
88+
// optionalDVNs omitted → inherit the default. To pin "no optional DVNs"
89+
// explicitly, set `optionalDVNs: []` (it now serializes to the NIL sentinel),
90+
// with `optionalDVNThreshold: 0`.
9391
},
9492
},
9593
// Optional Receive Configuration
@@ -103,11 +101,9 @@ const config: OAppOmniGraphHardhat = {
103101
requiredDVNs: [
104102
'4VDjp6XQaxoZf5RGwiPU9NR1EXSZn2TP4ATMmiSzLfhb', // LayerZero
105103
],
106-
// The address of the DVNs you will pay to verify a sent message on the source chain ).
107-
// The destination tx will wait until the configured threshold of `optionalDVNs` verify a message.
108-
optionalDVNs: [],
109-
// The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified.
110-
optionalDVNThreshold: 0,
104+
// optionalDVNs omitted → inherit the default. To pin "no optional DVNs"
105+
// explicitly, set `optionalDVNs: []` (it now serializes to the NIL sentinel),
106+
// with `optionalDVNThreshold: 0`.
111107
},
112108
},
113109
enforcedOptions: [

examples/lzapp-migration/lzapp.config.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,16 @@ const config: OAppOmniGraphHardhat = {
4040
ulnConfig: {
4141
confirmations: BigInt(1),
4242
requiredDVNs: ['0x53f488e93b4f1b60e8e83aa374dbe1780a1ee8a8'], // LayerZero Labs DVN for Arbitrum Sepolia
43-
optionalDVNs: [],
44-
optionalDVNThreshold: 0,
43+
// optionalDVNs omitted → inherit the default. Set `optionalDVNs: []` to
44+
// pin "no optional DVNs" (it now serializes to the NIL sentinel).
4545
},
4646
},
4747
receiveConfig: {
4848
ulnConfig: {
4949
confirmations: BigInt(1),
5050
requiredDVNs: ['0x53f488e93b4f1b60e8e83aa374dbe1780a1ee8a8'], // LayerZero Labs DVN for Arbitrum Sepolia
51-
optionalDVNs: [],
52-
optionalDVNThreshold: 0,
51+
// optionalDVNs omitted → inherit the default. Set `optionalDVNs: []` to
52+
// pin "no optional DVNs" (it now serializes to the NIL sentinel).
5353
},
5454
},
5555
},
@@ -71,16 +71,16 @@ const config: OAppOmniGraphHardhat = {
7171
ulnConfig: {
7272
confirmations: BigInt(1),
7373
requiredDVNs: ['0x8eebf8b423b73bfca51a1db4b7354aa0bfca9193'], // LayerZero Labs DVN on Ethereum Sepolia
74-
optionalDVNs: [],
75-
optionalDVNThreshold: 0,
74+
// optionalDVNs omitted → inherit the default. Set `optionalDVNs: []` to
75+
// pin "no optional DVNs" (it now serializes to the NIL sentinel).
7676
},
7777
},
7878
receiveConfig: {
7979
ulnConfig: {
8080
confirmations: BigInt(1),
8181
requiredDVNs: ['0x8eebf8b423b73bfca51a1db4b7354aa0bfca9193'], // LayerZero Labs DVN on Ethereum Sepolia
82-
optionalDVNs: [],
83-
optionalDVNThreshold: 0,
82+
// optionalDVNs omitted → inherit the default. Set `optionalDVNs: []` to
83+
// pin "no optional DVNs" (it now serializes to the NIL sentinel).
8484
},
8585
},
8686
},

examples/lzapp-migration/tasks/common/taskHelper.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ export async function getEpv1SendUlnConfig(
100100
const ulnConfig: Uln302UlnConfig = {
101101
confirmations: ulnConfigRaw.confirmations.toNumber(),
102102
requiredDVNs: ulnConfigRaw.requiredDVNs,
103+
requiredDVNCount: ulnConfigRaw.requiredDVNCount,
103104
optionalDVNs: ulnConfigRaw.optionalDVNs,
105+
optionalDVNCount: ulnConfigRaw.optionalDVNCount,
104106
optionalDVNThreshold: ulnConfigRaw.optionalDVNThreshold,
105107
}
106108

@@ -141,7 +143,9 @@ export async function getEpv1ReceiveUlnConfig(
141143
const ulnConfig: Uln302UlnConfig = {
142144
confirmations: ulnConfigRaw.confirmations.toNumber(),
143145
requiredDVNs: ulnConfigRaw.requiredDVNs,
146+
requiredDVNCount: ulnConfigRaw.requiredDVNCount,
144147
optionalDVNs: ulnConfigRaw.optionalDVNs,
148+
optionalDVNCount: ulnConfigRaw.optionalDVNCount,
145149
optionalDVNThreshold: ulnConfigRaw.optionalDVNThreshold,
146150
}
147151

@@ -330,7 +334,9 @@ export async function getEpv1DefaultSendConfig(
330334
const emptyUlnConfig: Uln302UlnConfig = {
331335
confirmations: BigInt(0),
332336
requiredDVNs: [zeroAddress],
337+
requiredDVNCount: 0,
333338
optionalDVNs: [],
339+
optionalDVNCount: 0,
334340
optionalDVNThreshold: 0,
335341
}
336342

@@ -348,7 +354,9 @@ export async function getEpv1DefaultSendConfig(
348354
const ulnConfig: Uln302UlnConfig = {
349355
confirmations: ulnConfigRaw.confirmations.toNumber(),
350356
requiredDVNs: ulnConfigRaw.requiredDVNs,
357+
requiredDVNCount: ulnConfigRaw.requiredDVNCount,
351358
optionalDVNs: ulnConfigRaw.optionalDVNs,
359+
optionalDVNCount: ulnConfigRaw.optionalDVNCount,
352360
optionalDVNThreshold: ulnConfigRaw.optionalDVNThreshold,
353361
}
354362

@@ -386,7 +394,9 @@ export async function getEpv1DefaultReceiveConfig(
386394
const emptyUlnConfig: Uln302UlnConfig = {
387395
confirmations: BigInt(0),
388396
requiredDVNs: [zeroAddress],
397+
requiredDVNCount: 0,
389398
optionalDVNs: [],
399+
optionalDVNCount: 0,
390400
optionalDVNThreshold: 0,
391401
}
392402

@@ -404,7 +414,9 @@ export async function getEpv1DefaultReceiveConfig(
404414
const ulnConfig: Uln302UlnConfig = {
405415
confirmations: ulnConfigRaw.confirmations.toNumber(),
406416
requiredDVNs: ulnConfigRaw.requiredDVNs,
417+
requiredDVNCount: ulnConfigRaw.requiredDVNCount,
407418
optionalDVNs: ulnConfigRaw.optionalDVNs,
419+
optionalDVNCount: ulnConfigRaw.optionalDVNCount,
408420
optionalDVNThreshold: ulnConfigRaw.optionalDVNThreshold,
409421
}
410422

@@ -471,7 +483,15 @@ function encodeExecutorConfig(config: Uln302ExecutorConfig): string {
471483

472484
/**
473485
* Encodes the UlnConfig into ABI-encoded bytes.
474-
* @param config Uln302UlnConfig object
486+
*
487+
* The DVN counts are derived from the array lengths rather than read from the config's
488+
* `requiredDVNCount`/`optionalDVNCount`. That is only valid because the callers here feed
489+
* RESOLVED configs (read via `getConfig` -> `getUlnConfig`, which collapses any stored NIL
490+
* sentinel to 0 before returning). Uln301 inherits `UlnBase`, so its stored config CAN hold a
491+
* NIL sentinel — if this were ever pointed at a raw/stored config (`getAppUlnConfig`), the
492+
* length derivation would silently drop that sentinel. Keep it on the resolved-config path.
493+
*
494+
* @param config Uln302UlnConfig object (resolved, not raw/stored)
475495
* @returns ABI-encoded string
476496
*/
477497
function encodeUlnConfig(config: Uln302UlnConfig): string {
@@ -481,10 +501,10 @@ function encodeUlnConfig(config: Uln302UlnConfig): string {
481501
],
482502
[
483503
[
484-
config.confirmations,
485-
config.requiredDVNs.length,
486-
config.optionalDVNs.length,
487-
config.optionalDVNThreshold,
504+
config.confirmations ?? 0,
505+
(config.requiredDVNs || []).length,
506+
(config.optionalDVNs || []).length,
507+
config.optionalDVNThreshold ?? 0,
488508
config.requiredDVNs || [],
489509
config.optionalDVNs || [],
490510
],

examples/oft-solana/layerzero.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export default async function () {
5353
[
5454
arbitrumContract, // Chain A contract
5555
solanaContract, // Chain B contract
56+
// DVN config: [ requiredDVN[], [ optionalDVN[], optionalDVNThreshold ] ]
57+
// - requiredDVNs: every listed DVN must verify each message — set these explicitly.
58+
// - optionalDVNs: an empty array pins "no optional DVNs"; it does NOT inherit the
59+
// on-chain default. For an optional quorum, pass [[...DVNs], threshold].
5660
[['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ]
5761
[15, 32], // [A to B confirmations, B to A confirmations]
5862
[SOLANA_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions

examples/oft/layerzero.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ const pathways: TwoWayConfig[] = [
3636
[
3737
baseContract, // Chain A contract
3838
arbitrumContract, // Chain B contract
39+
// DVN config: [ requiredDVN[], [ optionalDVN[], optionalDVNThreshold ] ]
40+
// - requiredDVNs: every listed DVN must verify each message — set these explicitly.
41+
// - optionalDVNs: an empty array pins "no optional DVNs"; it does NOT inherit the
42+
// on-chain default. For an optional quorum, pass [[...DVNs], threshold].
3943
[['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ]
4044
[1, 1], // [A to B confirmations, B to A confirmations]
4145
[EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions

0 commit comments

Comments
 (0)