Skip to content

Commit

Permalink
fix: add domain separator in the TSS message signature hash (#25)
Browse files Browse the repository at this point in the history
* add domain separator in the TSS message signature hash

* remove unnecessary package
  • Loading branch information
brewmaster012 authored Oct 1, 2024
1 parent 13e04d9 commit 82fa186
Show file tree
Hide file tree
Showing 2 changed files with 272 additions and 1 deletion.
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

0 comments on commit 82fa186

Please sign in to comment.