From eb9c99e471999e168610820e70bc13f995700a22 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Fri, 7 Oct 2022 14:23:36 -0500 Subject: [PATCH] xMint: solana documentation --- src/projects/evm-messenger/messenger.md | 2 + src/projects/xmint/xMintSolana.md | 373 ++++++++++++++++++++++++ 2 files changed, 375 insertions(+) diff --git a/src/projects/evm-messenger/messenger.md b/src/projects/evm-messenger/messenger.md index 9745309..f4a4ba9 100644 --- a/src/projects/evm-messenger/messenger.md +++ b/src/projects/evm-messenger/messenger.md @@ -1,9 +1,11 @@ # Messenger.sol + Messenger.sol is an application contract on EVM capable of communicating with the Wormhole Core Bridge. Start by hard coding the Wormhole core bridge address and creating an interfaced link to it. ```solidity + //SPDX-License-Identifier: Unlicense pragma solidity ^0.8.0; diff --git a/src/projects/xmint/xMintSolana.md b/src/projects/xmint/xMintSolana.md index e69de29..356e9dc 100644 --- a/src/projects/xmint/xMintSolana.md +++ b/src/projects/xmint/xMintSolana.md @@ -0,0 +1,373 @@ +# lib.rs + +lib.rs is the application contract on Solana that handles communication with the Wormhole Core and Token Bridge. + +Start by importing all necessary dependencies and declare the contract ID. + +```rs + +use anchor_lang::prelude::*; +use mpl_token_metadata::instruction::create_metadata_accounts_v3; +use mpl_token_metadata::ID as metadata_program_id; +use anchor_lang::solana_program::program::invoke_signed; +use anchor_lang::solana_program::instruction::{Instruction, AccountMeta}; +use anchor_lang::solana_program::{sysvar::rent, system_program}; +use anchor_spl::token::{ID as spl_id, mint_to, approve, MintTo, Approve}; + +use sha3::Digest; +use byteorder::{ + BigEndian, + WriteBytesExt, +}; +use std::io::{ + Cursor, + Write, +}; +use std::str::FromStr; + +mod account; +mod context; +mod constant; +mod error; +mod event; +mod portal; +mod wormhole; + +use account::*; +use context::*; +use wormhole::*; +use portal::*; +use error::*; +use constant::*; + +declare_id!("BHz6MJGvo8PJaBFqaxyzgJYdY6o8h1rBgsRrUmnHCU9k"); + + +``` + +## Initialize +This initializes all of the accounts that this program will access. + +```rust + +pub fn initialize(ctx: Context, name: String, symbol:String, uri: String) -> Result<()> { + ctx.accounts.config.owner = ctx.accounts.owner.key(); + ctx.accounts.config.nonce = 0; + ctx.accounts.mint_authority.mint = ctx.accounts.mint.key(); + + // Create Metadata Accounts for SPL Token + let ix = &create_metadata_accounts_v3( + metadata_program_id, + ctx.accounts.metadata_account.key(), + ctx.accounts.mint.key(), + ctx.accounts.mint_authority.key(), + ctx.accounts.owner.key(), + ctx.accounts.mint_authority.key(), + name, + symbol, + uri, + None, + 0, + true, + true, + None, + None, + None + ); + + let accounts = vec![ + ctx.accounts.metadata_program.to_account_info(), + ctx.accounts.metadata_account.to_account_info(), + ctx.accounts.mint.to_account_info(), + ctx.accounts.mint_authority.to_account_info(), + ctx.accounts.owner.to_account_info(), + ctx.accounts.mint_authority.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ctx.accounts.rent.to_account_info() + ]; + + let seeds:&[&[u8]] = &[ + b"mint_authority", + &[*ctx.bumps.get("mint_authority").unwrap()] + ]; + + invoke_signed( + ix, + accounts.as_slice(), + &[&seeds[..]] + )?; + + Ok(()) +} + +``` + +## register_chain +It's typically a good idea to register and track the contracts from foreign chains that you're accepting VAAs from, as anyone could deploy a contract and generate a fake VAA that looks like a real VAA you'd want to accept. + +```rust + +//Register Application Contracts +pub fn register_chain(ctx:Context, chain_id:u16, emitter_addr:Vec) -> Result<()> { + ctx.accounts.emitter_acc.emitter_chain = chain_id; + ctx.accounts.emitter_acc.emitter_address = emitter_addr.as_slice().try_into().unwrap(); + Ok(()) +} + +``` + +## submit_foriegn_purchase +This takes in a bytes payload and calls the Wormhole Token Bridge to receive the bridged tokens and mint the corresponding amount of native token. + +```rust + +pub fn submit_foreign_purchase(ctx:Context) -> Result<()> { + // Fetch the VAA + //Hash a VAA Extract and derive a VAA Key + let vaa = PostedMessageData::try_from_slice(&ctx.accounts.core_bridge_vaa.data.borrow())?.0; + let vaa_hash: [u8; 32] = vaa_hash(ctx.accounts.core_bridge_vaa.clone()); + + let (vaa_key, _) = Pubkey::find_program_address(&[ + b"PostedVAA", + &vaa_hash + ], &Pubkey::from_str(CORE_BRIDGE_ADDRESS).unwrap()); + + if ctx.accounts.core_bridge_vaa.key() != vaa_key { + return err!(XmintError::VAAKeyMismatch); + } + + // Already checked that the SignedVaa is owned by core bridge in account constraint logic + //Check that the emitter chain and address match up with the vaa + // This VAA will be from Portal Token Bridge so use the Emitter Portal Addr + let emitter: EmitterAddrAccount = EmitterAddrAccount::try_from_slice(&ctx.accounts.emitter_acc.data.borrow()).unwrap(); + if vaa.emitter_chain != emitter.emitter_chain || + vaa.emitter_address != emitter.emitter_address { + return err!(XmintError::VAAEmitterMismatch) + } + + + + // Complete Transfer of P3 on Portal + let complete_wrapped_ix = Instruction { + program_id: Pubkey::from_str(TOKEN_BRIDGE_ADDRESS).unwrap(), + accounts: vec![ + AccountMeta::new(ctx.accounts.payer.key(), true), + AccountMeta::new_readonly(ctx.accounts.token_bridge_config.key(), false), + AccountMeta::new_readonly(ctx.accounts.core_bridge_vaa.key(), false), + AccountMeta::new(ctx.accounts.token_bridge_claim_key.key(), false), + AccountMeta::new_readonly(ctx.accounts.emitter_acc.key(), false), + AccountMeta::new(ctx.accounts.weth_ata_account.key(), false), // to + AccountMeta::new_readonly(ctx.accounts.redeemer.key(), true), // to owner + AccountMeta::new(ctx.accounts.fee_recipient.key(), false), + AccountMeta::new(ctx.accounts.weth_mint.key(), false), + AccountMeta::new_readonly(ctx.accounts.weth_mint_wrapped_meta.key(), false), + AccountMeta::new_readonly(ctx.accounts.mint_authority_wrapped.key(), false), + AccountMeta::new_readonly(rent::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(ctx.accounts.core_bridge.key(), false), + AccountMeta::new_readonly(spl_id, false), + ], + data: ( + crate::portal::Instruction::CompleteWrappedWithPayload, + {} + ).try_to_vec()? + }; + + let accounts = vec![ + ctx.accounts.payer.to_account_info(), + ctx.accounts.token_bridge_config.to_account_info(), + ctx.accounts.core_bridge_vaa.to_account_info(), + ctx.accounts.token_bridge_claim_key.to_account_info(), + ctx.accounts.emitter_acc.to_account_info(), + ctx.accounts.weth_ata_account.to_account_info(), //to + ctx.accounts.redeemer.to_account_info(), // to_owner + ctx.accounts.fee_recipient.to_account_info(), + ctx.accounts.weth_mint.to_account_info(), + ctx.accounts.weth_mint_wrapped_meta.to_account_info(), + ctx.accounts.mint_authority_wrapped.to_account_info(), + ctx.accounts.rent_account.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ctx.accounts.core_bridge.to_account_info(), + ctx.accounts.spl_program.to_account_info() + ]; + + let complete_p3_seeds:&[&[u8]] = &[ + b"redeemer", + &[*ctx.bumps.get("redeemer").unwrap()] + ]; + + invoke_signed( + &complete_wrapped_ix, + &accounts, + &[&complete_p3_seeds[..]] + )?; + + //// Figure out how many tokens to mint based on p3 payload + let payload: PayloadTransferWithPayload = PayloadTransferWithPayload::deserialize(&mut vaa.payload.as_slice())?; + ctx.accounts.receipt.amt_to_mint = payload.amount.as_u64() * 100; + ctx.accounts.receipt.foreign_chain = vaa.emitter_chain; + ctx.accounts.receipt.foreign_receipient = payload.payload.as_slice().try_into().unwrap(); + ctx.accounts.receipt.claimed = false; + + Ok(()) +} + +``` + +## claim_foreign_purchase +Due to account constraints of Solana, this will take the amount of tokens that should be minted in the previous step and call the Wormhole Token Bridge to send the tokens back to the user. + +```rust + +pub fn claim_foreign_purchase(ctx:Context) -> Result<()> { + if ctx.accounts.receipt.claimed { + return err!(XmintError::ReceiptClaimed); + } + + let mint_seeds:&[&[u8]] = &[ + b"mint_authority", + &[*ctx.bumps.get("xmint_authority").unwrap()] + ]; + + mint_to( + CpiContext { + accounts: MintTo { + mint: ctx.accounts.xmint_token_mint.to_account_info(), + authority: ctx.accounts.xmint_authority.to_account_info(), + to: ctx.accounts.xmint_ata_account.to_account_info(), + }, + remaining_accounts: vec![], + program: ctx.accounts.spl_program.to_account_info(), + signer_seeds: &[&mint_seeds[..]] + }, + ctx.accounts.receipt.amt_to_mint + )?; + + // Delgate transfer authority to Token Bridge for the newly minted tokens + approve( + CpiContext::new_with_signer( + ctx.accounts.spl_program.to_account_info(), + Approve { + to: ctx.accounts.xmint_ata_account.to_account_info(), + delegate: ctx.accounts.token_bridge_authority_signer.to_account_info(), + authority: ctx.accounts.xmint_authority.to_account_info() + }, + &[&mint_seeds[..]] + ), + ctx.accounts.receipt.amt_to_mint + )?; + + // Transfer tokens from Contract PDA to P1 on Portal + // Instruction + let transfer_ix = Instruction { + program_id: Pubkey::from_str(TOKEN_BRIDGE_ADDRESS).unwrap(), + accounts: vec![ + AccountMeta::new(ctx.accounts.payer.key(), true), + AccountMeta::new_readonly(ctx.accounts.token_bridge_config.key(), false), + AccountMeta::new(ctx.accounts.xmint_ata_account.key(), false), + AccountMeta::new(ctx.accounts.xmint_token_mint.key(), false), + AccountMeta::new(ctx.accounts.token_bridge_mint_custody.key(), false), + AccountMeta::new_readonly(ctx.accounts.token_bridge_authority_signer.key(), false), + AccountMeta::new_readonly(ctx.accounts.token_bridge_custody_signer.key(), false), + AccountMeta::new(ctx.accounts.core_bridge_config.key(), false), + AccountMeta::new(ctx.accounts.xmint_transfer_msg_key.key(), true), + AccountMeta::new_readonly(ctx.accounts.token_bridge_emitter.key(), false), + AccountMeta::new(ctx.accounts.token_bridge_sequence_key.key(), false), + AccountMeta::new(ctx.accounts.core_bridge_fee_collector.key(), false), + AccountMeta::new_readonly(ctx.accounts.clock.key(), false), + // Dependencies + AccountMeta::new_readonly(ctx.accounts.rent_account.key(), false), + AccountMeta::new_readonly(ctx.accounts.system_program.key(), false), + // Program + AccountMeta::new_readonly(ctx.accounts.core_bridge.key(), false), + AccountMeta::new_readonly(ctx.accounts.spl_program.key(), false), + ], + data: ( + crate::portal::Instruction::TransferNative, + TransferNativeData { + nonce: ctx.accounts.config.nonce, + amount: ctx.accounts.receipt.amt_to_mint, + fee: 0, + target_address: ctx.accounts.receipt.foreign_receipient, + target_chain: ctx.accounts.receipt.foreign_chain + } + ).try_to_vec()? + }; + + // Accounts + let transfer_accs = vec![ + ctx.accounts.payer.to_account_info(), + ctx.accounts.token_bridge_config.to_account_info(), + ctx.accounts.xmint_ata_account.to_account_info(), + ctx.accounts.xmint_token_mint.to_account_info(), + ctx.accounts.token_bridge_mint_custody.to_account_info(), + ctx.accounts.token_bridge_authority_signer.to_account_info(), + ctx.accounts.token_bridge_custody_signer.to_account_info(), + ctx.accounts.core_bridge_config.to_account_info(), + ctx.accounts.xmint_transfer_msg_key.to_account_info(), + ctx.accounts.token_bridge_emitter.to_account_info(), + ctx.accounts.token_bridge_sequence_key.to_account_info(), + ctx.accounts.core_bridge_fee_collector.to_account_info(), + ctx.accounts.clock.to_account_info(), + // Dependencies + ctx.accounts.rent_account.to_account_info(), + ctx.accounts.system_program.to_account_info(), + // Program + ctx.accounts.core_bridge.to_account_info(), + ctx.accounts.spl_program.to_account_info(), + ]; + + // Signer Seeds + let xmint_authority_seeds:&[&[u8]] = &[ + b"mint_authority", + &[*ctx.bumps.get("xmint_authority").unwrap()] + ]; + + invoke_signed( + &transfer_ix, + &transfer_accs, + &[&xmint_authority_seeds[..]] + )?; + + ctx.accounts.config.nonce += 1; + ctx.accounts.receipt.claimed = true; + Ok(()) +} + +``` + +## serialize_vaa +This helpfer function takes a message payload and serializes it into a Wormhole Guardian compliant structure. + +```rust + +pub fn serialize_vaa(vaa: &MessageData) -> Vec { + let mut v = Cursor::new(Vec::new()); + v.write_u32::(vaa.vaa_time).unwrap(); + v.write_u32::(vaa.nonce).unwrap(); + v.write_u16::(vaa.emitter_chain.clone() as u16).unwrap(); + v.write(&vaa.emitter_address).unwrap(); + v.write_u64::(vaa.sequence).unwrap(); + v.write_u8(vaa.consistency_level).unwrap(); + v.write(&vaa.payload).unwrap(); + v.into_inner() +} + +``` + + +## vaa_hash +This helper function takes the serialized output from the previous helper function and hash it to be verified by a Wormhole Guardian. + +```rust + +pub fn vaa_hash(vaa: AccountInfo) -> [u8; 32] { + let vaa = PostedMessageData::try_from_slice(&vaa.data.borrow()).unwrap().0; + let serialized_vaa = serialize_vaa(&vaa); + let mut h = sha3::Keccak256::default(); + h.write(serialized_vaa.as_slice()).unwrap(); + let vaa_hash: [u8; 32] = h.finalize().into(); + return vaa_hash; +} + +``` \ No newline at end of file