From 32cbf1cb486a11c75f466efc1b3b6f2de53df255 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 28 Sep 2023 13:52:35 +0100 Subject: [PATCH] Validate Bridge pool client transfers --- shared/src/sdk/eth_bridge/bridge_pool.rs | 190 ++++++++++++++++++++--- 1 file changed, 167 insertions(+), 23 deletions(-) diff --git a/shared/src/sdk/eth_bridge/bridge_pool.rs b/shared/src/sdk/eth_bridge/bridge_pool.rs index 495a8e5a791..e0d28e88ee2 100644 --- a/shared/src/sdk/eth_bridge/bridge_pool.rs +++ b/shared/src/sdk/eth_bridge/bridge_pool.rs @@ -8,6 +8,8 @@ use std::sync::Arc; use borsh::BorshSerialize; use ethbridge_bridge_contract::Bridge; use ethers::providers::Middleware; +use futures::future::FutureExt; +use namada_core::ledger::eth_bridge::storage::bridge_pool::get_pending_key; use namada_core::ledger::eth_bridge::storage::wrapped_erc20s; use namada_core::types::ethereum_events::EthAddress; use namada_core::types::key::common; @@ -24,13 +26,15 @@ use crate::ledger::queries::{ use crate::proto::Tx; use crate::sdk::args; use crate::sdk::error::{ - EncodingError, Error, EthereumBridgeError, QueryError, + EncodingError, Error, EthereumBridgeError, QueryError, TxError, }; use crate::sdk::masp::{ShieldedContext, ShieldedUtils}; -use crate::sdk::rpc::{query_wasm_code_hash, validate_amount}; +use crate::sdk::rpc::{ + query_storage_value, query_wasm_code_hash, validate_amount, +}; use crate::sdk::tx::prepare_tx; use crate::sdk::wallet::{Wallet, WalletUtils}; -use crate::types::address::Address; +use crate::types::address::{Address, InternalAddress}; use crate::types::control_flow::install_shutdown_signal; use crate::types::control_flow::time::{Duration, Instant}; use crate::types::eth_abi::Encode; @@ -39,7 +43,7 @@ use crate::types::eth_bridge_pool::{ }; use crate::types::io::Io; use crate::types::keccak::KeccakHash; -use crate::types::token::{Amount, DenominatedAmount}; +use crate::types::token::{balance_key, Amount, DenominatedAmount}; use crate::types::voting_power::FractionalVotingPower; use crate::{display, display_line, edisplay_line}; @@ -123,26 +127,38 @@ where C: crate::ledger::queries::Client + Sync, IO: Io, { - let DenominatedAmount { amount, .. } = validate_amount::<_, IO>( - client, - amount, - &wrapped_erc20s::token(&asset), - force, - ) - .await - .map_err(|e| Error::Other(format!("Failed to validate amount. {}", e)))?; + let token_addr = wrapped_erc20s::token(&asset); + let validate_token_amount = + validate_amount::<_, IO>(client, amount, &token_addr, force).map( + |result| { + result.map_err(|e| { + Error::Other(format!( + "Failed to validate Bridge pool transfer amount: {e}" + )) + }) + }, + ); - let DenominatedAmount { - amount: fee_amount, .. - } = validate_amount::<_, IO>(client, fee_amount, &fee_token, force) - .await - .map_err(|e| { + let validate_fee_amount = validate_amount::<_, IO>( + client, fee_amount, &fee_token, force, + ) + .map(|result| { + result.map_err(|e| { Error::Other(format!( - "Failed to validate Bridge pool fee amount. {}", - e + "Failed to validate Bridge pool fee amount: {e}", )) - })?; + }) + }); + // validate amounts + let ( + tok_denominated @ DenominatedAmount { amount, .. }, + fee_denominated @ DenominatedAmount { + amount: fee_amount, .. + }, + ) = futures::try_join!(validate_token_amount, validate_fee_amount)?; + + // build pending Bridge pool transfer let fee_payer = fee_payer.unwrap_or_else(|| sender.clone()); let transfer = PendingTransfer { transfer: TransferToEthereum { @@ -163,9 +179,137 @@ where }, }; - // if force { - // return Ok(transfer); - //} + if force { + return Ok(transfer); + } + + //====================================================== + // XXX: the following validations should be kept in sync + // with the validations performed by the Bridge pool VP! + //====================================================== + + // check if an identical transfer is already in the Bridge pool + let transfer_in_pool = RPC + .shell() + .storage_has_key(client, &get_pending_key(&transfer)) + .await + .map_err(|e| Error::Query(QueryError::General(e.to_string())))?; + if transfer_in_pool { + return Err(Error::EthereumBridge( + EthereumBridgeError::TransferAlreadyInPool, + )); + } + + let wnam_addr = RPC + .shell() + .eth_bridge() + .read_native_erc20_contract(client) + .await + .map_err(|e| { + Error::EthereumBridge(EthereumBridgeError::RetrieveContract( + e.to_string(), + )) + })?; + + // validate gas fee token + match &transfer.gas_fee.token { + Address::Internal(InternalAddress::Nut(_)) => { + return Err(Error::EthereumBridge( + EthereumBridgeError::InvalidFeeToken(transfer.gas_fee.token), + )); + } + fee_token if fee_token == &wrapped_erc20s::token(&wnam_addr) => { + return Err(Error::EthereumBridge( + EthereumBridgeError::InvalidFeeToken(transfer.gas_fee.token), + )); + } + _ => {} + } + + // validate wnam token caps + whitelist + if transfer.transfer.asset == wnam_addr { + let flow_control = RPC + .shell() + .eth_bridge() + .get_erc20_flow_control(client, &wnam_addr) + .await + .map_err(|e| { + Error::Query(QueryError::General(format!( + "Failed to read wrapped NAM flow control data: {e}" + ))) + })?; + + if !flow_control.whitelisted { + return Err(Error::EthereumBridge( + EthereumBridgeError::Erc20NotWhitelisted(wnam_addr), + )); + } + + if flow_control.exceeds_token_caps(transfer.transfer.amount) { + return Err(Error::EthereumBridge( + EthereumBridgeError::Erc20TokenCapsExceeded(wnam_addr), + )); + } + } + + // validate balances + let maybe_balance_error = if token_addr == transfer.gas_fee.token { + let expected_debit = transfer.transfer.amount + transfer.gas_fee.amount; + let balance: Amount = query_storage_value( + client, + &balance_key(&token_addr, &transfer.transfer.sender), + ) + .await?; + + balance + .checked_sub(expected_debit) + .is_none() + .then_some((token_addr, tok_denominated)) + } else { + let check_tokens = async { + let balance: Amount = query_storage_value( + client, + &balance_key(&token_addr, &transfer.transfer.sender), + ) + .await?; + Result::<_, Error>::Ok( + balance + .checked_sub(transfer.transfer.amount) + .is_none() + .then_some((token_addr, tok_denominated)), + ) + }; + let check_fees = async { + let balance: Amount = query_storage_value( + client, + &balance_key( + &transfer.gas_fee.token, + &transfer.transfer.sender, + ), + ) + .await?; + Result::<_, Error>::Ok( + balance + .checked_sub(transfer.gas_fee.amount) + .is_none() + .then_some(( + transfer.gas_fee.token.clone(), + fee_denominated, + )), + ) + }; + + let (err_tokens, err_fees) = + futures::try_join!(check_tokens, check_fees)?; + err_tokens.or(err_fees) + }; + if let Some((token, amount)) = maybe_balance_error { + return Err(Error::Tx(TxError::NegativeBalanceAfterTransfer( + Box::new(transfer.transfer.sender), + amount.to_string(), + Box::new(token), + ))); + } Ok(transfer) }