LayerZero engaged Guardian to review the Stellar implementation of OneSig. From May 25 through June 3rd, 2026, Guardian reviewed OneSig on Stellar and recorded the findings in this report.
- Published
- Language
- Rust
- Chains
- Stellar
- Sector
- Wallets and custody
- 0 Critical
- 0 High
- 0 Medium
- 4 Low
- 3 Informational
Scope
Overview
LayerZero engaged Guardian to review the Stellar implementation of OneSig. From May 25 through June 3rd, 2026, Guardian reviewed OneSig on Stellar and recorded the findings in this report.
Findings 7
-
L-01 Low secp256k1 Signatures are not Restricted to low-s Form Best Practices Acknowledged
Description
OneSig Stellar recovers secp256k1 signers from
(r, s, v)signatures but does not explicitly reject high-s signatures. This allows multiple valid encodings for the same signer and digest.This does not appear to bypass threshold checks: the contract counts recovered signer addresses and enforces strictly increasing signer order, so the same signer cannot be counted twice even with malleable signatures.
The remaining risk is canonicalization and ecosystem compatibility. Off-chain tooling that assumes canonical Ethereum signatures may disagree with on-chain acceptance, and signature deduplication by bytes rather than recovered signer can be misleading.
Recommendation
Reject high-s signatures during recovery. Expected rule:
s <= secp256k1n / 2This aligns the Stellar verifier with standard Ethereum ECDSA canonical-signature expectations.
Resolution
LayerZero: Acknowledged.
-
L-02 Low secp256k1 Recovery IDs 2 and 3 are Accepted Best Practices Acknowledged
Description
The secp256k1 recovery helper accepts Ethereum-style
vvalues in the range27..=30, normalizing them to recovery IDs0..=3. Ethereum signatures conventionally usev = 27or28, corresponding to recovery IDs0and1.Accepting
29and30broadens the accepted signature domain beyond normal Ethereum-style expectations. The code may also accept raw recovery IDs outside the Ethereum-style range if the host recovery function accepts them.This does not appear to bypass threshold checks, but it can create differences between off-chain Ethereum-compatible validation and on-chain Stellar validation.
Recommendation
Restrict accepted values to one canonical set:
v in {27, 28}orv in {0, 1}. If both formats are supported, explicitly accept only{0, 1, 27, 28}and reject all other values.Resolution
LayerZero: Acknowledged.
-
L-03 Low Signer-as-executor Delegate Uses Dynamic Bytes While Stored as BytesN<32> Best Practices Acknowledged
Description
The signer-as-executor EIP-712 struct declares
delegateas dynamicbytes, while the Rust contract stores it asBytesN<32>. The implementation intentionally hashes the delegate as dynamic bytes, and the repository TypeScript helper matches this behavior.Independent clients may reasonably encode the field as
bytes32because the on-chain value is fixed-size. For EIP-712, dynamicbytesis encoded askeccak256(delegate), whilebytes32is encoded directly. These produce different struct hashes and can make signer-as-executor proofs fail due to off-chain/on-chain digest mismatch.Failures manifest as authorization rejection, not as a threshold bypass.
Recommendation
Either change the EIP-712 type to
bytes32 delegate, or keepbytes delegateand document that the 32-byte Ed25519 delegate key is encoded as dynamic bytes usingkeccak256(delegate).If external clients are expected, add cross-language test vectors for signer-as-executor proofs.
Resolution
LayerZero: Acknowledged.
-
L-04 Low Offchain Leaf Encoding Does not Enforce Bounds Validation Acknowledged
Description
The Stellar OneSig contract hashes
oneSigIdandnonceas Rustu64values when constructing each leaf. The shared TypeScript encoder accepts these fields as arbitrarybigintvalues, pads them to hex, and effectively reads only the first eight bytes without first checking theuint64bounds.Out-of-range or negative inputs can therefore be encoded non-canonically. Different bigint values can share the same effective on-chain
u64prefix, while duplicate detection uses the rawnonce.oneSigIdbigint pair before encoding.This does not let an attacker forge Merkle proofs or bypass threshold signatures. The risk is off-chain proposal canonicalization: signer tooling can display one
nonceoroneSigIdwhile the generated leaf commits to a different effectiveuint64, bypassing duplicate proposal checks or producing roots that are misleading or fail to execute.Recommendation
Reject non-canonical values before leaf construction and duplicate detection:
const U64_MAX = (1n << 64n) - 1n; if (value < 0n || value > U64_MAX) throw new Error(`${name} must fit uint64`);Apply this validation to both
nonceandoneSigId.Resolution
LayerZero: Acknowledged.
-
I-01 Informational OneSig Stellar Depends On Stellar Archival Restoration after Inactivity Warning Acknowledged
Description
OneSig Stellar stores critical contract state such as
Nonce,Seed, andOneSigIdin Soroban instance storage. Instance storage has a ledger-entry TTL. The contract uses LayerZero Stellar macros that initialize default TTL configuration in the constructor and inject instance TTL extension at the start of contract entrypoints.This means active contracts are maintained by call-triggered TTL extension. However, the extension does not run in the background. If a deployment receives no invocations until its instance TTL reaches zero, the instance entry becomes archived and must be restored before normal execution can proceed.
Impact
This is an operational and liveness dependency, not a direct authorization bypass or replay issue.
When the instance entry is archived:
- OneSig state is not reset to defaults.
- Previously consumed nonces should remain consumed after restoration.
- Execution still requires the normal OneSig authorization path: a valid Merkle root, threshold signatures, current nonce proof, and
executor or signer permission when configured.
- Clients or executors that do not handle Stellar archival restoration may fail to execute valid OneSig transactions after long
inactivity.
Restoration can be performed independently of OneSig:
- Automatically, when an
InvokeHostFunctiontransaction is simulated and the archived entries are included in the restore list. - Manually, by submitting a Stellar
RestoreFootprintOpfor the archived footprint.
Neither path requires OneSig multisig authorization because restoration is a Stellar ledger operation, not a OneSig business-logic operation.
Recommendation
Document this operational requirement for OneSig Stellar executors and clients:
- Always simulate Soroban invocations and preserve restore data in the assembled transaction.
- Ensure executor tooling can detect simulation restore preambles and retry or submit restoration as needed.
- Optionally run maintenance transactions to extend contract instance TTL before expiry for high-value deployments.
Resolution
LayerZero: Acknowledged.
-
I-02 Informational EIP-712 Expiry Fields Use Different Integer Types Across Signed Structs Best Practices Acknowledged
Description
The Merkle-root signature and signer-as-executor proof use different EIP-712 integer types for expiry values:
SignMerkleRoot(..., uint256 expiry) SignerProof(..., uint64 signerProofExpiry)Both values are represented as
u64in the Stellar contract and are encoded into 32-byte ABI-style words when constructing the digest. The repository's code and tests are internally consistent, so this is not a direct vulnerability.The issue is an interoperability footgun for independent clients that must exactly match the declared EIP-712 type strings.
Merkle root type:
SignMerkleRoot(bytes32 seed,bytes32 merkleRoot,uint256 expiry)Signer proof type:
SignerProof(bytes32 leafHash,bytes32 merkleRoot,bytes delegate,uint64 signerProofExpiry)Because EIP-712 type strings are part of the type hash, clients must use the exact declared type. Treating the signer proof expiry as
uint256, or changing the Merkle-root expiry touint64, will produce a different digest.Impact
- Independent clients may produce invalid signatures if they normalize both expiry fields to the same type.
- Reviewers may incorrectly assume the two expiry fields share the same typed-data encoding.
Recommendation
Standardize the expiry field type across OneSig typed-data structs if possible.
If the mismatch is intentional, document it and provide test vectors for:
- Merkle-root signing digest
- signer-as-executor proof digest
Resolution
LayerZero: Acknowledged.
-
I-03 Informational Signed Sub-invocation Trees Can Become Stale Warning Acknowledged
Description
OneSig Stellar commits to exact Soroban
sub_invocationsas part of the signed Merkle leaf. This is important for security because it prevents an executor from adding broader nested authorization after signers approve a root. However, it also means signed leaves are sensitive to changes in target-contract authorization behavior between simulation/signing and execution.If the required Soroban sub-invocation tree changes after signers approve a leaf, the executor cannot update the
sub_invocationswithout changing the leaf hash and invalidating the Merkle proof. The old leaf may become unexecutable and block later nonces.The original signed leaf may fail execution, and because OneSig uses a shared monotonic nonce per deployment, later nonce leaves cannot execute until the current nonce is resolved.
Recommendation
Prefer stable target-contract authorization paths for OneSig-managed operations.
Resolution
LayerZero: Acknowledged.
No findings match.
More from LayerZero
All 7 reports-
Canton VER Updates
169 findings2 critical · 12 high 169 findings: 2 critical, 12 high, 36 medium, 54 low, 65 informational -
Console EVM Updates
4 findings 4 findings: 1 low, 3 informational -
Solana Console
42 findings 42 findings: 6 low, 36 informational -
Solana OApp
54 findings 54 findings: 1 medium, 9 low, 44 informational
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.