Guardian's review of OneSig on Solana for LayerZero, published August 2026. The report records 27 findings across 4 review rounds, including 3 low and 24 informational.
- Published
- Review window
- June 11 to August 16, 2026
- Rounds
- Main Review 1, Remediation Review 1, Main Review 2, Remediation Review 2
- Language
- Rust
- Chains
- Solana
- Sector
- Wallets and custody, Governance
- 0 Critical
- 0 High
- 0 Medium
- 3 Low
- 24 Informational
Scope
-
GuardianOrg/layerzero-audit-external-source-team1-1781115004299
facaf223b7c73798b9a6c0197e4810ca2dff10e0 -
GuardianOrg/audit-external-team1-1783712973565
662a11b7e4
Findings 27
Main Review 1
8 findings · June 11 to 19, 2026-
I-01 Informational
EmptyExecutorSetcheck: EVM/Stellar mismatch Unexpected Behavior ResolvedDescription
Executors::set_executor_required(and, transitively,init_one_sig) andExecutors::remove_executorenforce the invariant that the executor set must be non-empty wheneverexecutor_required = true, reverting withOneSigError::EmptyExecutorSetotherwise. This is stricter than the EVM and Stellar implementations, which both allowexecutor_required = truewith an empty executor set. The invariant is unnecessary because execution does not depend on the executor set being non-empty. Whenexecutor_required = true, a registered signer can still execute throughsigner_execute_transaction, which validates the submission againstone_sig_state.multisig.signers(the signer set) and never readsexecutors. Sincethreshold >= 1always guarantees at least one signer, the state(executor_required = true, executors = [])is fully functional, it simply disables the standalone executor path while leaving the signer path available. It is not a lockout. The practical effect is a cross-VM behavioral mismatch: a valid "signers-only, no standalone executors" configuration that is representable on EVM and Stellar cannot be configured on Solana, because the guard rejects it at bothset_configandinit.Recommendation
Consider droping the non-empty executor-set requirement so
executor_required = trueis permitted with an empty executor set, matching EVM and Stellar. Or acknowledging and clearly documenting this mismatch. -
I-02 Informational Document Solana Signer Caps Informational Acknowledged
Description
The Solana implementation intentionally caps each OneSig signer set at 20 signers and caps the signature threshold at 13. These limits are enforced during initialization and config updates: add_signer rejects more than
SIGNERS_MAX_LEN, and set_threshold rejects 0, values aboveMAX_THRESHOLD, and thresholds greater than the current signer count. This appears intentional and is documented in code comments as a Solana transaction-size constraint, not a direct vulnerability. However, it is important operationally because other implementations, such as EVM, do not have the same explicit 20 signer / 13 threshold cap. A cross-chain deployment that assumes the same governance policy can be mirrored everywhere may fail on Solana, or may require a lower Solana threshold than the rest of the deployment. Operators should also be aware that having 20 signers does not mean a 20-of-20 policy is possible on Solana; the maximum threshold remains 13.Recommendation
Document the Solana-specific signer and threshold caps in public integration docs, deployment tooling, and operator runbooks. Off-chain tooling should validate these limits before producing cross-chain signer-set ceremonies, and should warn when an EVM/Stellar/etc. signer policy cannot be represented equivalently on Solana. For Solana execution, tooling should also prefer supplying only the required threshold number of signatures to avoid unnecessary transaction-size and compute pressure.
-
I-03 Informational Root PDA Omits Expiry Validation Acknowledged
Description
verify_merkle_rootderivesMerkleRootStateonly from[MERKLE_ROOT_SEED, one_sig_state, merkle_root], while expiry is only stored inside the account. Since expiry is part of the signed EIP-712 payload, the same Merkle root may validly be signed with a different expiry. However, once a PDA exists for that root, another verification for the same root with a new expiry cannot initialize a fresh MerkleRootState. This means a root verified with a shorter expiry can occupy the only PDA slot for that root. A later valid signature set extending the same root’s expiry cannot be cached through the two-step path until the original PDA expires and is closed. One-step execution with inline signatures remains possible, so this is not a full execution liveness break, but it can break the intended pre-verification flow and create an avoidable griefing/availability issue. EVM does not have the same issue because it does not persist verified roots; it verifies (merkleRoot, expiry, signatures) live on every execution.Recommendation
Include expiry in the MerkleRootState PDA seeds, or allow verify_merkle_root to update an existing PDA when the caller supplies a valid signature set for the same root with a later expiry.
-
I-04 Informational Inline verification ignores
merkle_root_stateBest Practices AcknowledgedDescription
resolve_merkle_root(execution.rs) branches onmerkle_root_verification:let root = if let Some(VerifyMerkleRootParams { merkle_root, expiry, signatures }) = merkle_root_verification { // Case 1: Direct verification with merkle root parameters MerkleValidator::verify_merkle_root( one_sig_state, merkle_root, *expiry, signatures.as_ref(), Clock::get()?.unix_timestamp, )?; *merkle_root } else { // Case 2: Two-step verification, using pre-verified merkle root state require!(merkle_root_state.is_some(), OneSigError::MissingMerkleRootState); merkle_root_state.unwrap().merkle_root };The two modes are mutually exclusive by construction: inline (
Some) verifies the root from the params and returns it; two-step (else) requires the pre-verified account. The inline branch never readsmerkle_root_state, so the intended pairing is inline ⇒merkle_root_state == None, two-step ⇒merkle_root_state == Some. Nothing enforces this. A caller of either entrypoint can supply bothmerkle_root_verification == Some(...)and a populatedmerkle_root_state; the inline branch wins and the account is silently ignored as contradictory dead input. The issue is that the program tolerates self-contradictory input instead of rejecting it, an inconsistency between the invariant the code is built around (inline ⇒ no stored account) and what is actually enforced (nothing).Recommendation
Reject a co-supplied account in the inline branch:
require!(merkle_root_state.is_none(), OneSigError::UnexpectedMerkleRootState); -
I-05 Informational Misleading "permissionless" doc comment Documentation Resolved
Description
The doc comment on
ExecuteTransaction::apply(instructions/execute_transaction.rs:33) reads:/// Executes a transaction with a pre-verified merkle root permissionlesslyThis is only true when
executor_required == false. Whenexecutor_required == true, the path is executor-gated, not permissionless:applyrequires theexecutor(aSigner) to be present inOneSigState.executors.executors, otherwise it aborts withOneSigError::ExecutorRequired:if ctx.accounts.one_sig_state.executors.executor_required { let executor = ctx.accounts.executor.key(); require!( ctx.accounts.one_sig_state.executors.executors.contains(&executor), OneSigError::ExecutorRequired ); }So in the executor-required configuration, only a pre-approved executor can drive this instruction, the call is permissioned. The gating logic itself is correct; only the comment is wrong. The same caveat applies to the "1. Verify signatures …" process note that follows, which describes the permissionless flow without mentioning the executor check that runs first.
Recommendation
Correct the comment to reflect both modes; for example, note that execution is permissionless only when
executor_required == false, and is restricted to an approved executor inOneSigState.executorswhenexecutor_required == true. -
I-06 Informational PDA Signer Propagates Through CPI Documentation Acknowledged
Description
Solana OneSig executes target instructions through invoke_signed, marking one_sig_signer as signer for the target CPI whenever the PDA is included in the signed leaf’s account list. On Solana, signer privileges are inherited by downstream CPIs, so the immediate target program can pass one_sig_signer as signer to other programs using the accounts it received. This differs from the EVM implementation. EVM OneSig uses normal .call, so a downstream contract called by the target sees msg.sender == target, not msg.sender == OneSig. In contrast, Solana signer privilege can continue through nested CPI. Solana runtime prevents indirect reentry back into OneSig, so this does not allow a target to call back into OneSig admin functions such as set_config, but it does mean that any target receiving one_sig_signer must be trusted to use that signer authority correctly for all downstream CPIs.
Recommendation
Document this Solana-specific trust boundary. Off-chain tooling should make it explicit when a signed leaf grants one_sig_signer signer authority to a target program. Operators should treat target programs that receive one_sig_signer as trusted for the full downstream CPI flow, not only for the immediate instruction.
-
I-07 Informational Solana Leaf Is Single-Instruction Informational Resolved
Description
This is the semantic/model mismatch.
- EVM leaf: contains Call[]
- Solana leaf: contains one target instruction
- Multi-call workflows need multiple leaves or a wrapper program
Recommendation
Beware of this constraint.
-
I-08 Informational Undocumented Solana per-leaf tx-size limit Best Practices Acknowledged
Description
Solana OneSig signers can authorize a leaf that is valid under the Merkle/signature scheme but cannot be submitted because the corresponding execute_transaction exceeds Solana transaction-size or account/compute limits. In the two-step path, each execution still carries the target instruction data, target accounts, and Merkle proof. Address Lookup Tables reduce account-address overhead, but they do not reduce instruction data or proof bytes. Large roots increase proof size logarithmically, reducing the remaining budget for the target instruction. If an unexecutable leaf corresponds to the current nonce, later leaves that commit to higher nonces cannot execute through the normal sequence until signers authorize a different executable leaf for that nonce.
Recommendation
Consider adding SDK/tooling preflight checks that estimate the serialized transaction size for every Solana leaf under its expected proof depth and account list. Tooling should warn or reject leaves that cannot fit even with ALTs and two-step verification, and should recommend splitting roots, reducing proof depth, reducing instruction data/accounts, or using a wrapper program where appropriate.
Remediation Review 1
2 findings · June 26 to 29, 2026-
I-01 Informational Stale
build_instructiondoc comment Documentation AcknowledgedDescription
The doc comment on
build_instruction(execution.rs:48-53) is out of date relative to the function it documents. After this rems,build_instructionnow (a) returnsResult<OneSigInstruction>instead ofOneSigInstruction, and (b) guards against an emptyremaining_accountsslice viarequire!(!remaining_accounts.is_empty(), OneSigError::MissingProgramId)before indexingremaining_accounts[0]. The comment still describes the old behavior: it lists "5. Returns the OneSigInstruction" (no mention of the fallibleResult) and step "1. Calculates the start and end indices for accounts" / "3. Creates an instruction with the program ID from the first account" without noting the new empty-accounts precondition that protects that first-element access. A reader relying on the doc would not learn that the function can now fail withMissingProgramId, nor that the empty-input panic was fixed here. The code itself is correct; only the comment is staleRecommendation
Update the comment to reflect the current signature and behavior: note that the function returns
Result<OneSigInstruction>and errors withOneSigError::MissingProgramIdwhenremaining_accountsis empty (before theremaining_accounts[0]program-id read). -
I-02 Informational Cached Signer Subset Can Be Front-Run Frontrunning Acknowledged
Description
The I-01 remediation stores the signers supplied during
verify_merkle_rootinMerkleRootState.signed_by. Subsequent execution through the two-step flow requires a sufficient number of those specific cached signers to remain active. Because Merkle root verification is permissionless and each Merkle root is associated with a unique PDA, an attacker can front-run a legitimate verification transaction by copying it, removing any unnecessary signatures, and initializing the PDA using only the minimum quorum required for verification. For example, if the signature threshold is 2 and the original verification includes signatures from A, B, and C, a front-runner can submit the same Merkle root using only A and B. The PDA is then permanently initialized with{A, B}as the cached signer set. If signer A is later removed, the cached root can no longer satisfy execution requirements through the two-step path, even though B and C still form a valid active quorum under the current signer configuration. Since the PDA cannot be updated or reverified, the legitimate verification containing the stronger signer snapshot ({A, B, C}) cannot replace the front-runner's minimal subset until the account becomes closeable. As a result, the cached Merkle root remains unusable for two-step execution despite the existence of a valid active quorum. Inline Merkle proof verification remains unaffected, making this primarily an availability and griefing issue that impacts the two-step execution flow rather than enabling unauthorized execution.Recommendation
Beware of this case when using pre-verified roots. If you intend to resolve it, consider adding a method that allows an existing root PDA to be refreshed with a newly verified signer set.
Main Review 2
15 findings · July 13 to 15, 2026-
L-01 Low Mixed writability breaks multicalls Logical Error Resolved
Description
Multicalls reference accounts only by index and do not encode the writability expected by each individual call. During reconstruction, the program copies the privilege with which the account was supplied to
execute_transactionfromAccountInfo.is_writable, rather than using the writability intended for the individual inner call.for &index in instruction_account_indices { let account_info = account_at(index)?; let pubkey = account_info.key(); metas.push(OneSigAccountMeta { pubkey, is_signer: pubkey == one_sig_signer_key, is_writable: account_info.is_writable, });Consider an ordered two-call leaf where both calls use account X. Call A expects X to be read-only, while Call B expects X to be writable. Because Call B requires write access, account X must be supplied as writable to the OneSig
execute_transactioninstruction. As a result,AccountInfo.is_writableis true when OneSig reconstructs both inner calls, even though Call A was signed with X as read-only. The signed Merkle root commits to a leaf containing X as read-only in Call A and writable in Call B, but the program reconstructs X as writable in both calls during execution. Since writability is included inOneSigInstruction, the reconstructed leaf hash differs from the signed leaf, causing Merkle-proof verification to fail before either CPI executes.Recommendation
Encode the expected writability for every account reference in each call instead of reconstructing it from
AccountInfo.is_writable. During reconstruction, use the per-call flag and reject writable access if the account was supplied as read-only. -
L-02 Low Foreign buffers can be consumed DoS Resolved
Description
init_buffered_transaction()andinit_buffered_signatures()derive their buffered account PDAs from the intendedone_sig_state,params.merkle_root, andauthorityvalues.init_buffered_transaction()also includesparams.nonce. However,execute_transaction()acceptsbuffered_transactionandbuffered_signaturesaccounts without revalidating that their PDA seeds match the executingOneSigState,merkle_root, nonce, or authority. As a result, a buffered account created for oneOneSigStatecan be supplied during execution against anotherOneSigStateif the attacker can make the buffered payload or signatures verify under the attacker-controlled execution context. ForBufferedTransaction, successful execution closes the account unconditionally, deleting the victim's staged transaction and forcing it to be re-uploaded. ForBufferedSignatures, successful execution can close the account when the executingone_sig_state.nonceexceeds the storedclosable_after_nonce, so an attacker-controlled state with a sufficiently advanced nonce can consume staged signatures as well.Recommendation
In
execute_transaction(), derive the expected PDA using theauthorityread from the provided buffered account together with the executing context seeds, then require the provided account key to match it. ForBufferedTransaction, derive the PDA fromBUFFERED_TRANSACTION_SEED, the currentone_sig_state,merkle_root, current nonce, andbuffered_transaction.authority. ForBufferedSignatures, derive the PDA fromBUFFERED_SIGNATURES_SEED, the currentone_sig_state,merkle_root, andbuffered_signatures.authority. -
L-03 Low Delegate authorization omits
seedSignatures ResolvedDescription
build_signer_execution_authorization_digest()signs only theleaf,merkle_root,delegate, andexpiry. Unlike root signatures, delegate execution authorizations are not bound to the currentseed. As a result, afterseedrotation invalidates old root signatures, an old delegate authorization can still be reused if the samemerkle_rootis signed again under the newseed.Recommendation
Include the current
seedin the signedSignerExecutionAuthorizationdata and pass it intobuild_signer_execution_authorization_digest(), so delegate authorizations are invalidated together with root signatures whenseedchanges.Resolution
Guardian: The remediation response for Delegate authorization omits seed states that omitting
seedfromSignerExecutionAuthorizationis intentional and that the behavior would be documented. The current signer authorization documentation explains whensigner_authorizationis verified or ignored, but it does not document thatSignerExecutionAuthorizationintentionally omitsseed, thatseedis enforced through root-signature verification, or that re-approving the samemerkleRootafter aseedrotation preserves existing unexpired delegate authorizations for that root.Recommendation Document the intended split between root validity and delegate authorization:
seedcontrols whether amerkleRootis valid, whileSignerExecutionAuthorizationauthorizes adelegatefor a specificleafHashandmerkleRoot. Also document that if signers re-approve the samemerkleRootafter aseedrotation, existing unexpired delegate authorizations for that root remain valid. -
I-01 Informational Non-unique
one_sig_idcan confuse indexing Validation ResolvedDescription
init_one_sig()accepts a caller-providedone_sig_idandInitOneSig::apply()stores it directly inOneSigStatewithout enforcing uniqueness. Thestateaccount is initialized as a normal caller-provided account rather than a PDA derived fromone_sig_id, and theone_sig_signerPDA is derived fromONE_SIG_SEEDandstate.key(). As a result, multiple unrelatedOneSigStateaccounts can share the sameone_sig_id.Recommendation
Document that
one_sig_idis not globally unique and that the canonical Solana instance identifier is theOneSigStatepubkey. If uniqueness is intended, derivestatefromone_sig_idwith PDA seeds or introduce a registry that reserves eachone_sig_id, with appropriate anti-squatting controls for permissionless initialization.Also consider using
one_sig_idfor chain separation if this is not already the case - for example, not letting a leaf from one EVM chain to be executed on another. -
I-02 Informational Expiry
uint256encoding lacks documentation Documentation ResolvedDescription
Solana accepts
expiryas au64for both root execution and signer-executor authorization, while the corresponding EIP-712 digests encodeexpiryinto a 32-byteuint256field. This is implemented correctly, and the Solana SDK handles the bound by serializing bothExecuteTransactionInstructionData.expiryandSignerAuthorization.expirywithu64(), which rejects values aboveu64::MAXbefore instruction construction. However, the Solana-facing documentation does not clearly explain thatexpiryis only represented asuint256inside the EIP-712 signing preimage, while the valid Solana input domain remains Unix-seconds timestamps representable asu64. Integrators constructing signatures or digests outside the standard Solana SDK path may miss this distinction.Recommendation
Document the Solana
expirysemantics for both root signatures and signer-executor authorizations:expirymust be a Unix-seconds timestamp that fits inu64, and it is left-padded into a 32-byte EIP-712uint256slot only when computing the signing digest. -
I-03 Informational Ambiguous
seeddocumentation Documentation ResolvedDescription
The comment above
InitOneSigParams::seedsaysseed* is the only cross-VM instance selector*, which may be understood as if theseeddetermines the VM the txs will be executed on. This is not the role of the seed, as it is signed together with the merkle root.Recommendation
Consider whether the comment needs to be modified.
-
I-04 Informational Misleading domain separator comment Documentation Resolved
Description
The comment for
DOMAIN_SEPARATORstates that the EIP-1910x1901prefix is included when computing the domain separator. This is inaccurate:DOMAIN_SEPARATORis correctly computed as the EIP-712 domain separator only. The EIP-191 prefix is applied later when constructing the final signing digest inbuild_merkle_root_digest()andbuild_signer_execution_authorization_digest().Recommendation
Remove the EIP-191 prefix bullet from the
DOMAIN_SEPARATORcomment so the documentation matches the implemented EIP-712 digest construction. -
I-05 Informational Cross-state OneSig execution is rejected Unexpected Behavior Acknowledged
Description
The reentrancy guard rejects every inner call to
execute_transactiontargeting the OneSig program, without distinguishing between recursion into the currently executing state and execution through an independent OneSig state:if solana_ix.program_id == ID { let exec_disc = crate::instruction::ExecuteTransaction::DISCRIMINATOR; require!(!solana_ix.data.starts_with(exec_disc), OneSigError::Reentrancy); }Multiple independent
OneSigStateaccounts can exist under the same program deployment. Consider states A and B, where B allowlists A’s signer PDA as an executor. Because the PDA can only sign through CPI, A must invoke B usinginvoke_signed. Although B would independently validate its executor, Merkle root, signatures, proof, and nonce, the guard rejects the call solely because it targets the OneSig program, without checking that the target is a different state. As a result, one OneSig instance cannot execute through another, even when authorized, what blocks valid cross-instance calls.Recommendation
Consider scoping the reentrancy protection to the currently executing
OneSigState. If all cross-state execution is intentionally unsupported, document this restriction explicitly. -
I-06 Informational SOL allowance documents wrong limit Documentation Resolved
Description
The
valuefield is documented as the maximum amount of SOL that a call may spend. However, the implementation does not measure the total amount transferred from the OneSig signer PDA. It only compares the signer’s balance before and after the CPI. For example, assume the signer begins with 10 SOL, receives 100 SOL during the CPI, transfers out 105 SOL, and finishes with 5 SOL. With value set to 5 SOL, the check passes because the signer’s final net loss is only 5 SOL, even though 105 SOL was transferred out. This may cause integrators to rely on a spending restriction that the implementation does not enforce.Recommendation
Document value as the maximum permitted net decrease in the signer PDA’s lamport balance.
-
I-07 Informational Unused signer authorization is ignored Documentation Resolved
Description
When
executor_requiredis disabled, execution is permissionless and any suppliedsigner_authorizationis ignored. The same applies when the executor is already allowlisted. Consequently, a successful transaction does not prove that the supplied authorization was validated or valid. Although this has no on-chain authorization impact, integrations may incorrectly interpret the presence of the field as evidence that its signature and expiry were validated.Recommendation
Explicitly document in the public SDK that
signerExecutionSignatureand its expiry are ignored when execution is permissionless or the executor is already allowlisted. -
I-08 Informational Executor policy is not signed Documentation Resolved
Description
execute_transaction()enforces executor authorization from the currentOneSigStateconfiguration. The signed Merkle root commits toseed,merkle_root, andexpiry, while each leaf commits to the target state, nonce, and call data. Neither the root nor the leaf commits toexecutor_requiredor the executor allowlist. As a result, an already-signed root can be executed under a different executor policy than the one operators expected at signing time. For example, ifexecutor_requiredis later disabled while an old root is still valid and nonce-compatible, leaves from that root become permissionlessly executable.Recommendation
Document that executor policy is live
OneSigStateconfiguration and is not part of the signed root or leaf. If executor policy is intended to be fixed at signing time, include it in the signed data or invalidate outstanding roots when executor configuration changes. -
I-09 Informational SVM accepts non-EVM recovery ids Signatures Resolved
Description
recover_signer()normalizes anyrecovery_idin27..=30by subtracting27, and otherwise forwards the rawrecovery_idtosecp256k1_recover(). This means Solana accepts signature encodings with rawvvalues0/1, and can also accept valid rawvvalues2/3or their29/30equivalents whensecp256k1_recover()successfully recovers the signer. This differs from the EVM path using OpenZeppelin-styleECDSA.tryRecover(), which accepts onlyv = 27/28for 65-byte signatures. As a result, the same signed root or executor delegate can be accepted on SVM with a recovery-id encoding that would be rejected on EVM. While this issue doesn't lead to direct exploitation of the system, it's of the same class as Permissive secp256k1 Recovery because it allows multiple variants of the same signature to be used. Also, the implementation accepts2and3values which are normally not expected for Ethereum-style signed messages.Recommendation
Reconsider whether this flexibility is desired or it's better to stick to only one variant.
Resolution
Guardian: The latest change in
signature.rsremoves support for29...30recovery_id values, but the underlying function still accepts raw0/1which is not documented in any user-facing docs.Recommendation Consider documenting the fact that both
0/1,27/28can be used for recovery_id. -
I-10 Informational Unlimited SOL allowance may overflow Documentation Resolved
Description
The program verifies each call’s SOL allowance by adding the approved value to the signer’s post-execution balance.
require!( balance_before <= balance_after + value, OneSigError::ExcessiveBalanceDeduction );When value is
u64::MAXandbalance_afteris nonzero, this addition overflows. Because release builds enable overflow checks, the transaction aborts and all CPI effects are rolled back. Integrators may interpret the maximum u64 value as an unlimited SOL spending allowance. Consequently, this assumption can make a valid authorized call unexecutable.Recommendation
Document that value is a finite allowance and that
u64::MAXmust not be treated as an unlimited approval. -
I-11 Informational PDA authority trust is underdocumented Documentation Resolved
Description
OneSig executes approved Solana calls by invoking the target program with
invoke_signed(), which grants theone_sig_signerPDA signer authority to the callee for the duration of the CPI. The signed leaf commits to the targetprogram_id, account metas, instruction data, lamport allowance, nonce,one_sig_id, and OneSig state, but it does not commit to the target program's code,ProgramData, or upgrade authority. As a result, if signers approve a call to an upgradeable target and that target is upgraded before execution, the new code can still receive the PDA signer authority over the signed account set. In addition, whenone_sig_signeris passed as writable, a target can temporarily initialize, assign, or otherwise use the PDA during nested CPIs and restore it to a system-owned empty account before returning, satisfying OneSig's post-CPI checks. This is consistent with Solana CPI semantics, but the trust assumption is not clearly documented for signers and integrators. This can be combined with SOL allowance documents wrong limit if there is value to be claimed on behalf of the PDA.Recommendation
Document that OneSig signatures authorize execution against the target
program_idand signed account set at execution time, not against immutable target code semantics. The documentation should warn signers to account for target upgradeability, target upgrade authority, and writableone_sig_signerusage before signing; prefer frozen or trusted target programs, short expiries, and avoid passingone_sig_signeras writable unless the intended call requires it. -
I-12 Informational Buffered close events are missing Events Resolved
Description
Buffered account lifecycle events are emitted when
BufferedSignaturesandBufferedTransactionaccounts are initialized, but no corresponding close events are emitted when those accounts are closed. This includes both manual close paths and the automatic cleanup paths inmaybe_close_buffered_signatures()andmaybe_close_buffered_transaction(). As a result, off-chain monitors and indexers can observeBufferedSignaturesInitializedandBufferedTransactionInitialized, but must infer closure by inspecting account state, lamport movement, or transaction effects rather than consuming an explicit lifecycle event.Recommendation
Consider emitting explicit close events for
BufferedSignaturesandBufferedTransactionaccounts from both manual close instructions and automatic cleanup inexecute_transaction().
Remediation Review 2
2 findings · July 28 to 30, 2026-
I-01 Informational Document
one_sig_idchain-id invariant Documentation ResolvedDescription
Solana documentation for
one_sig_idhas been revised to clarify that it is caller-provided and not globally unique. However, the broaderOneSigcodebase still assumesoneSigIdrepresents the chain EID: the shared leaf schema describesoneSigIdas the EID of the chain, andmakeOneSigTree()treats duplicate(nonce, oneSigId)pairs as duplicate transactions for the same chain and nonce. This cross-codebase assumption is not documented consistently in the Solana implementation, making it unclear whether Solana deployments must chooseone_sig_idas the chain EID or may choose arbitrary application identifiers.Recommendation
Document the intended invariant consistently across the Solana implementation and shared
OneSigmodel. -
I-02 Informational Close events missing from docs Documentation Resolved
Description
The
AUDIT.mdcomponent map lists the events emitted byevents.rs, but omits the newly addedBufferedSignaturesClosedandBufferedTransactionClosedevents. The runtime now emits these events fromclose_buffered_signatures(),close_buffered_transaction(), and the buffered auto-close paths inexecute_transaction(), so the documentation no longer reflects the full event surface.Recommendation
Update the
events.rsentry inAUDIT.mdto includeBufferedSignaturesClosedandBufferedTransactionClosed.
No findings match.
More from LayerZero
All 11 reports-
OneSig on Canton
32 findings 32 findings: 5 medium, 23 low, 4 informational -
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
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.