diff --git a/Cargo.lock b/Cargo.lock index 5376aec..fa5c5d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,6 +1478,7 @@ dependencies = [ "anchor-lang", "anchor-spl", "solana-program", + "spl-associated-token-account", ] [[package]] diff --git a/README.md b/README.md index cc0c714..26e8457 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ and `cargo` package manger must be installed. The program is built with the `anchor` framework so it needs to be installed as well; see [installation](https://www.anchor-lang.com/docs/installation) -To build +To build (this will require at least 2GB disk space) ```bash $ anchor build ``` @@ -67,9 +67,11 @@ $ anchor test 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 PDA derived ATA (Associated Token Account) is -used to hold the SPL token balance. +balance of the Gateway program. +For SPL tokens, the program stores the SPL token +in PDA derived ATAs. For each SPL token (different mint +account), the program creates ATA from PDA and the Mint +(standard way of deriving ATA in Solana SPL program). The PDA account itself is a data account that holds Gateway program state, namely the following data types diff --git a/programs/protocol-contracts-solana/Cargo.toml b/programs/protocol-contracts-solana/Cargo.toml index ebcb404..37298f9 100644 --- a/programs/protocol-contracts-solana/Cargo.toml +++ b/programs/protocol-contracts-solana/Cargo.toml @@ -20,4 +20,4 @@ idl-build = ["anchor-lang/idl-build"] anchor-lang = "0.30.0" anchor-spl = { version = "0.30.0" , features = ["idl-build"]} solana-program = "1.18.15" - +spl-associated-token-account = "3.0.2" \ No newline at end of file diff --git a/programs/protocol-contracts-solana/src/lib.rs b/programs/protocol-contracts-solana/src/lib.rs index 1dec055..35c1c8e 100644 --- a/programs/protocol-contracts-solana/src/lib.rs +++ b/programs/protocol-contracts-solana/src/lib.rs @@ -3,6 +3,7 @@ use anchor_lang::system_program; use anchor_spl::token::{transfer, Token, TokenAccount}; use solana_program::keccak::hash; use solana_program::secp256k1_recover::secp256k1_recover; +use spl_associated_token_account; use std::mem::size_of; #[error_code] @@ -15,6 +16,8 @@ pub enum Errors { NonceMismatch, #[msg("TSSAuthenticationFailed")] TSSAuthenticationFailed, + #[msg("DepositToAddressMismatch")] + DepositToAddressMismatch, } declare_id!("9WSwbVLthCsJXABeDJcVcw4UQMYuoNLTJTLqueFXU5Q2"); @@ -35,7 +38,7 @@ pub mod gateway { Ok(()) } - pub fn donate(ctx: Context, amount: u64) -> Result<()> { + pub fn deposit(ctx: Context, amount: u64) -> Result<()> { let cpi_context = CpiContext::new( ctx.accounts.system_program.to_account_info(), system_program::Transfer { @@ -43,12 +46,36 @@ pub mod gateway { to: ctx.accounts.pda.to_account_info().clone(), }, ); - system_program::transfer(cpi_context, amount)?; Ok(()) } + pub fn deposit_spl_token(ctx: Context, amount: u64) -> Result<()> { + let token = &ctx.accounts.token_program; + let from = &ctx.accounts.from; + + 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(()) + } + pub fn withdraw( ctx: Context, amount: u64, @@ -119,7 +146,6 @@ pub mod gateway { Ok(()) } - } fn recover_eth_address( @@ -145,14 +171,14 @@ pub struct Initialize<'info> { #[account(mut)] pub signer: Signer<'info>, - #[account(init, payer = signer, space=size_of::() + 8, seeds=[b"meta"], bump)] + #[account(init, payer = signer, space = size_of::< Pda > () + 8, seeds = [b"meta"], bump)] pub pda: Account<'info, Pda>, pub system_program: Program<'info, System>, } #[derive(Accounts)] -pub struct Donate<'info> { +pub struct Deposit<'info> { #[account(mut)] pub signer: Signer<'info>, @@ -161,6 +187,21 @@ pub struct Donate<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct DepositSplToken<'info> { + #[account(mut)] + pub signer: Signer<'info>, + + #[account(mut, seeds = [b"meta"], bump)] + pub pda: Account<'info, Pda>, + pub token_program: Program<'info, Token>, + + #[account(mut)] + pub from: Account<'info, TokenAccount>, // this must be owned by signer; normally the ATA of signer + #[account(mut)] + pub to: Account<'info, TokenAccount>, // this must be ATA of PDA +} + #[derive(Accounts)] pub struct Withdraw<'info> { #[account(mut)] diff --git a/tests/protocol-contracts-solana.ts b/tests/protocol-contracts-solana.ts index 8fdf677..4be0e20 100644 --- a/tests/protocol-contracts-solana.ts +++ b/tests/protocol-contracts-solana.ts @@ -17,7 +17,7 @@ describe("some tests", () => { const gatewayProgram = anchor.workspace.Gateway as Program; const wallet = anchor.workspace.Gateway.provider.wallet.payer; const mint = anchor.web3.Keypair.generate(); - let tokenAccount; + let tokenAccount: spl.Account; let wallet_ata: anchor.web3.PublicKey; let pdaAccount: anchor.web3.PublicKey; let pda_ata: spl.Account; @@ -88,7 +88,7 @@ describe("some tests", () => { console.log(`wallet_ata: ${wallet_ata.toString()}`); }) - it("Withdraw 500_000 USDC from Gateway with ECDSA signature", async () => { + it("Deposit 1_000_000 USDC to Gateway", async () => { let seeds = [Buffer.from("meta", "utf-8")]; [pdaAccount] = anchor.web3.PublicKey.findProgramAddressSync( seeds, @@ -103,14 +103,38 @@ describe("some tests", () => { true ); console.log("pda_ata address", pda_ata.address.toString()); - const tx_xfer = await spl.transfer( - conn, - wallet, - tokenAccount.address, - pda_ata.address, - wallet, - 1_000_000 - ); + await gatewayProgram.methods.depositSplToken(new anchor.BN(1_000_000)).accounts( + { + from: tokenAccount.address, + to: pda_ata.address, + } + ) + .rpc(); + try { + await gatewayProgram.methods.depositSplToken(new anchor.BN(1_000_000)).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 tx_xfer = await spl.transfer( + // conn, + // wallet, + // tokenAccount.address, + // pda_ata.address, + // wallet, + // 1_000_000 + // ); // console.log("xfer tx hash", tx_xfer); const account2 = await spl.getAccount(conn, pda_ata.address); expect(account2.amount).to.be.eq(1_000_000n); @@ -153,16 +177,17 @@ describe("some tests", () => { }); - it("withdraw 0.5 SOL from Gateway with ECDSA signature", async () => { - 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]); + it("deposit and withdraw 0.5 SOL from Gateway with ECDSA signature", async () => { + await gatewayProgram.methods.deposit(new anchor.BN(1_000_000_000)).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);