OneSig is a smart contract solution designed to streamline the signing and execution of arbitrary ‘calldata’ on multiple blockchains. This is achieved by introducing a pre-authorization mechanism. Instead of requiring individual approvals for every transaction, OneSig enables batched pre-authorization of many transactions. This eliminates inefficiencies and delays caused by repeated approvals, reducing operational overhead and improving deployment and contract configuration scalability.
When LayerZero deploys a new Endpoint, every other endpoint within every ecosystem LayerZero supports needs to be updated with the newly created address.
Currently adding or modifying an endpoint is an unsustainable process due to the following challenges:
- Exponential Configuration Growth: Each new endpoint requires reconfiguration of all existing endpoints, resulting in hundreds of configuration updates per addition.
- Multisig Execution Bottlenecks: Every configuration change must be manually approved by a multisig on each local chain, causing significant delays.
- Infrastructure Overhead: The LayerZero team must deploy a Gnosis Safe multisig and maintain an indexing server on every new chain, introducing complexity and unreliability.
- Stargate and LayerZero Applications(OApps): This issue extends to Stargate and all LayerZero-powered applications, making global scalability difficult.
OneSig is an extension of a multisig system that integrates Merkle Proofs, introducing a permissionless execution model where:
- A series of transactions for multiple chains is pre-authorized via a Merkle root + a group signature.
- Nonce enforces order
- Can be grouped atomically or successive via each leaf. I.e. A leaf can contain 1 or many individual transactions
- Series of transactions can be executed provided they:
- Have not been executed before.
- Previous nonce has been executed.
- Are not expired.
- Are executed on the correct chain.
- Have a valid Merkle proof.
- Submit Transaction Batch: The User submits a batch of transactions to the Off-Chain Server.
- Generate Merkle Tree & Request Signatures: The Off-Chain Server creates a Merkle tree and sends the Merkle Root, Transactions, and Expiry to Signers for approval.
- Sign and Aggregate Signatures: Signers (Multisig) sign the Merkle root, and the Off-Chain Server aggregates signatures.
- Provide Signed Merkle Root: The Off-Chain Server returns the signed Merkle root to the User.
- Submit Execution Request: The User (or anyone) submits the execution request to the Executor, including the Merkle proof and signatures. The OneSig Contract verifies these before allowing execution.
- Execute Transaction: The OneSig Contract executes the transaction calls on the Target Contracts.
- Confirm Execution: The Blockchain finalizes the execution, and the User confirms success.
- A list of transactions is pre-signed by a set of trusted signers.
- These transactions are committed as a Merkle tree, ensuring integrity.
- The signers only need to sign once for the entire batch of transactions across multiple chains/OneSig deployments
- Execution can be permissionless depending on executor configuration
- Execution is validated against the pre-approved Merkle root.
- Each transaction is checked for:
- Valid Merkle proof
- Expiry status
- Correct chain
- Correct contract
- Correct nonce (ordered execution)
- Transactions are stored as Merkle leaves.
- A Merkle proof confirms that an execution request is part of the approved set.
- Verification prevents unauthorized or duplicate executions.
[0][1..8][9..40][41..48]
| | | |
| | | \ --- 8bytes: nonce
| | |
| | \ --- 32bytes: contract
| |
| \ --- 8bytes: oneSigId
|
\ --- 1byte: version
version: Any implementations following this document should have this set to 1.oneSigId: an 8 byte identifier representing a deploymentcontract: a 32 byte identifier representing a deploymentnonce: an 8 byte unisgned integer representing ordering information
OneSig implementations can implement this process differently from one-another, but the first item that goes into the hash must be the Standard Leaf Header
Any data included pre-hash into the leaf can be unique per implementation, but to prevent payloads being valid on unintended contracts the prefix must be standardized.
Leafs are double hashed using keccak256.
// Double hash leaf data
LEAF_DATA = [Leaf Header][Leaf Prefix]
keccak256(keccak256(LEAF_DATA))
Although there can be differences within the Merkle-Tree payload for any given OneSig implementation, they all must adhere to the standardized signature specification.
OneSig Signatures are based on EIP-712, thus are signed by Ethereum-compatible addresses.
OneSig uses fixed values for the signature domain separator.
string name=OneSigstring version=0.0.1- The current version of the OneSig implementation.uint256 chainId=0x01- All signatures are signed to be used referencing the Ethereum Mainnet ChainId, no matter the target chain.address verifyingContract-0x000000000000000000000000000000000000dEaD- A static "dead" address that is shared across all implementations.
Note: salt can be provided here but is omitted.
Note: Although the domain-separator parameters are static across different deployments of OneSig, the risk of replay/unintentional signing attacks is mitigated due to the use of unique seeds, unique signers, and incrementing nonces.
The payload is encoded as an ABI in the format SignMerkleRoot(bytes32 seed,bytes32 merkleRoot,uint256 expiry)
bytes32 seedA seed defined that must match the seed defined in the contract at time of execution.bytes32 merkleRootThe root of the Merkle Tree being executed upon.uint256 expiryThe time in which the signature should expire.
OneSig implementations take multiple signatures as a concatenate of multiple EIP-712 outputs. Before concatenation, signatures must be sorted in ascending order by the numerical representation of the signer.
Provided are examples for signing+sorting in TypeScript and examples of validating the order in EVM
This example uses TypedDataSigner from @ethersproject/abstract-signer
interface SingerWithAddress extends TypedDataSigner {
address: string;
}
async function sign(tree: MerkleTree, unsortedSigners: SingerWithAddress[], seed: Uint8Array, expiry: BigNumber | string) {
const sortedSigners = [ ...unsortedSigners ].sort(function(a, b) {
const aNumeric = BigInt(a.address);
const bNumeric = BigInt(b.address);
if (aNumeric === bNumeric) {
return 0
} else {
return aNumeric < bNumeric ? -1 : 1
}
});
const signatures = [];
for (const signer of sortedSigners) {
const signature = await signer._signTypedData(
{
name: 'OneSig',
version: '0.0.1',
chainId: 1, // this is hardcoded to Ethereum mainnet
verifyingContract: '0x000000000000000000000000000000000000dEaD', // this is hardcoded to a dead address
},
{
SignMerkleRoot: [
{ name: 'seed', type: 'bytes32' },
{ name: 'merkleRoot', type: 'bytes32' },
{ name: 'expiry', type: 'uint256' },
],
},
{
seed: seed,
expiry: expiry,
merkleRoot: tree.getRoot(),
}
);
// Remove starting '0x' from signature;
const withoutPrefix = signature.substring(2);
const signatureBytes = Buffer.from(withoutPrefix, 'hex');
signatures.push(signatureBytes);
}
// Return the concatenated signatures
return Buffer.concat(signatures);
}// Example verification of order within solidity contract
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract VerifyOrder {
function ensureSignatureOrder(
bytes32 digest, // the signed payload -- not important for this example
bytes calldata combinedSig // the combined signature
) external view {
// One signature is 65 bytes long, so the length of any un-compressed combination will be a multiple of 65
require(combinedSig.length % 65 == 0, "Signature length must be multiples of 65");
address lastSigner = address(0);
uint256 pos = 0;
while (pos < combinedSig.length) {
bytes calldata signature = combinedSig[pos:pos + 65];
address signer = ECDSA.recover(digest, signature);
// Require that the signer is greater than and not equal to the previous
require(signer > lastSigner, "Signers out of order");
pos += 65;
}
}
}Each implementation must store a seed and a nonce, with the following rules applied for modification:
seed- The seed is an opaque piece of data defined by the contract owners to prevent replay-attacks.- Changing this follows the same multi-sig process as defined in (See Signer Management).
nonce- This nonce is an integer representing the count of successfully executed transactions within a specific OneSig implementation.- Must be initialized with zero.
- Must be incremented by one after a successful execution.
- Can never be modified outside of these two cases.
signerThreshold- This number represents the amount of unique signers needed to execute a transaction.
Adding or removing signers from a OneSig implementation must meet the following criteria:
- One or more signer(s) must always be defined (contract must be initialized as such).
- Modifying signers must require the same
signerThreshold(or more) of signatures as executing a transaction. - At any given point in time, the count of signers within the contract must not fall below the threshold defined.
Each implementation must be able to verify signatures.
These verifications must:
- Use the signers defined in the contract (See Signer Management).
- Use the signature specification referenced (See Signature/Verification).
Contracts must store two pieces of information
- executors
set<address>a set of (VM specific) addresses that can execute transactions - executorRequired
booleanifexecutorsis checked or if permissionless execution is enabled.
Modification to either of these variables should require signer approval.
Signers are considered executors, and the executors array is allowed to be empty when executorRequired is true.
When a call is made to execute a transaction, the executor must be added or permissionless execution must be enabled.
The flow of transaction execution is as follows:
- Check
executorRequired- If
truecheck that the sender is either a valid signer or within theexecutorsarray. - If
falsecontinue
- If
- Receives
merkleRoot,expiry,signature,transactionData, andproof. - Validates the
signaturecorrectly signs themerkleRoot(See Signature/Verification). - Ensures that
transactionData/proofare included within themerkleRoot.- Computes the leaf for the
transactionData(See Leaf Encoding). - Ensures that the
proofis valid referencing the computed leaf andmerkleRoot.
- Computes the leaf for the
- Execute each call within transaction data; how this data is formatted can be unique per-implementation.
- Increment the stored
nonceby1.
OneSig provides a scalable, execution model that can simplify LayerZero Endpoint and OApps configuration. By leveraging Merkle proofs, OneSig improves upon the inefficiencies of traditional multisig approvals, enabling faster, more reliable deployments across multiple chains.
