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:
parent
1b7bddb084
commit
fef0dec3c3
|
@ -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": {
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
@ -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");
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue