From 82fa1868d66494ffbf7fedbb8b539b7577562d1c Mon Sep 17 00:00:00 2001 From: brewmaster012 <88689859+brewmaster012@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:05:16 -0500 Subject: [PATCH] fix: add domain separator in the TSS message signature hash (#25) * add domain separator in the TSS message signature hash * remove unnecessary package --- programs/protocol-contracts-solana/src/lib.rs | 96 +++++++++- tests/protocol-contracts-solana.ts | 177 ++++++++++++++++++ 2 files changed, 272 insertions(+), 1 deletion(-) diff --git a/programs/protocol-contracts-solana/src/lib.rs b/programs/protocol-contracts-solana/src/lib.rs index 76572ef..92aa7e1 100644 --- a/programs/protocol-contracts-solana/src/lib.rs +++ b/programs/protocol-contracts-solana/src/lib.rs @@ -1,6 +1,6 @@ use anchor_lang::prelude::*; use anchor_lang::system_program; -use anchor_spl::token::{Token, TokenAccount}; +use anchor_spl::token::{transfer, Token, TokenAccount}; use solana_program::keccak::hash; use solana_program::secp256k1_recover::secp256k1_recover; use std::mem::size_of; @@ -136,6 +136,44 @@ pub mod gateway { Ok(()) } + pub fn deposit_spl_token( + ctx: Context, + amount: u64, + memo: Vec, + ) -> Result<()> { + require!(memo.len() >= 20, Errors::MemoLengthTooShort); + require!(memo.len() <= 512, Errors::MemoLengthExceeded); + let token = &ctx.accounts.token_program; + let from = &ctx.accounts.from; + + let pda = &mut ctx.accounts.pda; + require!(!pda.deposit_paused, Errors::DepositPaused); + + let pda_ata = spl_associated_token_account::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 + ); + + let xfer_ctx = CpiContext::new( + token.to_account_info(), + anchor_spl::token::Transfer { + from: ctx.accounts.from.to_account_info(), + to: ctx.accounts.to.to_account_info(), + authority: ctx.accounts.signer.to_account_info(), + }, + ); + transfer(xfer_ctx, amount)?; + + msg!("deposit spl token successfully"); + + Ok(()) + } + // only tss address stored in PDA can call this instruction pub fn withdraw( ctx: Context, @@ -152,6 +190,7 @@ pub mod gateway { return err!(Errors::NonceMismatch); } let mut concatenated_buffer = Vec::new(); + concatenated_buffer.extend_from_slice("withdraw".as_bytes()); 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()); @@ -176,6 +215,61 @@ pub mod gateway { Ok(()) } + + // only tss address stored in PDA can call this instruction + pub fn withdraw_spl_token( + ctx: Context, + 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"); + 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(&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.from.key().to_bytes()); + concatenated_buffer.extend_from_slice(&ctx.accounts.to.key().to_bytes()); + require!( + message_hash == hash(&concatenated_buffer[..]).to_bytes(), + Errors::MessageHashMismatch + ); + + 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); + if address != pda.tss_address { + msg!("ECDSA signature error"); + return err!(Errors::TSSAuthenticationFailed); + } + + let token = &ctx.accounts.token_program; + let signer_seeds: &[&[&[u8]]] = &[&[b"meta", &[ctx.bumps.pda]]]; + + let xfer_ctx = CpiContext::new_with_signer( + token.to_account_info(), + anchor_spl::token::Transfer { + from: ctx.accounts.from.to_account_info(), + to: ctx.accounts.to.to_account_info(), + authority: pda.to_account_info(), + }, + signer_seeds, + ); + transfer(xfer_ctx, amount)?; + msg!("withdraw spl token successfully"); + + pda.nonce += 1; + + Ok(()) + } } fn recover_eth_address( diff --git a/tests/protocol-contracts-solana.ts b/tests/protocol-contracts-solana.ts index 51e05e3..8fa8cd4 100644 --- a/tests/protocol-contracts-solana.ts +++ b/tests/protocol-contracts-solana.ts @@ -2,6 +2,7 @@ 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 * as memo from "@solana/spl-memo"; import {randomFillSync} from 'crypto'; import { ec as EC } from 'elliptic'; import { keccak256 } from 'ethereumjs-util'; @@ -70,19 +71,195 @@ describe("some tests", () => { } }); + it("Mint a SPL USDC token", async () => { + // now deploying a fake USDC SPL Token + // 1. create a mint account + const mintRent = await spl.getMinimumBalanceForRentExemptMint(conn); + const 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, + 6, + wallet.publicKey, + null, + ) + ); + await anchor.web3.sendAndConfirmTransaction(conn, tokenTransaction, [wallet, mint]); + console.log("mint account created!", mint.publicKey.toString()); + + // 2. create token account to receive mint + 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]); + console.log("Minted 10 USDC to:", tokenAccount.address.toString()); + const account = await spl.getAccount(conn, tokenAccount.address); + console.log("Account balance:", account.amount.toString()); + console.log("Account owner: ", account.owner.toString()); + + // OK; transfer some USDC SPL token to the gateway PDA + wallet_ata = await spl.getAssociatedTokenAddress( + mint.publicKey, + wallet.publicKey, + ); + console.log(`wallet_ata: ${wallet_ata.toString()}`); + }) + + it("Deposit 1_000_000 USDC to Gateway", async () => { + let seeds = [Buffer.from("meta", "utf-8")]; + [pdaAccount] = anchor.web3.PublicKey.findProgramAddressSync( + seeds, + gatewayProgram.programId, + ); + console.log("gateway pda account", pdaAccount.toString()); + pda_ata = await spl.getOrCreateAssociatedTokenAccount( + conn, + wallet, + mint.publicKey, + pdaAccount, + true + ); + console.log("pda_ata address", pda_ata.address.toString()); + const tx = new web3.Transaction(); + const memoInst = memo.createMemoInstruction( + "this is a memo", + [wallet.publicKey], + ); + tx.add(memoInst); + const depositInst = await gatewayProgram.methods.depositSplToken( + new anchor.BN(1_000_000), address).accounts( + { + from: tokenAccount.address, + to: pda_ata.address, + } + ).instruction(); + tx.add(depositInst); + const txsig = await anchor.web3.sendAndConfirmTransaction(conn, tx, [wallet]); + + + try { + await gatewayProgram.methods.depositSplToken(new anchor.BN(1_000_000), address).accounts( + { + from: tokenAccount.address, + to: wallet_ata, + } + ).rpc(); + throw new Error("Expected error not thrown"); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("DepositToAddressMismatch"); + // console.log("Error message: ", err.message); + } + }); + + it("Withdraw 500_000 USDC from Gateway with ECDSA signature", async () => { + const account2 = await spl.getAccount(conn, pda_ata.address); + expect(account2.amount).to.be.eq(1_000_000n); + // console.log("B4 withdraw: Account balance:", account2.amount.toString()); + + + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + console.log(`pda account data: nonce ${pdaAccountData.nonce}`); + const hexAddr = bufferToHex(Buffer.from(pdaAccountData.tssAddress)); + console.log(`pda account data: tss address ${hexAddr}`); + // const message_hash = fromHexString( + // "0a1e2723bd7f1996832b7ed7406df8ad975deba1aa04020b5bfc3e6fe70ecc29" + // ); + // const signature = fromHexString( + // "58be181f57b2d56b0c252127c9874a8fbe5ebd04f7632fb3966935a3e9a765807813692cebcbf3416cb1053ad9c8c83af471ea828242cca22076dd04ddbcd253" + // ); + const amount = new anchor.BN(500_000); + const nonce = pdaAccountData.nonce; + 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), + pda_ata.address.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(amount, Array.from(signatureBuffer), Number(recoveryParam), Array.from(message_hash), nonce) + .accounts({ + from: pda_ata.address, + to: wallet_ata, + }).rpc(); + + const account3 = await spl.getAccount(conn, pda_ata.address); + expect(account3.amount).to.be.eq(500_000n); + + + try { + (await gatewayProgram.methods.withdrawSplToken(new anchor.BN(500_000), Array.from(signatureBuffer), Number(recoveryParam), Array.from(message_hash), nonce) + .accounts({ + from: pda_ata.address, + to: wallet_ata, + }).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("NonceMismatch"); + 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(500_000n); + } + + }); 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(); + // const transaction = new anchor.web3.Transaction(); + // transaction.add( + // web3.SystemProgram.transfer({ + // fromPubkey: wallet.publicKey, + // toPubkey: pdaAccount, + // lamports: 1_000_000_000, + // }) + // ); + // await anchor.web3.sendAndConfirmTransaction(conn, transaction, [wallet]); let bal1 = await conn.getBalance(pdaAccount); console.log("pda account balance", bal1); expect(bal1).to.be.gte(1_000_000_000); const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); console.log(`pda account data: nonce ${pdaAccountData.nonce}`); + // const message_hash = fromHexString( + // "0a1e2723bd7f1996832b7ed7406df8ad975deba1aa04020b5bfc3e6fe70ecc29" + // ); + // const signature = fromHexString( + // "58be181f57b2d56b0c252127c9874a8fbe5ebd04f7632fb3966935a3e9a765807813692cebcbf3416cb1053ad9c8c83af471ea828242cca22076dd04ddbcd253" + // ); const nonce = pdaAccountData.nonce; const amount = new anchor.BN(500000000); const to = 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),