From 851014016559009198f1cb929e1152e7fe3ea4df Mon Sep 17 00:00:00 2001 From: Hendrik Hofstadt Date: Thu, 19 Nov 2020 22:47:09 +0100 Subject: [PATCH] subsidize guardian transactions using fees (#82) * subsidize guardian transactions using fees * reuse transfer function * evict signature state on inbound transfers * fix mutability issues due to copying * add fee refund * unify fee calculation * add fee documentation * Unflip tables * type annotation --- docs/protocol.md | 75 +++++++++++++- docs/solana_program.md | 25 ++--- solana/bridge/src/instruction.rs | 5 +- solana/bridge/src/processor.rs | 161 ++++++++++++++++++++++++------- solana/bridge/src/state.rs | 16 --- 5 files changed, 216 insertions(+), 66 deletions(-) diff --git a/docs/protocol.md b/docs/protocol.md index 14c61c8e..f1b5e100 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -217,7 +217,80 @@ The user can then get the VAA from the `LockProposal` and submit it on the forei ### Fees -TODO \o/ +Fees exist for 2 reasons: spam prevention and guardian cost coverage. + +Costs for guardians: + +Assuming no hosting costs for a guardian operation (blockchain and guardian nodes), the only costs +that need to be covered by a guardian operator are Solana transaction fees as well as rent costs for newly +created account (used to store application information). + +**For a transfer from Solana to a foreign chain (20 guardians; 14 quorum):** + +Transactions required: `3 (signatures + verify) + 1 (post VAA)` + +Accounts created: `1 ClaimedVAA + 1 SignatureState` + +Costs: +``` +4 TX (14 secp signatures + 4 ed25519) + ClaimedVAA (exemption rent) + SignatureState (exemption rent) +18 * 10_000 + (36+128) * 6962 + (1337+128) * 6962 +11521098 lamports = 0.0115 SOL +``` + +**For a transfer from a foreign chain to Solana (20 guardians; 14 quorum):** + +Transactions required: `3 (signatures + verify) + 1 (post VAA)` + +Accounts created: `1 ClaimedVAA + 1 SignatureState (temporary; evicted in PostVAA)` + +Costs: +``` +4 TX (14 secp signatures + 4 ed25519) + ClaimedVAA (exemption rent) +18 * 10_000 + (36+128) * 6962 +1321768 lamports = 0.0013 SOL +``` + +--- + +In order to cover rent costs there exists a subsidy pool controlled by the bridge to cover rent payments. +While the guardian needs to hold enough SOL to pay for the rent, it is automatically refunded by the pool, +in case the pool has sufficient balance. + +This subsidy pool is funded by transaction fees. +Additionally, the subsidy pool subsidizes the transactions fees paid by the guardian submitting the VAA. +As long as the pool has a sufficient balance, it will try to refund transaction fees to the guardian. + +Since Wormhole does not require foreign chain users to own SOL, Wormhole can't charge subsidy fees on inbound +transfers. Assuming a balance between inbound and outbound transfers, outbound transfers need to subsidize +inbound Solana transfers. + +Additionally, foreign chain contracts might start charging additional fees in the future. + +--- + +The bridge can handle at most transactions per second. Therefore, the fees should prevent spam +by dynamically adjusting to load. This is particularly useful on Solana where fees are low and spamming +would be cheap. + +Dynamic fees should be cheap while the system is under low and medium load and high while the system is +close or above its capacity. +To prevent sudden fee changes, the fee system has inertia. + +Fees scale as follows `fee = (tps/tps_max)^6`. +The result is the fee per transfer in SOL. So at max capacity, the price per transfer is 1SOL. +TPS is measured over a 30 second window. + +The minimum fee is the equivalent of 2x the rent of SignatureState and ClaimedVAA to cover the cost +of this transfer and about 10 inbound transfers. + +--- + +The above design can currently not be implemented due to limitations in the Solana BPF VM. + +In the current design, tx fees are refunded, rents are subsidized by the bridge and transfers out of Solana +cost a fixed fee of 2x (ClaimedVAA rent + SignatureState rent + VAA submission fee), which will roughly +pay for 1 outbound + ~10 inbound transfers. ### Config changes #### Guardian set changes diff --git a/docs/solana_program.md b/docs/solana_program.md index 67ab6911..4d498601 100644 --- a/docs/solana_program.md +++ b/docs/solana_program.md @@ -12,7 +12,7 @@ Initializes a new Bridge at `bridge`. | ----- | ------ | ------------ | ------ | --------- | ----- | ------- | | 0 | sys | SystemProgram | | | | | | 1 | clock | Sysvar | | | | ✅ | -| 2 | bridge | BridgeConfig | | | ✅ | ✅ | +| 2 | bridge | BridgeConfig | | ✅ | ✅ | ✅ | | 3 | guardian_set | GuardianSet | | ✅ | ✅ | ✅ | | 4 | payer | Account | ✅ | | | | @@ -42,14 +42,15 @@ Creates a new `WrappedAsset` to be used to create accounts and later receive tra Checks secp checks (in the previous instruction) and stores results. -| Index | Name | Type | signer | writeable | empty | derived | -| ----- | ------ | ------------ | ------ | --------- | ----- | ------- | -| 0 | bridge_p | BridgeProgram | | | | | -| 1 | sys | SystemProgram | | | | | -| 2 | instructions | Sysvar | | | | ✅ | -| 3 | sig_status | SignatureState | | ✅ | | | -| 4 | guardian_set | GuardianSet | | | | ✅ | -| 5 | payer | Account | ✅ | | | | +| Index | Name | Type | signer | writeable | empty | derived | +| ----- | ------ | ------------ | ------ | --------- | ----- | ------- | +| 0 | bridge_p | BridgeProgram | | | | | +| 1 | sys | SystemProgram | | | | | +| 2 | instructions | Sysvar | | | | ✅ | +| 3 | bridge_config | BridgeConfig | | ✅ | | ✅ | +| 4 | sig_status | SignatureState | | ✅ | | | +| 5 | guardian_set | GuardianSet | | | | ✅ | +| 6 | payer | Account | ✅ | | | | #### TransferOut @@ -65,7 +66,7 @@ Parameters: | 1 | sys | SystemProgram | | | | | | 2 | token_program | SplToken | | | | | | 3 | rent | Sysvar | | | | ✅ | -| 4 | clock | Sysvar | | | ✅ | | +| 4 | clock | Sysvar | | | ✅ | | | 5 | token_account | TokenAccount | | ✅ | | | | 6 | bridge | BridgeConfig | | | | | | 7 | proposal | TransferOutProposal | | ✅ | ✅ | ✅ | @@ -131,10 +132,10 @@ All require: | 1 | sys | SystemProgram | | | | | | 2 | rent | Sysvar | | | | ✅ | | 3 | clock | Sysvar | | | | ✅ | -| 4 | bridge | BridgeConfig | | | | | +| 4 | bridge | BridgeConfig | | ✅ | | | | 5 | guardian_set | GuardianSet | | | | | | 6 | claim | ExecutedVAA | | ✅ | ✅ | ✅ | -| 7 | sig_info | SigState | | | ✅ | | +| 7 | sig_info | SigState | | ✅ | ✅ | | | 8 | payer | Account | ✅ | | | | followed by: diff --git a/solana/bridge/src/instruction.rs b/solana/bridge/src/instruction.rs index c0d02d43..a271941c 100644 --- a/solana/bridge/src/instruction.rs +++ b/solana/bridge/src/instruction.rs @@ -330,7 +330,7 @@ pub fn transfer_out( AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false), AccountMeta::new_readonly(solana_program::sysvar::clock::id(), false), AccountMeta::new(*token_account, false), - AccountMeta::new(bridge_key, false), + AccountMeta::new_readonly(bridge_key, false), AccountMeta::new(transfer_key, false), AccountMeta::new(*token_mint, false), AccountMeta::new(*payer, true), @@ -368,6 +368,7 @@ pub fn verify_signatures( AccountMeta::new_readonly(*program_id, false), AccountMeta::new_readonly(solana_program::system_program::id(), false), AccountMeta::new_readonly(solana_program::sysvar::instructions::id(), false), + AccountMeta::new(bridge_key, false), AccountMeta::new(*signature_acc, false), AccountMeta::new_readonly(guardian_set_key, false), AccountMeta::new(*payer, true), @@ -494,7 +495,7 @@ pub fn create_wrapped( AccountMeta::new_readonly(solana_program::system_program::id(), false), AccountMeta::new_readonly(spl_token::id(), false), AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false), - AccountMeta::new(bridge_key, false), + AccountMeta::new_readonly(bridge_key, false), AccountMeta::new(*payer, true), AccountMeta::new(wrapped_mint_key, false), AccountMeta::new(wrapped_meta_key, false), diff --git a/solana/bridge/src/processor.rs b/solana/bridge/src/processor.rs index 82965463..f5897716 100644 --- a/solana/bridge/src/processor.rs +++ b/solana/bridge/src/processor.rs @@ -35,6 +35,10 @@ use crate::{ use solana_program::program_pack::Pack; use std::borrow::BorrowMut; use std::ops::Add; +use solana_program::fee_calculator::FeeCalculator; + +/// Tx fee of Signature checks and PostVAA (see docs for calculation) +const VAA_TX_FEE: u64 = 18 * 10000; /// SigInfo contains metadata about signers in a VerifySignature ix struct SigInfo { @@ -123,12 +127,13 @@ impl Bridge { program_id, accounts, new_bridge_info.key, - payer_info.key, + payer_info, program_id, &bridge_seed, + None, )?; - let mut new_account_data = new_bridge_info.try_borrow_mut_data()?; + let mut new_account_data = new_bridge_info.try_borrow_mut_data().map_err(|_| ProgramError::AccountBorrowFailed)?; let mut bridge: &mut Bridge = Self::unpack_unchecked(&mut new_account_data)?; if bridge.is_initialized { return Err(Error::AlreadyExists.into()); @@ -140,12 +145,13 @@ impl Bridge { program_id, accounts, new_guardian_info.key, - payer_info.key, + payer_info, program_id, &guardian_seed, + None, )?; - let mut new_guardian_data = new_guardian_info.try_borrow_mut_data()?; + let mut new_guardian_data = new_guardian_info.try_borrow_mut_data().map_err(|_| ProgramError::AccountBorrowFailed)?; let mut guardian_info: &mut GuardianSet = Self::unpack_unchecked(&mut new_guardian_data)?; if guardian_info.is_initialized { return Err(Error::AlreadyExists.into()); @@ -197,11 +203,13 @@ impl Bridge { next_account_info(account_info_iter)?; // Bridge program next_account_info(account_info_iter)?; // System program let instruction_accounts = next_account_info(account_info_iter)?; + let bridge_info = next_account_info(account_info_iter)?; let sig_info = next_account_info(account_info_iter)?; let guardian_set_info = next_account_info(account_info_iter)?; let payer_info = next_account_info(account_info_iter)?; - let guardian_set: GuardianSet = Self::guardian_set_deserialize(guardian_set_info)?; + let guardian_data = guardian_set_info.data.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?; + let guardian_set: &GuardianSet = Self::unpack_immutable(&guardian_data)?; let sig_infos: Vec = payload .signers @@ -232,7 +240,7 @@ impl Bridge { secp_ix_index as usize, &instruction_accounts.try_borrow_mut_data()?, ) - .map_err(|_| ProgramError::InvalidAccountData)?; + .map_err(|_| ProgramError::InvalidAccountData)?; // Check that the instruction is actually for the secp program if secp_ix.program_id != solana_program::secp256k1_program::id() { @@ -310,9 +318,10 @@ impl Bridge { program_id, accounts, sig_info.key, - payer_info.key, + payer_info, program_id, &sig_seeds, + Some(bridge_info), )?; } else if payload.initial_creation { return Err(Error::AlreadyExists.into()); @@ -377,10 +386,15 @@ impl Bridge { let payer_info = next_account_info(account_info_iter)?; let sender = Bridge::token_account_deserialize(sender_account_info)?; - let bridge = Bridge::bridge_deserialize(bridge_info)?; + let bridge_data = bridge_info.data.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?; + let bridge: &Bridge = Self::unpack_immutable(&bridge_data)?; let mint = Bridge::mint_deserialize(mint_info)?; let clock = Clock::from_account_info(clock_info)?; + // Fee handling + let fee = Self::transfer_fee(); + Self::transfer_sol(payer_info, bridge_info, fee)?; + // Does the token belong to the mint if sender.mint != *mint_info.key { return Err(Error::TokenMintMismatch.into()); @@ -412,9 +426,10 @@ impl Bridge { program_id, accounts, transfer_info.key, - payer_info.key, + payer_info, program_id, &transfer_seed, + None, )?; // Load transfer account @@ -472,9 +487,14 @@ 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 bridge_data = bridge_info.data.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?; + let bridge: &Bridge = Self::unpack_immutable(&bridge_data)?; let clock = Clock::from_account_info(clock_info)?; + // Fee handling + let fee = Self::transfer_fee(); + Self::transfer_sol(payer_info, bridge_info, fee)?; + // Does the token belong to the mint if sender.mint != *mint_info.key { return Err(Error::TokenMintMismatch.into()); @@ -494,9 +514,10 @@ impl Bridge { program_id, accounts, transfer_info.key, - payer_info.key, + payer_info, program_id, &transfer_seed, + None, )?; // Load transfer account @@ -519,7 +540,8 @@ impl Bridge { bridge_info.key, custody_info.key, mint_info.key, - payer_info.key, + payer_info, + None, )?; } @@ -562,6 +584,25 @@ impl Bridge { Ok(()) } + pub fn transfer_fee() -> u64 { + // Pay for 2 signature state and Claimed VAA rents + 2 * guardian tx fees + // This will pay for this transfer and ~10 inbound ones + Rent::default().minimum_balance((size_of::() + size_of::()) * 2) + VAA_TX_FEE * 2 + } + + pub fn transfer_sol( + payer_account: &AccountInfo, + recipient_account: &AccountInfo, + amount: u64, + ) -> ProgramResult { + let mut payer_balance = payer_account.try_borrow_mut_lamports()?; + **payer_balance = payer_balance.checked_sub(amount).ok_or(ProgramError::InsufficientFunds)?; + let mut recipient_balance = recipient_account.try_borrow_mut_lamports()?; + **recipient_balance = recipient_balance.checked_add(amount).ok_or(ProgramError::InvalidArgument)?; + + Ok(()) + } + /// Processes a VAA pub fn process_vaa( program_id: &Pubkey, @@ -582,9 +623,11 @@ impl Bridge { let sig_info = next_account_info(account_info_iter)?; let payer_info = next_account_info(account_info_iter)?; - let mut bridge = Bridge::bridge_deserialize(bridge_info)?; + let mut bridge_data = bridge_info.data.try_borrow_mut().map_err(|_| ProgramError::AccountBorrowFailed)?; + let bridge: &mut Bridge = Self::unpack(&mut bridge_data)?; let clock = Clock::from_account_info(clock_info)?; - let mut guardian_set = Bridge::guardian_set_deserialize(guardian_set_info)?; + let mut guardian_data = guardian_set_info.data.try_borrow_mut().map_err(|_| ProgramError::AccountBorrowFailed)?; + let guardian_set: &mut GuardianSet = Bridge::unpack(&mut guardian_data)?; // Check that the guardian set is valid let expected_guardian_set = @@ -599,9 +642,10 @@ impl Bridge { program_id, accounts, claim_info.key, - payer_info.key, + payer_info, program_id, &claim_seeds, + Some(bridge_info), )?; // Check that the guardian set is still active @@ -635,19 +679,23 @@ impl Bridge { return Err(ProgramError::InvalidArgument); } + let mut evict_signatures = false; let payload = vaa.payload.as_ref().ok_or(Error::InvalidVAAAction)?; match payload { - VAABody::UpdateGuardianSet(v) => Self::process_vaa_set_update( - program_id, - accounts, - account_info_iter, - &clock, - bridge_info, - payer_info, - &mut bridge, - &mut guardian_set, - &v, - ), + VAABody::UpdateGuardianSet(v) => { + evict_signatures = true; + Self::process_vaa_set_update( + program_id, + accounts, + account_info_iter, + &clock, + bridge_info, + payer_info, + bridge, + guardian_set, + &v, + ) + } VAABody::Transfer(v) => { if v.source_chain == CHAIN_ID_SOLANA { Self::process_vaa_transfer_post( @@ -660,18 +708,30 @@ impl Bridge { sig_info.key, ) } else { + evict_signatures = true; Self::process_vaa_transfer( program_id, accounts, account_info_iter, bridge_info, - &mut bridge, + bridge, &v, ) } } }?; + // If the signatures are not needed anymore, evict them and reclaim rent. + // This should cover most of the costs of the guardian. + if evict_signatures { + Self::transfer_sol(sig_info, payer_info, sig_info.lamports())?; + } + + // Refund tx fee if possible + if bridge_info.lamports().checked_sub(Self::MIN_BRIDGE_BALANCE).unwrap_or(0) >= VAA_TX_FEE { + Self::transfer_sol(bridge_info, payer_info, VAA_TX_FEE)?; + } + // Load claim account let mut claim_data = claim_info.try_borrow_mut_data()?; let claim: &mut ClaimedVAA = Bridge::unpack_unchecked(&mut claim_data)?; @@ -721,9 +781,10 @@ impl Bridge { program_id, accounts, new_guardian_info.key, - payer_info.key, + payer_info, program_id, &guardian_seed, + Some(bridge_info), )?; let mut guardian_set_new_data = new_guardian_info.try_borrow_mut_data()?; @@ -891,7 +952,8 @@ impl Bridge { let mint_info = next_account_info(account_info_iter)?; let wrapped_meta_info = next_account_info(account_info_iter)?; - let bridge = Bridge::bridge_deserialize(bridge_info)?; + let bridge_data = bridge_info.data.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?; + let bridge: &Bridge = Self::unpack_immutable(&bridge_data)?; // Foreign chain asset, mint wrapped asset let expected_mint_address = Bridge::derive_wrapped_asset_id( @@ -912,9 +974,10 @@ impl Bridge { &bridge.config.token_program, mint_info.key, bridge_info.key, - payer_info.key, + payer_info, &a, a.decimals, + None, )?; // Check and create wrapped asset meta to allow reverse resolution of info @@ -923,9 +986,10 @@ impl Bridge { program_id, accounts, wrapped_meta_info.key, - payer_info.key, + payer_info, program_id, &wrapped_meta_seeds, + None, )?; let mut wrapped_meta_data = wrapped_meta_info.try_borrow_mut_data()?; @@ -1030,7 +1094,8 @@ impl Bridge { bridge: &Pubkey, account: &Pubkey, mint: &Pubkey, - payer: &Pubkey, + payer: &AccountInfo, + subsidizer: Option<&AccountInfo>, ) -> Result<(), ProgramError> { Self::check_and_create_account::<[u8; spl_token::state::Account::LEN]>( program_id, @@ -1039,6 +1104,7 @@ impl Bridge { payer, token_program, &Self::derive_custody_seeds(bridge, mint), + subsidizer, )?; info!(token_program.to_string().as_str()); let ix = spl_token::instruction::initialize_account( @@ -1057,9 +1123,10 @@ impl Bridge { token_program: &Pubkey, mint: &Pubkey, bridge: &Pubkey, - payer: &Pubkey, + payer: &AccountInfo, asset: &AssetMeta, decimals: u8, + subsidizer: Option<&AccountInfo>, ) -> Result<(), ProgramError> { Self::check_and_create_account::<[u8; spl_token::state::Mint::LEN]>( program_id, @@ -1068,6 +1135,7 @@ impl Bridge { payer, token_program, &Self::derive_wrapped_asset_seeds(bridge, asset.chain, asset.decimals, asset.address), + subsidizer, )?; let ix = spl_token::instruction::initialize_mint( token_program, @@ -1099,14 +1167,21 @@ impl Bridge { invoke_signed(instruction, account_infos, &[s.as_slice()]) } + /// The amount of sol that needs to be held in the BridgeConfig account in order to make it + /// exempt of rent payments. + const MIN_BRIDGE_BALANCE: u64 = (((solana_program::rent::ACCOUNT_STORAGE_OVERHEAD + size_of::() as u64) * + solana_program::rent::DEFAULT_LAMPORTS_PER_BYTE_YEAR) as f64 + * solana_program::rent::DEFAULT_EXEMPTION_THRESHOLD) as u64; + /// Check that a key was derived correctly and create account pub fn check_and_create_account( program_id: &Pubkey, accounts: &[AccountInfo], new_account: &Pubkey, - payer: &Pubkey, + payer: &AccountInfo, owner: &Pubkey, seeds: &Vec>, + subsidizer: Option<&AccountInfo>, ) -> Result>, ProgramError> { info!("deriving key"); let (expected_key, full_seeds) = Bridge::derive_key(program_id, seeds)?; @@ -1114,12 +1189,28 @@ impl Bridge { return Err(Error::InvalidDerivedAccount.into()); } + // The subsidizer refunds the rent that needs to be paid to create the account. + // This mechanism is intended to reduce the cost of operating a guardian. + // The subsidizer account should be of the type BridgeConfig and will only pay out + // the subsidy if the account holds at least MIN_BRIDGE_BALANCE+rent + match subsidizer { + None => {} + Some(v) => { + let bal = v.try_lamports()?; + let rent = Rent::default().minimum_balance(size_of::()); + if bal.checked_sub(Self::MIN_BRIDGE_BALANCE).ok_or(ProgramError::InsufficientFunds)? >= rent { + // Refund rent to payer + Self::transfer_sol(v, payer, rent)?; + } + } + } + info!("deploying contract"); Self::create_account_raw::( program_id, accounts, new_account, - payer, + payer.key, owner, &full_seeds, )?; diff --git a/solana/bridge/src/state.rs b/solana/bridge/src/state.rs index ca6b32e8..f68d2ea2 100644 --- a/solana/bridge/src/state.rs +++ b/solana/bridge/src/state.rs @@ -218,22 +218,6 @@ impl Bridge { .map_err(|_| Error::ExpectedToken)?) } - /// Deserializes a `Bridge`. - pub fn bridge_deserialize(info: &AccountInfo) -> Result { - Ok(*Bridge::unpack(&mut info.data.borrow_mut()).map_err(|_| Error::ExpectedBridge)?) - } - - /// Deserializes a `GuardianSet`. - pub fn guardian_set_deserialize(info: &AccountInfo) -> Result { - Ok(*Bridge::unpack(&mut info.data.borrow_mut()).map_err(|_| Error::ExpectedGuardianSet)?) - } - - /// Deserializes a `WrappedAssetMeta`. - pub fn wrapped_meta_deserialize(info: &AccountInfo) -> Result { - Ok(*Bridge::unpack(&mut info.data.borrow_mut()) - .map_err(|_| Error::ExpectedWrappedAssetMeta)?) - } - /// Unpacks a state from a bytes buffer while assuring that the state is initialized. pub fn unpack(input: &mut [u8]) -> Result<&mut T, ProgramError> { let mut_ref: &mut T = Self::unpack_unchecked(input)?;