From 4a4dd3ec4d9f7be640f54a0f432d15db288a3382 Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Tue, 8 Oct 2024 12:12:22 +0200 Subject: [PATCH 1/3] ci: add semantic pr workflow (#45) --- .github/workflows/semantic-pr.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/semantic-pr.yml diff --git a/.github/workflows/semantic-pr.yml b/.github/workflows/semantic-pr.yml new file mode 100644 index 0000000..11af7d5 --- /dev/null +++ b/.github/workflows/semantic-pr.yml @@ -0,0 +1,16 @@ +name: "Semantic PR" +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-22.04 + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From b695c2cc930343eda2ef52f2728ec979a3a6b886 Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Tue, 8 Oct 2024 15:40:11 +0200 Subject: [PATCH 2/3] docs: make some reorg in the readme (#49) --- README.md | 120 +++++++++++++----------------------------------------- 1 file changed, 29 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 6867183..4d62eb1 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,20 @@ -# Introduction +**Note**: Mainnet-beta, testnet, devnet gateway program address: -Mainnet-beta, testnet, devnet gateway program address: ``` ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis ``` -This repository hosts the smart contracts (program) -on Solana network to support ZetaChain cross-chain -functionality. Specifically, it consists of a single -program deployed -which allows the following two actions: - -1. Users on Solana network can send SOL or selected -SPL tokens to the program to deposit into ZetaChain -and optionally invoke a ZetaChain EVM contract. -2. Allows contracts on ZetaChain EVM to withdraw -SOL and SPL tokens to users on Solana; -3. (TO DO) In the withdraw above, optionally allow -a contract on ZetaChain EVM to withdraw SOL/SPL tokens -and call a user specified contract (program) with -parameters. - -# Authentication and Authorization +# Introduction -Anyone can deposit and remote invoke ZetaChain contracts. +This repository hosts the smart contract (program) deployed on the Solana network to enable ZetaChain's cross-chain functionality. It consists of a single program that supports the following actions: -Only ZetaChain TSS account can make withdraw or withdrawAndCall -transactions on the program. The ZetaChain TSS account -is a collection of Observer/KeySigners which uses -ECDSA TSS (Threshold Signature Scheme) to sign -outbound transactions. The TSS address will appear in this program -a single ECDSA secp256k1 address; but it does -not have a single private key, rather its private -key consists of multiple key shares and they collectively -sign a message in a KeySign MPC ceremony. -The program authenticates -via verifying the TSS signature and is authorized -by ZetaChain to perform outbound transactions as -part of ZetaChain cross-chain machinery. - -The ZetaChain TSS is on ECDSA secp256k1 curve; -But Solana native digital signature scheme is -EdDSA Ed25519 curve. Therefore the program uses -custom logic to verify the TSS ECDSA signature -(like alternative authentication in smart contract wallet); -the native transaction signer (fee payer on Solana) -does not carry authorization and it's only used -to build the transaction and pay tx fees. There -are no restrictions on who the native transaction -signer/fee payer is. The following code excerpt is -for authenticating TSS signature in the contract itself, -using the [Rust secp256k1 library bundled with solana](https://docs.rs/solana-program/latest/solana_program/secp256k1_recover/index.html): -https://github.com/zeta-chain/protocol-contracts-solana/blob/01eeb9733a00b6e972de0578b0e07ebc5837ec54/programs/protocol-contracts-solana/src/lib.rs#L116-L121 - -The function `recover_eth_address` is implemented in the gateway program: -https://github.com/zeta-chain/protocol-contracts-solana/blob/01eeb9733a00b6e972de0578b0e07ebc5837ec54/programs/protocol-contracts-solana/src/lib.rs#L180-L196 - -The TSS signature is a ECDSA secp256k1 signature; its public key therefore address -(Ethereum compatible hashing from pubkey) is therefore verifiable using the `secp256k1_recover` -function. Alternatively, Solana runtime also provides a program to provide this verification service -via CPI; see [proposal 48](https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0048-native-program-for-secp256r1-sigverify.md) -which might be more cost efficient. - -In both `withdraw` and `withdrawAndCall` instructions, -the ECDSA signed message_hash must commit to the -`nonce`, `amount`, and `to` address. See the -check in these instructions like: -https://github.com/zeta-chain/protocol-contracts-solana/blob/01eeb9733a00b6e972de0578b0e07ebc5837ec54/programs/protocol-contracts-solana/src/lib.rs#L110-L114 -The commitment of `nonce` in the signature prevents replay of the TSS ECDSA signed message. -Also (to be added in https://github.com/zeta-chain/protocol-contracts-solana/issues/6) a chain id of Solana -should be added to the commitment of the message hash, so that the signature cannot be replayed on *other blockchains* -that potentially uses similar authentication (say in TON). +1. Users on the Solana network can send SOL to the program to deposit into ZetaChain, with the option to invoke a ZetaChain EVM contract. +2. Contracts on the ZetaChain EVM can withdraw SOL to users on the Solana network. # Build and Test Instructions -Prerequisites: a recent version of `rust` compiler -and `cargo` package manger must be installed. The program -is built with the `anchor` framework so it needs to be -installed as well; see [installation](https://www.anchor-lang.com/docs/installation) +Prerequisites: a recent version of `rust` compiler and `cargo` package manger must be installed. The program is built with the `anchor` framework so it needs to be installed as well; see [installation](https://www.anchor-lang.com/docs/installation) Please install compatible Solana tools and anchor-cli before build, otherwise the program will not be built successfully ```bash @@ -108,27 +44,29 @@ To run the tests $ anchor test ``` +# Authentication and Authorization + +Anyone can deposit and remote invoke ZetaChain contracts. + +Only ZetaChain TSS account can call `withdraw` on the program. The ZetaChain TSS account is a collection of Observer/Signers which uses ECDSA TSS (Threshold Signature Scheme) to sign outbound transactions. The TSS address will appear in this program a single ECDSA secp256k1 address; but it does not have a single private key, rather its private key consists of multiple key shares and they collectively sign a message in a KeySign MPC ceremony. The program authenticates via verifying the TSS signature and is authorized by ZetaChain to perform outbound transactions as part of ZetaChain cross-chain machinery. + +The ZetaChain TSS is on ECDSA secp256k1 curve; But Solana native digital signature scheme is EdDSA Ed25519 curve. Therefore the program uses custom logic to verify the TSS ECDSA signature (like alternative authentication in smart contract wallet); the native transaction signer (fee payer on Solana) does not carry authorization and it's only used to build the transaction and pay tx fees. There are no restrictions on who the native transaction signer/fee payer is. The following code excerpt is for authenticating TSS signature in the contract itself, using the [Rust secp256k1 library bundled with solana](https://docs.rs/solana-program/latest/solana_program/secp256k1_recover/index.html): https://github.com/zeta-chain/protocol-contracts-solana/blob/01eeb9733a00b6e972de0578b0e07ebc5837ec54/programs/protocol-contracts-solana/src/lib.rs#L116-L121 + +The function `recover_eth_address` is implemented in the gateway program: https://github.com/zeta-chain/protocol-contracts-solana/blob/01eeb9733a00b6e972de0578b0e07ebc5837ec54/programs/protocol-contracts-solana/src/lib.rs#L180-L196 + +The TSS signature is a ECDSA secp256k1 signature; its public key therefore address (Ethereum compatible hashing from pubkey) is therefore verifiable using the `secp256k1_recover` function. Alternatively, Solana runtime also provides a program to provide this verification service via CPI; see [proposal 48](https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0048-native-program-for-secp256r1-sigverify.md) which might be more cost efficient. + +In the instruction, the ECDSA signed message_hash must commit to the `nonce`, `amount`, and `to` address. See the check in these instructions like: https://github.com/zeta-chain/protocol-contracts-solana/blob/01eeb9733a00b6e972de0578b0e07ebc5837ec54/programs/protocol-contracts-solana/src/lib.rs#L110-L114 The commitment of `nonce` in the signature prevents replay of the TSS ECDSA signed message. Also (to be added in https://github.com/zeta-chain/protocol-contracts-solana/issues/6) a chain id of Solana should be added to the commitment of the message hash, so that the signature cannot be replayed on *other blockchains* that potentially uses similar authentication (say in TON). + + # Relevant Account and Addresses -The Gateway program derive a PDA (Program Derived Address) -with seeds `b"meta"` and canonical bump. -This PDA account/address actually holds the SOL -balance of the Gateway program. -For SPL tokens, the program stores the SPL token -in PDA derived ATAs. For each SPL token (different mint -account), the program creates ATA from PDA and the Mint -(standard way of deriving ATA in Solana SPL program). - -The PDA account itself is a data account that holds -Gateway program state, namely the following data types -https://github.com/zeta-chain/protocol-contracts-solana/blob/01eeb9733a00b6e972de0578b0e07ebc5837ec54/programs/protocol-contracts-solana/src/lib.rs#L271-L275 - -The `nonce` is incremented on each successful withdraw transaction, -and it's used to prevent replay of signed ECDSA messages. -The `tss_address` is the TSS address of ZetaChain (20Bytes, -Ethereum style). `authority` is the one who can update -the TSS address stored in PDA account. +The Gateway program derive a PDA (Program Derived Address) with seeds `b"meta"` and canonical bump. This PDA account/address actually holds the SOL balance of the Gateway program. For SPL tokens, the program stores the SPL token in PDA derived ATAs. For each SPL token (different mint account), the program creates ATA from PDA and the Mint (standard way of deriving ATA in Solana SPL program). + +The PDA account itself is a data account that holds Gateway program state, namely the following data types https://github.com/zeta-chain/protocol-contracts-solana/blob/01eeb9733a00b6e972de0578b0e07ebc5837ec54/programs/protocol-contracts-solana/src/lib.rs#L271-L275 +The `nonce` is incremented on each successful withdraw transaction, and it's used to prevent replay of signed ECDSA messages. +The `tss_address` is the TSS address of ZetaChain (20Bytes, Ethereum style). `authority` is the one who can update the TSS address stored in PDA account. The `initialize` instruction sets nonce to 0. # Troubleshooting @@ -150,9 +88,9 @@ Error: failed to start validator: Failed to create ledger at test-ledger: io err This is because the BSD tar program is not compatible with the GNU tar program. -FIX: install GNU tar program using homebrew and export it's executable path in your .zshrc file. +To fix the issue: install GNU tar program using homebrew and export it's executable path in your `.zshrc` file: -## Mac with Apple Silicon +**Mac with Apple Silicon** ```bash brew install gnu-tar @@ -160,7 +98,7 @@ brew install gnu-tar export PATH="/opt/homebrew/opt/gnu-tar/libexec/gnubin:$PATH" ``` -## Intel-based Mac +**Intel-based Mac** ```bash brew install gnu-tar From 74c9d8162f804f480cdce0edcb843e0f109a54b6 Mon Sep 17 00:00:00 2001 From: brewmaster012 <88689859+brewmaster012@users.noreply.github.com> Date: Tue, 8 Oct 2024 11:06:44 -0500 Subject: [PATCH 3/3] refactor: enhance PDA validation (#40) * add PDA address to README * enhance pda validation * Update programs/protocol-contracts-solana/src/lib.rs Co-authored-by: skosito * Update README.md Co-authored-by: skosito * add pda_ata validation as constraints --------- Co-authored-by: skosito --- README.md | 7 +++ programs/protocol-contracts-solana/src/lib.rs | 18 +++---- tests/protocol-contracts-solana.ts | 50 +++++++++++++++---- 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4d62eb1..69a1603 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,15 @@ ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis ``` +The PDA account address (derived from seeds `b"meta"` and canonical bump) is +``` +2f9SLuUNb7TNeM6gzBwT4ZjbL5ZyKzzHg1Ce9yiquEjj +``` + + # Introduction + This repository hosts the smart contract (program) deployed on the Solana network to enable ZetaChain's cross-chain functionality. It consists of a single program that supports the following actions: 1. Users on the Solana network can send SOL to the program to deposit into ZetaChain, with the option to invoke a ZetaChain EVM contract. diff --git a/programs/protocol-contracts-solana/src/lib.rs b/programs/protocol-contracts-solana/src/lib.rs index e8bb085..0c0e8e8 100644 --- a/programs/protocol-contracts-solana/src/lib.rs +++ b/programs/protocol-contracts-solana/src/lib.rs @@ -34,7 +34,6 @@ declare_id!("ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis"); #[program] pub mod gateway { - use super::*; pub fn initialize( @@ -338,8 +337,9 @@ pub struct Deposit<'info> { #[account(mut)] pub signer: Signer<'info>, - #[account(mut)] + #[account(mut, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, + pub system_program: Program<'info, System>, } @@ -348,8 +348,9 @@ pub struct DepositSplToken<'info> { #[account(mut)] pub signer: Signer<'info>, - #[account(mut, seeds = [b"meta"], bump)] + #[account(seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, + pub token_program: Program<'info, Token>, #[account(mut)] @@ -363,7 +364,7 @@ pub struct Withdraw<'info> { #[account(mut)] pub signer: Signer<'info>, - #[account(mut)] + #[account(mut, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, /// CHECK: to account is not read so no need to check its owners; the program neither knows nor cares who the owner is. #[account(mut)] @@ -378,10 +379,9 @@ pub struct WithdrawSPLToken<'info> { #[account(mut, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, - #[account(mut)] + #[account(mut, token::mint = mint_account, token::authority = pda)] pub pda_ata: Account<'info, TokenAccount>, // associated token address of PDA - #[account()] pub mint_account: Account<'info, Mint>, #[account(mut)] @@ -392,7 +392,7 @@ pub struct WithdrawSPLToken<'info> { #[derive(Accounts)] pub struct UpdateTss<'info> { - #[account(mut)] + #[account(mut, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, #[account(mut)] pub signer: Signer<'info>, @@ -400,7 +400,7 @@ pub struct UpdateTss<'info> { #[derive(Accounts)] pub struct UpdateAuthority<'info> { - #[account(mut)] + #[account(mut, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, #[account(mut)] pub signer: Signer<'info>, @@ -408,7 +408,7 @@ pub struct UpdateAuthority<'info> { #[derive(Accounts)] pub struct UpdatePaused<'info> { - #[account(mut)] + #[account(mut, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, #[account(mut)] pub signer: Signer<'info>, diff --git a/tests/protocol-contracts-solana.ts b/tests/protocol-contracts-solana.ts index 9511e50..a50b3f7 100644 --- a/tests/protocol-contracts-solana.ts +++ b/tests/protocol-contracts-solana.ts @@ -169,7 +169,7 @@ describe("some tests", () => { await gatewayProgram.methods.depositSplToken(new anchor.BN(1_000_000), Array.from(address)).accounts({ from: tokenAccount.address, to: pda_ata.address, - }).rpc({commitment: 'confirmed'}); + }).rpc({commitment: 'processed'}); acct = await spl.getAccount(conn, pda_ata.address); let bal1 = acct.amount; expect(bal1-bal0).to.be.eq(1_000_000n); @@ -298,7 +298,7 @@ describe("some tests", () => { } catch (err) { expect(err).to.be.instanceof(anchor.AnchorError); console.log("Error message: ", err.message); - expect(err.message).to.include("SPLAtaAndMintAddressMismatch"); + expect(err.message).to.include("ConstraintTokenMint"); const account4 = await spl.getAccount(conn, pda_ata.address); console.log("After 2nd withdraw: Account balance:", account4.amount.toString()); expect(account4.amount).to.be.eq(2_500_000n); @@ -307,7 +307,7 @@ describe("some tests", () => { }); it("deposit and withdraw 0.5 SOL from Gateway with ECDSA signature", async () => { - await gatewayProgram.methods.deposit(new anchor.BN(1_000_000_000), Array.from(address)).accounts({pda: pdaAccount}).rpc(); + await gatewayProgram.methods.deposit(new anchor.BN(1_000_000_000), Array.from(address)).accounts({}).rpc(); let bal1 = await conn.getBalance(pdaAccount); console.log("pda account balance", bal1); expect(bal1).to.be.gte(1_000_000_000); @@ -341,7 +341,6 @@ describe("some tests", () => { await gatewayProgram.methods.withdraw( amount, Array.from(signatureBuffer), Number(recoveryParam), Array.from(message_hash), nonce) .accounts({ - pda: pdaAccount, to: to, }).rpc(); let bal2 = await conn.getBalance(pdaAccount); @@ -353,7 +352,7 @@ describe("some tests", () => { it("deposit and call", async () => { let bal1 = await conn.getBalance(pdaAccount); - const txsig = await gatewayProgram.methods.depositAndCall(new anchor.BN(1_000_000_000), Array.from(address), Buffer.from("hello", "utf-8")).accounts({pda: pdaAccount}).rpc({commitment: 'confirmed'}); + const txsig = await gatewayProgram.methods.depositAndCall(new anchor.BN(1_000_000_000), Array.from(address), Buffer.from("hello", "utf-8")).accounts({}).rpc({commitment: 'confirmed'}); const tx = await conn.getParsedTransaction(txsig, 'confirmed'); console.log("deposit and call parsed tx", tx); let bal2 = await conn.getBalance(pdaAccount); @@ -375,7 +374,6 @@ describe("some tests", () => { // only the authority stored in PDA can update the TSS address; the following should fail try { await gatewayProgram.methods.updateTss(Array.from(newTss)).accounts({ - pda: pdaAccount, signer: mint.publicKey, }).signers([mint]).rpc(); } catch (err) { @@ -389,12 +387,12 @@ describe("some tests", () => { randomFillSync(newTss); // console.log("generated new TSS address", newTss); await gatewayProgram.methods.setDepositPaused(true).accounts({ - pda: pdaAccount, + }).rpc(); // now try deposit, should fail try { - await gatewayProgram.methods.depositAndCall(new anchor.BN(1_000_000), Array.from(address), Buffer.from('hi', 'utf-8')).accounts({pda: pdaAccount}).rpc(); + await gatewayProgram.methods.depositAndCall(new anchor.BN(1_000_000), Array.from(address), Buffer.from('hi', 'utf-8')).accounts({}).rpc(); } catch (err) { console.log("Error message: ", err.message); expect(err).to.be.instanceof(anchor.AnchorError); @@ -405,7 +403,7 @@ describe("some tests", () => { it("update authority", async () => { const newAuthority = anchor.web3.Keypair.generate(); await gatewayProgram.methods.updateAuthority(newAuthority.publicKey).accounts({ - pda: pdaAccount, + }).rpc(); // const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); // expect(pdaAccountData.authority).to.be.eq(newAuthority.publicKey); @@ -413,7 +411,7 @@ describe("some tests", () => { // now the old authority cannot update TSS address and will fail try { await gatewayProgram.methods.updateTss(Array.from(new Uint8Array(20))).accounts({ - pda: pdaAccount, + }).rpc(); } catch (err) { console.log("Error message: ", err.message); @@ -422,6 +420,38 @@ describe("some tests", () => { } }); + it("create an account owned by the gateway program", async () => { + const gateway_id =gatewayProgram.programId; + console.log("gateway program id", gateway_id.toString()); + const fake_pda = anchor.web3.Keypair.generate(); + const rentExemption = await conn.getMinimumBalanceForRentExemption(100); + const instr1 = anchor.web3.SystemProgram.createAccount( + { + fromPubkey: wallet.publicKey, + newAccountPubkey: fake_pda.publicKey, + lamports: rentExemption, + space: 100, + programId: gatewayProgram.programId, + } + ) + const tx = new anchor.web3.Transaction(); + tx.add(instr1, ); + await anchor.web3.sendAndConfirmTransaction(conn, tx, [wallet, fake_pda]); + + const newTss = new Uint8Array(20); + randomFillSync(newTss); + // console.log("generated new TSS address", newTss); + try { + await gatewayProgram.methods.updateTss(Array.from(newTss)).accounts({ + pda: fake_pda.publicKey, + }).rpc(); + } catch (err) { + console.log("Error message: ", err.message); + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("AccountDiscriminatorMismatch."); + } + }); + });