From dc790a7c6aa6c9f8a955627a26c01466852e0cf5 Mon Sep 17 00:00:00 2001 From: brewmaster012 <88689859+brewmaster012@users.noreply.github.com> Date: Thu, 10 Oct 2024 13:29:50 -0500 Subject: [PATCH] squash commits from branch wthdraw-spl-to-new-account --- README.md | 2 +- programs/protocol-contracts-solana/Cargo.toml | 2 +- programs/protocol-contracts-solana/src/lib.rs | 97 +++++++++++- tests/protocol-contracts-solana.ts | 147 +++++++++--------- 4 files changed, 163 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 8fdd5c8..15706a0 100644 --- a/README.md +++ b/README.md @@ -110,4 +110,4 @@ brew install gnu-tar # Put this in ~/.zshrc export PATH="/usr/local/opt/gnu-tar/libexec/gnubin:$PATH" ``` -see https://solana.stackexchange.com/questions/4499/blockstore-error-when-starting-solana-test-validator-on-macos-13-0-1/16319#16319 +see https://solana.stackexchange.com/questions/4499/blockstore-error-when-starting-solana-test-validator-on-macos-13-0-1/16319#16319 \ No newline at end of file diff --git a/programs/protocol-contracts-solana/Cargo.toml b/programs/protocol-contracts-solana/Cargo.toml index f22c7b5..4462e31 100644 --- a/programs/protocol-contracts-solana/Cargo.toml +++ b/programs/protocol-contracts-solana/Cargo.toml @@ -18,7 +18,7 @@ idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { version = "=0.30.0" } -anchor-spl = { version = "=0.30.0" , features = ["idl-build"]} +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/src/lib.rs b/programs/protocol-contracts-solana/src/lib.rs index 6f062e7..bbc4c09 100644 --- a/programs/protocol-contracts-solana/src/lib.rs +++ b/programs/protocol-contracts-solana/src/lib.rs @@ -1,9 +1,12 @@ use anchor_lang::prelude::*; use anchor_lang::system_program; -use anchor_spl::associated_token::get_associated_token_address; +use anchor_spl::associated_token::{get_associated_token_address, AssociatedToken}; use anchor_spl::token::{transfer, transfer_checked, Mint, Token, TokenAccount}; use solana_program::keccak::hash; +use solana_program::program::invoke; use solana_program::secp256k1_recover::secp256k1_recover; +use solana_program::sysvar::{rent::Rent, Sysvar}; +use spl_associated_token_account::instruction::create_associated_token_account; use std::mem::size_of; #[error_code] @@ -97,6 +100,10 @@ pub mod gateway { Ok(()) } + pub fn initialize_rent_payer(_ctx: Context) -> Result<()> { + 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 @@ -267,7 +274,7 @@ pub mod gateway { 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.to.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 @@ -286,18 +293,77 @@ pub mod gateway { let pda_ata = get_associated_token_address(&pda.key(), &ctx.accounts.mint_account.key()); require!( pda_ata == ctx.accounts.pda_ata.to_account_info().key(), - Errors::SPLAtaAndMintAddressMismatch + Errors::SPLAtaAndMintAddressMismatch, ); let token = &ctx.accounts.token_program; let signer_seeds: &[&[&[u8]]] = &[&[b"meta", &[ctx.bumps.pda]]]; + // make sure that ctx.accounts.recipient_ata is ATA (PDA account of token program) + let recipient_ata = get_associated_token_address( + &ctx.accounts.recipient.key(), + &ctx.accounts.mint_account.key(), + ); + require!( + recipient_ata == ctx.accounts.recipient_ata.to_account_info().key(), + Errors::SPLAtaAndMintAddressMismatch, + ); + + // test whether the recipient_ata is created or not; if not, create it + let recipient_ata_account = ctx.accounts.recipient_ata.to_account_info(); + if recipient_ata_account.lamports() == 0 + || *recipient_ata_account.owner == ctx.accounts.system_program.key() + { + // if lamports of recipient_ata_account is 0 or its owner being system program then it's not created + msg!( + "Creating associated token account {:?} for recipient {:?}...", + recipient_ata_account.key(), + ctx.accounts.recipient.key(), + ); + let signer_info = &ctx.accounts.signer.to_account_info(); + let bal0 = signer_info.lamports(); + invoke( + &create_associated_token_account( + ctx.accounts.signer.to_account_info().key, + ctx.accounts.recipient.to_account_info().key, + ctx.accounts.mint_account.to_account_info().key, + ctx.accounts.token_program.key, + ), + &[ + ctx.accounts.mint_account.to_account_info().clone(), + ctx.accounts.recipient_ata.clone(), + ctx.accounts.recipient.to_account_info().clone(), + ctx.accounts.signer.to_account_info().clone(), + ctx.accounts.system_program.to_account_info().clone(), + ctx.accounts.token_program.to_account_info().clone(), + ctx.accounts + .associated_token_program + .to_account_info() + .clone(), + ], + )?; + let bal1 = signer_info.lamports(); + + msg!("Associated token account for recipient created!"); + msg!( + "Refunding the rent paid by the signer {:?}", + ctx.accounts.signer.to_account_info().key + ); + + let rent_payer_info = ctx.accounts.rent_payer_pda.to_account_info(); + rent_payer_info.sub_lamports(bal0 - bal1)?; + signer_info.add_lamports(bal0 - bal1)?; + msg!( + "Signer refunded the ATA account creation rent amount {:?} lamports", + bal0 - bal1 + ); + } let xfer_ctx = CpiContext::new_with_signer( token.to_account_info(), anchor_spl::token::TransferChecked { from: ctx.accounts.pda_ata.to_account_info(), mint: ctx.accounts.mint_account.to_account_info(), - to: ctx.accounts.to.to_account_info(), + to: ctx.accounts.recipient_ata.to_account_info(), authority: pda.to_account_info(), }, signer_seeds, @@ -393,15 +459,22 @@ pub struct WithdrawSPLToken<'info> { #[account(mut, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, - #[account(mut, token::mint = mint_account, token::authority = pda)] + #[account(mut, associated_token::mint = mint_account, associated_token::authority = pda)] pub pda_ata: Account<'info, TokenAccount>, // associated token address of PDA 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. #[account(mut)] - pub to: Account<'info, TokenAccount>, + pub recipient_ata: AccountInfo<'info>, + #[account(mut, seeds = [b"rent-payer"], bump)] + pub rent_payer_pda: Account<'info, RentPayerPda>, pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, } #[derive(Accounts)] @@ -473,6 +546,15 @@ pub struct Unwhitelist<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct InitializeRentPayer<'info> { + #[account(init, payer = authority, space = 8, seeds = [b"rent-payer"], bump)] + pub rent_payer_pda: Account<'info, RentPayerPda>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + #[account] pub struct Pda { nonce: u64, // ensure that each signature can only be used once @@ -485,6 +567,9 @@ pub struct Pda { #[account] pub struct WhitelistEntry {} +#[account] +pub struct RentPayerPda {} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/protocol-contracts-solana.ts b/tests/protocol-contracts-solana.ts index daec59b..d8d849f 100644 --- a/tests/protocol-contracts-solana.ts +++ b/tests/protocol-contracts-solana.ts @@ -10,14 +10,14 @@ import {expect} from 'chai'; import {ecdsaRecover} from 'secp256k1'; import {getOrCreateAssociatedTokenAccount} from "@solana/spl-token"; -console.log = function() {} - 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); @@ -38,7 +38,6 @@ async function mintSPLToken(conn: anchor.web3.Connection, wallet: anchor.web3.Ke ) ); const txsig = await anchor.web3.sendAndConfirmTransaction(conn, tokenTransaction, [wallet, mint]); - console.log("mint account created!", mint.publicKey.toString()); return txsig; } @@ -48,7 +47,6 @@ async function depositSplTokens(gatewayProgram: Program, conn: anchor.w seeds, gatewayProgram.programId, ); - console.log("gateway pda account", pdaAccount.toString()); const pda_ata = await spl.getOrCreateAssociatedTokenAccount( conn, wallet, @@ -56,7 +54,6 @@ async function depositSplTokens(gatewayProgram: Program, conn: anchor.w pdaAccount, true ); - console.log("pda_ata address", pda_ata.address.toString()); let tokenAccount = await spl.getOrCreateAssociatedTokenAccount( conn,wallet, mint.publicKey, wallet.publicKey @@ -68,6 +65,32 @@ async function depositSplTokens(gatewayProgram: Program, conn: anchor.w }).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("some tests", () => { // Configure the client to use the local cluster. @@ -85,26 +108,16 @@ describe("some tests", () => { const message_hash = keccak256(Buffer.from("hello world")); const signature = keyPair.sign(message_hash, 'hex'); const { r, s, recoveryParam } = signature; - console.log("r", recoveryParam); const signatureBuffer = Buffer.concat([ r.toArrayLike(Buffer, 'be', 32), s.toArrayLike(Buffer, 'be', 32), ]); const recoveredPubkey = ecdsaRecover(signatureBuffer, recoveryParam, message_hash, false); - console.log("recovered pubkey ", bufferToHex(Buffer.from(recoveredPubkey))); const publicKeyBuffer = Buffer.from(keyPair.getPublic(false, 'hex').slice(2), 'hex'); // Uncompressed form of public key, remove the '04' prefix - console.log("generated public key", bufferToHex(publicKeyBuffer)); const addressBuffer = keccak256(publicKeyBuffer); // Skip the first byte (format indicator) const address = addressBuffer.slice(-20); - console.log("address", bufferToHex(address)); - // console.log("address", address); - // const tssAddress = [239, 36, 74, 232, 12, 58, 220, 53, 101, 185, 127, 45, 0, 144, 15, 163, 104, 163, 74, 178,]; const tssAddress = Array.from(address); - console.log("tss address", tssAddress); - - const chain_id = 111111; - const chain_id_bn = new anchor.BN(chain_id); let seeds = [Buffer.from("meta", "utf-8")]; [pdaAccount] = anchor.web3.PublicKey.findProgramAddressSync( @@ -112,6 +125,12 @@ describe("some tests", () => { gatewayProgram.programId, ); + let rentPayerSeeds = [Buffer.from("rent-payer", "utf-8")]; + let [rentPayerPdaAccount] = anchor.web3.PublicKey.findProgramAddressSync( + rentPayerSeeds, + gatewayProgram.programId, + ); + it("Initializes the program", async () => { await gatewayProgram.methods.initialize(tssAddress, chain_id_bn).rpc(); @@ -121,10 +140,19 @@ describe("some tests", () => { 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; - // console.log("Error message: ", err.message) } }); - + it("intialize the rent payer PDA",async() => { + await gatewayProgram.methods.initializeRentPayer().rpc(); + let instr = web3.SystemProgram.transfer({ + fromPubkey: wallet.publicKey, + toPubkey: rentPayerPdaAccount, + lamports: 100000000, + }); + let tx = new web3.Transaction(); + tx.add(instr); + await web3.sendAndConfirmTransaction(conn,tx,[wallet]); + }); it("Mint a SPL USDC token", async () => { @@ -149,21 +177,16 @@ describe("some tests", () => { ) ); 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()}`); // create a fake USDC token account await mintSPLToken(conn, wallet, mint_fake); - console.log("fake mint account created!", mint_fake.publicKey.toString()); }) it("whitelist USDC spl token", async () => { @@ -177,7 +200,6 @@ describe("some tests", () => { gatewayProgram.programId, ); let entry = await gatewayProgram.account.whitelistEntry.fetch(entryAddress) - console.log("whitelist entry", entry); try { seeds = [Buffer.from("whitelist", "utf-8"), mint_fake.publicKey.toBuffer()]; @@ -186,7 +208,6 @@ describe("some tests", () => { gatewayProgram.programId, ); entry = await gatewayProgram.account.whitelistEntry.fetch(entryAddress); - console.log("whitelist entry", entry); } catch(err) { expect(err.message).to.include("Account does not exist or has no data"); } @@ -217,7 +238,6 @@ describe("some tests", () => { } catch (err) { expect(err).to.be.instanceof(anchor.AnchorError); expect(err.message).to.include("DepositToAddressMismatch"); - // console.log("Error message: ", err.message); } // test depositSplTokenAndCall @@ -239,7 +259,6 @@ describe("some tests", () => { seeds, gatewayProgram.programId, ); - console.log("gateway pda account", pdaAccount.toString()); let fake_pda_ata = await spl.getOrCreateAssociatedTokenAccount( conn, wallet, @@ -247,7 +266,6 @@ describe("some tests", () => { pdaAccount, true ); - console.log("fake_mint fake_pda_ata address", fake_pda_ata.address.toString()); let tokenAccount = await spl.getOrCreateAssociatedTokenAccount( conn,wallet, mint_fake.publicKey, wallet.publicKey, true @@ -267,59 +285,26 @@ describe("some tests", () => { 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); - 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 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), - mint.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), nonce) - .accounts({ - pdaAta: pda_ata, - mintAccount: mint.publicKey, - to: wallet_ata, - }).rpc(); - + 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 gatewayProgram.methods.withdrawSplToken(usdcDecimals,new anchor.BN(500_000), Array.from(signatureBuffer), Number(recoveryParam), Array.from(message_hash), nonce) - .accounts({ - pdaAta: pda_ata, - mintAccount: mint.publicKey, - to: wallet_ata, - }).rpc()); + 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); - console.log("After 2nd withdraw: Account balance:", account4.amount.toString()); expect(account4.amount).to.be.eq(2_500_000n); } @@ -345,28 +330,43 @@ describe("some tests", () => { .accounts({ pdaAta: pda_ata, mintAccount: mint_fake.publicKey, - to: wallet_ata, + 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); - console.log("Error message: ", err.message); - expect(err.message).to.include("ConstraintTokenMint"); + expect(err.message).to.include("ConstraintAssociated"); const account4 = await spl.getAccount(conn, pda_ata); - console.log("After 2nd withdraw: Account balance:", account4.amount.toString()); expect(account4.amount).to.be.eq(2_500_000n); } }); + it("withdraw SPL token to a non-existent account", async () => { + let pda_ata = await spl.getAssociatedTokenAddress(mint.publicKey, pdaAccount, true); + const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); + const hexAddr = bufferToHex(Buffer.from(pdaAccountData.tssAddress)); + const amount = new anchor.BN(500_000); + const nonce = pdaAccountData.nonce; + const wallet2 = anchor.web3.Keypair.generate(); + { // fund the wallet2, otherwise the wallet2 is considered non-existent + let sig = await conn.requestAirdrop(wallet2.publicKey, 100000000); + await conn.confirmTransaction(sig); + } + + + const to = await spl.getAssociatedTokenAddress(mint.publicKey, wallet2.publicKey); + console.log("wallet2 ata: ", to.toBase58()); + const txsig = await withdrawSplToken(mint, usdcDecimals, amount, nonce, pda_ata, to, wallet2.publicKey, keyPair, gatewayProgram); + const tx = await conn.getParsedTransaction(txsig, 'confirmed'); + }); + 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); - 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" // ); @@ -397,7 +397,6 @@ describe("some tests", () => { to: to, }).rpc(); let bal2 = await conn.getBalance(pdaAccount); - console.log("pda account balance", bal2); expect(bal2).to.be.eq(bal1 - 500_000_000); let bal3 = await conn.getBalance(to); expect(bal3).to.be.gte(500_000_000); @@ -407,7 +406,6 @@ describe("some tests", () => { 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'); - console.log("deposit and call parsed tx", tx); let bal2 = await conn.getBalance(pdaAccount); expect(bal2-bal1).to.be.gte(1_000_000_000); }) @@ -436,12 +434,10 @@ describe("some tests", () => { it("update TSS address", async () => { const newTss = new Uint8Array(20); randomFillSync(newTss); - // console.log("generated new TSS address", newTss); await gatewayProgram.methods.updateTss(Array.from(newTss)).accounts({ // pda: pdaAccount, }).rpc(); const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); - // console.log("updated TSS address", pdaAccountData.tssAddress); 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 @@ -458,7 +454,6 @@ describe("some tests", () => { it("pause deposit and deposit should fail", async () => { const newTss = new Uint8Array(20); randomFillSync(newTss); - // console.log("generated new TSS address", newTss); await gatewayProgram.methods.setDepositPaused(true).accounts({ }).rpc(); @@ -467,7 +462,6 @@ describe("some tests", () => { try { 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); expect(err.message).to.include("DepositPaused"); } @@ -490,7 +484,6 @@ describe("some tests", () => { }).rpc(); } catch (err) { - console.log("Error message: ", err.message); expect(err).to.be.instanceof(anchor.AnchorError); expect(err.message).to.include("SignerIsNotAuthority"); }