Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · June 2026

OneSig on Stellar

for LayerZero

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

7 acknowledged

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

  1. L-01 Low secp256k1 Signatures are not Restricted to low-s Form Best Practices Acknowledged
    Location
    contracts/protocol/stellar/contracts/utils/sr c/multisig.rs

    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 / 2
    

    This aligns the Stellar verifier with standard Ethereum ECDSA canonical-signature expectations.

    Resolution

    LayerZero: Acknowledged.

  2. L-02 Low secp256k1 Recovery IDs 2 and 3 are Accepted Best Practices Acknowledged
    Location
    contracts/protocol/stellar/contracts/utils/sr c/multisig.rs

    Description

    The secp256k1 recovery helper accepts Ethereum-style v values in the range 27..=30, normalizing them to recovery IDs 0..=3. Ethereum signatures conventionally use v = 27 or 28, corresponding to recovery IDs 0 and 1.

    Accepting 29 and 30 broadens 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} or v in {0, 1}. If both formats are supported, explicitly accept only {0, 1, 27, 28} and reject all other values.

    Resolution

    LayerZero: Acknowledged.

  3. L-03 Low Signer-as-executor Delegate Uses Dynamic Bytes While Stored as BytesN<32> Best Practices Acknowledged
    Location
    packages/onesig/onesig-stellar/contracts/o nesig/src/interfaces/onesig.rs

    Description

    The signer-as-executor EIP-712 struct declares delegate as dynamic bytes, while the Rust contract stores it as BytesN<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 bytes32 because the on-chain value is fixed-size. For EIP-712, dynamic bytes is encoded as keccak256(delegate), while bytes32 is 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 keep bytes delegate and document that the 32-byte Ed25519 delegate key is encoded as dynamic bytes using keccak256(delegate).

    If external clients are expected, add cross-language test vectors for signer-as-executor proofs.

    Resolution

    LayerZero: Acknowledged.

  4. L-04 Low Offchain Leaf Encoding Does not Enforce Bounds Validation Acknowledged
    Location
    packages/onesig/onesig-core/src/

    Description

    The Stellar OneSig contract hashes oneSigId and nonce as Rust u64 values when constructing each leaf. The shared TypeScript encoder accepts these fields as arbitrary bigint values, pads them to hex, and effectively reads only the first eight bytes without first checking the uint64 bounds.

    Out-of-range or negative inputs can therefore be encoded non-canonically. Different bigint values can share the same effective on-chain u64 prefix, while duplicate detection uses the raw nonce.oneSigId bigint 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 nonce or oneSigId while the generated leaf commits to a different effective uint64, 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 nonce and oneSigId.

    Resolution

    LayerZero: Acknowledged.

  5. I-01 Informational OneSig Stellar Depends On Stellar Archival Restoration after Inactivity Warning Acknowledged
    Location
    Global

    Description

    OneSig Stellar stores critical contract state such as Nonce, Seed, and OneSigId in 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 InvokeHostFunction transaction is simulated and the archived entries are included in the restore list.
    • Manually, by submitting a Stellar RestoreFootprintOp for 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.

  6. I-02 Informational EIP-712 Expiry Fields Use Different Integer Types Across Signed Structs Best Practices Acknowledged
    Location
    packages/onesig/onesig-stellar/contracts/o nesig/src/eip712.rs

    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 u64 in 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 to uint64, 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.

  7. I-03 Informational Signed Sub-invocation Trees Can Become Stale Warning Acknowledged
    Location
    Global

    Description

    OneSig Stellar commits to exact Soroban sub_invocations as 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_invocations without 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.

More from LayerZero

All 7 reports
  1. Canton VER Updates

    169 findings2 critical · 12 high 169 findings: 2 critical, 12 high, 36 medium, 54 low, 65 informational
  2. Console EVM Updates

    4 findings 4 findings: 1 low, 3 informational
  3. Solana Console

    42 findings 42 findings: 6 low, 36 informational
  4. 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.

Get a quote