Certik: Fix Solana Contributor's ATA Validation (#61)

* Add Invalid Token Handling

* Clean up

Co-authored-by: skojenov <sekoje@users.noreply.github.com>
Co-authored-by: Karl Kempe <karlkempe@users.noreply.github.com>
This commit is contained in:
Serguei 2022-08-03 18:31:34 +00:00 committed by GitHub
parent 1b7bddb084
commit fef0dec3c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1435 additions and 464 deletions

View File

@ -3,7 +3,7 @@
"unit-test": "bash -ac '. test.env && cargo test'",
"integration-test": "bash -ac '. test.env && anchor test'",
"deploy-devnet": "bash migrations/deploy-devnet.sh",
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w --print-width 120",
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
},
"dependencies": {

View File

@ -46,7 +46,6 @@ pub struct CreateCustodian<'info> {
/// * `custodian`
/// * `core_bridge_vaa`
/// * `sale_token_mint`
/// * `custodian_sale_token_acct`
///
/// Mutable
/// * `sale`
@ -76,21 +75,25 @@ pub struct InitSale<'info> {
#[account(
constraint = core_bridge_vaa.owner.key() == Custodian::wormhole()?
)]
/// CHECK: This account is owned by Core Bridge so we trust it
/// CHECK: Posted VAA Message Data
pub core_bridge_vaa: AccountInfo<'info>,
pub sale_token_mint: Account<'info, Mint>,
/// CHECK: This can be a non-existent token. We need to check this in
/// the init_sale instruction to allow a Sale account to be created.
/// When a non-existent mint is provided, we need to allow the program
/// to send an Attest Contributions VAA (payload 2). See init_sale for
/// more details.
pub sale_token_mint: AccountInfo<'info>,
#[account(
associated_token::mint = sale_token_mint,
associated_token::authority = custodian,
constraint = token_bridge.key() == Custodian::token_bridge()?
)]
/// This must be an associated token account
pub custodian_sale_token_acct: Account<'info, TokenAccount>,
/// CHECK: Token Bridge Program
pub token_bridge: AccountInfo<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
pub associated_token_program: Program<'info, AssociatedToken>,
}
/// Context provides all accounts required for user to send contribution
@ -196,9 +199,7 @@ pub struct AttestContributions<'info> {
#[account(
mut,
seeds = [
b"Bridge".as_ref()
],
seeds = [b"Bridge"],
bump,
seeds::program = Custodian::wormhole()?
)]
@ -207,9 +208,7 @@ pub struct AttestContributions<'info> {
#[account(
mut,
seeds = [
b"fee_collector".as_ref()
],
seeds = [b"fee_collector"],
bump,
seeds::program = Custodian::wormhole()?
)]
@ -218,9 +217,7 @@ pub struct AttestContributions<'info> {
#[account(
mut,
seeds = [
b"emitter".as_ref(),
],
seeds = [b"emitter"],
bump
)]
/// CHECK: Wormhole Emitter is this program
@ -229,7 +226,7 @@ pub struct AttestContributions<'info> {
#[account(
mut,
seeds = [
b"Sequence".as_ref(),
b"Sequence",
wormhole_emitter.key().as_ref()
],
bump,
@ -241,7 +238,7 @@ pub struct AttestContributions<'info> {
#[account(
mut,
seeds = [
b"attest-contributions".as_ref(),
b"attest-contributions",
&sale.id,
],
bump,
@ -367,9 +364,7 @@ pub struct BridgeSealedContribution<'info> {
#[account(
mut,
seeds = [
b"config".as_ref()
],
seeds = [b"config"],
bump,
seeds::program = Custodian::token_bridge()?
)]
@ -384,9 +379,7 @@ pub struct BridgeSealedContribution<'info> {
#[account(
mut,
seeds = [
b"Bridge".as_ref()
],
seeds = [b"Bridge"],
bump,
seeds::program = Custodian::wormhole()?
)]
@ -395,9 +388,7 @@ pub struct BridgeSealedContribution<'info> {
#[account(
mut,
seeds = [
b"fee_collector".as_ref()
],
seeds = [b"fee_collector"],
bump,
seeds::program = Custodian::wormhole()?
)]
@ -406,9 +397,7 @@ pub struct BridgeSealedContribution<'info> {
#[account(
mut,
seeds = [
b"emitter".as_ref(),
],
seeds = [b"emitter"],
bump,
seeds::program = Custodian::token_bridge()?
)]
@ -418,7 +407,7 @@ pub struct BridgeSealedContribution<'info> {
#[account(
mut,
seeds = [
b"Sequence".as_ref(),
b"Sequence",
wormhole_emitter.key().as_ref()
],
bump,
@ -430,7 +419,7 @@ pub struct BridgeSealedContribution<'info> {
#[account(
mut,
seeds = [
b"bridge-sealed".as_ref(),
b"bridge-sealed",
&sale.id,
&accepted_mint.key().as_ref(),
],
@ -486,7 +475,7 @@ pub struct AbortSale<'info> {
#[account(
constraint = core_bridge_vaa.owner.key() == Custodian::wormhole()?
)]
/// CHECK: This account is owned by Core Bridge so we trust it
/// CHECK: Posted VAA Message Data
pub core_bridge_vaa: AccountInfo<'info>,
pub system_program: Program<'info, System>,
@ -527,14 +516,19 @@ pub struct SealSale<'info> {
#[account(
constraint = core_bridge_vaa.owner.key() == Custodian::wormhole()?
)]
/// CHECK: This account is owned by Core Bridge so we trust it
/// CHECK: Posted VAA Message Data
pub core_bridge_vaa: AccountInfo<'info>,
#[account(
associated_token::mint = sale.sale_token_mint,
associated_token::authority = custodian,
constraint = custodian_sale_token_acct.key() == sale.sale_token_ata
)]
/// This must be an associated token account
///
/// Prior to sealing the sale, the sale token needed to be bridged to the custodian's
/// associated token account. We need to make sure that there are enough allocations
/// in the custodian's associated token account for distribution to all of the
/// participants of the sale. If there aren't, we cannot allow the instruction to
/// continue.
pub custodian_sale_token_acct: Account<'info, TokenAccount>,
pub system_program: Program<'info, System>,
@ -586,8 +580,7 @@ pub struct ClaimAllocation<'info> {
#[account(
mut,
associated_token::mint = sale.sale_token_mint,
associated_token::authority = custodian,
constraint = custodian_sale_token_acct.key() == sale.sale_token_ata
)]
/// This must be an associated token account
pub custodian_sale_token_acct: Account<'info, TokenAccount>,

View File

@ -89,6 +89,9 @@ pub enum ContributorError {
#[msg("SaleNotSealed")]
SaleNotSealed,
#[msg("SaleTokenNotAttested")]
SaleTokenNotAttested,
#[msg("TooManyAcceptedTokens")]
TooManyAcceptedTokens,
@ -97,4 +100,13 @@ pub enum ContributorError {
#[msg("AllocationsLocked")]
AllocationsLocked,
#[msg("SaleContributionsAreBlocked")]
SaleContributionsAreBlocked,
#[msg("AssetContributionsAreBlocked")]
AssetContributionsAreBlocked,
#[msg("InvalidAcceptedTokenATA")]
InvalidAcceptedTokenATA,
}

View File

@ -15,7 +15,7 @@ use error::*;
use token_bridge::*;
use wormhole::*;
declare_id!("Efzc4SLs1ZdTPRq95oWxdMUr9XiX5M14HABwHpvrc9Fm"); // Solana devnet same
declare_id!("Efzc4SLs1ZdTPRq95oWxdMUr9XiX5M14HABwHpvrc9Fm");
#[program]
pub mod anchor_contributor {
@ -28,7 +28,8 @@ pub mod anchor_contributor {
system_instruction::transfer,
sysvar::*,
};
use anchor_spl::*;
use anchor_spl::token;
use itertools::izip;
use state::custodian::Custodian;
@ -65,47 +66,119 @@ pub mod anchor_contributor {
let sale = &mut ctx.accounts.sale;
sale.parse_sale_init(&msg.payload)?;
// The VAA encoded the Custodian's associated token account for the sale token. We
// need to verify that the ATA that we have in the context is the same one the message
// refers to.
require!(
sale.associated_sale_token_address == ctx.accounts.custodian_sale_token_acct.key(),
ContributorError::InvalidVaaPayload
);
// Check that sale_token_mint is legitimate
let mint_acct_info = &ctx.accounts.sale_token_mint;
// We assume that the conductor is sending a legitimate token, whether it is
// a Solana native token or minted by the token bridge program.
if sale.token_chain == CHAIN_ID {
// In the case that the token chain is Solana, we will attempt to deserialize the Mint
// account and be on our way. If for any reason we cannot, we will block the sale
// as a precaution.
match mint_acct_info.try_borrow_data() {
Err(_) => {
sale.block_contributions();
}
Ok(data) => {
let mut bf: &[u8] = &data;
match token::Mint::try_deserialize(&mut bf) {
Err(_) => {
sale.block_contributions();
}
Ok(mint_info) => {
// We want to save the sale token's mint information in the Sale struct. Most
// important of which is the number of decimals for this SPL token. The sale
// token that lives on the conductor chain can have a different number of decimals.
// Given how Portal works in attesting tokens, the foreign decimals will always
// be at least the amount found here.
sale.set_sale_token_mint_info(
&ctx.accounts.sale_token_mint.key(),
&mint_info,
&ctx.accounts.custodian.key(),
)?;
}
}
}
};
} else {
// In the case that the token chain isn't Solana, we will assume that the token
// has not been attestd yet if there is no account found.
let mut buf: &[u8] = &mint_acct_info
.try_borrow_data()
.map_err(|_| ContributorError::SaleTokenNotAttested)?;
let mint_info = token::Mint::try_deserialize(&mut buf)
.map_err(|_| ContributorError::SaleTokenNotAttested)?;
// since the token chain ID is not Solana's, presumably the token bridge program
// minted this token. But as a precaution, we will double-check the mint address
// derivation. If for some reason the address doesn't line up with how we derive
// it using the seeds, we will block contributions.
let (mint, _) = Pubkey::find_program_address(
&[
b"wrapped",
&sale.token_chain.to_be_bytes(),
&sale.native_sale_token_mint,
],
&ctx.accounts.token_bridge.key(),
);
if mint != ctx.accounts.sale_token_mint.key() {
sale.block_contributions();
}
// We want to save the sale token's mint information in the Sale struct. Most
// important of which is the number of decimals for this SPL token. The sale
// token that lives on the conductor chain can have a different number of decimals.
// Given how Portal works in attesting tokens, the foreign decimals will always
// be at least the amount found here.
sale.set_sale_token_mint_info(
&ctx.accounts.sale_token_mint.key(),
&mint_info,
&ctx.accounts.custodian.key(),
)?;
}
// We need to verify that the accepted tokens are actual mints.
let assets = &sale.totals;
// We set status to invalid on bad ones.
let assets = &mut sale.totals;
let accepted_mints = &ctx.remaining_accounts[..];
require!(
assets.len() == accepted_mints.len(),
ContributorError::InvalidRemainingAccounts
);
for (asset, accepted_mint_acct_info) in izip!(assets, accepted_mints) {
require!(
*accepted_mint_acct_info.owner == token::ID,
ContributorError::InvalidAcceptedToken
);
// If the remaining account does not match the key of the accepted asset's mint,
// throw because wrong account is passed into instruction.
require!(
accepted_mint_acct_info.key() == asset.mint,
ContributorError::InvalidAcceptedToken
ContributorError::InvalidRemainingAccounts,
);
// try_deserialize calls Mint::unpack, which checks if
// SPL is_intialized is true
let mut bf: &[u8] = &accepted_mint_acct_info.try_borrow_data()?;
let _ = token::Mint::try_deserialize(&mut bf)?;
// Check whether we should invalidate the accepted asset.
match *accepted_mint_acct_info.owner == token::ID {
false => {
// If the remaining account is not owned by token program, it is invalid.
asset.invalidate();
}
_ => {
match accepted_mint_acct_info.try_borrow_data() {
Err(_) => {
// If the remaining account is not a real account, it is invalid.
asset.invalidate();
}
Ok(data) => {
// If the remaining account does not deserialize to Mint account, it is invalid.
let mut bf: &[u8] = &data;
if token::Mint::try_deserialize(&mut bf).is_err() {
asset.invalidate();
}
}
}
}
};
}
// We want to save the sale token's mint information in the Sale struct. Most
// important of which is the number of decimals for this SPL token. The sale
// token that lives on the conductor chain can have a different number of decimals.
// Given how Portal works in attesting tokens, the foreign decimals will always
// be at least the amount found here.
sale.set_sale_token_mint_info(
&ctx.accounts.sale_token_mint.key(),
&ctx.accounts.sale_token_mint,
)?;
// Write sale id in program log for reference.
msg!("sale: {}", hex::encode(&sale.id));
// Finish instruction.
Ok(())
@ -132,13 +205,25 @@ pub mod anchor_contributor {
// for the SPL transfer that will happen at the end of all the accounting processing.
let transfer_authority = &ctx.accounts.owner;
// Find indices used for contribution accounting
// Check that sale on Solana is not blocked.
let sale = &ctx.accounts.sale;
require!(
!sale.is_blocked_contributions(),
ContributorError::SaleContributionsAreBlocked
);
// Find indices used for contribution accounting
// We need to use the buyer's associated token account to help us find the token index
// for this particular mint he wishes to contribute.
let (idx, asset) = sale.get_total_info(&buyer_token_acct.mint)?;
// This should never happen because the ATA will not deserialize correctly,
// but we have this here just in case.
require!(
asset.is_valid_for_contribution(),
ContributorError::AssetContributionsAreBlocked
);
// If the buyer account wasn't initialized before, we will do so here. This initializes
// the state for all of this buyer's contributions.
let buyer = &mut ctx.accounts.buyer;
@ -146,13 +231,11 @@ pub mod anchor_contributor {
buyer.initialize(sale.totals.len());
}
let token_index = asset.token_index;
// We verify the KYC signature by encoding specific details of this contribution the
// same way the KYC entity signed for the transaction. If we cannot recover the KYC's
// public key using ecdsa recovery, we cannot allow the contribution to continue.
sale.verify_kyc_authority(
token_index,
asset.token_index,
amount,
&transfer_authority.key(),
buyer.contributions[idx].amount,
@ -288,11 +371,6 @@ pub mod anchor_contributor {
// accepted asset. Change the state from Active to Sealed.
sale.parse_sale_sealed(&msg.payload)?;
// Prior to sealing the sale, the sale token needed to be bridged to the custodian's
// associated token account. We need to make sure that there are enough allocations
// in the custodian's associated token account for distribution to all of the
// participants of the sale. If there aren't, we cannot allow the instruction to
// continue.
let total_allocations: u64 = sale.totals.iter().map(|total| total.allocations).sum();
require!(
ctx.accounts.custodian_sale_token_acct.amount >= total_allocations,
@ -314,12 +392,14 @@ pub mod anchor_contributor {
for (asset, custodian_token_acct) in izip!(totals, custodian_token_accts) {
// re-derive custodian_token_acct address and check it.
// Verifies the authority and mint of the custodian's associated token account
let ata = asset
.deserialize_associated_token_account(custodian_token_acct, &custodian.key())?;
require!(
ata.amount >= asset.contributions,
ContributorError::InsufficientFunds
);
if let Some(ata) = asset
.deserialize_associated_token_account(custodian_token_acct, &custodian.key())?
{
require!(
ata.amount >= asset.contributions,
ContributorError::InsufficientFunds
);
};
asset.prepare_for_transfer();
}
@ -357,138 +437,148 @@ pub mod anchor_contributor {
ContributorError::TransferNotAllowed
);
// We will need the custodian seeds to sign one to two transactions
let custodian_seeds = &[SEED_PREFIX_CUSTODIAN.as_bytes(), &[ctx.bumps["custodian"]]];
// We need to delegate authority to the token bridge program's
// authority signer to spend the custodian's token
let amount = asset.contributions - asset.excess_contributions;
let authority_signer = &ctx.accounts.authority_signer;
token::approve(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Approve {
to: custodian_token_acct.to_account_info(),
delegate: authority_signer.to_account_info(),
authority: custodian.to_account_info(),
},
&[&custodian_seeds[..]],
),
amount,
)?;
let transfer_data = TransferData {
nonce: 0,
amount,
fee: 0,
target_address: sale.recipient,
target_chain: Custodian::conductor_chain()?,
};
if amount > 0 {
// We will need the custodian seeds to sign one to two transactions
let custodian_seeds = &[SEED_PREFIX_CUSTODIAN.as_bytes(), &[ctx.bumps["custodian"]]];
let token_bridge_key = &ctx.accounts.token_bridge.key();
// We will need the wormhole message seeds for both types
// of token bridge transfers.
let wormhole_message_seeds = &[
&b"bridge-sealed".as_ref(),
&sale.id[..],
accepted_mint_key.as_ref(),
&[ctx.bumps["wormhole_message"]],
];
// There are two instructions to bridge assets depending on
// whether the accepted token's mint authority is the token
// bridge program's.
let token_mint_signer = &ctx.accounts.token_mint_signer;
let minted_by_token_bridge = match accepted_mint_acct.mint_authority {
COption::Some(authority) => authority == token_mint_signer.key(),
_ => false,
};
if minted_by_token_bridge {
let wrapped_meta_key = &ctx.accounts.custody_or_wrapped_meta.key();
// Because we don't have an account check for wrapped_meta,
// let's do it here.
let (derived_key, _) = Pubkey::find_program_address(
&[b"meta".as_ref(), accepted_mint_key.as_ref()],
token_bridge_key,
);
require!(
*wrapped_meta_key == derived_key,
ContributorError::InvalidAccount
);
// Now bridge
invoke_signed(
&Instruction {
program_id: *token_bridge_key,
accounts: vec![
AccountMeta::new(ctx.accounts.payer.key(), true),
AccountMeta::new_readonly(ctx.accounts.token_bridge_config.key(), false),
AccountMeta::new(custodian_token_acct.key(), false),
AccountMeta::new_readonly(custodian.key(), true),
AccountMeta::new(*accepted_mint_key, false),
AccountMeta::new_readonly(*wrapped_meta_key, false),
AccountMeta::new_readonly(authority_signer.key(), false),
AccountMeta::new(ctx.accounts.wormhole_config.key(), false),
AccountMeta::new(ctx.accounts.wormhole_message.key(), true),
AccountMeta::new_readonly(ctx.accounts.wormhole_emitter.key(), false),
AccountMeta::new(ctx.accounts.wormhole_sequence.key(), false),
AccountMeta::new(ctx.accounts.wormhole_fee_collector.key(), false),
AccountMeta::new_readonly(clock::id(), false),
AccountMeta::new_readonly(rent::id(), false),
AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
AccountMeta::new_readonly(ctx.accounts.wormhole.key(), false),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: (TRANSFER_WRAPPED_INSTRUCTION, transfer_data).try_to_vec()?,
},
&ctx.accounts.to_account_infos(),
&[&custodian_seeds[..], &wormhole_message_seeds[..]],
// We need to delegate authority to the token bridge program's
// authority signer to spend the custodian's token
let authority_signer = &ctx.accounts.authority_signer;
token::approve(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Approve {
to: custodian_token_acct.to_account_info(),
delegate: authority_signer.to_account_info(),
authority: custodian.to_account_info(),
},
&[&custodian_seeds[..]],
),
amount,
)?;
} else {
let token_bridge_custody = &ctx.accounts.custody_or_wrapped_meta;
// Because we don't have an account check for token_bridge_custody,
// let's do it here.
let (derived_key, _) =
Pubkey::find_program_address(&[accepted_mint_key.as_ref()], token_bridge_key);
require!(
token_bridge_custody.key() == derived_key,
ContributorError::InvalidAccount
);
let transfer_data = TransferData {
nonce: 0,
amount,
fee: 0,
target_address: sale.recipient,
target_chain: Custodian::conductor_chain()?,
};
// Now bridge
invoke_signed(
&Instruction {
program_id: *token_bridge_key,
accounts: vec![
AccountMeta::new(ctx.accounts.payer.key(), true),
AccountMeta::new_readonly(ctx.accounts.token_bridge_config.key(), false),
AccountMeta::new(custodian_token_acct.key(), false),
AccountMeta::new(*accepted_mint_key, false),
AccountMeta::new(token_bridge_custody.key(), false),
AccountMeta::new_readonly(authority_signer.key(), false),
AccountMeta::new_readonly(ctx.accounts.custody_signer.key(), false),
AccountMeta::new(ctx.accounts.wormhole_config.key(), false),
AccountMeta::new(ctx.accounts.wormhole_message.key(), true),
AccountMeta::new_readonly(ctx.accounts.wormhole_emitter.key(), false),
AccountMeta::new(ctx.accounts.wormhole_sequence.key(), false),
AccountMeta::new(ctx.accounts.wormhole_fee_collector.key(), false),
AccountMeta::new_readonly(clock::id(), false),
AccountMeta::new_readonly(rent::id(), false),
AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
AccountMeta::new_readonly(ctx.accounts.wormhole.key(), false),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: (TRANSFER_NATIVE_INSTRUCTION, transfer_data).try_to_vec()?,
},
&ctx.accounts.to_account_infos(),
&[&wormhole_message_seeds[..]],
)?;
let token_bridge_key = &ctx.accounts.token_bridge.key();
// We will need the wormhole message seeds for both types
// of token bridge transfers.
let wormhole_message_seeds = &[
&b"bridge-sealed".as_ref(),
&sale.id[..],
accepted_mint_key.as_ref(),
&[ctx.bumps["wormhole_message"]],
];
// There are two instructions to bridge assets depending on
// whether the accepted token's mint authority is the token
// bridge program's.
let token_mint_signer = &ctx.accounts.token_mint_signer;
let minted_by_token_bridge = match accepted_mint_acct.mint_authority {
COption::Some(authority) => authority == token_mint_signer.key(),
_ => false,
};
if minted_by_token_bridge {
let wrapped_meta_key = &ctx.accounts.custody_or_wrapped_meta.key();
// Because we don't have an account check for wrapped_meta,
// let's do it here.
let (derived_key, _) = Pubkey::find_program_address(
&[b"meta", accepted_mint_key.as_ref()],
token_bridge_key,
);
require!(
*wrapped_meta_key == derived_key,
ContributorError::InvalidAccount
);
// Now bridge
invoke_signed(
&Instruction {
program_id: *token_bridge_key,
accounts: vec![
AccountMeta::new(ctx.accounts.payer.key(), true),
AccountMeta::new_readonly(
ctx.accounts.token_bridge_config.key(),
false,
),
AccountMeta::new(custodian_token_acct.key(), false),
AccountMeta::new_readonly(custodian.key(), true),
AccountMeta::new(*accepted_mint_key, false),
AccountMeta::new_readonly(*wrapped_meta_key, false),
AccountMeta::new_readonly(authority_signer.key(), false),
AccountMeta::new(ctx.accounts.wormhole_config.key(), false),
AccountMeta::new(ctx.accounts.wormhole_message.key(), true),
AccountMeta::new_readonly(ctx.accounts.wormhole_emitter.key(), false),
AccountMeta::new(ctx.accounts.wormhole_sequence.key(), false),
AccountMeta::new(ctx.accounts.wormhole_fee_collector.key(), false),
AccountMeta::new_readonly(clock::id(), false),
AccountMeta::new_readonly(rent::id(), false),
AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
AccountMeta::new_readonly(ctx.accounts.wormhole.key(), false),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: (TRANSFER_WRAPPED_INSTRUCTION, transfer_data).try_to_vec()?,
},
&ctx.accounts.to_account_infos(),
&[&custodian_seeds[..], &wormhole_message_seeds[..]],
)?;
} else {
let token_bridge_custody = &ctx.accounts.custody_or_wrapped_meta;
// Because we don't have an account check for token_bridge_custody,
// let's do it here.
let (derived_key, _) =
Pubkey::find_program_address(&[accepted_mint_key.as_ref()], token_bridge_key);
require!(
token_bridge_custody.key() == derived_key,
ContributorError::InvalidAccount
);
// Now bridge
invoke_signed(
&Instruction {
program_id: *token_bridge_key,
accounts: vec![
AccountMeta::new(ctx.accounts.payer.key(), true),
AccountMeta::new_readonly(
ctx.accounts.token_bridge_config.key(),
false,
),
AccountMeta::new(custodian_token_acct.key(), false),
AccountMeta::new(*accepted_mint_key, false),
AccountMeta::new(token_bridge_custody.key(), false),
AccountMeta::new_readonly(authority_signer.key(), false),
AccountMeta::new_readonly(ctx.accounts.custody_signer.key(), false),
AccountMeta::new(ctx.accounts.wormhole_config.key(), false),
AccountMeta::new(ctx.accounts.wormhole_message.key(), true),
AccountMeta::new_readonly(ctx.accounts.wormhole_emitter.key(), false),
AccountMeta::new(ctx.accounts.wormhole_sequence.key(), false),
AccountMeta::new(ctx.accounts.wormhole_fee_collector.key(), false),
AccountMeta::new_readonly(clock::id(), false),
AccountMeta::new_readonly(rent::id(), false),
AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
AccountMeta::new_readonly(ctx.accounts.wormhole.key(), false),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: (TRANSFER_NATIVE_INSTRUCTION, transfer_data).try_to_vec()?,
},
&ctx.accounts.to_account_infos(),
&[&wormhole_message_seeds[..]],
)?;
}
}
// Even if there is nothing to transfer, we will change the state.
ctx.accounts.sale.totals[idx].set_transferred();
// Finish instruction.
@ -566,30 +656,45 @@ pub mod anchor_contributor {
for (idx, (asset, custodian_token_acct, buyer_token_acct)) in
izip!(totals, custodian_token_accts, buyer_token_accts).enumerate()
{
// Verify the custodian's associated token account
asset.deserialize_associated_token_account(custodian_token_acct, &ctx.accounts.custodian.key(),)?;
// And verify the buyer's token account
asset.deserialize_associated_token_account(buyer_token_acct, &owner.key())?;
// Now calculate the refund and transfer to the buyer's associated
// token account if there is any amount to refund.
let refund = buyer.claim_refund(idx)?;
if refund == 0 {
continue;
}
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: custodian_token_acct.to_account_info(),
to: buyer_token_acct.to_account_info(),
authority: transfer_authority.to_account_info(),
},
&[&[SEED_PREFIX_CUSTODIAN.as_bytes(), &[ctx.bumps["custodian"]]]],
),
refund,
)?;
// Verify remaining accounts are associated token accounts.
// Either both are valid or both are invalid. If only one
// is valid, then there is something wrong.
// In the case that both are invalid, this is when the accepted
// token itself is invalid.
match (
asset.deserialize_associated_token_account(
custodian_token_acct,
&ctx.accounts.custodian.key(),
)?,
asset.deserialize_associated_token_account(buyer_token_acct, &owner.key())?,
) {
(Some(_), Some(_)) => {
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: custodian_token_acct.to_account_info(),
to: buyer_token_acct.to_account_info(),
authority: transfer_authority.to_account_info(),
},
&[&[SEED_PREFIX_CUSTODIAN.as_bytes(), &[ctx.bumps["custodian"]]]],
),
refund,
)?;
}
(None, None) => {
// This scenario is expected for an invalid token because
// neither will have an associated token account
}
_ => return Err(ContributorError::InvalidAccount.into()),
};
}
// Finish instruction.
@ -692,30 +797,45 @@ pub mod anchor_contributor {
for (idx, (asset, custodian_token_acct, buyer_token_acct)) in
izip!(totals, custodian_token_accts, buyer_token_accts).enumerate()
{
// Verify the custodian's associated token account
asset.deserialize_associated_token_account(custodian_token_acct, &ctx.accounts.custodian.key())?;
// And verify the buyer's token account
asset.deserialize_associated_token_account(buyer_token_acct, &owner.key())?;
// Now calculate the excess contribution and transfer to the
// buyer's associated token account if there is any amount calculated.
let excess = buyer.claim_excess(idx, asset)?;
if excess == 0 {
continue;
}
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: custodian_token_acct.to_account_info(),
to: buyer_token_acct.to_account_info(),
authority: transfer_authority.to_account_info(),
},
&[&[SEED_PREFIX_CUSTODIAN.as_bytes(), &[ctx.bumps["custodian"]]]],
),
excess,
)?;
// Verify remaining accounts are associated token accounts.
// Either both are valid or both are invalid. If only one
// is valid, then there is something wrong.
// In the case that both are invalid, this is when the accepted
// token itself is invalid.
match (
asset.deserialize_associated_token_account(
custodian_token_acct,
&ctx.accounts.custodian.key(),
)?,
asset.deserialize_associated_token_account(buyer_token_acct, &owner.key())?,
) {
(Some(_), Some(_)) => {
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: custodian_token_acct.to_account_info(),
to: buyer_token_acct.to_account_info(),
authority: transfer_authority.to_account_info(),
},
&[&[SEED_PREFIX_CUSTODIAN.as_bytes(), &[ctx.bumps["custodian"]]]],
),
excess,
)?;
}
(None, None) => {
// This scenario is expected for an invalid token because
// neither will have an associated token account
}
_ => return Err(ContributorError::InvalidAccount.into()),
};
}
// Finish instruction.

View File

@ -19,7 +19,7 @@ pub struct AssetTotal {
pub contributions: u64, // 8
pub allocations: u64, // 8
pub excess_contributions: u64, // 8
pub status: AssetStatus, // 1
pub asset_status: AssetStatus, // 1
}
#[derive(
@ -30,6 +30,7 @@ pub enum AssetStatus {
NothingToTransfer,
ReadyForTransfer,
TransferredToConductor,
InvalidToken,
}
#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq)]
@ -50,19 +51,21 @@ pub enum SaleStatus {
#[account]
pub struct Sale {
pub id: [u8; 32], // 32
pub associated_sale_token_address: Pubkey, // 32
pub token_chain: u16, // 2
pub token_decimals: u8, // 1
pub times: SaleTimes, // SaleStatus::LEN
pub recipient: [u8; 32], // 32
pub status: SaleStatus, // 1
pub kyc_authority: [u8; 20], // 20 (this is an evm pubkey)
pub initialized: bool, // 1
pub id: [u8; 32], // 32
pub native_sale_token_mint: [u8; 32], // 32 Native for sale token chain.
pub token_chain: u16, // 2
pub token_decimals: u8, // 1
pub times: SaleTimes, // SaleTimes::LEN
pub recipient: [u8; 32], // 32
pub status: SaleStatus, // 1
pub kyc_authority: [u8; 20], // 20 (this is an evm pubkey)
pub initialized: bool, // 1
pub totals: Vec<AssetTotal>, // 4 + AssetTotal::LEN * ACCEPTED_TOKENS_MAX
pub native_token_decimals: u8, // 1
pub sale_token_mint: Pubkey, // 32
pub sale_token_mint: Pubkey, // 32 Solana Native or wrapped.
pub sale_token_ata: Pubkey, // 32
pub contributions_blocked: bool, // 1 Bad sale token mint address.
}
impl SaleTimes {
@ -84,12 +87,16 @@ impl AssetTotal {
contributions: 0,
allocations: 0,
excess_contributions: 0,
status: AssetStatus::Active,
asset_status: AssetStatus::Active,
})
}
pub fn invalidate(&mut self) {
self.asset_status = AssetStatus::InvalidToken;
}
pub fn prepare_for_transfer(&mut self) {
self.status = {
self.asset_status = {
if self.contributions == 0 {
AssetStatus::NothingToTransfer
} else {
@ -98,29 +105,43 @@ impl AssetTotal {
};
}
pub fn is_valid_for_contribution(&self) -> bool {
self.asset_status != AssetStatus::InvalidToken
}
pub fn is_ready_for_transfer(&self) -> bool {
self.status == AssetStatus::ReadyForTransfer
self.asset_status == AssetStatus::ReadyForTransfer
}
pub fn set_transferred(&mut self) {
self.status = AssetStatus::TransferredToConductor;
self.asset_status = AssetStatus::TransferredToConductor;
}
pub fn deserialize_associated_token_account(
&self,
token_acct_info: &AccountInfo,
authority: &Pubkey,
) -> Result<TokenAccount> {
require!(
get_associated_token_address(authority, &self.mint) == token_acct_info.key(),
ContributorError::InvalidAccount
);
AssetTotal::deserialize_token_account_unchecked(token_acct_info)
) -> Result<Option<TokenAccount>> {
Ok(
match AssetTotal::deserialize_token_account_unchecked(token_acct_info) {
Ok(account) => {
// If we successfully deserialize TokenAccount, we require
// that it is an associated token account
require!(
get_associated_token_address(authority, &self.mint)
== token_acct_info.key(),
ContributorError::InvalidAccount
);
Some(account)
}
// Otherwise (in the case of an invalid remaining account),
// we will return None.
Err(_) => None,
},
)
}
fn deserialize_token_account_unchecked(
token_acct_info: &AccountInfo,
) -> Result<TokenAccount> {
fn deserialize_token_account_unchecked(token_acct_info: &AccountInfo) -> Result<TokenAccount> {
let mut bf: &[u8] = &token_acct_info.try_borrow_data()?;
TokenAccount::try_deserialize_unchecked(&mut bf)
}
@ -138,7 +159,9 @@ impl Sale {
+ 1
+ (4 + AssetTotal::LEN * ACCEPTED_TOKENS_MAX)
+ 1
+ 32;
+ 32
+ 32
+ 1;
pub fn parse_sale_init(&mut self, payload: &[u8]) -> Result<()> {
require!(!self.initialized, ContributorError::SaleAlreadyInitialized);
@ -153,9 +176,12 @@ impl Sale {
let num_accepted = payload[INDEX_SALE_INIT_ACCEPTED_TOKENS_START] as usize;
// msg!("payloadlen: {}, exp: {}", payload.len(), INDEX_SALE_INIT_ACCEPTED_TOKENS_START + 1 + ACCEPTED_TOKEN_NUM_BYTES * num_accepted + SALE_INIT_TAIL);
require!(
payload.len() == INDEX_SALE_INIT_ACCEPTED_TOKENS_START + 1 + ACCEPTED_TOKEN_NUM_BYTES * num_accepted + SALE_INIT_TAIL,
payload.len()
== INDEX_SALE_INIT_ACCEPTED_TOKENS_START
+ 1
+ ACCEPTED_TOKEN_NUM_BYTES * num_accepted
+ SALE_INIT_TAIL,
ContributorError::InvalidVaaPayload
);
@ -175,11 +201,9 @@ impl Sale {
self.id = Sale::get_id(payload);
// deserialize other things
let mut addr = [0u8; 32];
addr.copy_from_slice(
self.native_sale_token_mint.copy_from_slice(
&payload[INDEX_SALE_INIT_TOKEN_ADDRESS..(INDEX_SALE_INIT_TOKEN_ADDRESS + 32)],
);
self.associated_sale_token_address = Pubkey::new(&addr);
self.token_chain = to_u16_be(payload, INDEX_SALE_INIT_TOKEN_CHAIN);
self.token_decimals = payload[INDEX_SALE_INIT_TOKEN_DECIMALS];
@ -203,18 +227,26 @@ impl Sale {
// finally set the status to active
self.status = SaleStatus::Active;
self.contributions_blocked = false;
Ok(())
}
pub fn set_sale_token_mint_info(&mut self, mint: &Pubkey, mint_info: &Mint) -> Result<()> {
pub fn set_sale_token_mint_info(
&mut self,
mint: &Pubkey,
mint_info: &Mint,
custodian: &Pubkey,
) -> Result<()> {
let decimals = mint_info.decimals;
require!(
self.token_decimals >= decimals,
ContributorError::InvalidTokenDecimals
);
self.native_token_decimals = decimals;
self.sale_token_mint = mint.clone();
self.sale_token_mint = *mint;
// Derive sale_token_ata and store it in this sale for later verification and to send it to the conductor in attest VAA.
self.sale_token_ata = get_associated_token_address(custodian, &self.sale_token_mint);
Ok(())
}
@ -261,6 +293,7 @@ impl Sale {
let mut attested: Vec<u8> = Vec::with_capacity(
PAYLOAD_HEADER_LEN
+ size_of_val(&CHAIN_ID)
+ size_of_val(&self.sale_token_ata)
+ size_of_val(&contributions_len)
+ totals.len() * ATTEST_CONTRIBUTIONS_ELEMENT_LEN,
);
@ -269,6 +302,7 @@ impl Sale {
attested.push(PAYLOAD_ATTEST_CONTRIBUTIONS);
attested.extend(self.id.iter());
attested.extend(CHAIN_ID.to_be_bytes());
attested.extend(self.sale_token_ata.to_bytes());
// push contributions length
attested.push(contributions_len);
@ -415,6 +449,21 @@ impl Sale {
return self.initialized && self.status == SaleStatus::Aborted;
}
pub fn block_contributions(&mut self) {
msg!(
"contributions are blocked for sale {}",
hex::encode(&self.id)
);
self.sale_token_mint = Pubkey::new_from_array([0u8; 32]);
self.sale_token_ata = Pubkey::new_from_array([0u8; 32]);
self.contributions_blocked = true;
}
pub fn is_blocked_contributions(&self) -> bool {
self.contributions_blocked
}
pub fn allocation_unlocked(&self, block_time: i64) -> bool {
block_time as u64 >= self.times.unlock_allocation
}

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,9 @@
import { web3 } from "@project-serum/anchor";
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA, tryNativeToHexString } from "@certusone/wormhole-sdk";
import { createMint, mintTo } from "@solana/spl-token";
import { BigNumber, BigNumberish } from "ethers";
import { web3, BN } from "@project-serum/anchor";
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA, tryNativeToHexString } from "@certusone/wormhole-sdk";
import { mintTo } from "@solana/spl-token";
import { getPdaAssociatedTokenAddress, toBigNumberHex } from "./utils";
import { signAndEncodeVaa } from "./wormhole";
import { BN } from "bn.js";
import { SolanaAcceptedToken } from "./types";
// sale struct info
@ -24,11 +22,17 @@ export class DummyConductor {
saleEnd: number;
saleUnlock: number;
tokenAddress: string;
tokenChain: number;
tokenDecimals: number;
nativeTokenDecimals: number;
initSaleVaa: Buffer;
saleTokenOnSolana: string;
acceptedTokens: SolanaAcceptedToken[];
allocations: Allocation[];
totalAllocations: BN;
constructor(chainId: number, address: string) {
this.chainId = chainId;
@ -45,10 +49,8 @@ export class DummyConductor {
this.allocations = [];
}
async attestSaleToken(connection: web3.Connection, payer: web3.Keypair): Promise<void> {
const mint = await createMint(connection, payer, payer.publicKey, payer.publicKey, this.nativeTokenDecimals);
saveSaleTokenMint(mint: web3.PublicKey) {
this.saleTokenOnSolana = mint.toString();
return;
}
async redeemAllocationsOnSolana(
@ -56,18 +58,10 @@ export class DummyConductor {
payer: web3.Keypair,
custodian: web3.PublicKey
): Promise<void> {
const mint = new web3.PublicKey(this.saleTokenOnSolana);
const mint = this.getSaleTokenOnSolana();
const custodianTokenAcct = await getPdaAssociatedTokenAddress(mint, custodian);
const amount = this.allocations.map((item) => new BN(item.allocation)).reduce((prev, curr) => prev.add(curr));
await mintTo(
connection,
payer,
mint,
custodianTokenAcct,
payer,
BigInt(amount.toString()) // 20,000,000,000 lamports
);
await mintTo(connection, payer, mint, custodianTokenAcct, payer, BigInt(this.totalAllocations.toString()));
return;
}
@ -86,8 +80,10 @@ export class DummyConductor {
createSale(
startTime: number,
duration: number,
associatedSaleTokenAddress: web3.PublicKey,
lockPeriod: number
lockPeriod: number,
tokenAddress: string,
tokenChain: number,
tokenDecimals: number
): Buffer {
// uptick saleId for every new sale
++this.saleId;
@ -98,6 +94,12 @@ export class DummyConductor {
this.saleEnd = this.saleStart + duration;
this.saleUnlock = this.saleEnd + lockPeriod;
this.tokenAddress = tokenAddress;
this.tokenChain = tokenChain;
this.tokenDecimals = tokenDecimals;
this.nativeTokenDecimals = this.tokenChain == 1 ? this.tokenDecimals : 8;
this.initSaleVaa = signAndEncodeVaa(
startTime,
this.nonce,
@ -106,7 +108,7 @@ export class DummyConductor {
this.wormholeSequence,
encodeSaleInit(
this.saleId,
tryNativeToHexString(associatedSaleTokenAddress.toString(), CHAIN_ID_SOLANA),
tryNativeToHexString(this.tokenAddress, this.tokenChain as ChainId),
this.tokenChain,
this.tokenDecimals,
this.saleStart,
@ -121,31 +123,33 @@ export class DummyConductor {
}
getAllocationMultiplier(): string {
const decimalDifference = this.tokenDecimals - this.nativeTokenDecimals;
return BigNumber.from("10").pow(decimalDifference).toString();
const decimalDifference = new BN(this.tokenDecimals - this.nativeTokenDecimals);
return new BN("10").pow(decimalDifference).toString();
}
sealSale(blockTime: number, contributions: Map<number, string[]>): Buffer {
++this.wormholeSequence;
this.allocations = [];
const allocationMultiplier = this.getAllocationMultiplier();
const allocationMultiplier = new BN(this.getAllocationMultiplier());
// make up allocations and excess contributions
const excessContributionDivisor = BigNumber.from("5");
const excessContributionDivisor = new BN("5");
const acceptedTokens = this.acceptedTokens;
this.totalAllocations = new BN(0);
for (let i = 0; i < acceptedTokens.length; ++i) {
const tokenIndex = acceptedTokens[i].index;
const contributionSubset = contributions.get(tokenIndex);
if (contributionSubset === undefined) {
this.allocations.push(makeAllocation(tokenIndex, "0", "0"));
} else {
const total = contributionSubset.map((x) => BigNumber.from(x)).reduce((prev, curr) => prev.add(curr));
const total = contributionSubset.map((x) => new BN(x)).reduce((prev, curr) => prev.add(curr));
const excessContribution = total.div(excessContributionDivisor).toString();
const allocation = BigNumber.from(this.expectedAllocations[i]).mul(allocationMultiplier).toString();
this.allocations.push(makeAllocation(tokenIndex, allocation, excessContribution));
const allocation = new BN(this.expectedAllocations[i]).mul(allocationMultiplier);
this.allocations.push(makeAllocation(tokenIndex, allocation.toString(), excessContribution));
this.totalAllocations = this.totalAllocations.add(allocation);
}
}
return signAndEncodeVaa(
@ -172,9 +176,6 @@ export class DummyConductor {
// sale parameters that won't change for the test
//associatedTokenAddress = "00000000000000000000000083752ecafebf4707258dedffbd9c7443148169db";
tokenChain = CHAIN_ID_ETH as number;
tokenDecimals = 18;
nativeTokenDecimals = 7;
recipient = tryNativeToHexString("0x22d491bde2303f2f43325b2108d26f1eaba1e32b", CHAIN_ID_ETH);
kycAuthority = "1df62f291b2e969fb0849d99d9ce41e2f137006e";
@ -214,9 +215,9 @@ export function encodeAcceptedTokens(acceptedTokens: SolanaAcceptedToken[]): Buf
return encoded;
}
export function encodeSaleInit(
function encodeSaleInit(
saleId: number,
associatedTokenAddress: string, // 32 bytes
saleTokenMint: string, // 32 bytes, left-padded with 0 if needed
tokenChain: number,
tokenDecimals: number,
saleStart: number,
@ -231,7 +232,7 @@ export function encodeSaleInit(
encoded.writeUInt8(5, 0); // initSale payload for solana = 5
encoded.write(toBigNumberHex(saleId, 32), 1, "hex");
encoded.write(associatedTokenAddress, 33, "hex");
encoded.write(saleTokenMint, 33, "hex");
encoded.writeUint16BE(tokenChain, 65);
encoded.writeUint8(tokenDecimals, 67);
encoded.write(toBigNumberHex(saleStart, 32), 68, "hex");

View File

@ -1,18 +1,20 @@
import { BN, Program, web3 } from "@project-serum/anchor";
import { AnchorContributor } from "../../target/types/anchor_contributor";
import {
getAccount,
getAssociatedTokenAddress,
getMint,
getOrCreateAssociatedTokenAccount,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import * as byteify from "byteify";
import { deriveAddress, getPdaAssociatedTokenAddress, makeReadOnlyAccountMeta, makeWritableAccountMeta } from "./utils";
import { PostVaaMethod } from "./types";
import keccak256 from "keccak256";
import { TOKEN_BRIDGE_ADDRESS } from "./consts";
const INDEX_SALE_INIT_TOKEN_ADDRESS = 33;
const INDEX_SALE_INIT_NATIVE_MINT_ADDRESS = 33;
const INDEX_SALE_INIT_TOKEN_CHAIN_START = 65; // u16
const INDEX_SALE_INIT_ACCEPTED_TOKENS_START = 132;
const ACCEPTED_TOKEN_NUM_BYTES = 33;
@ -55,6 +57,7 @@ export class IccoContributor {
async initSale(payer: web3.Keypair, initSaleVaa: Buffer): Promise<string> {
const program = this.program;
const connection = program.provider.connection;
const custodian = this.custodian;
@ -62,14 +65,30 @@ export class IccoContributor {
await this.postVaa(payer, initSaleVaa);
const coreBridgeVaa = this.deriveSignedVaaAccount(initSaleVaa);
const saleId = await parseSaleId(initSaleVaa);
const saleId = parseSaleId(initSaleVaa);
const sale = this.deriveSaleAccount(saleId);
const payload = getVaaBody(initSaleVaa);
const tokenAccount = await getAccount(
program.provider.connection,
new web3.PublicKey(payload.subarray(INDEX_SALE_INIT_TOKEN_ADDRESS, INDEX_SALE_INIT_TOKEN_ADDRESS + 32))
const saleTokenChainId = payload.readInt16BE(INDEX_SALE_INIT_TOKEN_CHAIN_START);
const saleTokenAddress = payload.subarray(
INDEX_SALE_INIT_NATIVE_MINT_ADDRESS,
INDEX_SALE_INIT_NATIVE_MINT_ADDRESS + 32
);
const saleTokenMint = (() => {
if (saleTokenChainId == 1) {
return new web3.PublicKey(saleTokenAddress);
}
return deriveAddress(
[Buffer.from("wrapped"), byteify.serializeUint16(saleTokenChainId), saleTokenAddress],
TOKEN_BRIDGE_ADDRESS
);
})();
await getOrCreateAssociatedTokenAccount(connection, payer, saleTokenMint, custodian, true).catch((_) => {
// error because of invalid token
});
const numAccepted = payload.at(INDEX_SALE_INIT_ACCEPTED_TOKENS_START);
const remainingAccounts: web3.AccountMeta[] = [];
@ -78,6 +97,11 @@ export class IccoContributor {
INDEX_SALE_INIT_ACCEPTED_TOKENS_START + 1 + ACCEPTED_TOKEN_NUM_BYTES * i + INDEX_ACCEPTED_TOKEN_ADDRESS;
const mint = new web3.PublicKey(payload.subarray(start, start + 32));
remainingAccounts.push(makeReadOnlyAccountMeta(mint));
// create ATAs
await getOrCreateAssociatedTokenAccount(connection, payer, mint, custodian, true).catch((_) => {
// error because of invalid token
});
}
return program.methods
@ -86,9 +110,9 @@ export class IccoContributor {
custodian,
sale,
coreBridgeVaa,
saleTokenMint: tokenAccount.mint,
custodianSaleTokenAcct: tokenAccount.address,
saleTokenMint,
payer: payer.publicKey,
tokenBridge: this.tokenBridge,
systemProgram: web3.SystemProgram.programId,
})
.remainingAccounts(remainingAccounts)
@ -108,19 +132,33 @@ export class IccoContributor {
const totals: any = state.totals;
const found = totals.find((item) => item.tokenIndex == tokenIndex);
if (found == undefined) {
throw "tokenIndex not found";
throw new Error("tokenIndex not found");
}
const mint = found.mint;
// now prepare instruction
const program = this.program;
const connection = program.provider.connection;
const custodian = this.custodian;
const buyer = this.deriveBuyerAccount(saleId, payer.publicKey);
const sale = this.deriveSaleAccount(saleId);
const buyerTokenAcct = await getAssociatedTokenAddress(mint, payer.publicKey);
const buyerTokenAcct = await getOrCreateAssociatedTokenAccount(connection, payer, mint, payer.publicKey)
.catch((_) => {
// illegimate accepted token... don't throw and derive address anyway
return null;
})
.then(async (account) => {
if (account != null) {
return new web3.PublicKey(account.address);
}
// we still want to generate an address here
return getAssociatedTokenAddress(mint, payer.publicKey);
});
const custodianTokenAcct = await getPdaAssociatedTokenAddress(mint, custodian);
return program.methods
@ -257,6 +295,7 @@ export class IccoContributor {
units: 420690,
additionalFee: 0,
});
return program.methods
.bridgeSealedContribution()
.accounts({

View File

@ -32,7 +32,7 @@ export class KycAuthority {
const idx = totals.findIndex((item) => item.tokenIndex == tokenIndex);
if (idx < 0) {
throw Error("tokenIndex not found");
throw new Error("tokenIndex not found");
}
const state = await this.getBuyer(saleId, buyer);

View File

@ -29,25 +29,24 @@ export function encodeAttestMeta(
name: string
) {
if (tokenAddress.length != 32) {
throw Error("tokenAddress.length != 32");
throw new Error("tokenAddress.length != 32");
}
if (symbol.length > 64) {
throw Error("symbol.length > 64");
throw new Error("symbol.length > 64");
}
if (name.length > 64) {
throw Error("name.length > 64");
throw new Error("name.length > 64");
}
const encoded = Buffer.alloc(100);
encoded.writeUint8(2, 0);
encoded.write(tokenAddress.toString("hex"), 1, "hex");
encoded.writeUint16BE(tokenChain, 33);
encoded.writeUint8(decimals, 35);
encoded.write(symbol, 37);
encoded.write(symbol, 36);
encoded.write(name, 68);
// console.log("buffer:\n", encoded.toString("hex"));
return encoded;
}
@ -219,7 +218,11 @@ export class TokenBridgeProgram {
{ pubkey: splMetadata, isSigner: false, isWritable: true },
{ pubkey: mintAuthorityKey, isSigner: false, isWritable: false },
{ pubkey: web3.SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
{ pubkey: web3.SystemProgram.programId, isSigner: false, isWritable: false },
{
pubkey: web3.SystemProgram.programId,
isSigner: false,
isWritable: false,
},
{ pubkey: this.wormhole, isSigner: false, isWritable: false },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
{ pubkey: SPL_METADATA_PROGRAM, isSigner: false, isWritable: false },

View File

@ -2,13 +2,12 @@ import { web3, BN } from "@project-serum/anchor";
import { findProgramAddressSync } from "@project-serum/anchor/dist/cjs/utils/pubkey";
import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token";
import { tryHexToNativeString, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import { BigNumber, BigNumberish } from "ethers";
export function toBigNumberHex(value: BigNumberish, numBytes: number): string {
return BigNumber.from(value)
.toHexString()
.substring(2)
.padStart(numBytes * 2, "0");
export function toBigNumberHex(value: string | number, numBytes: number): string {
const valueBytes = new BN(value).toBuffer();
const buffer = Buffer.alloc(numBytes);
buffer.write(valueBytes.toString("hex"), numBytes - valueBytes.length, "hex");
return buffer.toString("hex");
}
export async function wait(timeInSeconds: number): Promise<void> {
@ -21,15 +20,17 @@ export async function getBlockTime(connection: web3.Connection): Promise<number>
}
export async function getSplBalance(connection: web3.Connection, mint: web3.PublicKey, owner: web3.PublicKey) {
const tokenAccount = await getAssociatedTokenAddress(mint, owner);
const account = await getAccount(connection, tokenAccount);
return new BN(account.amount.toString());
return getAssociatedTokenAddress(mint, owner)
.then(async (addr) => getAccount(connection, addr))
.catch((_) => null)
.then((account) => new BN(account == null ? 0 : account.amount.toString()));
}
export async function getPdaSplBalance(connection: web3.Connection, mint: web3.PublicKey, owner: web3.PublicKey) {
const tokenAccount = await getPdaAssociatedTokenAddress(mint, owner);
const account = await getAccount(connection, tokenAccount);
return new BN(account.amount.toString());
return getPdaAssociatedTokenAddress(mint, owner)
.then(async (addr) => getAccount(connection, addr))
.catch((_) => null)
.then((account) => new BN(account == null ? 0 : account.amount.toString()));
}
export function hexToPublicKey(hexlified: string): web3.PublicKey {

View File

@ -12,7 +12,7 @@ export function signAndEncodeVaa(
data: Buffer
): Buffer {
if (emitterAddress.length != 32) {
throw Error("emitterAddress != 32 bytes");
throw new Error("emitterAddress != 32 bytes");
}
// wormhole initialized with only one guardian in devnet