diff --git a/README.md b/README.md index 69a1603..8fdd5c8 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,8 @@ The PDA account address (derived from seeds `b"meta"` and canonical bump) is 2f9SLuUNb7TNeM6gzBwT4ZjbL5ZyKzzHg1Ce9yiquEjj ``` - # Introduction - This repository hosts the smart contract (program) deployed on the Solana network to enable ZetaChain's cross-chain functionality. It consists of a single program that supports the following actions: 1. Users on the Solana network can send SOL to the program to deposit into ZetaChain, with the option to invoke a ZetaChain EVM contract. @@ -112,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 \ No newline at end of file +see https://solana.stackexchange.com/questions/4499/blockstore-error-when-starting-solana-test-validator-on-macos-13-0-1/16319#16319 diff --git a/programs/protocol-contracts-solana/src/lib.rs b/programs/protocol-contracts-solana/src/lib.rs index 0c0e8e8..6f062e7 100644 --- a/programs/protocol-contracts-solana/src/lib.rs +++ b/programs/protocol-contracts-solana/src/lib.rs @@ -88,6 +88,15 @@ pub mod gateway { Ok(()) } + // whitelisting SPL tokens + pub fn whitelist_spl_mint(_ctx: Context) -> Result<()> { + Ok(()) + } + + pub fn unwhitelist_spl_mint(_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 @@ -351,6 +360,11 @@ pub struct DepositSplToken<'info> { #[account(seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, + #[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 mint_account: Account<'info, Mint>, + pub token_program: Program<'info, Token>, #[account(mut)] @@ -414,6 +428,51 @@ pub struct UpdatePaused<'info> { pub signer: Signer<'info>, } +#[derive(Accounts)] +pub struct Whitelist<'info> { + #[account( + init, + space=8, + payer=authority, + seeds=[ + b"whitelist", + whitelist_candidate.key().as_ref() + ], + bump + )] + pub whitelist_entry: Account<'info, WhitelistEntry>, + pub whitelist_candidate: Account<'info, Mint>, + + #[account(mut, seeds = [b"meta"], bump, has_one = authority)] + pub pda: Account<'info, Pda>, + #[account(mut)] + pub authority: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct Unwhitelist<'info> { + #[account( + mut, + seeds=[ + b"whitelist", + whitelist_candidate.key().as_ref() + ], + bump, + close = authority, + )] + pub whitelist_entry: Account<'info, WhitelistEntry>, + pub whitelist_candidate: Account<'info, Mint>, + + #[account(mut, seeds = [b"meta"], bump, has_one = authority)] + pub pda: Account<'info, Pda>, + #[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 @@ -423,6 +482,9 @@ pub struct Pda { deposit_paused: bool, } +#[account] +pub struct WhitelistEntry {} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/protocol-contracts-solana.ts b/tests/protocol-contracts-solana.ts index a50b3f7..daec59b 100644 --- a/tests/protocol-contracts-solana.ts +++ b/tests/protocol-contracts-solana.ts @@ -2,15 +2,15 @@ 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'; import { bufferToHex } from 'ethereumjs-util'; 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(); @@ -19,6 +19,56 @@ const keyPair = ec.keyFromPrivate('5b81cdf52ba0766983acf8dd0072904733d92afe4dd34 const usdcDecimals = 6; +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]); + console.log("mint account created!", mint.publicKey.toString()); + 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, + ); + console.log("gateway pda account", pdaAccount.toString()); + const pda_ata = await spl.getOrCreateAssociatedTokenAccount( + conn, + wallet, + mint.publicKey, + pdaAccount, + true + ); + console.log("pda_ata address", pda_ata.address.toString()); + + 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; +} + describe("some tests", () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); @@ -28,10 +78,10 @@ describe("some tests", () => { const mint = anchor.web3.Keypair.generate(); const mint_fake = anchor.web3.Keypair.generate(); // for testing purpose - let tokenAccount: spl.Account; + let wallet_ata: anchor.web3.PublicKey; let pdaAccount: anchor.web3.PublicKey; - let pda_ata: spl.Account; + const message_hash = keccak256(Buffer.from("hello world")); const signature = keyPair.sign(message_hash, 'hex'); const { r, s, recoveryParam } = signature; @@ -75,31 +125,15 @@ 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); - 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, - ) - ); - await anchor.web3.sendAndConfirmTransaction(conn, tokenTransaction, [wallet, mint]); - console.log("mint account created!", mint.publicKey.toString()); + await mintSPLToken(conn, wallet, mint); // 2. create token account to receive mint - tokenAccount = await spl.getOrCreateAssociatedTokenAccount( + let tokenAccount = await spl.getOrCreateAssociatedTokenAccount( conn, wallet, mint.publicKey, @@ -128,58 +162,55 @@ describe("some tests", () => { console.log(`wallet_ata: ${wallet_ata.toString()}`); // create a fake USDC token account - tokenTransaction = new anchor.web3.Transaction(); - tokenTransaction.add( - anchor.web3.SystemProgram.createAccount({ - fromPubkey: wallet.publicKey, - newAccountPubkey: mint_fake.publicKey, - lamports: mintRent, - space: spl.MINT_SIZE, - programId: spl.TOKEN_PROGRAM_ID - }), - spl.createInitializeMintInstruction( - mint_fake.publicKey, - usdcDecimals, - wallet.publicKey, - null, - ) - ); - await anchor.web3.sendAndConfirmTransaction(conn, tokenTransaction, [wallet, mint_fake]); + await mintSPLToken(conn, wallet, mint_fake); console.log("fake mint account created!", mint_fake.publicKey.toString()); }) - it("Deposit 1_000_000 USDC to Gateway", async () => { - let seeds = [Buffer.from("meta", "utf-8")]; - [pdaAccount] = anchor.web3.PublicKey.findProgramAddressSync( + it("whitelist USDC spl token", async () => { + await gatewayProgram.methods.whitelistSplMint().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, ); - 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()); + let entry = await gatewayProgram.account.whitelistEntry.fetch(entryAddress) + console.log("whitelist entry", entry); + + try { + seeds = [Buffer.from("whitelist", "utf-8"), mint_fake.publicKey.toBuffer()]; + [entryAddress] = anchor.web3.PublicKey.findProgramAddressSync( + seeds, + 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"); + } + }); + 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 gatewayProgram.methods.depositSplToken(new anchor.BN(1_000_000), Array.from(address)).accounts({ - from: tokenAccount.address, - to: pda_ata.address, - }).rpc({commitment: 'processed'}); + 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"); @@ -195,22 +226,49 @@ describe("some tests", () => { 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, - }).rpc({commitment: 'confirmed'}); + 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, + ); + console.log("gateway pda account", pdaAccount.toString()); + let fake_pda_ata = await spl.getOrCreateAssociatedTokenAccount( + conn, + wallet, + mint_fake.publicKey, + 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 + ) + 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'}); + } catch (err) { + expect(err).to.be.instanceof(anchor.AnchorError); + expect(err.message).to.include("AccountNotInitialized"); + } - // try { - // await gatewayProgram.methods.depositSplTokenAndCall(new anchor.BN(1_000_000), Array.from(address), Buffer.from("hello", "utf-8")).accounts({ - // from: tokenAccount.address, - // to: pda_ata.address, - // }).rpc(); - // - // } }); it("Withdraw 500_000 USDC from Gateway with ECDSA signature", async () => { - const account2 = await spl.getAccount(conn, pda_ata.address); + 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()); @@ -219,12 +277,7 @@ describe("some tests", () => { 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([ @@ -245,19 +298,19 @@ describe("some tests", () => { await gatewayProgram.methods.withdrawSplToken(usdcDecimals,amount, Array.from(signatureBuffer), Number(recoveryParam), Array.from(message_hash), nonce) .accounts({ - pdaAta: pda_ata.address, + pdaAta: pda_ata, mintAccount: mint.publicKey, to: wallet_ata, }).rpc(); - const account3 = await spl.getAccount(conn, pda_ata.address); + const account3 = await spl.getAccount(conn, pda_ata); expect(account3.amount-account2.amount).to.be.eq(-500_000n); 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.address, + pdaAta: pda_ata, mintAccount: mint.publicKey, to: wallet_ata, }).rpc()); @@ -265,7 +318,7 @@ describe("some tests", () => { } catch (err) { expect(err).to.be.instanceof(anchor.AnchorError); expect(err.message).to.include("NonceMismatch"); - const account4 = await spl.getAccount(conn, pda_ata.address); + 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); } @@ -290,7 +343,7 @@ describe("some tests", () => { ]); await gatewayProgram.methods.withdrawSplToken(usdcDecimals,amount, Array.from(signatureBuffer), Number(recoveryParam), Array.from(message_hash), nonce2 ) .accounts({ - pdaAta: pda_ata.address, + pdaAta: pda_ata, mintAccount: mint_fake.publicKey, to: wallet_ata, }).rpc(); @@ -299,7 +352,7 @@ describe("some tests", () => { expect(err).to.be.instanceof(anchor.AnchorError); console.log("Error message: ", err.message); expect(err.message).to.include("ConstraintTokenMint"); - const account4 = await spl.getAccount(conn, pda_ata.address); + 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); } @@ -322,7 +375,7 @@ describe("some tests", () => { // ); const nonce = pdaAccountData.nonce; const amount = new anchor.BN(500000000); - const to = pda_ata.address; + 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), @@ -352,20 +405,40 @@ describe("some tests", () => { it("deposit and call", async () => { let bal1 = await conn.getBalance(pdaAccount); - const txsig = await gatewayProgram.methods.depositAndCall(new anchor.BN(1_000_000_000), Array.from(address), Buffer.from("hello", "utf-8")).accounts({}).rpc({commitment: 'confirmed'}); + const txsig = await gatewayProgram.methods.depositAndCall(new anchor.BN(1_000_000_000), Array.from(address), Buffer.from("hello", "utf-8")).accounts({}).rpc({commitment: '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); }) + it("unwhitelist SPL token and deposit should fail", async () => { + await gatewayProgram.methods.unwhitelistSplMint().accounts({ + whitelistCandidate: mint.publicKey, + }).rpc(); + + try { + await depositSplTokens(gatewayProgram, conn, wallet, mint, address) + } 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().accounts({ + whitelistCandidate: mint.publicKey, + }).rpc(); + await depositSplTokens(gatewayProgram, conn, wallet, mint, address); + }); + 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, + // pda: pdaAccount, }).rpc(); const pdaAccountData = await gatewayProgram.account.pda.fetch(pdaAccount); // console.log("updated TSS address", pdaAccountData.tssAddress); @@ -400,8 +473,11 @@ describe("some tests", () => { } }); + + + + const newAuthority = anchor.web3.Keypair.generate(); it("update authority", async () => { - const newAuthority = anchor.web3.Keypair.generate(); await gatewayProgram.methods.updateAuthority(newAuthority.publicKey).accounts({ }).rpc(); @@ -420,39 +496,9 @@ describe("some tests", () => { } }); - it("create an account owned by the gateway program", async () => { - const gateway_id =gatewayProgram.programId; - console.log("gateway program id", gateway_id.toString()); - const fake_pda = anchor.web3.Keypair.generate(); - const rentExemption = await conn.getMinimumBalanceForRentExemption(100); - const instr1 = anchor.web3.SystemProgram.createAccount( - { - fromPubkey: wallet.publicKey, - newAccountPubkey: fake_pda.publicKey, - lamports: rentExemption, - space: 100, - programId: gatewayProgram.programId, - } - ) - const tx = new anchor.web3.Transaction(); - tx.add(instr1, ); - await anchor.web3.sendAndConfirmTransaction(conn, tx, [wallet, fake_pda]); +}); - const newTss = new Uint8Array(20); - randomFillSync(newTss); - // console.log("generated new TSS address", newTss); - try { - await gatewayProgram.methods.updateTss(Array.from(newTss)).accounts({ - pda: fake_pda.publicKey, - }).rpc(); - } catch (err) { - console.log("Error message: ", err.message); - expect(err).to.be.instanceof(anchor.AnchorError); - expect(err.message).to.include("AccountDiscriminatorMismatch."); - } - }); -});