diff --git a/Anchor.toml b/Anchor.toml index 351af0a..0f16268 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -5,7 +5,7 @@ resolution = true skip-lint = false [programs.localnet] -protocol_contracts_solana = "ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis" +gateway = "ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis" [registry] url = "https://api.apr.dev" diff --git a/Cargo.lock b/Cargo.lock index 44aed93..e1b27c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -975,6 +975,16 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "gateway" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl", + "solana-program", + "spl-associated-token-account", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1481,17 +1491,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "protocol-contracts-solana" -version = "0.1.0" -dependencies = [ - "anchor-lang", - "anchor-spl", - "anchor-syn", - "solana-program", - "spl-associated-token-account", -] - [[package]] name = "qstring" version = "0.7.2" diff --git a/Makefile b/Makefile deleted file mode 100644 index 1b5fa24..0000000 --- a/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -# assets -C_GREEN=\033[0;32m -C_RED=\033[0;31m -C_BLUE=\033[0;34m -C_END=\033[0m - -.PHONY: fmt -fmt: - @echo "$(C_GREEN)# Formatting rust code$(C_END)" - @./scripts/fmt.sh \ No newline at end of file diff --git a/README.md b/README.md index 51f61a8..b6fbe17 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ In the instruction, the ECDSA signed message_hash must commit to the `nonce`, ` # 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 Gateway program derives 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 an ATA from the 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 diff --git a/package.json b/package.json index 77b4ffc..b3c9b19 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@zetachain/protocol-contracts-solana", + "name": "@zetachain/gateway", "private": false, "license": "MIT", "version": "0.0.0-set-on-publish", @@ -8,8 +8,9 @@ "idl" ], "scripts": { - "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", - "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" + "fmt:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "fmt": "prettier */*.js \"*/**/*{.js,.ts}\" --check", + "rust:fmt": "./scripts/fmt.sh" }, "dependencies": { "@coral-xyz/anchor": "^0.30.0", diff --git a/programs/protocol-contracts-solana/Cargo.toml b/programs/gateway/Cargo.toml similarity index 82% rename from programs/protocol-contracts-solana/Cargo.toml rename to programs/gateway/Cargo.toml index 4462e31..766a846 100644 --- a/programs/protocol-contracts-solana/Cargo.toml +++ b/programs/gateway/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "protocol-contracts-solana" +name = "gateway" version = "0.1.0" description = "Created with Anchor" edition = "2021" [lib] crate-type = ["cdylib", "lib"] -name = "protocol_contracts_solana" +name = "gateway" [features] default = [] @@ -19,6 +19,5 @@ idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { version = "=0.30.0" } anchor-spl = { version = "=0.30.0", features = ["idl-build"] } -anchor-syn = "=0.30.0" spl-associated-token-account = "3.0.2" solana-program = "=1.18.15" diff --git a/programs/protocol-contracts-solana/Xargo.toml b/programs/gateway/Xargo.toml similarity index 100% rename from programs/protocol-contracts-solana/Xargo.toml rename to programs/gateway/Xargo.toml diff --git a/programs/protocol-contracts-solana/src/lib.rs b/programs/gateway/src/lib.rs similarity index 59% rename from programs/protocol-contracts-solana/src/lib.rs rename to programs/gateway/src/lib.rs index 1c5cc6b..70b8b63 100644 --- a/programs/protocol-contracts-solana/src/lib.rs +++ b/programs/gateway/src/lib.rs @@ -8,12 +8,11 @@ use solana_program::secp256k1_recover::secp256k1_recover; use spl_associated_token_account::instruction::create_associated_token_account; use std::mem::size_of; +/// Errors that can occur during execution. #[error_code] pub enum Errors { #[msg("SignerIsNotAuthority")] SignerIsNotAuthority, - #[msg("InsufficientPoints")] - InsufficientPoints, #[msg("NonceMismatch")] NonceMismatch, #[msg("TSSAuthenticationFailed")] @@ -24,8 +23,6 @@ pub enum Errors { MessageHashMismatch, #[msg("MemoLengthExceeded")] MemoLengthExceeded, - #[msg("MemoLengthTooShort")] - MemoLengthTooShort, #[msg("DepositPaused")] DepositPaused, #[msg("SPLAtaAndMintAddressMismatch")] @@ -34,14 +31,30 @@ pub enum Errors { EmptyReceiver, } +/// Enumeration for instruction identifiers in message hashes. +#[repr(u8)] +enum InstructionId { + Withdraw = 1, + WithdrawSplToken = 2, + WhitelistSplToken = 3, + UnwhitelistSplToken = 4, +} + declare_id!("ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis"); #[program] pub mod gateway { use super::*; + /// Deposit fee used when depositing SOL or SPL tokens. const DEPOSIT_FEE: u64 = 2_000_000; + /// Initializes the gateway PDA. + /// + /// # Arguments + /// * `ctx` - The instruction context. + /// * `tss_address` - The Ethereum TSS address (20 bytes). + /// * `chain_id` - The chain ID associated with the PDA. pub fn initialize( ctx: Context, tss_address: [u8; 20], @@ -49,16 +62,29 @@ pub mod gateway { ) -> Result<()> { let initialized_pda = &mut ctx.accounts.pda; - initialized_pda.nonce = 0; - initialized_pda.tss_address = tss_address; - initialized_pda.authority = ctx.accounts.signer.key(); - initialized_pda.chain_id = chain_id; - initialized_pda.deposit_paused = false; + **initialized_pda = Pda { + nonce: 0, + tss_address, + authority: ctx.accounts.signer.key(), + chain_id, + deposit_paused: false, + }; + + msg!( + "Gateway initialized: PDA authority = {}, chain_id = {}, TSS address = {:?}", + ctx.accounts.signer.key(), + chain_id, + tss_address + ); Ok(()) } - // admin function to pause or unpause deposit + /// Pauses or unpauses deposits. Caller is authority stored in PDA. + /// + /// # Arguments + /// * `ctx` - The instruction context. + /// * `deposit_paused` - Boolean flag to pause or unpause deposits. pub fn set_deposit_paused(ctx: Context, deposit_paused: bool) -> Result<()> { let pda = &mut ctx.accounts.pda; require!( @@ -66,11 +92,16 @@ pub mod gateway { Errors::SignerIsNotAuthority ); pda.deposit_paused = deposit_paused; - msg!("set_deposit_paused: {:?}", deposit_paused); + + msg!("Set deposit paused: {:?}", deposit_paused); Ok(()) } - // the authority stored in PDA can call this instruction to update tss address + /// Updates the TSS address. Caller is authority stored in PDA. + /// + /// # Arguments + /// * `ctx` - The instruction context. + /// * `tss_address` - The new Ethereum TSS address (20 bytes). pub fn update_tss(ctx: Context, tss_address: [u8; 20]) -> Result<()> { let pda = &mut ctx.accounts.pda; require!( @@ -78,10 +109,21 @@ pub mod gateway { Errors::SignerIsNotAuthority ); pda.tss_address = tss_address; + + msg!( + "TSS address updated: new TSS address = {:?}, PDA authority = {}", + tss_address, + ctx.accounts.signer.key() + ); + Ok(()) } - // the authority stored in PDA can call this instruction to update the authority address + /// Updates the PDA authority. Caller is authority stored in PDA. + /// + /// # Arguments + /// * `ctx` - The instruction context. + /// * `new_authority_address` - The new authority's public key. pub fn update_authority( ctx: Context, new_authority_address: Pubkey, @@ -92,90 +134,109 @@ pub mod gateway { Errors::SignerIsNotAuthority ); pda.authority = new_authority_address; + + msg!( + "PDA authority updated: new authority = {}, previous authority = {}", + new_authority_address, + ctx.accounts.signer.key() + ); + Ok(()) } - // whitelist new spl token - // in case signature is provided, check if tss is the signer, otherwise check if authority is pda.authority - // if succeeds, new whitelist entry account is created + /// Whitelists a new SPL token. Caller is TSS. + /// + /// # Arguments + /// * `ctx` - The instruction context. + /// * `signature` - The TSS signature. + /// * `recovery_id` - The recovery ID for signature verification. + /// * `nonce` - The current nonce value. pub fn whitelist_spl_mint( ctx: Context, signature: [u8; 64], recovery_id: u8, - message_hash: [u8; 32], nonce: u64, ) -> Result<()> { let pda = &mut ctx.accounts.pda; let whitelist_candidate = &mut ctx.accounts.whitelist_candidate; let authority = &ctx.accounts.authority; - // signature provided, recover and verify that tss is the signer if signature != [0u8; 64] { validate_whitelist_tss_signature( pda, whitelist_candidate, signature, recovery_id, - message_hash, nonce, - "whitelist_spl_mint", + InstructionId::WhitelistSplToken as u8, )?; } else { - // no signature provided, fallback to authority check require!( authority.key() == pda.authority, Errors::SignerIsNotAuthority ); } + msg!( + "SPL token whitelisted: mint = {}, whitelist_entry = {}, authority = {}", + whitelist_candidate.key(), + ctx.accounts.whitelist_entry.key(), + ctx.accounts.authority.key() + ); + Ok(()) } - // unwhitelist new spl token - // in case signature is provided, check if tss is the signer, otherwise check if authority is pda.authority - // if succeeds, whitelist entry account is deleted + /// Unwhitelists an SPL token. Caller is TSS. + /// + /// # Arguments + /// * `ctx` - The instruction context. + /// * `signature` - The TSS signature. + /// * `recovery_id` - The recovery ID for signature verification. + /// * `nonce` - The current nonce value. pub fn unwhitelist_spl_mint( ctx: Context, signature: [u8; 64], recovery_id: u8, - message_hash: [u8; 32], nonce: u64, ) -> Result<()> { let pda = &mut ctx.accounts.pda; let whitelist_candidate: &mut Account<'_, Mint> = &mut ctx.accounts.whitelist_candidate; let authority = &ctx.accounts.authority; - // signature provided, recover and verify that tss is the signer if signature != [0u8; 64] { validate_whitelist_tss_signature( pda, whitelist_candidate, signature, recovery_id, - message_hash, nonce, - "unwhitelist_spl_mint", + InstructionId::UnwhitelistSplToken as u8, )?; } else { - // no signature provided, fallback to authority check require!( authority.key() == pda.authority, Errors::SignerIsNotAuthority ); } + msg!( + "SPL token unwhitelisted: mint = {}, whitelist_entry = {}, authority = {}", + whitelist_candidate.key(), + ctx.accounts.whitelist_entry.key(), + ctx.accounts.authority.key() + ); + Ok(()) } - // deposit SOL into this program and the `receiver` on ZetaChain zEVM - // will get corresponding ZRC20 credit. - // amount: amount of lamports (10^-9 SOL) to deposit - // receiver: ethereum address (20Bytes) of the receiver on ZetaChain zEVM - pub fn deposit( - ctx: Context, - amount: u64, - receiver: [u8; 20], // not used in this program; for directing zetachain protocol only - ) -> Result<()> { + /// Deposits SOL into the program and credits the `receiver` on ZetaChain zEVM. + /// + /// # Arguments + /// * `ctx` - The instruction context. + /// * `amount` - The amount of lamports to deposit. + /// * `receiver` - The Ethereum address of the receiver on ZetaChain zEVM. + pub fn deposit(ctx: Context, amount: u64, receiver: [u8; 20]) -> Result<()> { let pda = &mut ctx.accounts.pda; require!(!pda.deposit_paused, Errors::DepositPaused); require!(receiver != [0u8; 20], Errors::EmptyReceiver); @@ -189,21 +250,25 @@ pub mod gateway { }, ); system_program::transfer(cpi_context, amount_with_fees)?; + msg!( - "{:?} deposits {:?} lamports to PDA with fee {:?}; receiver {:?}", - ctx.accounts.signer.key(), + "Deposit executed: amount = {}, fee = {}, receiver = {:?}, pda = {}", amount, DEPOSIT_FEE, receiver, + ctx.accounts.pda.key() ); Ok(()) } - // deposit SOL into this program and the `receiver` on ZetaChain zEVM - // will get corresponding ZRC20 credit. The `receiver` should be a contract - // on zEVM and the `message` will be used as input data for the contract call. - // The `receiver` contract on zEVM will get the SOL ZRC20 credit and receive the `message`. + /// Deposits SOL and calls a contract on ZetaChain zEVM. + /// + /// # Arguments + /// * `ctx` - The instruction context. + /// * `amount` - The amount of lamports to deposit. + /// * `receiver` - The Ethereum address of the receiver on ZetaChain zEVM. + /// * `message` - The message passed to the contract. pub fn deposit_and_call( ctx: Context, amount: u64, @@ -212,18 +277,22 @@ pub mod gateway { ) -> Result<()> { require!(message.len() <= 512, Errors::MemoLengthExceeded); deposit(ctx, amount, receiver)?; + + msg!("Deposit and call executed with message = {:?}", message); + Ok(()) } - // deposit SPL token into this program and the `receiver` on ZetaChain zEVM - // will get corresponding ZRC20 credit. - // amount: amount of SPL token to deposit - // receiver: ethereum address (20Bytes) of the receiver on ZetaChain zEVM - #[allow(unused)] + /// Deposits SPL tokens and credits the `receiver` on ZetaChain zEVM. + /// + /// # Arguments + /// * `ctx` - The instruction context. + /// * `amount` - The amount of SPL tokens to deposit. + /// * `receiver` - The Ethereum address of the receiver on ZetaChain zEVM. pub fn deposit_spl_token( ctx: Context, amount: u64, - receiver: [u8; 20], // unused in this program; for directing zetachain protocol only + receiver: [u8; 20], ) -> Result<()> { let token = &ctx.accounts.token_program; let from = &ctx.accounts.from; @@ -232,7 +301,6 @@ pub mod gateway { require!(!pda.deposit_paused, Errors::DepositPaused); require!(receiver != [0u8; 20], Errors::EmptyReceiver); - // transfer deposit_fee let cpi_context = CpiContext::new( ctx.accounts.system_program.to_account_info(), system_program::Transfer { @@ -243,7 +311,6 @@ pub mod gateway { system_program::transfer(cpi_context, DEPOSIT_FEE)?; let pda_ata = get_associated_token_address(&ctx.accounts.pda.key(), &from.mint); - // must deposit to the ATA from PDA in order to receive credit require!( pda_ata == ctx.accounts.to.to_account_info().key(), Errors::DepositToAddressMismatch @@ -259,17 +326,25 @@ pub mod gateway { ); transfer(xfer_ctx, amount)?; - msg!("deposit spl token successfully"); + msg!( + "Deposit SPL executed: amount = {}, fee = {}, receiver = {:?}, pda = {}, mint = {}", + amount, + DEPOSIT_FEE, + receiver, + ctx.accounts.pda.key(), + ctx.accounts.mint_account.key() + ); Ok(()) } - // like `deposit_spl_token` instruction, - // deposit SPL token into this program and the `receiver` on ZetaChain zEVM - // will get corresponding ZRC20 credit. The `receiver` should be a contract - // on zEVM and the `message` will be used as input data for the contract call. - // The `receiver` contract on zEVM will get the SPL token ZRC20 credit and receive the `message`. - #[allow(unused)] + /// Deposits SPL tokens and calls a contract on ZetaChain zEVM. + /// + /// # Arguments + /// * `ctx` - The instruction context. + /// * `amount` - The amount of SPL tokens to deposit. + /// * `receiver` - The Ethereum address of the receiver on ZetaChain zEVM. + /// * `message` - The message passed to the contract. pub fn deposit_spl_token_and_call( ctx: Context, amount: u64, @@ -278,83 +353,110 @@ pub mod gateway { ) -> Result<()> { require!(message.len() <= 512, Errors::MemoLengthExceeded); deposit_spl_token(ctx, amount, receiver)?; + + msg!("Deposit SPL and call executed with message = {:?}", message); + Ok(()) } - // require tss address signature on the internal message defined in the following - // concatenated_buffer vec. + /// Withdraws SOL. Caller is TSS. + /// + /// # Arguments + /// * `ctx` - The instruction context. + /// * `amount` - The amount of SOL to withdraw. + /// * `signature` - The TSS signature. + /// * `recovery_id` - The recovery ID for signature verification. + /// * `nonce` - The current nonce value. pub fn withdraw( ctx: Context, amount: u64, signature: [u8; 64], recovery_id: u8, - message_hash: [u8; 32], nonce: u64, ) -> Result<()> { let pda = &mut ctx.accounts.pda; if nonce != pda.nonce { - msg!("mismatch nonce"); + msg!( + "Mismatch nonce: provided nonce = {}, expected nonce = {}", + nonce, + pda.nonce, + ); return err!(Errors::NonceMismatch); } + let mut concatenated_buffer = Vec::new(); - concatenated_buffer.extend_from_slice("withdraw".as_bytes()); + concatenated_buffer.extend_from_slice(b"ZETACHAIN"); + concatenated_buffer.push(InstructionId::Withdraw as u8); concatenated_buffer.extend_from_slice(&pda.chain_id.to_be_bytes()); concatenated_buffer.extend_from_slice(&nonce.to_be_bytes()); concatenated_buffer.extend_from_slice(&amount.to_be_bytes()); - concatenated_buffer.extend_from_slice(&ctx.accounts.to.key().to_bytes()); - require!( - message_hash == hash(&concatenated_buffer[..]).to_bytes(), - Errors::MessageHashMismatch - ); + concatenated_buffer.extend_from_slice(&ctx.accounts.recipient.key().to_bytes()); + let computed_message_hash = hash(&concatenated_buffer[..]).to_bytes(); - let address = recover_eth_address(&message_hash, recovery_id, &signature)?; // ethereum address is the last 20 Bytes of the hashed pubkey + msg!("Computed message hash: {:?}", computed_message_hash); + + let address = recover_eth_address(&computed_message_hash, recovery_id, &signature)?; if address != pda.tss_address { msg!("ECDSA signature error"); return err!(Errors::TSSAuthenticationFailed); } - // transfer amount of SOL from PDA to the payer pda.sub_lamports(amount)?; - ctx.accounts.to.add_lamports(amount)?; + ctx.accounts.recipient.add_lamports(amount)?; pda.nonce += 1; + msg!( + "Withdraw executed: amount = {}, recipient = {}, pda = {}", + amount, + ctx.accounts.recipient.key(), + ctx.accounts.pda.key() + ); + Ok(()) } - // require tss address signature on the internal message defined in the following - // concatenated_buffer vec. + /// Withdraws SPL tokens. Caller is TSS. + /// + /// # Arguments + /// * `ctx` - The instruction context. + /// * `decimals` - Token decimals for precision. + /// * `amount` - The amount of tokens to withdraw. + /// * `signature` - The TSS signature. + /// * `recovery_id` - The recovery ID for signature verification. + /// * `nonce` - The current nonce value. pub fn withdraw_spl_token( ctx: Context, decimals: u8, amount: u64, signature: [u8; 64], recovery_id: u8, - message_hash: [u8; 32], nonce: u64, ) -> Result<()> { let pda = &mut ctx.accounts.pda; - // let program_id = &mut ctx.accounts if nonce != pda.nonce { - msg!("mismatch nonce"); + msg!( + "Mismatch nonce: provided nonce = {}, expected nonce = {}", + nonce, + pda.nonce, + ); return err!(Errors::NonceMismatch); } let mut concatenated_buffer = Vec::new(); - concatenated_buffer.extend_from_slice("withdraw_spl_token".as_bytes()); + concatenated_buffer.extend_from_slice(b"ZETACHAIN"); + concatenated_buffer.push(InstructionId::WithdrawSplToken as u8); concatenated_buffer.extend_from_slice(&pda.chain_id.to_be_bytes()); concatenated_buffer.extend_from_slice(&nonce.to_be_bytes()); concatenated_buffer.extend_from_slice(&amount.to_be_bytes()); concatenated_buffer.extend_from_slice(&ctx.accounts.mint_account.key().to_bytes()); concatenated_buffer.extend_from_slice(&ctx.accounts.recipient_ata.key().to_bytes()); - require!( - message_hash == hash(&concatenated_buffer[..]).to_bytes(), - Errors::MessageHashMismatch - ); + let computed_message_hash = hash(&concatenated_buffer[..]).to_bytes(); + + msg!("Computed message hash: {:?}", computed_message_hash); - let address = recover_eth_address(&message_hash, recovery_id, &signature)?; // ethereum address is the last 20 Bytes of the hashed pubkey - msg!("recovered address {:?}", address); + let address = recover_eth_address(&computed_message_hash, recovery_id, &signature)?; // ethereum address is the last 20 Bytes of the hashed pubkey if address != pda.tss_address { msg!("ECDSA signature error"); return err!(Errors::TSSAuthenticationFailed); @@ -442,7 +544,6 @@ pub mod gateway { pda.nonce += 1; transfer_checked(xfer_ctx, amount, decimals)?; - msg!("withdraw spl token successfully"); // Note: this pda.sub_lamports() must be done here due to this issue https://github.com/solana-labs/solana/issues/9711 // otherwise the previous CPI calls might fail with error: // "sum of account balances before and after instruction do not match" @@ -452,10 +553,20 @@ pub mod gateway { pda.sub_lamports(reimbursement)?; ctx.accounts.signer.add_lamports(reimbursement)?; + msg!( + "Withdraw SPL executed: amount = {}, decimals = {}, recipient = {}, mint = {}, pda = {}", + amount, + decimals, + ctx.accounts.recipient.key(), + ctx.accounts.mint_account.key(), + ctx.accounts.pda.key() + ); + Ok(()) } } +/// Recovers eth address from signature. fn recover_eth_address( message_hash: &[u8; 32], recovery_id: u8, @@ -467,39 +578,42 @@ fn recover_eth_address( // pubkey is 64 Bytes, uncompressed public secp256k1 public key let h = hash(pubkey.to_bytes().as_slice()).to_bytes(); let address = &h.as_slice()[12..32]; // ethereum address is the last 20 Bytes of the hashed pubkey - msg!("recovered address {:?}", address); + msg!("Recovered address {:?}", address); let mut eth_address = [0u8; 20]; eth_address.copy_from_slice(address); Ok(eth_address) } -// recover and verify tss signature for whitelist and unwhitelist instructions +/// Recovers and verifies tss signature for whitelist and unwhitelist instructions. fn validate_whitelist_tss_signature( pda: &mut Account, whitelist_candidate: &mut Account, signature: [u8; 64], recovery_id: u8, - message_hash: [u8; 32], nonce: u64, - instruction_name: &str, + instruction: u8, ) -> Result<()> { if nonce != pda.nonce { - msg!("mismatch nonce"); + msg!( + "Mismatch nonce: provided nonce = {}, expected nonce = {}", + nonce, + pda.nonce, + ); return err!(Errors::NonceMismatch); } let mut concatenated_buffer = Vec::new(); - concatenated_buffer.extend_from_slice(instruction_name.as_bytes()); + concatenated_buffer.extend_from_slice(b"ZETACHAIN"); + concatenated_buffer.push(instruction); concatenated_buffer.extend_from_slice(&pda.chain_id.to_be_bytes()); - concatenated_buffer.extend_from_slice(&whitelist_candidate.key().to_bytes()); concatenated_buffer.extend_from_slice(&nonce.to_be_bytes()); - require!( - message_hash == hash(&concatenated_buffer[..]).to_bytes(), - Errors::MessageHashMismatch - ); + concatenated_buffer.extend_from_slice(&whitelist_candidate.key().to_bytes()); + let computed_message_hash = hash(&concatenated_buffer[..]).to_bytes(); + + msg!("Computed message hash: {:?}", computed_message_hash); - let address = recover_eth_address(&message_hash, recovery_id, &signature)?; + let address = recover_eth_address(&computed_message_hash, recovery_id, &signature)?; if address != pda.tss_address { msg!("ECDSA signature error"); return err!(Errors::TSSAuthenticationFailed); @@ -510,196 +624,226 @@ fn validate_whitelist_tss_signature( Ok(()) } +/// Instruction context for initializing the program. #[derive(Accounts)] pub struct Initialize<'info> { + /// The account of the signer initializing the program. #[account(mut)] pub signer: Signer<'info>, - #[account(init, payer = signer, space = size_of::< Pda > () + 8, seeds = [b"meta"], bump)] + /// Gateway PDA. + #[account(init, payer = signer, space = size_of::() + 8, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, + /// The system program. pub system_program: Program<'info, System>, } +/// Instruction context for SOL deposit operations. #[derive(Accounts)] pub struct Deposit<'info> { + /// The account of the signer making the deposit. #[account(mut)] pub signer: Signer<'info>, + /// Gateway PDA. #[account(mut, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, + /// The system program. pub system_program: Program<'info, System>, } +/// Instruction context for depositing SPL tokens. #[derive(Accounts)] pub struct DepositSplToken<'info> { + /// The account of the signer making the deposit. #[account(mut)] pub signer: Signer<'info>, + /// Gateway PDA. #[account(mut, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, + /// The whitelist entry account for the SPL token. #[account(seeds = [b"whitelist", mint_account.key().as_ref()], bump)] - pub whitelist_entry: Account<'info, WhitelistEntry>, // attach whitelist entry to show the mint_account is whitelisted + pub whitelist_entry: Account<'info, WhitelistEntry>, + /// The mint account of the SPL token being deposited. pub mint_account: Account<'info, Mint>, + /// The token program. pub token_program: Program<'info, Token>, + /// The source token account owned by the signer. #[account(mut)] - pub from: Account<'info, TokenAccount>, // this must be owned by signer; normally the ATA of signer + pub from: Account<'info, TokenAccount>, + /// The destination token account owned by the PDA. #[account(mut)] - pub to: Account<'info, TokenAccount>, // this must be ATA of PDA + pub to: Account<'info, TokenAccount>, + /// The system program. pub system_program: Program<'info, System>, } +/// Instruction context for SOL withdrawal operations. #[derive(Accounts)] pub struct Withdraw<'info> { + /// The account of the signer making the withdrawal. #[account(mut)] pub signer: Signer<'info>, + /// Gateway PDA. #[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. + /// The recipient account for the withdrawn SOL. + /// CHECK: Recipient account is not read; ownership validation is unnecessary. #[account(mut)] - pub to: UncheckedAccount<'info>, + pub recipient: UncheckedAccount<'info>, } +/// Instruction context for SPL token withdrawal operations. #[derive(Accounts)] pub struct WithdrawSPLToken<'info> { + /// The account of the signer making the withdrawal. #[account(mut)] pub signer: Signer<'info>, + /// Gateway PDA. #[account(mut, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, + /// The associated token account for the Gateway PDA. #[account(mut, associated_token::mint = mint_account, associated_token::authority = pda)] - pub pda_ata: Account<'info, TokenAccount>, // associated token address of PDA + pub pda_ata: Account<'info, TokenAccount>, + /// The mint account of the SPL token being withdrawn. pub mint_account: Account<'info, Mint>, - pub recipient: SystemAccount<'info>, - /// CHECK: recipient_ata might not have been created; avoid checking its content. - /// the validation will be done in the instruction processor. + /// The recipient account for the withdrawn tokens. + /// CHECK: Recipient account is not read; ownership validation is unnecessary. + pub recipient: UncheckedAccount<'info>, + + /// The recipient's associated token account. + /// CHECK: Validation will occur during instruction processing. #[account(mut)] pub recipient_ata: AccountInfo<'info>, + /// The token program. pub token_program: Program<'info, Token>, + /// The associated token program. pub associated_token_program: Program<'info, AssociatedToken>, + /// The system program. pub system_program: Program<'info, System>, } +/// Instruction context for updating the TSS address. #[derive(Accounts)] pub struct UpdateTss<'info> { - #[account(mut, seeds = [b"meta"], bump)] - pub pda: Account<'info, Pda>, - + /// The account of the signer performing the update. #[account(mut)] pub signer: Signer<'info>, -} -#[derive(Accounts)] -pub struct UpdateAuthority<'info> { + /// Gateway PDA. #[account(mut, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, +} +/// Instruction context for updating the PDA authority. +#[derive(Accounts)] +pub struct UpdateAuthority<'info> { + /// The account of the signer performing the update. #[account(mut)] pub signer: Signer<'info>, -} -#[derive(Accounts)] -pub struct UpdatePaused<'info> { + /// Gateway PDA. #[account(mut, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, +} +/// Instruction context for pausing or unpausing deposits. +#[derive(Accounts)] +pub struct UpdatePaused<'info> { + /// The account of the signer performing the update. #[account(mut)] pub signer: Signer<'info>, + + /// Gateway PDA. + #[account(mut, seeds = [b"meta"], bump)] + pub pda: Account<'info, Pda>, } +/// Instruction context for whitelisting SPL tokens. #[derive(Accounts)] pub struct Whitelist<'info> { + /// The account of the authority performing the operation. + #[account(mut)] + pub authority: Signer<'info>, + + /// Gateway PDA. + #[account(mut, seeds = [b"meta"], bump)] + pub pda: Account<'info, Pda>, + + /// The whitelist entry account being initialized. #[account( init, space = 8, - payer=authority, - seeds = [ - b"whitelist", - whitelist_candidate.key().as_ref() - ], + payer = authority, + seeds = [b"whitelist", whitelist_candidate.key().as_ref()], bump )] pub whitelist_entry: Account<'info, WhitelistEntry>, + /// The mint account of the SPL token being whitelisted. pub whitelist_candidate: Account<'info, Mint>, - #[account(mut, seeds = [b"meta"], bump)] - pub pda: Account<'info, Pda>, - - #[account(mut)] - pub authority: Signer<'info>, - + /// The system program. pub system_program: Program<'info, System>, } +/// Instruction context for unwhitelisting SPL tokens. #[derive(Accounts)] pub struct Unwhitelist<'info> { + /// The account of the authority performing the operation. + #[account(mut)] + pub authority: Signer<'info>, + + /// Gateway PDA. + #[account(mut, seeds = [b"meta"], bump)] + pub pda: Account<'info, Pda>, + + /// The whitelist entry account being closed. #[account( mut, - seeds = [ - b"whitelist", - whitelist_candidate.key().as_ref() - ], + seeds = [b"whitelist", whitelist_candidate.key().as_ref()], bump, close = authority, )] pub whitelist_entry: Account<'info, WhitelistEntry>, + /// The mint account of the SPL token being unwhitelisted. pub whitelist_candidate: Account<'info, Mint>, - - #[account(mut, seeds = [b"meta"], bump)] - pub pda: Account<'info, Pda>, - - #[account(mut)] - pub authority: Signer<'info>, - - pub system_program: Program<'info, System>, } +/// PDA account storing program state and settings. #[account] pub struct Pda { - nonce: u64, // ensure that each signature can only be used once - tss_address: [u8; 20], // 20 bytes address format of ethereum + /// The nonce to ensure each signature can only be used once. + nonce: u64, + /// The Ethereum TSS address (20 bytes). + tss_address: [u8; 20], + /// The authority controlling the PDA. authority: Pubkey, + /// The chain ID associated with the PDA. chain_id: u64, + /// Flag to indicate whether deposits are paused. deposit_paused: bool, } +/// Whitelist entry account for whitelisted SPL tokens. #[account] pub struct WhitelistEntry {} - -#[account] -pub struct RentPayerPda {} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let nonce: u64 = 0; - let amount: u64 = 500_000; - let mut concatenated_buffer = Vec::new(); - concatenated_buffer.extend_from_slice(&nonce.to_be_bytes()); - concatenated_buffer.extend_from_slice(&amount.to_be_bytes()); - println!("concatenated_buffer: {:?}", concatenated_buffer); - - let message_hash = hash(&concatenated_buffer[..]).to_bytes(); - println!("message_hash: {:?}", message_hash); - } -} diff --git a/scripts/fmt.sh b/scripts/fmt.sh index 21baf59..c4ea4ac 100755 --- a/scripts/fmt.sh +++ b/scripts/fmt.sh @@ -3,16 +3,16 @@ # Exit on any error set -e -if ! command -v brew &> /dev/null +if ! command -v rustup &> /dev/null then - echo "brew is required to run the script." + echo "rustup is required to run the script." exit 1 fi if ! command -v rustfmt &> /dev/null then echo "rustfmt could not be found, installing..." - brew install rustfmt + rustup component add rustfmt fi cargo fmt diff --git a/tests/gateway.ts b/tests/gateway.ts new file mode 100644 index 0000000..4c20590 --- /dev/null +++ b/tests/gateway.ts @@ -0,0 +1,959 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { Gateway } from "../target/types/gateway"; +import * as spl from "@solana/spl-token"; +import { randomFillSync } from "crypto"; +import { ec as EC } from "elliptic"; +import { keccak256 } from "ethereumjs-util"; +import { expect } from "chai"; +import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; + +const ec = new EC("secp256k1"); +// read private key from hex dump +const keyPair = ec.keyFromPrivate( + "5b81cdf52ba0766983acf8dd0072904733d92afe4dd3499e83e879b43ccb73e8" +); + +const usdcDecimals = 6; +const chain_id = 111111; +const chain_id_bn = new anchor.BN(chain_id); + +async function mintSPLToken( + conn: anchor.web3.Connection, + wallet: anchor.web3.Keypair, + mint: anchor.web3.Keypair +) { + const mintRent = await spl.getMinimumBalanceForRentExemptMint(conn); + let tokenTransaction = new anchor.web3.Transaction(); + tokenTransaction.add( + anchor.web3.SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: mint.publicKey, + lamports: mintRent, + space: spl.MINT_SIZE, + programId: spl.TOKEN_PROGRAM_ID, + }), + spl.createInitializeMintInstruction( + mint.publicKey, + usdcDecimals, + wallet.publicKey, + null + ) + ); + const txsig = await anchor.web3.sendAndConfirmTransaction( + conn, + tokenTransaction, + [wallet, mint] + ); + return txsig; +} + +async function depositSplTokens( + gatewayProgram: Program, + conn: anchor.web3.Connection, + wallet: anchor.web3.Keypair, + mint: anchor.web3.Keypair, + address: Buffer +) { + let seeds = [Buffer.from("meta", "utf-8")]; + const [pdaAccount] = anchor.web3.PublicKey.findProgramAddressSync( + seeds, + gatewayProgram.programId + ); + const pda_ata = await spl.getOrCreateAssociatedTokenAccount( + conn, + wallet, + mint.publicKey, + pdaAccount, + true + ); + + let tokenAccount = await spl.getOrCreateAssociatedTokenAccount( + conn, + wallet, + mint.publicKey, + wallet.publicKey + ); + await gatewayProgram.methods + .depositSplToken(new anchor.BN(1_000_000), Array.from(address)) + .accounts({ + from: tokenAccount.address, + to: pda_ata.address, + mintAccount: mint.publicKey, + }) + .rpc({ commitment: "processed" }); + return; +} + +async function withdrawSplToken( + mint, + decimals, + amount, + nonce, + from, + to, + to_owner, + gatewayProgram: Program +) { + const buffer = Buffer.concat([ + Buffer.from("ZETACHAIN", "utf-8"), + Buffer.from([0x02]), + chain_id_bn.toArrayLike(Buffer, "be", 8), + nonce.toArrayLike(Buffer, "be", 8), + amount.toArrayLike(Buffer, "be", 8), + mint.publicKey.toBuffer(), + to.toBuffer(), + ]); + const message_hash = keccak256(buffer); + const signature = keyPair.sign(message_hash, "hex"); + const { r, s, recoveryParam } = signature; + const signatureBuffer = Buffer.concat([ + r.toArrayLike(Buffer, "be", 32), + s.toArrayLike(Buffer, "be", 32), + ]); + return gatewayProgram.methods + .withdrawSplToken( + decimals, + amount, + Array.from(signatureBuffer), + Number(recoveryParam), + nonce + ) + .accounts({ + pdaAta: from, + mintAccount: mint.publicKey, + recipientAta: to, + recipient: to_owner, + }) + .rpc({ commitment: "processed" }); +} + +describe("Gateway", () => { + // Configure the client to use the local cluster. + anchor.setProvider(anchor.AnchorProvider.env()); + const conn = anchor.getProvider().connection; + const gatewayProgram = anchor.workspace.Gateway as Program; + const wallet = anchor.workspace.Gateway.provider.wallet.payer; + const mint = anchor.web3.Keypair.generate(); + const mint_fake = anchor.web3.Keypair.generate(); // for testing purpose + + let wallet_ata: anchor.web3.PublicKey; + let pdaAccount: anchor.web3.PublicKey; + + const publicKeyBuffer = Buffer.from( + keyPair.getPublic(false, "hex").slice(2), + "hex" + ); // Uncompressed form of public key, remove the '04' prefix + + const addressBuffer = keccak256(publicKeyBuffer); // Skip the first byte (format indicator) + const address = addressBuffer.slice(-20); + const tssAddress = Array.from(address); + + let seeds = [Buffer.from("meta", "utf-8")]; + [pdaAccount] = anchor.web3.PublicKey.findProgramAddressSync( + seeds, + gatewayProgram.programId + ); + + it("Initializes the program", async () => { + await gatewayProgram.methods.initialize(tssAddress, chain_id_bn).rpc(); + + // repeated initialization should fail + try { + await gatewayProgram.methods.initialize(tssAddress, chain_id_bn).rpc(); + throw new Error("Expected error not thrown"); // This line will make the test fail if no error is thrown + } catch (err) { + expect(err).to.be.not.null; + } + }); + + it("Mint a SPL USDC token", async () => { + // now deploying a fake USDC SPL Token + // 1. create a mint account + await mintSPLToken(conn, wallet, mint); + + // 2. create token account to receive mint + let tokenAccount = await spl.getOrCreateAssociatedTokenAccount( + conn, + wallet, + mint.publicKey, + wallet.publicKey + ); + // 3. mint some tokens + const mintToTransaction = new anchor.web3.Transaction().add( + spl.createMintToInstruction( + mint.publicKey, + tokenAccount.address, + wallet.publicKey, + 10_000_000 + ) + ); + await anchor.web3.sendAndConfirmTransaction( + anchor.getProvider().connection, + mintToTransaction, + [wallet] + ); + + // OK; transfer some USDC SPL token to the gateway PDA + wallet_ata = await spl.getAssociatedTokenAddress( + mint.publicKey, + wallet.publicKey + ); + + // create a fake USDC token account + await mintSPLToken(conn, wallet, mint_fake); + }); + + it("Whitelist USDC SPL token", async () => { + await gatewayProgram.methods + .whitelistSplMint([], 0, new anchor.BN(0)) + .accounts({ + whitelistCandidate: mint.publicKey, + }) + .signers([]) + .rpc(); + + let seeds = [Buffer.from("whitelist", "utf-8"), mint.publicKey.toBuffer()]; + let [entryAddress] = anchor.web3.PublicKey.findProgramAddressSync( + seeds, + gatewayProgram.programId + ); + + try { + seeds = [ + Buffer.from("whitelist", "utf-8"), + mint_fake.publicKey.toBuffer(), + ]; + [entryAddress] = anchor.web3.PublicKey.findProgramAddressSync( + seeds, + gatewayProgram.programId + ); + await gatewayProgram.account.whitelistEntry.fetch(entryAddress); + } catch (err) { + expect(err.message).to.include("Account does not exist or has no data"); + } + }); + + it("Deposit 1_000_000 USDC to Gateway", async () => { + let pda_ata = await getOrCreateAssociatedTokenAccount( + conn, + wallet, + mint.publicKey, + pdaAccount, + true + ); + let acct = await spl.getAccount(conn, pda_ata.address); + let bal0 = acct.amount; + await depositSplTokens(gatewayProgram, conn, wallet, mint, address); + acct = await spl.getAccount(conn, pda_ata.address); + let bal1 = acct.amount; + expect(bal1 - bal0).to.be.eq(1_000_000n); + + let tokenAccount = await getOrCreateAssociatedTokenAccount( + conn, + wallet, + mint.publicKey, + wallet.publicKey + ); + try { + await gatewayProgram.methods + .depositSplToken(new anchor.BN(1_000_000), Array.from(address)) + .accounts({ + from: tokenAccount.address, + to: wallet_ata, + mintAccount: mint.publicKey, + }) + .rpc(); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("DepositToAddressMismatch"); + } + + // test depositSplTokenAndCall + acct = await spl.getAccount(conn, pda_ata.address); + bal0 = acct.amount; + await gatewayProgram.methods + .depositSplTokenAndCall( + new anchor.BN(2_000_000), + Array.from(address), + Buffer.from("hi", "utf-8") + ) + .accounts({ + from: tokenAccount.address, + to: pda_ata.address, + mintAccount: mint.publicKey, + }) + .rpc({ commitment: "processed" }); + acct = await spl.getAccount(conn, pda_ata.address); + bal1 = acct.amount; + expect(bal1 - bal0).to.be.eq(2_000_000n); + }); + + it("Deposit non-whitelisted SPL tokens should fail", async () => { + let seeds = [Buffer.from("meta", "utf-8")]; + [pdaAccount] = anchor.web3.PublicKey.findProgramAddressSync( + seeds, + gatewayProgram.programId + ); + let fake_pda_ata = await spl.getOrCreateAssociatedTokenAccount( + conn, + wallet, + mint_fake.publicKey, + pdaAccount, + true + ); + + let tokenAccount = await spl.getOrCreateAssociatedTokenAccount( + conn, + wallet, + mint_fake.publicKey, + wallet.publicKey, + true + ); + try { + await gatewayProgram.methods + .depositSplToken(new anchor.BN(1_000_000), Array.from(address)) + .accounts({ + from: tokenAccount.address, + to: fake_pda_ata.address, + mintAccount: mint_fake.publicKey, + }) + .rpc({ commitment: "processed" }); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("AccountNotInitialized"); + } + }); + + it("Withdraw 500_000 USDC from Gateway with ECDSA signature", async () => { + let pda_ata = await spl.getAssociatedTokenAddress( + mint.publicKey, + pdaAccount, + true + ); + const account2 = await spl.getAccount(conn, pda_ata); + + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + const amount = new anchor.BN(500_000); + const nonce = pdaAccountData.nonce; + await withdrawSplToken( + mint, + usdcDecimals, + amount, + nonce, + pda_ata, + wallet_ata, + wallet.publicKey, + gatewayProgram + ); + const account3 = await spl.getAccount(conn, pda_ata); + expect(account3.amount - account2.amount).to.be.eq(-500_000n); + + // should trigger nonce mismatch in withdraw + try { + await withdrawSplToken( + mint, + usdcDecimals, + amount, + nonce, + pda_ata, + wallet_ata, + wallet.publicKey, + gatewayProgram + ); + throw new Error("Expected error not thrown"); // This line will make the test fail if no error is thrown + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("NonceMismatch"); + const account4 = await spl.getAccount(conn, pda_ata); + expect(account4.amount).to.be.eq(2_500_000n); + } + + try { + const nonce2 = nonce.addn(1); + const buffer = Buffer.concat([ + Buffer.from("ZETACHAIN", "utf-8"), + Buffer.from([0x02]), + chain_id_bn.toArrayLike(Buffer, "be", 8), + nonce2.toArrayLike(Buffer, "be", 8), + amount.toArrayLike(Buffer, "be", 8), + mint_fake.publicKey.toBuffer(), + wallet_ata.toBuffer(), + ]); + const message_hash = keccak256(buffer); + const signature = keyPair.sign(message_hash, "hex"); + const { r, s, recoveryParam } = signature; + const signatureBuffer = Buffer.concat([ + r.toArrayLike(Buffer, "be", 32), + s.toArrayLike(Buffer, "be", 32), + ]); + await gatewayProgram.methods + .withdrawSplToken( + usdcDecimals, + amount, + Array.from(signatureBuffer), + Number(recoveryParam), + nonce2 + ) + .accounts({ + pdaAta: pda_ata, + mintAccount: mint_fake.publicKey, + recipientAta: wallet_ata, + recipient: wallet.publicKey, + }) + .rpc(); + throw new Error("Expected error not thrown"); // This line will make the test fail if no error is thrown + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("ConstraintAssociated"); + const account4 = await spl.getAccount(conn, pda_ata); + expect(account4.amount).to.be.eq(2_500_000n); + } + }); + + it("Deposit if receiver is empty address should fail", async () => { + try { + await gatewayProgram.methods + .deposit(new anchor.BN(1_000_000_000), Array(20).fill(0)) + .rpc(); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("EmptyReceiver"); + } + }); + + 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)) + .rpc(); + let bal1 = await conn.getBalance(pdaAccount); + // amount + deposit fee + expect(bal1).to.be.gte(1_000_000_000 + 2_000_000); + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + const nonce = pdaAccountData.nonce; + const amount = new anchor.BN(500000000); + const to = await spl.getAssociatedTokenAddress( + mint.publicKey, + wallet.publicKey + ); + const buffer = Buffer.concat([ + Buffer.from("ZETACHAIN", "utf-8"), + Buffer.from([0x01]), + chain_id_bn.toArrayLike(Buffer, "be", 8), + nonce.toArrayLike(Buffer, "be", 8), + amount.toArrayLike(Buffer, "be", 8), + to.toBuffer(), + ]); + const message_hash = keccak256(buffer); + const signature = keyPair.sign(message_hash, "hex"); + const { r, s, recoveryParam } = signature; + const signatureBuffer = Buffer.concat([ + r.toArrayLike(Buffer, "be", 32), + s.toArrayLike(Buffer, "be", 32), + ]); + + await gatewayProgram.methods + .withdraw( + amount, + Array.from(signatureBuffer), + Number(recoveryParam), + nonce + ) + .accounts({ + recipient: to, + }) + .rpc(); + let bal2 = await conn.getBalance(pdaAccount); + expect(bal2).to.be.eq(bal1 - 500_000_000); + let bal3 = await conn.getBalance(to); + expect(bal3).to.be.gte(500_000_000); + }); + + it("Withdraw with wrong nonce should fail", async () => { + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + const nonce = pdaAccountData.nonce; + const amount = new anchor.BN(500000000); + const to = await spl.getAssociatedTokenAddress( + mint.publicKey, + wallet.publicKey + ); + + const buffer = Buffer.concat([ + Buffer.from("ZETACHAIN", "utf-8"), + Buffer.from([0x01]), + chain_id_bn.toArrayLike(Buffer, "be", 8), + nonce.toArrayLike(Buffer, "be", 8), + amount.toArrayLike(Buffer, "be", 8), + to.toBuffer(), + ]); + const message_hash = keccak256(buffer); + const signature = keyPair.sign(message_hash, "hex"); + const { r, s, recoveryParam } = signature; + const signatureBuffer = Buffer.concat([ + r.toArrayLike(Buffer, "be", 32), + s.toArrayLike(Buffer, "be", 32), + ]); + + try { + await gatewayProgram.methods + .withdraw( + amount, + Array.from(signatureBuffer), + Number(recoveryParam), + nonce.subn(1) + ) + .accounts({ + recipient: to, + }) + .rpc(); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("NonceMismatch."); + } + }); + + it("Withdraw with wrong signature should fail", async () => { + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + const nonce = pdaAccountData.nonce; + const amount = new anchor.BN(500000000); + const to = await spl.getAssociatedTokenAddress( + mint.publicKey, + wallet.publicKey + ); + + const buffer = Buffer.concat([ + Buffer.from("ZETACHAIN", "utf-8"), + Buffer.from([0x01]), + chain_id_bn.toArrayLike(Buffer, "be", 8), + nonce.subn(1).toArrayLike(Buffer, "be", 8), // wrong nonce + amount.toArrayLike(Buffer, "be", 8), + to.toBuffer(), + ]); + const message_hash = keccak256(buffer); + const signature = keyPair.sign(message_hash, "hex"); + const { r, s, recoveryParam } = signature; + const signatureBuffer = Buffer.concat([ + r.toArrayLike(Buffer, "be", 32), + s.toArrayLike(Buffer, "be", 32), + ]); + + try { + await gatewayProgram.methods + .withdraw( + amount, + Array.from(signatureBuffer), + Number(recoveryParam), + nonce + ) + .accounts({ + recipient: to, + }) + .rpc(); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("TSSAuthenticationFailed"); + } + }); + + it("Withdraw SPL token to a non-existent account should succeed by creating it", async () => { + let rentPayerPdaBal0 = await conn.getBalance(pdaAccount); + let pda_ata = await spl.getAssociatedTokenAddress( + mint.publicKey, + pdaAccount, + true + ); + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + const amount = new anchor.BN(500_000); + const nonce = pdaAccountData.nonce; + const wallet2 = anchor.web3.Keypair.generate(); + const to = await spl.getAssociatedTokenAddress( + mint.publicKey, + wallet2.publicKey + ); + + let to_ata_bal = await conn.getBalance(to); + expect(to_ata_bal).to.be.eq(0); // the new ata account (owned by wallet2) should be non-existent; + await withdrawSplToken( + mint, + usdcDecimals, + amount, + nonce, + pda_ata, + to, + wallet2.publicKey, + gatewayProgram + ); + to_ata_bal = await conn.getBalance(to); + expect(to_ata_bal).to.be.gt(2_000_000); // the new ata account (owned by wallet2) should be created + + // pda should have reduced balance + let rentPayerPdaBal1 = await conn.getBalance(pdaAccount); + // expected reimbursement to be gas fee (5000 lamports) + ATA creation cost 2039280 lamports + expect(rentPayerPdaBal0 - rentPayerPdaBal1).to.be.eq(to_ata_bal + 5000); // rentPayer pays rent + }); + + it("Withdraw SPL token with wrong nonce should fail", async () => { + let pda_ata = await spl.getAssociatedTokenAddress( + mint.publicKey, + pdaAccount, + true + ); + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + const amount = new anchor.BN(500_000); + const nonce = pdaAccountData.nonce; + const wallet2 = anchor.web3.Keypair.generate(); + const to = await spl.getAssociatedTokenAddress( + mint.publicKey, + wallet2.publicKey + ); + + try { + await withdrawSplToken( + mint, + usdcDecimals, + amount, + nonce.subn(1), + pda_ata, + to, + wallet2.publicKey, + gatewayProgram + ); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("NonceMismatch."); + } + }); + + it("Withdraw SPL token with wrong signature should fail", async () => { + let pda_ata = await spl.getAssociatedTokenAddress( + mint.publicKey, + pdaAccount, + true + ); + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + const amount = new anchor.BN(500_000); + const nonce = pdaAccountData.nonce; + const wallet2 = anchor.web3.Keypair.generate(); + const to = await spl.getAssociatedTokenAddress( + mint.publicKey, + wallet2.publicKey + ); + + const buffer = Buffer.concat([ + Buffer.from("ZETACHAIN", "utf-8"), + Buffer.from([0x02]), + chain_id_bn.toArrayLike(Buffer, "be", 8), + nonce.subn(1).toArrayLike(Buffer, "be", 8), // wrong nonce + amount.toArrayLike(Buffer, "be", 8), + mint.publicKey.toBuffer(), + to.toBuffer(), + ]); + const message_hash = keccak256(buffer); + const signature = keyPair.sign(message_hash, "hex"); + const { r, s, recoveryParam } = signature; + const signatureBuffer = Buffer.concat([ + r.toArrayLike(Buffer, "be", 32), + s.toArrayLike(Buffer, "be", 32), + ]); + try { + gatewayProgram.methods + .withdrawSplToken( + usdcDecimals, + amount, + Array.from(signatureBuffer), + Number(recoveryParam), + nonce + ) + .accounts({ + pdaAta: pda_ata, + mintAccount: mint.publicKey, + recipientAta: to, + recipient: wallet2.publicKey, + }) + .rpc({ commitment: "processed" }); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("TSSAuthenticationFailed"); + } + }); + + it("Deposit and call with empty address receiver should fail", async () => { + try { + await gatewayProgram.methods + .depositAndCall( + new anchor.BN(1_000_000_000), + Array(20).fill(0), + Buffer.from("hello", "utf-8") + ) + .rpc(); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("EmptyReceiver"); + } + }); + + 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") + ) + .rpc({ commitment: "processed" }); + await conn.getParsedTransaction(txsig, "confirmed"); + let bal2 = await conn.getBalance(pdaAccount); + expect(bal2 - bal1).to.be.gte(1_000_000_000); + }); + + it("Deposit SPL with empty address receiver should fail", async () => { + try { + await depositSplTokens( + gatewayProgram, + conn, + wallet, + mint, + Buffer.alloc(20) + ); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("EmptyReceiver"); + } + }); + + it("Unwhitelist SPL token and deposit should fail", async () => { + await gatewayProgram.methods + .unwhitelistSplMint([], 0, new anchor.BN(0)) + .accounts({ + whitelistCandidate: mint.publicKey, + }) + .rpc(); + + try { + await depositSplTokens(gatewayProgram, conn, wallet, mint, address); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("AccountNotInitialized"); + } + }); + + it("Re-whitelist SPL token and deposit should succeed", async () => { + await gatewayProgram.methods + .whitelistSplMint([], 0, new anchor.BN(0)) + .accounts({ + whitelistCandidate: mint.publicKey, + }) + .rpc(); + await depositSplTokens(gatewayProgram, conn, wallet, mint, address); + }); + + it("Unwhitelist SPL token using TSS signature and deposit should fail", async () => { + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + const nonce = pdaAccountData.nonce; + + const buffer = Buffer.concat([ + Buffer.from("ZETACHAIN", "utf-8"), + Buffer.from([0x04]), + chain_id_bn.toArrayLike(Buffer, "be", 8), + nonce.toArrayLike(Buffer, "be", 8), + mint.publicKey.toBuffer(), + ]); + const message_hash = keccak256(buffer); + const signature = keyPair.sign(message_hash, "hex"); + const { r, s, recoveryParam } = signature; + const signatureBuffer = Buffer.concat([ + r.toArrayLike(Buffer, "be", 32), + s.toArrayLike(Buffer, "be", 32), + ]); + + await gatewayProgram.methods + .unwhitelistSplMint( + Array.from(signatureBuffer), + Number(recoveryParam), + nonce + ) + .accounts({ + whitelistCandidate: mint.publicKey, + }) + .rpc(); + + try { + await depositSplTokens(gatewayProgram, conn, wallet, mint, address); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("AccountNotInitialized"); + } + }); + + it("Re-whitelist SPL token using TSS signature and deposit should succeed", async () => { + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + const nonce = pdaAccountData.nonce; + + const buffer = Buffer.concat([ + Buffer.from("ZETACHAIN", "utf-8"), + Buffer.from([0x03]), + chain_id_bn.toArrayLike(Buffer, "be", 8), + nonce.toArrayLike(Buffer, "be", 8), + mint.publicKey.toBuffer(), + ]); + const message_hash = keccak256(buffer); + const signature = keyPair.sign(message_hash, "hex"); + const { r, s, recoveryParam } = signature; + const signatureBuffer = Buffer.concat([ + r.toArrayLike(Buffer, "be", 32), + s.toArrayLike(Buffer, "be", 32), + ]); + + await gatewayProgram.methods + .whitelistSplMint( + Array.from(signatureBuffer), + Number(recoveryParam), + nonce + ) + .accounts({ + whitelistCandidate: mint.publicKey, + }) + .rpc(); + await depositSplTokens(gatewayProgram, conn, wallet, mint, address); + }); + + it("Unwhitelist SPL token using wrong TSS signature should fail", async () => { + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + const nonce = pdaAccountData.nonce; + + const buffer = Buffer.concat([ + Buffer.from("ZETACHAIN", "utf-8"), + Buffer.from([0x03]), + chain_id_bn.toArrayLike(Buffer, "be", 8), + nonce.subn(1).toArrayLike(Buffer, "be", 8), // wrong nonce + mint.publicKey.toBuffer(), + ]); + const message_hash = keccak256(buffer); + const signature = keyPair.sign(message_hash, "hex"); + const { r, s, recoveryParam } = signature; + const signatureBuffer = Buffer.concat([ + r.toArrayLike(Buffer, "be", 32), + s.toArrayLike(Buffer, "be", 32), + ]); + + try { + await gatewayProgram.methods + .unwhitelistSplMint( + Array.from(signatureBuffer), + Number(recoveryParam), + nonce + ) + .accounts({ + whitelistCandidate: mint.publicKey, + }) + .rpc(); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("TSSAuthenticationFailed."); + } + }); + + it("Unwhitelist SPL token using wrong nonce should fail", async () => { + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + const nonce = pdaAccountData.nonce; + + const buffer = Buffer.concat([ + Buffer.from("ZETACHAIN", "utf-8"), + Buffer.from([0x03]), + chain_id_bn.toArrayLike(Buffer, "be", 8), + nonce.toArrayLike(Buffer, "be", 8), + mint.publicKey.toBuffer(), + ]); + const message_hash = keccak256(buffer); + const signature = keyPair.sign(message_hash, "hex"); + const { r, s, recoveryParam } = signature; + const signatureBuffer = Buffer.concat([ + r.toArrayLike(Buffer, "be", 32), + s.toArrayLike(Buffer, "be", 32), + ]); + + try { + await gatewayProgram.methods + .unwhitelistSplMint( + Array.from(signatureBuffer), + Number(recoveryParam), + nonce.subn(1) + ) + .accounts({ + whitelistCandidate: mint.publicKey, + }) + .rpc(); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("NonceMismatch."); + } + }); + + it("Update TSS address", async () => { + const newTss = new Uint8Array(20); + randomFillSync(newTss); + await gatewayProgram.methods.updateTss(Array.from(newTss)).rpc(); + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + expect(pdaAccountData.tssAddress).to.be.deep.eq(Array.from(newTss)); + + // only the authority stored in PDA can update the TSS address; the following should fail + try { + await gatewayProgram.methods + .updateTss(Array.from(newTss)) + .accounts({ + signer: mint.publicKey, + }) + .signers([mint]) + .rpc(); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("SignerIsNotAuthority"); + } + }); + + it("Pause deposit and deposit should fail", async () => { + const newTss = new Uint8Array(20); + randomFillSync(newTss); + await gatewayProgram.methods.setDepositPaused(true).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") + ) + .rpc(); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("DepositPaused"); + } + }); + + const newAuthority = anchor.web3.Keypair.generate(); + it("Update authority", async () => { + await gatewayProgram.methods.updateAuthority(newAuthority.publicKey).rpc(); + // now the old authority cannot update TSS address and will fail + try { + await gatewayProgram.methods + .updateTss(Array.from(new Uint8Array(20))) + .rpc(); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("SignerIsNotAuthority"); + } + }); +}); diff --git a/tests/protocol-contracts-solana.ts b/tests/protocol-contracts-solana.ts deleted file mode 100644 index 183e6da..0000000 --- a/tests/protocol-contracts-solana.ts +++ /dev/null @@ -1,566 +0,0 @@ -import * as anchor from "@coral-xyz/anchor"; -import {Program, web3} from "@coral-xyz/anchor"; -import {Gateway} from "../target/types/gateway"; -import * as spl from "@solana/spl-token"; -import {randomFillSync} from 'crypto'; -import { ec as EC } from 'elliptic'; -import { keccak256 } from 'ethereumjs-util'; -import { bufferToHex } from 'ethereumjs-util'; -import {expect} from 'chai'; -import {ecdsaRecover} from 'secp256k1'; -import {getOrCreateAssociatedTokenAccount} from "@solana/spl-token"; - -const ec = new EC('secp256k1'); -// const keyPair = ec.genKeyPair(); -// read private key from hex dump -const keyPair = ec.keyFromPrivate('5b81cdf52ba0766983acf8dd0072904733d92afe4dd3499e83e879b43ccb73e8'); - -const usdcDecimals = 6; -const chain_id = 111111; -const chain_id_bn = new anchor.BN(chain_id); - -async function mintSPLToken(conn: anchor.web3.Connection, wallet: anchor.web3.Keypair, mint: anchor.web3.Keypair) { - const mintRent = await spl.getMinimumBalanceForRentExemptMint(conn); - let tokenTransaction = new anchor.web3.Transaction(); - tokenTransaction.add( - anchor.web3.SystemProgram.createAccount({ - fromPubkey: wallet.publicKey, - newAccountPubkey: mint.publicKey, - lamports: mintRent, - space: spl.MINT_SIZE, - programId: spl.TOKEN_PROGRAM_ID - }), - spl.createInitializeMintInstruction( - mint.publicKey, - usdcDecimals, - wallet.publicKey, - null, - ) - ); - const txsig = await anchor.web3.sendAndConfirmTransaction(conn, tokenTransaction, [wallet, mint]); - return txsig; -} - -async function depositSplTokens(gatewayProgram: Program, conn: anchor.web3.Connection, wallet: anchor.web3.Keypair, mint: anchor.web3.Keypair, address: Buffer) { - let seeds = [Buffer.from("meta", "utf-8")]; - const [pdaAccount] = anchor.web3.PublicKey.findProgramAddressSync( - seeds, - gatewayProgram.programId, - ); - const pda_ata = await spl.getOrCreateAssociatedTokenAccount( - conn, - wallet, - mint.publicKey, - pdaAccount, - true - ); - - let tokenAccount = await spl.getOrCreateAssociatedTokenAccount( - conn,wallet, mint.publicKey, wallet.publicKey - ) - await gatewayProgram.methods.depositSplToken(new anchor.BN(1_000_000), Array.from(address)).accounts({ - from: tokenAccount.address, - to: pda_ata.address, - mintAccount: mint.publicKey, - }).rpc({commitment: 'processed'}); - return; -} - -async function withdrawSplToken( mint, decimals, amount, nonce,from, to, to_owner, tssKey, gatewayProgram: Program) { - const buffer = Buffer.concat([ - Buffer.from("withdraw_spl_token","utf-8"), - chain_id_bn.toArrayLike(Buffer, 'be', 8), - nonce.toArrayLike(Buffer, 'be', 8), - amount.toArrayLike(Buffer, 'be', 8), - mint.publicKey.toBuffer(), - to.toBuffer(), - ]); - const message_hash = keccak256(buffer); - const signature = keyPair.sign(message_hash, 'hex'); - const { r, s, recoveryParam } = signature; - const signatureBuffer = Buffer.concat([ - r.toArrayLike(Buffer, 'be', 32), - s.toArrayLike(Buffer, 'be', 32), - ]); - return gatewayProgram.methods.withdrawSplToken(decimals, amount, Array.from(signatureBuffer), Number(recoveryParam), Array.from(message_hash), nonce) - .accounts({ - pdaAta: from, - mintAccount: mint.publicKey, - recipientAta: to, - recipient: to_owner, - - }).rpc({commitment: 'processed'}); -} - - -describe("Gateway", () => { - // Configure the client to use the local cluster. - anchor.setProvider(anchor.AnchorProvider.env()); - const conn = anchor.getProvider().connection; - const gatewayProgram = anchor.workspace.Gateway as Program; - const wallet = anchor.workspace.Gateway.provider.wallet.payer; - const mint = anchor.web3.Keypair.generate(); - const mint_fake = anchor.web3.Keypair.generate(); // for testing purpose - - - let wallet_ata: anchor.web3.PublicKey; - let pdaAccount: anchor.web3.PublicKey; - - const message_hash = keccak256(Buffer.from("hello world")); - const signature = keyPair.sign(message_hash, 'hex'); - const { r, s, recoveryParam } = signature; - const signatureBuffer = Buffer.concat([ - r.toArrayLike(Buffer, 'be', 32), - s.toArrayLike(Buffer, 'be', 32), - ]); - const recoveredPubkey = ecdsaRecover(signatureBuffer, recoveryParam, message_hash, false); - const publicKeyBuffer = Buffer.from(keyPair.getPublic(false, 'hex').slice(2), 'hex'); // Uncompressed form of public key, remove the '04' prefix - - const addressBuffer = keccak256(publicKeyBuffer); // Skip the first byte (format indicator) - const address = addressBuffer.slice(-20); - const tssAddress = Array.from(address); - - let seeds = [Buffer.from("meta", "utf-8")]; - [pdaAccount] = anchor.web3.PublicKey.findProgramAddressSync( - seeds, - gatewayProgram.programId, - ); - - it("Initializes the program", async () => { - await gatewayProgram.methods.initialize(tssAddress, chain_id_bn).rpc(); - - // repeated initialization should fail - try { - await gatewayProgram.methods.initialize(tssAddress,chain_id_bn).rpc(); - throw new Error("Expected error not thrown"); // This line will make the test fail if no error is thrown - } catch (err) { - expect(err).to.be.not.null; - } - }); - - it("Mint a SPL USDC token", async () => { - // now deploying a fake USDC SPL Token - // 1. create a mint account - await mintSPLToken(conn, wallet, mint); - - // 2. create token account to receive mint - let tokenAccount = await spl.getOrCreateAssociatedTokenAccount( - conn, - wallet, - mint.publicKey, - wallet.publicKey, - ); - // 3. mint some tokens - const mintToTransaction = new anchor.web3.Transaction().add( - spl.createMintToInstruction( - mint.publicKey, - tokenAccount.address, - wallet.publicKey, - 10_000_000, - ) - ); - await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, mintToTransaction, [wallet]); - const account = await spl.getAccount(conn, tokenAccount.address); - - // OK; transfer some USDC SPL token to the gateway PDA - wallet_ata = await spl.getAssociatedTokenAddress( - mint.publicKey, - wallet.publicKey, - ); - - // create a fake USDC token account - await mintSPLToken(conn, wallet, mint_fake); - }) - - it("whitelist USDC spl token", async () => { - await gatewayProgram.methods.whitelistSplMint([], 0, [], new anchor.BN(0)).accounts({ - whitelistCandidate: mint.publicKey, - }).signers([]).rpc(); - - let seeds = [Buffer.from("whitelist", "utf-8"), mint.publicKey.toBuffer()]; - let [entryAddress] = anchor.web3.PublicKey.findProgramAddressSync( - seeds, - gatewayProgram.programId, - ); - - try { - seeds = [Buffer.from("whitelist", "utf-8"), mint_fake.publicKey.toBuffer()]; - [entryAddress] = anchor.web3.PublicKey.findProgramAddressSync( - seeds, - gatewayProgram.programId, - ); - await gatewayProgram.account.whitelistEntry.fetch(entryAddress); - } catch(err) { - expect(err.message).to.include("Account does not exist or has no data"); - } - }); - - it("Deposit 1_000_000 USDC to Gateway", async () => { - let pda_ata = await getOrCreateAssociatedTokenAccount(conn, wallet, mint.publicKey, pdaAccount, true); - let acct = await spl.getAccount(conn, pda_ata.address); - let bal0 = acct.amount; - await depositSplTokens(gatewayProgram, conn, wallet, mint, address); - acct = await spl.getAccount(conn, pda_ata.address); - let bal1 = acct.amount; - expect(bal1-bal0).to.be.eq(1_000_000n); - - - let tokenAccount = await getOrCreateAssociatedTokenAccount( - conn,wallet, mint.publicKey, wallet.publicKey, - ) - try { - await gatewayProgram.methods.depositSplToken(new anchor.BN(1_000_000), Array.from(address)).accounts( - { - from: tokenAccount.address, - to: wallet_ata, - mintAccount: mint.publicKey, - } - ).rpc(); - throw new Error("Expected error not thrown"); - } catch (err) { - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("DepositToAddressMismatch"); - } - - // test depositSplTokenAndCall - acct = await spl.getAccount(conn, pda_ata.address); - bal0 = acct.amount; - await gatewayProgram.methods.depositSplTokenAndCall(new anchor.BN(2_000_000), Array.from(address), Buffer.from('hi', 'utf-8')).accounts({ - from: tokenAccount.address, - to: pda_ata.address, - mintAccount: mint.publicKey, - }).rpc({commitment: 'processed'}); - acct = await spl.getAccount(conn, pda_ata.address); - bal1 = acct.amount; - expect(bal1-bal0).to.be.eq(2_000_000n); - }); - - it("deposit non-whitelisted SPL tokens should fail", async () => { - let seeds = [Buffer.from("meta", "utf-8")]; - [pdaAccount] = anchor.web3.PublicKey.findProgramAddressSync( - seeds, - gatewayProgram.programId, - ); - let fake_pda_ata = await spl.getOrCreateAssociatedTokenAccount( - conn, - wallet, - mint_fake.publicKey, - pdaAccount, - true - ); - - let tokenAccount = await spl.getOrCreateAssociatedTokenAccount( - conn,wallet, mint_fake.publicKey, wallet.publicKey, true - ) - try { - await gatewayProgram.methods.depositSplToken(new anchor.BN(1_000_000), Array.from(address)).accounts({ - from: tokenAccount.address, - to: fake_pda_ata.address, - mintAccount: mint_fake.publicKey, - }).rpc({commitment: 'processed'}); - throw new Error("Expected error not thrown"); - } catch (err) { - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("AccountNotInitialized"); - } - - }); - - it("Withdraw 500_000 USDC from Gateway with ECDSA signature", async () => { - let pda_ata = await spl.getAssociatedTokenAddress(mint.publicKey, pdaAccount, true); - const account2 = await spl.getAccount(conn, pda_ata); - // expect(account2.amount).to.be.eq(1_000_000n); - - const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); - const amount = new anchor.BN(500_000); - const nonce = pdaAccountData.nonce; - await withdrawSplToken(mint, usdcDecimals, amount, nonce, pda_ata, wallet_ata, wallet.publicKey, keyPair, gatewayProgram); - const account3 = await spl.getAccount(conn, pda_ata); - expect(account3.amount-account2.amount).to.be.eq(-500_000n); - - - // should trigger nonce mismatch in withdraw - try { - await withdrawSplToken(mint, usdcDecimals, amount, nonce, pda_ata, wallet_ata, wallet.publicKey, keyPair, gatewayProgram); - throw new Error("Expected error not thrown"); // This line will make the test fail if no error is thrown - } catch (err) { - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("NonceMismatch"); - const account4 = await spl.getAccount(conn, pda_ata); - expect(account4.amount).to.be.eq(2_500_000n); - } - - - try { - const nonce2 = nonce.addn(1) - const buffer = Buffer.concat([ - Buffer.from("withdraw_spl_token","utf-8"), - chain_id_bn.toArrayLike(Buffer, 'be', 8), - nonce2.toArrayLike(Buffer, 'be', 8), - amount.toArrayLike(Buffer, 'be', 8), - mint_fake.publicKey.toBuffer(), - wallet_ata.toBuffer(), - ]); - const message_hash = keccak256(buffer); - const signature = keyPair.sign(message_hash, 'hex'); - const { r, s, recoveryParam } = signature; - const signatureBuffer = Buffer.concat([ - r.toArrayLike(Buffer, 'be', 32), - s.toArrayLike(Buffer, 'be', 32), - ]); - await gatewayProgram.methods.withdrawSplToken(usdcDecimals,amount, Array.from(signatureBuffer), Number(recoveryParam), Array.from(message_hash), nonce2 ) - .accounts({ - pdaAta: pda_ata, - mintAccount: mint_fake.publicKey, - recipientAta: wallet_ata, - recipient: wallet.publicKey, - }).rpc(); - throw new Error("Expected error not thrown"); // This line will make the test fail if no error is thrown - } catch (err) { - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("ConstraintAssociated"); - const account4 = await spl.getAccount(conn, pda_ata); - expect(account4.amount).to.be.eq(2_500_000n); - } - - }); - - - it("fails to deposit if receiver is empty address", async() => { - try { - await gatewayProgram.methods.deposit(new anchor.BN(1_000_000_000), Array(20).fill(0)).accounts({}).rpc(); - throw new Error("Expected error not thrown"); - } catch(err) { - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("EmptyReceiver"); - } - }); - - 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({}).rpc(); - let bal1 = await conn.getBalance(pdaAccount); - // amount + deposit fee - expect(bal1).to.be.gte(1_000_000_000 + 2_000_000); - const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); - const nonce = pdaAccountData.nonce; - const amount = new anchor.BN(500000000); - const to = await spl.getAssociatedTokenAddress(mint.publicKey, wallet.publicKey); - const buffer = Buffer.concat([ - Buffer.from("withdraw","utf-8"), - chain_id_bn.toArrayLike(Buffer, 'be', 8), - nonce.toArrayLike(Buffer, 'be', 8), - amount.toArrayLike(Buffer, 'be', 8), - to.toBuffer(), - ]); - const message_hash = keccak256(buffer); - const signature = keyPair.sign(message_hash, 'hex'); - const { r, s, recoveryParam } = signature; - const signatureBuffer = Buffer.concat([ - r.toArrayLike(Buffer, 'be', 32), - s.toArrayLike(Buffer, 'be', 32), - ]); - - await gatewayProgram.methods.withdraw( - amount, Array.from(signatureBuffer), Number(recoveryParam), Array.from(message_hash), nonce) - .accounts({ - to: to, - }).rpc(); - let bal2 = await conn.getBalance(pdaAccount); - expect(bal2).to.be.eq(bal1 - 500_000_000); - let bal3 = await conn.getBalance(to); - expect(bal3).to.be.gte(500_000_000); - }) - - it("withdraw SPL token to a non-existent account should succeed by creating it", async () => { - let rentPayerPdaBal0 = await conn.getBalance(pdaAccount); - let pda_ata = await spl.getAssociatedTokenAddress(mint.publicKey, pdaAccount, true); - const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); - const amount = new anchor.BN(500_000); - const nonce = pdaAccountData.nonce; - const wallet2 = anchor.web3.Keypair.generate(); - const to = await spl.getAssociatedTokenAddress(mint.publicKey, wallet2.publicKey); - - let to_ata_bal = await conn.getBalance(to); - expect(to_ata_bal).to.be.eq(0); // the new ata account (owned by wallet2) should be non-existent; - const txsig = await withdrawSplToken(mint, usdcDecimals, amount, nonce, pda_ata, to, wallet2.publicKey, keyPair, gatewayProgram); - to_ata_bal = await conn.getBalance(to); - expect(to_ata_bal).to.be.gt(2_000_000); // the new ata account (owned by wallet2) should be created - - // pda should have reduced balance - let rentPayerPdaBal1 = await conn.getBalance(pdaAccount); - // expected reimbursement to be gas fee (5000 lamports) + ATA creation cost 2039280 lamports - expect(rentPayerPdaBal0-rentPayerPdaBal1).to.be.eq(to_ata_bal + 5000); // rentPayer pays rent - }); - - - it("fails to deposit and call if receiver is empty address", async() => { - try { - await gatewayProgram.methods.depositAndCall(new anchor.BN(1_000_000_000), Array(20).fill(0), Buffer.from("hello", "utf-8")).accounts({}).rpc(); - throw new Error("Expected error not thrown"); - } catch(err) { - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("EmptyReceiver"); - } - }); - - 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({}).rpc({commitment: 'processed'}); - const tx = await conn.getParsedTransaction(txsig, 'confirmed'); - let bal2 = await conn.getBalance(pdaAccount); - expect(bal2-bal1).to.be.gte(1_000_000_000); - }) - - it("fails to deposit spl if receiver is empty address", async () => { - try { - await depositSplTokens(gatewayProgram, conn, wallet, mint, Buffer.alloc(20)) - throw new Error("Expected error not thrown"); - } catch (err) { - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("EmptyReceiver"); - } - }); - - it("unwhitelist SPL token and deposit should fail", async () => { - await gatewayProgram.methods.unwhitelistSplMint([], 0, [], new anchor.BN(0)).accounts({ - whitelistCandidate: mint.publicKey, - }).rpc(); - - try { - await depositSplTokens(gatewayProgram, conn, wallet, mint, address); - throw new Error("Expected error not thrown"); - } catch (err) { - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("AccountNotInitialized"); - } - }); - - it("re-whitelist SPL token and deposit should succeed", async () => { - await gatewayProgram.methods.whitelistSplMint([], 0, [], new anchor.BN(0)).accounts({ - whitelistCandidate: mint.publicKey, - }).rpc(); - await depositSplTokens(gatewayProgram, conn, wallet, mint, address); - }); - - it("unwhitelist SPL token using TSS signature and deposit should fail", async () => { - const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); - const nonce = pdaAccountData.nonce; - - const buffer = Buffer.concat([ - Buffer.from("unwhitelist_spl_mint","utf-8"), - chain_id_bn.toArrayLike(Buffer, 'be', 8), - mint.publicKey.toBuffer(), - nonce.toArrayLike(Buffer, 'be', 8), - ]); - const message_hash = keccak256(buffer); - const signature = keyPair.sign(message_hash, 'hex'); - const { r, s, recoveryParam } = signature; - const signatureBuffer = Buffer.concat([ - r.toArrayLike(Buffer, 'be', 32), - s.toArrayLike(Buffer, 'be', 32), - ]); - - await gatewayProgram.methods.unwhitelistSplMint( - Array.from(signatureBuffer), - Number(recoveryParam), - Array.from(message_hash), - nonce, - ).accounts({ - whitelistCandidate: mint.publicKey, - }).rpc(); - - try { - await depositSplTokens(gatewayProgram, conn, wallet, mint, address); - throw new Error("Expected error not thrown"); - } catch (err) { - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("AccountNotInitialized"); - } - }); - - it("re-whitelist SPL token using TSS signature and deposit should succeed", async () => { - const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); - const nonce = pdaAccountData.nonce; - - const buffer = Buffer.concat([ - Buffer.from("whitelist_spl_mint","utf-8"), - chain_id_bn.toArrayLike(Buffer, 'be', 8), - mint.publicKey.toBuffer(), - nonce.toArrayLike(Buffer, 'be', 8), - ]); - const message_hash = keccak256(buffer); - const signature = keyPair.sign(message_hash, 'hex'); - const { r, s, recoveryParam } = signature; - const signatureBuffer = Buffer.concat([ - r.toArrayLike(Buffer, 'be', 32), - s.toArrayLike(Buffer, 'be', 32), - ]); - - await gatewayProgram.methods.whitelistSplMint( - Array.from(signatureBuffer), - Number(recoveryParam), - Array.from(message_hash), - nonce, - ).accounts({ - whitelistCandidate: mint.publicKey, - }).rpc(); - await depositSplTokens(gatewayProgram, conn, wallet, mint, address); - }); - - it("update TSS address", async () => { - const newTss = new Uint8Array(20); - randomFillSync(newTss); - await gatewayProgram.methods.updateTss(Array.from(newTss)).accounts({ - // pda: pdaAccount, - }).rpc(); - const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); - expect(pdaAccountData.tssAddress).to.be.deep.eq(Array.from(newTss)); - - // only the authority stored in PDA can update the TSS address; the following should fail - try { - await gatewayProgram.methods.updateTss(Array.from(newTss)).accounts({ - signer: mint.publicKey, - }).signers([mint]).rpc(); - throw new Error("Expected error not thrown"); - } catch (err) { - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("SignerIsNotAuthority"); - } - }); - - it("pause deposit and deposit should fail", async () => { - const newTss = new Uint8Array(20); - randomFillSync(newTss); - await gatewayProgram.methods.setDepositPaused(true).accounts({ - - }).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({}).rpc(); - throw new Error("Expected error not thrown"); - } catch (err) { - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("DepositPaused"); - } - }); - - const newAuthority = anchor.web3.Keypair.generate(); - it("update authority", async () => { - await gatewayProgram.methods.updateAuthority(newAuthority.publicKey).accounts({}).rpc(); - // now the old authority cannot update TSS address and will fail - try { - await gatewayProgram.methods.updateTss(Array.from(new Uint8Array(20))).accounts({}).rpc(); - throw new Error("Expected error not thrown"); - } catch (err) { - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("SignerIsNotAuthority"); - } - }); - -}); - - - - - diff --git a/tsconfig.json b/tsconfig.json index 56fd5c5..99b8fd5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "lib": ["es2015"], "module": "commonjs", "target": "es2020", - "esModuleInterop": true + "esModuleInterop": true, + "noUnusedLocals": true } }