Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add domain separator in the TSS message signature hash #25

Merged
merged 3 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 95 additions & 1 deletion programs/protocol-contracts-solana/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -136,6 +136,44 @@ pub mod gateway {
Ok(())
}

pub fn deposit_spl_token(
ctx: Context<DepositSplToken>,
amount: u64,
memo: Vec<u8>,
) -> 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<Withdraw>,
Expand All @@ -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());
Expand All @@ -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<WithdrawSPLToken>,
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(
Expand Down
177 changes: 177 additions & 0 deletions tests/protocol-contracts-solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
Expand Down
Loading