parent
2747839bd4
commit
4ba7885c62
|
@ -16,6 +16,14 @@ Initializes a new Bridge at `bridge`.
|
|||
| 3 | guardian_set | GuardianSet | | ✅ | ✅ | ✅ |
|
||||
| 4 | payer | Account | ✅ | | | |
|
||||
|
||||
#### PokeProposal
|
||||
|
||||
Pokes a `TransferOutProposal` so it is reprocessed by the guardians.
|
||||
|
||||
| Index | Name | Type | signer | writeable | empty | derived |
|
||||
| ----- | ------ | ------------ | ------ | --------- | ----- | ------- |
|
||||
| 0 | proposal | TransferOutProposal | | ✅ | ️ | ✅ |
|
||||
|
||||
#### TransferOut
|
||||
|
||||
Burns a wrapped asset `token` from `sender` on the Solana chain.
|
||||
|
@ -29,11 +37,12 @@ Parameters:
|
|||
| 0 | bridge_p | BridgeProgram | | | ️ | |
|
||||
| 1 | sys | SystemProgram | | | ️ | |
|
||||
| 2 | token_program | SplToken | | | ️ | |
|
||||
| 3 | token_account | TokenAccount | | ✅ | | |
|
||||
| 4 | bridge | BridgeConfig | | | | |
|
||||
| 5 | proposal | TransferOutProposal | | ✅ | ✅ | ✅ |
|
||||
| 6 | token | WrappedAsset | | ✅ | | ✅ |
|
||||
| 7 | payer | Account | ✅ | | | |
|
||||
| 3 | clock | Sysvar | | | ️ | ✅ |
|
||||
| 4 | token_account | TokenAccount | | ✅ | | |
|
||||
| 5 | bridge | BridgeConfig | | | | |
|
||||
| 6 | proposal | TransferOutProposal | | ✅ | ✅ | ✅ |
|
||||
| 7 | token | WrappedAsset | | ✅ | | ✅ |
|
||||
| 8 | payer | Account | ✅ | | | |
|
||||
|
||||
#### TransferOutNative
|
||||
|
||||
|
@ -47,12 +56,13 @@ The transfer proposal will be tracked at a new account `proposal` where a VAA wi
|
|||
| 0 | bridge_p | BridgeProgram | | | ️ | |
|
||||
| 1 | sys | SystemProgram | | | ️ | |
|
||||
| 2 | token_program | SplToken | | | ️ | |
|
||||
| 3 | token_account | TokenAccount | | ✅ | | |
|
||||
| 4 | bridge | BridgeConfig | | | | |
|
||||
| 5 | proposal | TransferOutProposal | | ✅ | ✅ | ✅ |
|
||||
| 6 | token | Mint | | ✅ | | |
|
||||
| 7 | payer | Account | ✅ | | | |
|
||||
| 8 | custody_account | TokenAccount | | ✅ | opt | ✅ |
|
||||
| 3 | clock | Sysvar | | | ️ | ✅ |
|
||||
| 4 | token_account | TokenAccount | | ✅ | | |
|
||||
| 5 | bridge | BridgeConfig | | | | |
|
||||
| 6 | proposal | TransferOutProposal | | ✅ | ✅ | ✅ |
|
||||
| 7 | token | Mint | | ✅ | | |
|
||||
| 8 | payer | Account | ✅ | | | |
|
||||
| 9 | custody_account | TokenAccount | | ✅ | opt | ✅ |
|
||||
|
||||
#### EvictTransferOut
|
||||
|
||||
|
|
|
@ -137,14 +137,6 @@ impl Agent for AgentImpl {
|
|||
|
||||
println!("lockup changed in slot: {}", v.context.slot);
|
||||
|
||||
let time = match rpc.get_block_time(v.context.slot) {
|
||||
Ok(v) => v as u64,
|
||||
Err(e) => {
|
||||
println!("failed to fetch block time for event: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let b = match Bridge::unpack_immutable::<TransferOutProposal>(
|
||||
v.value.account.data.as_slice(),
|
||||
) {
|
||||
|
@ -163,7 +155,7 @@ impl Agent for AgentImpl {
|
|||
LockupEvent {
|
||||
slot: v.context.slot,
|
||||
lockup_address: v.value.pubkey.to_string(),
|
||||
time,
|
||||
time: b.lockup_time as u64,
|
||||
event: Some(Event::New(LockupEventNew {
|
||||
nonce: b.nonce,
|
||||
source_chain: CHAIN_ID_SOLANA as u32,
|
||||
|
@ -181,7 +173,7 @@ impl Agent for AgentImpl {
|
|||
LockupEvent {
|
||||
slot: v.context.slot,
|
||||
lockup_address: v.value.pubkey.to_string(),
|
||||
time,
|
||||
time: b.lockup_time as u64,
|
||||
event: Some(Event::VaaPosted(LockupEventVaaPosted {
|
||||
nonce: b.nonce,
|
||||
source_chain: CHAIN_ID_SOLANA as u32,
|
||||
|
|
|
@ -14,7 +14,7 @@ use solana_sdk::{
|
|||
|
||||
use crate::error::Error;
|
||||
use crate::error::Error::VAATooLong;
|
||||
use crate::instruction::BridgeInstruction::{Initialize, PostVAA, TransferOut};
|
||||
use crate::instruction::BridgeInstruction::{Initialize, PokeProposal, PostVAA, TransferOut};
|
||||
use crate::state::{AssetMeta, Bridge, BridgeConfig};
|
||||
use crate::vaa::{VAABody, VAA};
|
||||
|
||||
|
@ -123,6 +123,9 @@ pub enum BridgeInstruction {
|
|||
/// Deletes a `ExecutedVAA` after the `VAA_EXPIRATION_TIME` is over to free up space on chain.
|
||||
/// This returns the rent to the sender.
|
||||
EvictClaimedVAA(),
|
||||
|
||||
/// Pokes a proposal with no valid VAAs attached so guardians reprocess it.
|
||||
PokeProposal(),
|
||||
}
|
||||
|
||||
impl BridgeInstruction {
|
||||
|
@ -153,6 +156,7 @@ impl BridgeInstruction {
|
|||
let payload: VAAData = input[1..].to_vec();
|
||||
PostVAA(payload)
|
||||
}
|
||||
5 => PokeProposal(),
|
||||
_ => return Err(ProgramError::InvalidInstructionData),
|
||||
})
|
||||
}
|
||||
|
@ -201,6 +205,9 @@ impl BridgeInstruction {
|
|||
Self::EvictClaimedVAA() => {
|
||||
output[0] = 4;
|
||||
}
|
||||
Self::PokeProposal() => {
|
||||
output[0] = 5;
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
@ -273,6 +280,7 @@ pub fn transfer_out(
|
|||
AccountMeta::new_readonly(*program_id, false),
|
||||
AccountMeta::new_readonly(solana_sdk::system_program::id(), false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
AccountMeta::new_readonly(solana_sdk::sysvar::clock::id(), false),
|
||||
AccountMeta::new(*token_account, false),
|
||||
AccountMeta::new(bridge_key, false),
|
||||
AccountMeta::new(transfer_key, false),
|
||||
|
@ -374,6 +382,23 @@ pub fn post_vaa(
|
|||
})
|
||||
}
|
||||
|
||||
/// Creates an 'PokeProposal' instruction.
|
||||
#[cfg(not(target_arch = "bpf"))]
|
||||
pub fn poke_proposal(
|
||||
program_id: &Pubkey,
|
||||
transfer_proposal: &Pubkey,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let data = BridgeInstruction::PokeProposal().serialize()?;
|
||||
|
||||
let mut accounts = vec![AccountMeta::new(*transfer_proposal, false)];
|
||||
|
||||
Ok(Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/// Unpacks a reference from a bytes buffer.
|
||||
pub fn unpack<T>(input: &[u8]) -> Result<&T, ProgramError> {
|
||||
if input.len() < size_of::<u8>() + size_of::<T>() {
|
||||
|
|
|
@ -59,6 +59,11 @@ impl Bridge {
|
|||
|
||||
Self::process_vaa(program_id, accounts, vaa_body, &vaa)
|
||||
}
|
||||
PokeProposal() => {
|
||||
info!("Instruction: PokeProposal");
|
||||
|
||||
Self::process_poke(program_id, accounts)
|
||||
}
|
||||
_ => panic!(""),
|
||||
}
|
||||
}
|
||||
|
@ -133,6 +138,23 @@ impl Bridge {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Transfers a wrapped asset out
|
||||
pub fn process_poke(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
let proposal_info = next_account_info(account_info_iter)?;
|
||||
|
||||
let mut transfer_data = proposal_info.data.borrow_mut();
|
||||
let mut proposal: &mut TransferOutProposal = Self::unpack(&mut transfer_data)?;
|
||||
if proposal.vaa_time != 0 {
|
||||
return Err(Error::VAAAlreadySubmitted.into());
|
||||
}
|
||||
|
||||
// Increase poke counter
|
||||
proposal.poke_counter += 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transfers a wrapped asset out
|
||||
pub fn process_transfer_out(
|
||||
program_id: &Pubkey,
|
||||
|
@ -144,6 +166,7 @@ impl Bridge {
|
|||
next_account_info(account_info_iter)?; // Bridge program
|
||||
next_account_info(account_info_iter)?; // System program
|
||||
next_account_info(account_info_iter)?; // Token program
|
||||
let clock_info = next_account_info(account_info_iter)?;
|
||||
let sender_account_info = next_account_info(account_info_iter)?;
|
||||
let bridge_info = next_account_info(account_info_iter)?;
|
||||
let transfer_info = next_account_info(account_info_iter)?;
|
||||
|
@ -153,6 +176,7 @@ impl Bridge {
|
|||
let sender = Bridge::token_account_deserialize(sender_account_info)?;
|
||||
let bridge = Bridge::bridge_deserialize(bridge_info)?;
|
||||
let mint = Bridge::mint_deserialize(mint_info)?;
|
||||
let clock = Clock::from_account_info(clock_info)?;
|
||||
|
||||
// Does the token belong to the mint
|
||||
if sender.mint != *mint_info.key {
|
||||
|
@ -209,6 +233,7 @@ impl Bridge {
|
|||
transfer.foreign_address = t.target;
|
||||
transfer.amount = t.amount;
|
||||
transfer.to_chain_id = t.chain_id;
|
||||
transfer.lockup_time = clock.unix_timestamp as u32;
|
||||
|
||||
// Make sure decimals are correct
|
||||
transfer.asset = AssetMeta {
|
||||
|
@ -231,6 +256,7 @@ impl Bridge {
|
|||
next_account_info(account_info_iter)?; // Bridge program
|
||||
next_account_info(account_info_iter)?; // System program
|
||||
next_account_info(account_info_iter)?; // Token program
|
||||
let clock_info = next_account_info(account_info_iter)?;
|
||||
let sender_account_info = next_account_info(account_info_iter)?;
|
||||
let bridge_info = next_account_info(account_info_iter)?;
|
||||
let transfer_info = next_account_info(account_info_iter)?;
|
||||
|
@ -241,6 +267,7 @@ impl Bridge {
|
|||
let sender = Bridge::token_account_deserialize(sender_account_info)?;
|
||||
let mint = Bridge::mint_deserialize(mint_info)?;
|
||||
let bridge = Bridge::bridge_deserialize(bridge_info)?;
|
||||
let clock = Clock::from_account_info(clock_info)?;
|
||||
|
||||
// Does the token belong to the mint
|
||||
if sender.mint != *mint_info.key {
|
||||
|
@ -317,6 +344,7 @@ impl Bridge {
|
|||
transfer.source_address = sender_account_info.key.to_bytes();
|
||||
transfer.foreign_address = t.target;
|
||||
transfer.nonce = t.nonce;
|
||||
transfer.lockup_time = clock.unix_timestamp as u32;
|
||||
|
||||
// Don't use the user-given data as we don't check mint = AssetMeta.address
|
||||
transfer.asset = AssetMeta {
|
||||
|
|
|
@ -69,6 +69,10 @@ pub struct TransferOutProposal {
|
|||
pub vaa: [u8; MAX_VAA_SIZE + 1],
|
||||
/// time the vaa was submitted
|
||||
pub vaa_time: u32,
|
||||
/// time the lockup was created
|
||||
pub lockup_time: u32,
|
||||
/// times the proposal has been poked
|
||||
pub poke_counter: u8,
|
||||
|
||||
/// Is `true` if this structure has been initialized.
|
||||
pub is_initialized: bool,
|
||||
|
|
|
@ -85,6 +85,18 @@ fn command_deploy_bridge(
|
|||
Ok(Some(transaction))
|
||||
}
|
||||
|
||||
fn command_poke_proposal(config: &Config, bridge: &Pubkey, proposal: &Pubkey) -> CommmandResult {
|
||||
println!("Poking lockup");
|
||||
|
||||
let ix = poke_proposal(bridge, proposal)?;
|
||||
let mut transaction = Transaction::new_with_payer(&[ix], Some(&config.fee_payer.pubkey()));
|
||||
|
||||
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
|
||||
check_fee_payer_balance(config, fee_calculator.calculate_fee(&transaction.message()))?;
|
||||
transaction.sign(&[&config.fee_payer, &config.owner], recent_blockhash);
|
||||
Ok(Some(transaction))
|
||||
}
|
||||
|
||||
fn command_lock_tokens(
|
||||
config: &Config,
|
||||
bridge: &Pubkey,
|
||||
|
@ -954,6 +966,34 @@ fn main() {
|
|||
.help("The vaa to be posted"),
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("poke")
|
||||
.about("Poke a proposal so it's retried")
|
||||
.arg(
|
||||
Arg::with_name("bridge")
|
||||
.long("bridge")
|
||||
.value_name("BRIDGE_KEY")
|
||||
.validator(is_pubkey_or_keypair)
|
||||
.takes_value(true)
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help(
|
||||
"Specify the bridge program public key"
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("proposal")
|
||||
.long("proposal")
|
||||
.value_name("PROPOSAL_KEY")
|
||||
.validator(is_pubkey_or_keypair)
|
||||
.takes_value(true)
|
||||
.index(2)
|
||||
.required(true)
|
||||
.help(
|
||||
"Specify the transfer proposal to poke"
|
||||
),
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("wrapped-address")
|
||||
.about("Derive wrapped asset address")
|
||||
|
@ -1114,6 +1154,11 @@ fn main() {
|
|||
let vaa = hex::decode(vaa_string).unwrap();
|
||||
command_submit_vaa(&config, &bridge, vaa.as_slice())
|
||||
}
|
||||
("poke", Some(arg_matches)) => {
|
||||
let bridge = pubkey_of(arg_matches, "bridge").unwrap();
|
||||
let proposal = pubkey_of(arg_matches, "proposal").unwrap();
|
||||
command_poke_proposal(&config, &bridge, &proposal)
|
||||
}
|
||||
("wrapped-address", Some(arg_matches)) => {
|
||||
let bridge = pubkey_of(arg_matches, "bridge").unwrap();
|
||||
let chain = value_t_or_exit!(arg_matches, "chain", u8);
|
||||
|
|
|
@ -9,7 +9,9 @@ import {WormholeFactory} from "../contracts/WormholeFactory";
|
|||
import {BRIDGE_ADDRESS} from "../config";
|
||||
import {keccak256} from "ethers/utils";
|
||||
import BN from 'bn.js';
|
||||
import {PublicKey} from "@solana/web3.js";
|
||||
import {PublicKey, Transaction} from "@solana/web3.js";
|
||||
import KeyContext from "../providers/KeyContext";
|
||||
import ClientContext from "../providers/ClientContext";
|
||||
|
||||
// @ts-ignore
|
||||
window.ethereum.enable();
|
||||
|
@ -32,6 +34,8 @@ function TransferProposals() {
|
|||
let t = useContext(SolanaTokenContext);
|
||||
let tokens = useContext(SolanaTokenContext);
|
||||
let b = useContext(BridgeContext);
|
||||
let k = useContext(KeyContext);
|
||||
let c = useContext(ClientContext);
|
||||
|
||||
let [lockups, setLockups] = useState<LockupWithStatus[]>([])
|
||||
|
||||
|
@ -84,13 +88,31 @@ function TransferProposals() {
|
|||
message.loading({content: "Waiting for transaction to be mined...", key: "eth_tx", duration: 1000})
|
||||
await tx.wait(1)
|
||||
message.success({content: "Execution of VAA succeeded", key: "eth_tx"})
|
||||
}
|
||||
|
||||
let pokeProposal = async (proposalAddress: PublicKey) => {
|
||||
message.loading({content: "Poking lockup ...", key: "poke"}, 1000)
|
||||
|
||||
let ix = await b.createPokeProposalInstruction(proposalAddress);
|
||||
let recentHash = await c.getRecentBlockhash();
|
||||
let tx = new Transaction();
|
||||
tx.recentBlockhash = recentHash.blockhash
|
||||
tx.add(ix)
|
||||
tx.sign(k)
|
||||
try {
|
||||
await c.sendTransaction(tx, [k])
|
||||
message.success({content: "Poke succeeded", key: "poke"})
|
||||
} catch (e) {
|
||||
message.error({content: "Poke failed", key: "poke"})
|
||||
}
|
||||
}
|
||||
|
||||
let statusToPrompt = (v: LockupWithStatus) => {
|
||||
switch (v.status) {
|
||||
case LockupStatus.AWAITING_VAA:
|
||||
return ("Awaiting VAA");
|
||||
return (<>Awaiting VAA (<a onClick={() => {
|
||||
pokeProposal(v.lockupAddress)
|
||||
}}>poke</a>)</>);
|
||||
case LockupStatus.UNCLAIMED_VAA:
|
||||
return (<Button onClick={() => {
|
||||
executeVAA(v)
|
||||
|
|
|
@ -14,6 +14,7 @@ export interface AssetMeta {
|
|||
}
|
||||
|
||||
export interface Lockup {
|
||||
lockupAddress: PublicKey,
|
||||
amount: BN,
|
||||
toChain: number,
|
||||
sourceAddress: PublicKey,
|
||||
|
@ -24,6 +25,7 @@ export interface Lockup {
|
|||
nonce: number,
|
||||
vaa: Uint8Array,
|
||||
vaaTime: number,
|
||||
pokeCounter: number,
|
||||
initialized: boolean,
|
||||
}
|
||||
|
||||
|
@ -93,6 +95,7 @@ class SolanaBridge {
|
|||
{pubkey: this.programID, isSigner: false, isWritable: false},
|
||||
{pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false},
|
||||
{pubkey: this.tokenProgram, isSigner: false, isWritable: false},
|
||||
{pubkey: solanaWeb3.SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false},
|
||||
{pubkey: tokenAccount, isSigner: false, isWritable: true},
|
||||
{pubkey: configKey, isSigner: false, isWritable: false},
|
||||
|
||||
|
@ -115,6 +118,30 @@ class SolanaBridge {
|
|||
});
|
||||
}
|
||||
|
||||
createPokeProposalInstruction(
|
||||
proposalAccount: PublicKey,
|
||||
): TransactionInstruction {
|
||||
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'),]);
|
||||
|
||||
const data = Buffer.alloc(dataLayout.span);
|
||||
dataLayout.encode(
|
||||
{
|
||||
instruction: 5, // PokeProposal instruction
|
||||
},
|
||||
data,
|
||||
);
|
||||
|
||||
const keys = [
|
||||
{pubkey: proposalAccount, isSigner: false, isWritable: true},
|
||||
];
|
||||
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: this.programID,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// fetchAssetMeta fetches the AssetMeta for an SPL token
|
||||
async fetchAssetMeta(
|
||||
mint: PublicKey,
|
||||
|
@ -183,14 +210,15 @@ class SolanaBridge {
|
|||
BufferLayout.blob(1001, 'vaa'),
|
||||
BufferLayout.seq(BufferLayout.u8(), 3), // 4 byte alignment because a u32 is following
|
||||
BufferLayout.u32('vaaTime'),
|
||||
BufferLayout.u8('pokeCounter'),
|
||||
BufferLayout.u8('initialized'),
|
||||
]);
|
||||
|
||||
let accounts: Lockup[] = [];
|
||||
for (let acc of raw_accounts) {
|
||||
acc = acc.account;
|
||||
let parsedAccount = dataLayout.decode(bs58.decode(acc.data))
|
||||
let parsedAccount = dataLayout.decode(bs58.decode(acc.account.data))
|
||||
accounts.push({
|
||||
lockupAddress: acc.pubkey,
|
||||
amount: new BN(parsedAccount.amount, 2, "le"),
|
||||
assetAddress: parsedAccount.assetAddress,
|
||||
assetChain: parsedAccount.assetChain,
|
||||
|
@ -201,7 +229,8 @@ class SolanaBridge {
|
|||
targetAddress: parsedAccount.targetAddress,
|
||||
toChain: parsedAccount.toChain,
|
||||
vaa: parsedAccount.vaa,
|
||||
vaaTime: parsedAccount.vaaTime
|
||||
vaaTime: parsedAccount.vaaTime,
|
||||
pokeCounter: parsedAccount.pokeCounter
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue