From 9ec1833f20b1319ff916ddecf0dd86d5b21f706c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 11 Jul 2023 10:15:26 +0100 Subject: [PATCH 01/94] Add Eth whitelist storage keys --- core/src/ledger/eth_bridge/storage/mod.rs | 1 + .../ledger/eth_bridge/storage/whitelist.rs | 117 ++++++++++++++++++ .../src/storage/eth_bridge_queries.rs | 51 +++++++- 3 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 core/src/ledger/eth_bridge/storage/whitelist.rs diff --git a/core/src/ledger/eth_bridge/storage/mod.rs b/core/src/ledger/eth_bridge/storage/mod.rs index 0906be3d5d..b777caa265 100644 --- a/core/src/ledger/eth_bridge/storage/mod.rs +++ b/core/src/ledger/eth_bridge/storage/mod.rs @@ -1,5 +1,6 @@ //! Functionality for accessing the storage subspace pub mod bridge_pool; +pub mod whitelist; pub mod wrapped_erc20s; use super::ADDRESS; diff --git a/core/src/ledger/eth_bridge/storage/whitelist.rs b/core/src/ledger/eth_bridge/storage/whitelist.rs new file mode 100644 index 0000000000..15ebddfa8b --- /dev/null +++ b/core/src/ledger/eth_bridge/storage/whitelist.rs @@ -0,0 +1,117 @@ +//! ERC20 token whitelist storage data. +//! +//! These storage keys should only ever be written to by governance, +//! or `InitChain`. + +use std::str::FromStr; + +use super::super::ADDRESS as BRIDGE_ADDRESS; +use super::{prefix as ethbridge_key_prefix, wrapped_erc20s}; +use crate::types::ethereum_events::EthAddress; +use crate::types::storage; +use crate::types::storage::DbKeySeg; +use crate::types::token::{denom_key, minted_balance_key}; + +mod segments { + //! Storage key segments under the token whitelist. + use namada_macros::StorageKeys; + + use crate::types::address::Address; + use crate::types::storage::{DbKeySeg, Key}; + + /// The name of the main storage segment. + pub(super) const MAIN_SEGMENT: &str = "whitelist"; + + /// Storage key segments under the token whitelist. + #[derive(StorageKeys)] + pub(super) struct Segments { + /// Whether an ERC20 asset is whitelisted or not. + pub whitelisted: &'static str, + /// The token cap of an ERC20 asset. + pub cap: &'static str, + } + + /// All the values of the generated [`Segments`]. + pub(super) const VALUES: Segments = Segments::VALUES; + + /// Listing of each of the generated [`Segments`]. + pub(super) const ALL: &[&str] = Segments::ALL; +} + +/// Represents the type of a key relating to whitelisted ERC20. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +pub enum KeyType { + /// Whether an ERC20 asset is whitelisted or not. + Whitelisted, + /// The token cap of an ERC20 asset. + Cap, + /// The current supply of a wrapped ERC20 asset, + /// circulating in Namada. + WrappedSupply, + /// The denomination of the ERC20 asset. + Denomination, +} + +/// Whitelisted ERC20 token storage sub-space. +pub struct Key { + /// The specific ERC20 as identified by its Ethereum address. + pub asset: EthAddress, + /// The type of this key. + pub suffix: KeyType, +} + +/// Return the whitelist storage key sub-space prefix. +fn whitelist_prefix(asset: &EthAddress) -> storage::Key { + ethbridge_key_prefix() + .push(&segments::MAIN_SEGMENT.to_owned()) + .expect("Should be able to push a storage key segment") + .push(&asset.to_canonical()) + .expect("Should be able to push a storage key segment") +} + +impl From for storage::Key { + #[inline] + fn from(key: Key) -> Self { + (&key).into() + } +} + +impl From<&Key> for storage::Key { + fn from(key: &Key) -> Self { + match &key.suffix { + KeyType::Whitelisted => whitelist_prefix(&key.asset) + .push(&segments::VALUES.whitelisted.to_owned()) + .expect("Should be able to push a storage key segment"), + KeyType::Cap => whitelist_prefix(&key.asset) + .push(&segments::VALUES.cap.to_owned()) + .expect("Should be able to push a storage key segment"), + KeyType::WrappedSupply => { + let token = wrapped_erc20s::token(&key.asset); + minted_balance_key(&token) + } + KeyType::Denomination => { + let token = wrapped_erc20s::token(&key.asset); + denom_key(&token) + } + } + } +} + +/// Check if some [`storage::Key`] is an Ethereum bridge whitelist key +/// of type [`KeyType::Cap`] or [`KeyType::Whitelisted`]. +pub fn is_cap_or_whitelisted_key(key: &storage::Key) -> bool { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(s1), + DbKeySeg::StringSeg(s2), + DbKeySeg::StringSeg(s3), + DbKeySeg::StringSeg(s4), + ] => { + s1 == &BRIDGE_ADDRESS + && s2 == segments::MAIN_SEGMENT + && EthAddress::from_str(s3).is_ok() + && segments::ALL.binary_search(&s4.as_str()).is_ok() + } + _ => false, + } +} diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index 827745def0..ae380c8433 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -1,9 +1,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::hints; -use namada_core::ledger::eth_bridge::storage::active_key; use namada_core::ledger::eth_bridge::storage::bridge_pool::{ get_nonce_key, get_signed_root_key, }; +use namada_core::ledger::eth_bridge::storage::{active_key, whitelist}; use namada_core::ledger::storage; use namada_core::ledger::storage::{StoreType, WlStorage}; use namada_core::ledger::storage_api::StorageRead; @@ -392,6 +392,55 @@ where voting_powers_map, ) } + + /// Check if the token at the given [`EthAddress`] is whitelisted. + pub fn is_token_whitelisted(self, &token: &EthAddress) -> bool { + let key = whitelist::Key { + asset: token, + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + + self.wl_storage + .read(&key) + .expect("Reading from storage should not fail") + .unwrap_or(false) + } + + /// Fetch the token cap of the asset associated with the given + /// [`EthAddress`]. + /// + /// If the asset has never been whitelisted, return [`None`]. + pub fn get_token_cap(self, &token: &EthAddress) -> Option { + let key = whitelist::Key { + asset: token, + suffix: whitelist::KeyType::Cap, + } + .into(); + + self.wl_storage + .read(&key) + .expect("Reading from storage should not fail") + } + + /// Fetch the token supply of the asset associated with the given + /// [`EthAddress`]. + /// + /// If the asset has never been minted, return [`None`]. + pub fn get_token_supply( + self, + &token: &EthAddress, + ) -> Option { + let key = whitelist::Key { + asset: token, + suffix: whitelist::KeyType::WrappedSupply, + } + .into(); + + self.wl_storage + .read(&key) + .expect("Reading from storage should not fail") + } } /// A handle to the Ethereum addresses of the set of consensus From ed2afb257fa50197983dd166e7c2d1a2aec3da7f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 11 Jul 2023 10:23:16 +0100 Subject: [PATCH 02/94] Add Eth whitelist key tests --- .../ledger/eth_bridge/storage/whitelist.rs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/core/src/ledger/eth_bridge/storage/whitelist.rs b/core/src/ledger/eth_bridge/storage/whitelist.rs index 15ebddfa8b..77b0860a8c 100644 --- a/core/src/ledger/eth_bridge/storage/whitelist.rs +++ b/core/src/ledger/eth_bridge/storage/whitelist.rs @@ -115,3 +115,50 @@ pub fn is_cap_or_whitelisted_key(key: &storage::Key) -> bool { _ => false, } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; + + /// Test that storage key serialization yields the expected value. + #[test] + fn test_keys_whitelisted_to_string() { + let key: storage::Key = Key { + asset: DAI_ERC20_ETH_ADDRESS, + suffix: KeyType::Whitelisted, + } + .into(); + let expected = "#atest1v9hx7w36g42ysgzzwf5kgem9ypqkgerjv4ehxgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpq8f99ew/whitelist/0x6b175474e89094c44da98b954eedeac495271d0f/whitelisted"; + assert_eq!(expected, key.to_string()); + } + + /// Test that checking if a key is of type "cap" or "whitelisted" works. + #[test] + fn test_cap_or_whitelisted_key() { + let whitelisted_key: storage::Key = Key { + asset: DAI_ERC20_ETH_ADDRESS, + suffix: KeyType::Whitelisted, + } + .into(); + assert!(is_cap_or_whitelisted_key(&whitelisted_key)); + + let cap_key: storage::Key = Key { + asset: DAI_ERC20_ETH_ADDRESS, + suffix: KeyType::Cap, + } + .into(); + assert!(is_cap_or_whitelisted_key(&cap_key)); + + let unexpected_key = { + let mut k: storage::Key = Key { + asset: DAI_ERC20_ETH_ADDRESS, + suffix: KeyType::Cap, + } + .into(); + k.segments[3] = DbKeySeg::StringSeg("abc".to_owned()); + k + }; + assert!(!is_cap_or_whitelisted_key(&unexpected_key)); + } +} From 6ca25fd08f9afde306d20ac7238d74aa783a5727 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 11 Jul 2023 11:44:18 +0100 Subject: [PATCH 03/94] Add non-usable tokens for Ethereum bridge protocol bookkeeping --- .../eth_bridge/storage/wrapped_erc20s.rs | 5 +++ core/src/types/address.rs | 33 ++++++++++++++++--- shared/src/ledger/protocol/mod.rs | 3 +- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/core/src/ledger/eth_bridge/storage/wrapped_erc20s.rs b/core/src/ledger/eth_bridge/storage/wrapped_erc20s.rs index 0062dd50c9..36ce04141b 100644 --- a/core/src/ledger/eth_bridge/storage/wrapped_erc20s.rs +++ b/core/src/ledger/eth_bridge/storage/wrapped_erc20s.rs @@ -14,6 +14,11 @@ pub fn token(address: &EthAddress) -> Address { Address::Internal(InternalAddress::Erc20(*address)) } +/// Construct a NUT token address from an ERC20 address. +pub fn nut(address: &EthAddress) -> Address { + Address::Internal(InternalAddress::Nut(*address)) +} + /// Represents the type of a key relating to a wrapped ERC20 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] pub enum KeyType { diff --git a/core/src/types/address.rs b/core/src/types/address.rs index d587d20280..4f300c76ed 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -99,6 +99,8 @@ const PREFIX_INTERNAL: &str = "ano"; const PREFIX_IBC: &str = "ibc"; /// Fixed-length address strings prefix for Ethereum addresses. const PREFIX_ETH: &str = "eth"; +/// Fixed-length address strings prefix for Non-Usable-Token addresses. +const PREFIX_NUT: &str = "nut"; #[allow(missing_docs)] #[derive(Error, Debug)] @@ -234,6 +236,11 @@ impl Address { eth_addr.to_canonical().replace("0x", ""); format!("{}::{}", PREFIX_ETH, eth_addr) } + InternalAddress::Nut(eth_addr) => { + let eth_addr = + eth_addr.to_canonical().replace("0x", ""); + format!("{PREFIX_NUT}::{eth_addr}") + } InternalAddress::ReplayProtection => { internal::REPLAY_PROTECTION.to_string() } @@ -330,12 +337,18 @@ impl Address { "Invalid IBC internal address", )), }, - Some((PREFIX_ETH, raw)) => match string { + Some((prefix @ (PREFIX_ETH | PREFIX_NUT), raw)) => match string { _ if raw.len() == HASH_HEX_LEN => { match EthAddress::from_str(&format!("0x{}", raw)) { - Ok(eth_addr) => Ok(Address::Internal( - InternalAddress::Erc20(eth_addr), - )), + Ok(eth_addr) => Ok(match prefix { + PREFIX_ETH => Address::Internal( + InternalAddress::Erc20(eth_addr), + ), + PREFIX_NUT => Address::Internal( + InternalAddress::Nut(eth_addr), + ), + _ => unreachable!(), + }), Err(e) => Err(Error::new( ErrorKind::InvalidData, e.to_string(), @@ -543,6 +556,8 @@ pub enum InternalAddress { EthBridgePool, /// ERC20 token for Ethereum bridge Erc20(EthAddress), + /// Non-usable ERC20 tokens + Nut(EthAddress), /// Replay protection contains transactions' hash ReplayProtection, /// Multitoken @@ -566,6 +581,7 @@ impl Display for InternalAddress { Self::EthBridge => "EthBridge".to_string(), Self::EthBridgePool => "EthBridgePool".to_string(), Self::Erc20(eth_addr) => format!("Erc20: {}", eth_addr), + Self::Nut(eth_addr) => format!("Non-usable token: {eth_addr}"), Self::ReplayProtection => "ReplayProtection".to_string(), Self::Multitoken => "Multitoken".to_string(), Self::Pgf => "PublicGoodFundings".to_string(), @@ -861,6 +877,7 @@ pub mod testing { InternalAddress::EthBridge => {} InternalAddress::EthBridgePool => {} InternalAddress::Erc20(_) => {} + InternalAddress::Nut(_) => {} InternalAddress::ReplayProtection => {} InternalAddress::Pgf => {} InternalAddress::Multitoken => {} /* Add new addresses in the @@ -876,6 +893,7 @@ pub mod testing { Just(InternalAddress::EthBridge), Just(InternalAddress::EthBridgePool), Just(arb_erc20()), + Just(arb_nut()), Just(InternalAddress::ReplayProtection), Just(InternalAddress::Multitoken), Just(InternalAddress::Pgf), @@ -900,6 +918,13 @@ pub mod testing { fn arb_erc20() -> InternalAddress { use crate::types::ethereum_events::testing::arbitrary_eth_address; + // TODO: generate random erc20 addr data InternalAddress::Erc20(arbitrary_eth_address()) } + + fn arb_nut() -> InternalAddress { + use crate::types::ethereum_events::testing::arbitrary_eth_address; + // TODO: generate random erc20 addr data + InternalAddress::Nut(arbitrary_eth_address()) + } } diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index 7a0244932a..eabe47ddca 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -586,7 +586,8 @@ where result } InternalAddress::IbcToken(_) - | InternalAddress::Erc20(_) => { + | InternalAddress::Erc20(_) + | InternalAddress::Nut(_) => { // The address should be a part of a multitoken key gas_meter = ctx.gas_meter.into_inner(); Ok(verifiers.contains(&Address::Internal( From 5208f3d815d849bdb08871ab9f26f1df9dfba501 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 11 Jul 2023 16:37:22 +0100 Subject: [PATCH 04/94] Add a VP for NUT transfers --- .../ledger/native_vp/ethereum_bridge/mod.rs | 1 + .../ledger/native_vp/ethereum_bridge/nut.rs | 119 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 shared/src/ledger/native_vp/ethereum_bridge/nut.rs diff --git a/shared/src/ledger/native_vp/ethereum_bridge/mod.rs b/shared/src/ledger/native_vp/ethereum_bridge/mod.rs index 85df785e79..250d51d1b5 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/mod.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/mod.rs @@ -3,4 +3,5 @@ //! pool. pub mod bridge_pool_vp; +pub mod nut; pub mod vp; diff --git a/shared/src/ledger/native_vp/ethereum_bridge/nut.rs b/shared/src/ledger/native_vp/ethereum_bridge/nut.rs new file mode 100644 index 0000000000..dec56adbad --- /dev/null +++ b/shared/src/ledger/native_vp/ethereum_bridge/nut.rs @@ -0,0 +1,119 @@ +//! Validity predicate for Non Usable Tokens (NUTs). + +use std::collections::BTreeSet; + +use eyre::WrapErr; +use namada_core::ledger::storage as ledger_storage; +use namada_core::ledger::storage::traits::StorageHasher; +use namada_core::types::address::{Address, InternalAddress}; +use namada_core::types::storage::Key; +use namada_core::types::token::Amount; + +use crate::ledger::native_vp::{Ctx, NativeVp, VpEnv}; +use crate::proto::Tx; +use crate::types::token::is_any_token_balance_key; +use crate::vm::WasmCacheAccess; + +/// Generic error that may be returned by the validity predicate +#[derive(thiserror::Error, Debug)] +#[error(transparent)] +pub struct Error(#[from] eyre::Report); + +/// Validity predicate for non-usable tokens. +/// +/// All this VP does is reject NUT transfers whose destination +/// address is not the Bridge pool escrow address. +pub struct NonUsableTokens<'ctx, DB, H, CA> +where + DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, + H: StorageHasher, + CA: 'static + WasmCacheAccess, +{ + /// Context to interact with the host structures. + pub ctx: Ctx<'ctx, DB, H, CA>, +} + +impl<'a, DB, H, CA> NativeVp for NonUsableTokens<'a, DB, H, CA> +where + DB: 'static + ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, + H: 'static + StorageHasher, + CA: 'static + WasmCacheAccess, +{ + type Error = Error; + + fn validate_tx( + &self, + _: &Tx, + keys_changed: &BTreeSet, + verifiers: &BTreeSet
, + ) -> Result { + tracing::debug!( + keys_changed_len = keys_changed.len(), + verifiers_len = verifiers.len(), + "Non usable tokens VP triggered", + ); + + let is_multitoken = + verifiers.contains(&Address::Internal(InternalAddress::Multitoken)); + if !is_multitoken { + tracing::debug!("Rejecting non-multitoken transfer tx"); + return Ok(false); + } + + let nut_owners = + keys_changed.iter().filter_map( + |key| match is_any_token_balance_key(key) { + Some( + [Address::Internal(InternalAddress::Nut(_)), owner], + ) => Some((key, owner)), + _ => None, + }, + ); + + for (changed_key, token_owner) in nut_owners { + let pre: Amount = self + .ctx + .read_pre(changed_key) + .context("Reading pre amount failed") + .map_err(Error)? + .unwrap_or_default(); + let post: Amount = self + .ctx + .read_post(changed_key) + .context("Reading post amount failed") + .map_err(Error)? + .unwrap_or_default(); + + match token_owner { + // the NUT balance of the bridge pool should increase + Address::Internal(InternalAddress::EthBridgePool) => { + if post < pre { + tracing::debug!( + %changed_key, + pre_amount = ?pre, + post_amount = ?post, + "Bridge pool balance should have increased" + ); + return Ok(false); + } + } + // arbitrary addresses should have their balance decrease + _addr => { + if post > pre { + tracing::debug!( + %changed_key, + pre_amount = ?pre, + post_amount = ?post, + "Balance should have decreased" + ); + return Ok(false); + } + } + } + } + + Ok(true) + } +} + +// TODO: add tests From 9c14b65de3c6eaf60828525f97330e9313645f86 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 10:32:38 +0100 Subject: [PATCH 05/94] Test NUT VP --- .../ledger/native_vp/ethereum_bridge/nut.rs | 122 +++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/nut.rs b/shared/src/ledger/native_vp/ethereum_bridge/nut.rs index dec56adbad..afea1da1d4 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/nut.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/nut.rs @@ -116,4 +116,124 @@ where } } -// TODO: add tests +#[cfg(test)] +mod test_nuts { + use std::env::temp_dir; + + use assert_matches::assert_matches; + use borsh::BorshSerialize; + use namada_core::ledger::storage::testing::TestWlStorage; + use namada_core::ledger::storage_api::StorageWrite; + use namada_core::types::address::testing::arb_non_internal_address; + use namada_core::types::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; + use namada_core::types::storage::TxIndex; + use namada_core::types::token::balance_key; + use namada_core::types::transaction::TxType; + use namada_ethereum_bridge::storage::wrapped_erc20s; + use proptest::prelude::*; + + use super::*; + use crate::ledger::gas::VpGasMeter; + use crate::vm::wasm::VpCache; + use crate::vm::WasmCacheRwAccess; + + /// Run a VP check on a NUT transfer between the two provided addresses. + fn check_nut_transfer(src: Address, dst: Address) -> Option { + let nut = wrapped_erc20s::nut(&DAI_ERC20_ETH_ADDRESS); + let src_balance_key = balance_key(&nut, &src); + let dst_balance_key = balance_key(&nut, &dst); + + let wl_storage = { + let mut wl = TestWlStorage::default(); + + // write initial balances + wl.write(&src_balance_key, Amount::from(200_u64)) + .expect("Test failed"); + wl.write(&dst_balance_key, Amount::from(100_u64)) + .expect("Test failed"); + wl.commit_block().expect("Test failed"); + + // write the updated balances + wl.write_log + .write( + &src_balance_key, + Amount::from(100_u64).try_to_vec().expect("Test failed"), + ) + .expect("Test failed"); + wl.write_log + .write( + &dst_balance_key, + Amount::from(200_u64).try_to_vec().expect("Test failed"), + ) + .expect("Test failed"); + + wl + }; + + let keys_changed = { + let mut keys = BTreeSet::new(); + keys.insert(src_balance_key); + keys.insert(dst_balance_key); + keys + }; + let verifiers = { + let mut v = BTreeSet::new(); + v.insert(Address::Internal(InternalAddress::Multitoken)); + v + }; + + let tx = Tx::from_type(TxType::Raw); + let ctx = Ctx::<_, _, WasmCacheRwAccess>::new( + &Address::Internal(InternalAddress::Nut(DAI_ERC20_ETH_ADDRESS)), + &wl_storage.storage, + &wl_storage.write_log, + &tx, + &TxIndex(0), + VpGasMeter::new(0u64), + &keys_changed, + &verifiers, + VpCache::new(temp_dir(), 100usize), + ); + let vp = NonUsableTokens { ctx }; + + // print debug info in case we run into failures + for key in &keys_changed { + let pre: Amount = vp + .ctx + .read_pre(key) + .expect("Test failed") + .unwrap_or_default(); + let post: Amount = vp + .ctx + .read_post(key) + .expect("Test failed") + .unwrap_or_default(); + println!("{key}: PRE={pre:?} POST={post:?}"); + } + + vp.validate_tx(&tx, &keys_changed, &verifiers).ok() + } + + proptest! { + /// Test that transferring NUTs between two arbitrary addresses + /// will always fail. + #[test] + fn test_nut_transfer_rejected( + (src, dst) in (arb_non_internal_address(), arb_non_internal_address()) + ) { + let status = check_nut_transfer(src, dst); + assert_matches!(status, Some(false)); + } + + /// Test that transferring NUTs from an arbitrary address to the + /// Bridge pool address passes. + #[test] + fn test_nut_transfer_passes(src in arb_non_internal_address()) { + let status = check_nut_transfer( + src, + Address::Internal(InternalAddress::EthBridgePool), + ); + assert_matches!(status, Some(true)); + } + } +} From 4901296831b129eea78d192f0850ffcdb5016271 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 10:39:26 +0100 Subject: [PATCH 06/94] Trigger the NUT native VP --- shared/src/ledger/protocol/mod.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index eabe47ddca..f3002ca723 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -11,6 +11,7 @@ use crate::ledger::gas::{self, BlockGasMeter, VpGasMeter}; use crate::ledger::governance::GovernanceVp; use crate::ledger::ibc::vp::Ibc; use crate::ledger::native_vp::ethereum_bridge::bridge_pool_vp::BridgePoolVp; +use crate::ledger::native_vp::ethereum_bridge::nut::NonUsableTokens; use crate::ledger::native_vp::ethereum_bridge::vp::EthBridge; use crate::ledger::native_vp::multitoken::MultitokenVp; use crate::ledger::native_vp::parameters::{self, ParametersVp}; @@ -72,6 +73,8 @@ pub enum Error { ReplayProtectionNativeVpError( crate::ledger::native_vp::replay_protection::Error, ), + #[error("Non usable tokens native VP error: {0}")] + NutNativeVpError(native_vp::ethereum_bridge::nut::Error), #[error("Access to an internal address {0} is forbidden")] AccessForbidden(InternalAddress), } @@ -585,9 +588,17 @@ where gas_meter = pgf_vp.ctx.gas_meter.into_inner(); result } + InternalAddress::Nut(_) => { + let non_usable_tokens = NonUsableTokens { ctx }; + let result = non_usable_tokens + .validate_tx(tx, &keys_changed, &verifiers) + .map_err(Error::NutNativeVpError); + gas_meter = + non_usable_tokens.ctx.gas_meter.into_inner(); + result + } InternalAddress::IbcToken(_) - | InternalAddress::Erc20(_) - | InternalAddress::Nut(_) => { + | InternalAddress::Erc20(_) => { // The address should be a part of a multitoken key gas_meter = ctx.gas_meter.into_inner(); Ok(verifiers.contains(&Address::Internal( From 9b6df799b1dab7e37fcd21e0b0346a5d7a1e9eac Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 13:12:50 +0100 Subject: [PATCH 07/94] Update ethbridge-rs to v0.19.0 --- Cargo.lock | 24 ++++++++++++------------ apps/Cargo.toml | 6 +++--- core/Cargo.toml | 2 +- shared/Cargo.toml | 4 ++-- wasm/Cargo.lock | 20 ++++++++++---------- wasm_for_tests/wasm_source/Cargo.lock | 20 ++++++++++---------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a069b0956..4359495965 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,8 +1999,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2010,8 +2010,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethbridge-structs", @@ -2021,8 +2021,8 @@ dependencies = [ [[package]] name = "ethbridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", @@ -2032,8 +2032,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2043,8 +2043,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethbridge-structs", @@ -2054,8 +2054,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethers", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 992abbef33..a5c4dec197 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -87,9 +87,9 @@ derivative.workspace = true directories.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0"} -ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0"} -ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} eyre.workspace = true fd-lock.workspace = true ferveo-common.workspace = true diff --git a/core/Cargo.toml b/core/Cargo.toml index 7c5addad7c..5da1f1f6d6 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -66,7 +66,7 @@ data-encoding.workspace = true derivative.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0" } +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0" } eyre.workspace = true ferveo = {optional = true, git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} ferveo-common = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 28711a0329..d6c64d6e01 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -100,8 +100,8 @@ clru.workspace = true data-encoding.workspace = true derivation-path.workspace = true derivative.workspace = true -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0"} +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} ethers.workspace = true eyre.workspace = true futures.workspace = true diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index be65562b6d..2e8816fd54 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethers", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 77b2082a4e..95d016ca57 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethers", From b9328901218bf418c842ffff6e24b2539ac27e20 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 13:41:09 +0100 Subject: [PATCH 08/94] Add transfer to Ethereum kinds (ERC20 or NUT) --- .../lib/node/ledger/ethereum_oracle/events.rs | 13 ++++- .../lib/node/ledger/ethereum_oracle/mod.rs | 6 +- .../lib/node/ledger/shell/finalize_block.rs | 3 +- .../shell/vote_extensions/eth_events.rs | 10 +++- .../ledger/eth_bridge/storage/bridge_pool.rs | 21 ++++++- core/src/types/eth_bridge_pool.rs | 5 ++ core/src/types/ethereum_events.rs | 56 +++++++++++++++++++ .../transactions/ethereum_events/events.rs | 4 ++ shared/src/ledger/eth_bridge/bridge_pool.rs | 6 +- .../ethereum_bridge/bridge_pool_vp.rs | 12 +++- shared/src/ledger/queries/shell/eth_bridge.rs | 10 +++- tests/src/native_vp/eth_bridge_pool.rs | 5 +- 12 files changed, 142 insertions(+), 9 deletions(-) diff --git a/apps/src/lib/node/ledger/ethereum_oracle/events.rs b/apps/src/lib/node/ledger/ethereum_oracle/events.rs index 28cd61f743..10837829de 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/events.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/events.rs @@ -15,7 +15,7 @@ pub mod eth_events { use namada::types::address::Address; use namada::types::ethereum_events::{ EthAddress, EthereumEvent, TokenWhitelist, TransferToEthereum, - TransferToNamada, Uint, + TransferToEthereumKind, TransferToNamada, Uint, }; use namada::types::keccak::KeccakHash; use namada::types::token::Amount; @@ -180,6 +180,7 @@ pub mod eth_events { /// Trait to add parsing methods to foreign types. trait Parse: Sized { + parse_method! { parse_eth_transfer_kind -> TransferToEthereumKind } parse_method! { parse_eth_address -> EthAddress } parse_method! { parse_address -> Address } parse_method! { parse_amount -> Amount } @@ -198,6 +199,13 @@ pub mod eth_events { parse_method! { parse_transfer_to_eth -> TransferToEthereum } } + impl Parse for u8 { + fn parse_eth_transfer_kind(self) -> Result { + self.try_into() + .map_err(|err| Error::Decode(format!("{:?}", err))) + } + } + impl Parse for ethabi::Address { fn parse_eth_address(self) -> Result { Ok(EthAddress(self.0)) @@ -296,6 +304,7 @@ pub mod eth_events { impl Parse for ethereum_structs::Erc20Transfer { fn parse_transfer_to_eth(self) -> Result { + let kind = self.kind.parse_eth_transfer_kind()?; let asset = self.from.parse_eth_address()?; let receiver = self.to.parse_eth_address()?; let sender = self.sender.parse_address()?; @@ -303,6 +312,7 @@ pub mod eth_events { let gas_payer = self.fee_from.parse_address()?; let gas_amount = self.fee.parse_amount()?; Ok(TransferToEthereum { + kind, asset, amount, sender, @@ -524,6 +534,7 @@ pub mod eth_events { let eth_transfers = TransferToErcFilter { transfers: vec![ ethereum_structs::Erc20Transfer { + kind: TransferToEthereumKind::Erc20 as u8, from: H160([1; 20]), to: H160([2; 20]), sender: address.clone(), diff --git a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs index 1e967b12b4..05486aa11e 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs @@ -569,7 +569,9 @@ mod test_oracle { use namada::eth_bridge::ethers::types::H160; use namada::eth_bridge::structs::Erc20Transfer; use namada::types::address::testing::gen_established_address; - use namada::types::ethereum_events::{EthAddress, TransferToEthereum}; + use namada::types::ethereum_events::{ + EthAddress, TransferToEthereum, TransferToEthereumKind, + }; use tokio::sync::oneshot::channel; use tokio::time::timeout; @@ -826,6 +828,7 @@ mod test_oracle { let gas_payer = gen_established_address(); let second_event = TransferToErcFilter { transfers: vec![Erc20Transfer { + kind: TransferToEthereumKind::Erc20 as u8, amount: 0.into(), from: H160([0; 20]), sender: gas_payer.to_string(), @@ -895,6 +898,7 @@ mod test_oracle { assert_eq!( transfer, TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: Default::default(), asset: EthAddress([0; 20]), sender: gas_payer.clone(), diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 8af2d1a746..f8fb4fb0a4 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1039,7 +1039,7 @@ mod test_finalize_block { use namada::proto::{Code, Data, Section, Signature}; use namada::types::dec::POS_DECIMAL_PRECISION; use namada::types::ethereum_events::{ - EthAddress, TransferToEthereum, Uint as ethUint, + EthAddress, TransferToEthereum, TransferToEthereumKind, Uint as ethUint, }; use namada::types::hash::Hash; use namada::types::keccak::KeccakHash; @@ -1697,6 +1697,7 @@ mod test_finalize_block { let transfer = { use namada::core::types::eth_bridge_pool::PendingTransfer; let transfer = TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 10u64.into(), asset, receiver, diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs index 891a403f90..a7006abecc 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs @@ -466,7 +466,8 @@ mod test_vote_extensions { #[cfg(feature = "abcipp")] use namada::types::eth_abi::Encode; use namada::types::ethereum_events::{ - EthAddress, EthereumEvent, TransferToEthereum, Uint, + EthAddress, EthereumEvent, TransferToEthereum, TransferToEthereumKind, + Uint, }; #[cfg(feature = "abcipp")] use namada::types::keccak::keccak_hash; @@ -598,6 +599,7 @@ mod test_vote_extensions { let event_1 = EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), asset: EthAddress([1; 20]), sender: gen_established_address(), @@ -611,6 +613,7 @@ mod test_vote_extensions { let event_2 = EthereumEvent::TransfersToEthereum { nonce: 1.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), asset: EthAddress([1; 20]), sender: gen_established_address(), @@ -662,6 +665,7 @@ mod test_vote_extensions { let event_1 = EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), asset: EthAddress([1; 20]), sender: gen_established_address(), @@ -724,6 +728,7 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), sender: gen_established_address(), asset: EthAddress([1; 20]), @@ -818,6 +823,7 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), sender: gen_established_address(), asset: EthAddress([1; 20]), @@ -895,6 +901,7 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), sender: gen_established_address(), asset: EthAddress([1; 20]), @@ -977,6 +984,7 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), sender: gen_established_address(), asset: EthAddress([1; 20]), diff --git a/core/src/ledger/eth_bridge/storage/bridge_pool.rs b/core/src/ledger/eth_bridge/storage/bridge_pool.rs index 5134094f3f..0c20c50ff7 100644 --- a/core/src/ledger/eth_bridge/storage/bridge_pool.rs +++ b/core/src/ledger/eth_bridge/storage/bridge_pool.rs @@ -415,7 +415,9 @@ mod test_bridge_pool_tree { use proptest::prelude::*; use super::*; - use crate::types::eth_bridge_pool::{GasFee, TransferToEthereum}; + use crate::types::eth_bridge_pool::{ + GasFee, TransferToEthereum, TransferToEthereumKind, + }; use crate::types::ethereum_events::EthAddress; /// An established user address for testing & development @@ -432,6 +434,7 @@ mod test_bridge_pool_tree { assert_eq!(tree.root().0, [0; 32]); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -458,6 +461,7 @@ mod test_bridge_pool_tree { for i in 0..2 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -485,6 +489,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -522,6 +527,7 @@ mod test_bridge_pool_tree { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -549,6 +555,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -579,6 +586,7 @@ mod test_bridge_pool_tree { fn test_parse_key() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -602,6 +610,7 @@ mod test_bridge_pool_tree { fn test_key_multiple_segments() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -637,6 +646,7 @@ mod test_bridge_pool_tree { let mut tree = BridgePoolTree::default(); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -655,6 +665,7 @@ mod test_bridge_pool_tree { ); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([0; 20]), @@ -686,6 +697,7 @@ mod test_bridge_pool_tree { fn test_single_leaf() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), sender: bertha_address(), recipient: EthAddress([0; 20]), @@ -714,6 +726,7 @@ mod test_bridge_pool_tree { for i in 0..2 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -743,6 +756,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -772,6 +786,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -799,6 +814,7 @@ mod test_bridge_pool_tree { for i in 0..2 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -826,6 +842,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -853,6 +870,7 @@ mod test_bridge_pool_tree { for i in 0..5 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -884,6 +902,7 @@ mod test_bridge_pool_tree { .into_iter() .map(|addr| PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress(addr), sender: bertha_address(), recipient: EthAddress(addr), diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index d70c55ab78..aa471ce7d1 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::types::address::Address; use crate::types::eth_abi::Encode; +pub use crate::types::ethereum_events::TransferToEthereumKind; use crate::types::ethereum_events::{ EthAddress, TransferToEthereum as TransferToEthereumEvent, }; @@ -33,6 +34,8 @@ const NAMESPACE: &str = "transfer"; BorshSchema, )] pub struct TransferToEthereum { + /// The kind of transfer to Ethereum. + pub kind: TransferToEthereumKind, /// The type of token pub asset: EthAddress, /// The recipient address @@ -70,6 +73,7 @@ pub struct PendingTransfer { impl From for ethbridge_structs::Erc20Transfer { fn from(pending: PendingTransfer) -> Self { Self { + kind: pending.transfer.kind as u8, from: pending.transfer.asset.0.into(), to: pending.transfer.recipient.0.into(), amount: pending.transfer.amount.into(), @@ -98,6 +102,7 @@ impl Encode<8> for PendingTransfer { impl From<&TransferToEthereumEvent> for PendingTransfer { fn from(event: &TransferToEthereumEvent) -> Self { let transfer = TransferToEthereum { + kind: event.kind, asset: event.asset, recipient: event.receiver, sender: event.sender.clone(), diff --git a/core/src/types/ethereum_events.rs b/core/src/types/ethereum_events.rs index bab4d46bd2..75bfc5f5cc 100644 --- a/core/src/types/ethereum_events.rs +++ b/core/src/types/ethereum_events.rs @@ -376,6 +376,60 @@ pub struct TransferToNamada { pub receiver: Address, } +/// Transfer to Ethereum kinds. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, +)] +#[repr(u8)] +pub enum TransferToEthereumKind { + /// Transfer ERC20 assets from Namada to Ethereum. + /// + /// These transfers burn wrapped ERC20 assets in Namada, once + /// they have been confirmed. + Erc20 = Self::KIND_ERC20, + /// Refund non-usable tokens. + /// + /// These Bridge pool transfers should be crafted for assets + /// that have been transferred to Namada, that had either not + /// been whitelisted or whose token caps had been exceeded in + /// Namada at the time of the transfer. + Nut = Self::KIND_NUT, +} + +// XXX: keep these values in sync with the smart contracts +impl TransferToEthereumKind { + const KIND_ERC20: u8 = 0; + const KIND_NUT: u8 = 1; +} + +impl TryFrom for TransferToEthereumKind { + type Error = eyre::Error; + + fn try_from(kind: u8) -> Result { + match kind { + Self::KIND_ERC20 => Ok(Self::Erc20), + Self::KIND_NUT => Ok(Self::Nut), + _ => Err(eyre!( + "Only valid kinds are {} (ERC20) and {} (NUT)", + Self::KIND_ERC20, + Self::KIND_NUT + )), + } + } +} + /// An event transferring some kind of value from Namada to Ethereum #[derive( Clone, @@ -392,6 +446,8 @@ pub struct TransferToNamada { Deserialize, )] pub struct TransferToEthereum { + /// The kind of transfer to Ethereum. + pub kind: TransferToEthereumKind, /// Quantity of wrapped Asset in the transfer pub amount: Amount, /// Address of the smart contract issuing the token diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 0052fb01b1..1fa1347b73 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -596,6 +596,7 @@ mod tests { sender: sender.clone(), recipient: EthAddress([i as u8 + 1; 20]), amount: Amount::from(10), + kind: eth_bridge_pool::TransferToEthereumKind::Erc20, }, gas_fee: GasFee { amount: Amount::from(1), @@ -822,6 +823,7 @@ mod tests { let mut transfers = vec![]; for transfer in pending_transfers { let transfer_to_eth = TransferToEthereum { + kind: transfer.transfer.kind, amount: transfer.transfer.amount, asset: transfer.transfer.asset, receiver: transfer.transfer.recipient, @@ -912,6 +914,7 @@ mod tests { sender: address::testing::established_address_1(), recipient: EthAddress([5; 20]), amount: Amount::from(10), + kind: eth_bridge_pool::TransferToEthereumKind::Erc20, }, gas_fee: GasFee { amount: Amount::from(1), @@ -1077,6 +1080,7 @@ mod tests { .into_iter() .map(|transfer| { let transfer_to_eth = TransferToEthereum { + kind: transfer.transfer.kind, amount: transfer.transfer.amount, asset: transfer.transfer.asset, receiver: transfer.transfer.recipient, diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 5b8a8eaa5e..596538bb29 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -28,7 +28,7 @@ use crate::types::control_flow::{ }; use crate::types::eth_abi::Encode; use crate::types::eth_bridge_pool::{ - GasFee, PendingTransfer, TransferToEthereum, + GasFee, PendingTransfer, TransferToEthereum, TransferToEthereumKind, }; use crate::types::keccak::KeccakHash; use crate::types::token::{Amount, DenominatedAmount}; @@ -60,6 +60,9 @@ pub async fn build_bridge_pool_tx( recipient, sender: sender.clone(), amount, + // TODO: dispatch on the transfer kind, based + // on CLI arguments + kind: TransferToEthereumKind::Erc20, }, gas_fee: GasFee { amount: fee_amount, @@ -672,6 +675,7 @@ mod recommendations { pub fn transfer(gas_amount: u64) -> PendingTransfer { PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), recipient: EthAddress([2; 20]), sender: bertha_address(), diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 323633b563..df5696e60a 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -357,7 +357,9 @@ mod test_bridge_pool_vp { use crate::ledger::storage_api::StorageWrite; use crate::types::address::{nam, wnam}; use crate::types::chain::ChainId; - use crate::types::eth_bridge_pool::{GasFee, TransferToEthereum}; + use crate::types::eth_bridge_pool::{ + GasFee, TransferToEthereum, TransferToEthereumKind, + }; use crate::types::hash::Hash; use crate::types::storage::TxIndex; use crate::types::transaction::TxType; @@ -406,6 +408,7 @@ mod test_bridge_pool_vp { fn initial_pool() -> PendingTransfer { PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: ASSET, sender: bertha_address(), recipient: EthAddress([0; 20]), @@ -570,6 +573,7 @@ mod test_bridge_pool_vp { // the transfer to be added to the pool let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: ASSET, sender: bertha_address(), recipient: EthAddress([1; 20]), @@ -826,6 +830,7 @@ mod test_bridge_pool_vp { |transfer, log| { let t = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), sender: bertha_address(), recipient: EthAddress([11; 20]), @@ -856,6 +861,7 @@ mod test_bridge_pool_vp { |transfer, log| { let t = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), sender: bertha_address(), recipient: EthAddress([11; 20]), @@ -977,6 +983,7 @@ mod test_bridge_pool_vp { // the transfer to be added to the pool let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: ASSET, sender: bertha_address(), recipient: EthAddress([1; 20]), @@ -1043,6 +1050,7 @@ mod test_bridge_pool_vp { // the transfer to be added to the pool let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: wnam(), sender: bertha_address(), recipient: EthAddress([1; 20]), @@ -1130,6 +1138,7 @@ mod test_bridge_pool_vp { // the transfer to be added to the pool let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: wnam(), sender: bertha_address(), recipient: EthAddress([1; 20]), @@ -1236,6 +1245,7 @@ mod test_bridge_pool_vp { // the transfer to be added to the pool let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: wnam(), sender: bertha_address(), recipient: EthAddress([1; 20]), diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index d7d0ed249e..f7c0731c70 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -588,7 +588,7 @@ mod test_ethbridge_router { use crate::ledger::queries::RPC; use crate::types::eth_abi::Encode; use crate::types::eth_bridge_pool::{ - GasFee, PendingTransfer, TransferToEthereum, + GasFee, PendingTransfer, TransferToEthereum, TransferToEthereumKind, }; use crate::types::ethereum_events::EthAddress; @@ -788,6 +788,7 @@ mod test_ethbridge_router { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -829,6 +830,7 @@ mod test_ethbridge_router { let mut client = TestClient::new(RPC); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -889,6 +891,7 @@ mod test_ethbridge_router { let mut client = TestClient::new(RPC); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -998,6 +1001,7 @@ mod test_ethbridge_router { let mut client = TestClient::new(RPC); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -1088,6 +1092,7 @@ mod test_ethbridge_router { let mut client = TestClient::new(RPC); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -1159,6 +1164,7 @@ mod test_ethbridge_router { let mut client = TestClient::new(RPC); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -1183,6 +1189,7 @@ mod test_ethbridge_router { let event_transfer = namada_core::types::ethereum_events::TransferToEthereum { + kind: transfer.transfer.kind, asset: transfer.transfer.asset, receiver: transfer.transfer.recipient, amount: transfer.transfer.amount, @@ -1269,6 +1276,7 @@ mod test_ethbridge_router { test_utils::init_default_storage(&mut client.wl_storage); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index 544889b2d1..ee3c659591 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -12,7 +12,7 @@ mod test_bridge_pool_vp { use namada::types::address::{nam, wnam}; use namada::types::chain::ChainId; use namada::types::eth_bridge_pool::{ - GasFee, PendingTransfer, TransferToEthereum, + GasFee, PendingTransfer, TransferToEthereum, TransferToEthereumKind, }; use namada::types::ethereum_events::EthAddress; use namada::types::key::{common, ed25519, SecretKey}; @@ -119,6 +119,7 @@ mod test_bridge_pool_vp { fn validate_erc20_tx() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: ASSET, recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -136,6 +137,7 @@ mod test_bridge_pool_vp { fn validate_mint_wnam_tx() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: wnam(), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -153,6 +155,7 @@ mod test_bridge_pool_vp { fn validate_mint_wnam_different_sender_tx() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: wnam(), recipient: EthAddress([0; 20]), sender: bertha_address(), From 8297842597fd0aab5431dfbb87e5fca42b2d24db Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 15:20:22 +0100 Subject: [PATCH 09/94] Mint NUTs upon receiving non-whitelisted assets or exceeding token caps --- .../transactions/ethereum_events/events.rs | 119 +++++++++++------- .../src/storage/eth_bridge_queries.rs | 57 +++++++++ 2 files changed, 132 insertions(+), 44 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 1fa1347b73..05f374c64b 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -30,7 +30,7 @@ use namada_core::types::token::{balance_key, minted_balance_key}; use crate::parameters::read_native_erc20_address; use crate::protocol::transactions::update; -use crate::storage::eth_bridge_queries::EthBridgeQueries; +use crate::storage::eth_bridge_queries::{EthAssetMint, EthBridgeQueries}; /// Updates storage based on the given confirmed `event`. For example, for a /// confirmed [`EthereumEvent::TransfersToNamada`], mint the corresponding @@ -140,15 +140,25 @@ where receiver, } = transfer; let mut changed = if asset != &wrapped_native_erc20 { - let changed = - mint_wrapped_erc20s(wl_storage, asset, receiver, amount)?; + let (asset_count, changed) = + mint_eth_assets(wl_storage, asset, receiver, amount)?; // TODO: query denomination of the whitelisted token from storage, // and print this amount with the proper formatting; for now, use // NAM's formatting - tracing::info!( - "Minted wrapped ERC20s - (receiver - {receiver}, amount - {})", - amount.to_string_native(), - ); + if !asset_count.erc20_amount.is_zero() { + tracing::info!( + "Minted wrapped ERC20s - (asset - {asset}, receiver - \ + {receiver}, amount - {})", + asset_count.erc20_amount.to_string_native(), + ); + } + if !asset_count.nut_amount.is_zero() { + tracing::info!( + "Minted NUTs - (asset - {asset}, receiver - {receiver}, \ + amount - {})", + asset_count.nut_amount.to_string_native(), + ); + } changed } else { redeem_native_token(wl_storage, receiver, amount)? @@ -220,55 +230,76 @@ where ])) } +/// Helper function to mint assets originating from Ethereum +/// on Namada. +/// /// Mints `amount` of a wrapped ERC20 `asset` for `receiver`. -fn mint_wrapped_erc20s( +/// If the given asset is not whitelisted or has exceeded the +/// token caps, mint NUTs, too. +fn mint_eth_assets( wl_storage: &mut WlStorage, asset: &EthAddress, receiver: &Address, - amount: &token::Amount, -) -> Result> + &amount: &token::Amount, +) -> Result<(EthAssetMint, BTreeSet)> where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { let mut changed_keys = BTreeSet::default(); - let token = wrapped_erc20s::token(asset); - let balance_key = balance_key(&token, receiver); - update::amount(wl_storage, &balance_key, |balance| { - tracing::debug!( - %balance_key, - ?balance, - "Existing value found", - ); - balance.receive(amount); - tracing::debug!( - %balance_key, - ?balance, - "New value calculated", - ); - })?; - _ = changed_keys.insert(balance_key); - let supply_key = minted_balance_key(&token); - update::amount(wl_storage, &supply_key, |supply| { - tracing::debug!( - %supply_key, - ?supply, - "Existing value found", - ); - supply.receive(amount); - tracing::debug!( - %supply_key, - ?supply, - "New value calculated", - ); - })?; - _ = changed_keys.insert(supply_key); + let asset_count = wl_storage + .ethbridge_queries() + .get_eth_assets_to_mint(asset, amount); + + let assets_to_mint = [ + // check if we should mint nuts + (!asset_count.nut_amount.is_zero()) + .then(|| (wrapped_erc20s::nut(asset), asset_count.nut_amount)), + // check if we should mint erc20s + (!asset_count.erc20_amount.is_zero()) + .then(|| (wrapped_erc20s::token(asset), asset_count.erc20_amount)), + ] + .into_iter() + // remove assets that do not need to be + // minted from the iterator + .flatten(); + + for (token, ref amount) in assets_to_mint { + let balance_key = balance_key(&token, receiver); + update::amount(wl_storage, &balance_key, |balance| { + tracing::debug!( + %balance_key, + ?balance, + "Existing value found", + ); + balance.receive(amount); + tracing::debug!( + %balance_key, + ?balance, + "New value calculated", + ); + })?; + _ = changed_keys.insert(balance_key); - // mint the token without a minter because a protocol tx doesn't need to - // trigger a VP + let supply_key = minted_balance_key(&token); + update::amount(wl_storage, &supply_key, |supply| { + tracing::debug!( + %supply_key, + ?supply, + "Existing value found", + ); + supply.receive(amount); + tracing::debug!( + %supply_key, + ?supply, + "New value calculated", + ); + })?; + _ = changed_keys.insert(supply_key); + } - Ok(changed_keys) + Ok((asset_count, changed_keys)) } fn act_on_transfers_to_eth( diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index ae380c8433..0a738d96a4 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -441,6 +441,63 @@ where .read(&key) .expect("Reading from storage should not fail") } + + /// Return the number of ERC20 and NUT assets to be minted, + /// after receiving a "transfer to Namada" Ethereum event. + /// + /// NUTs are minted when: + /// + /// 1. `token` is not whitelisted. + /// 2. `token` has exceeded the configured token caps, + /// after minting `amount_to_mint`. + pub fn get_eth_assets_to_mint( + self, + token: &EthAddress, + amount_to_mint: token::Amount, + ) -> EthAssetMint { + if !self.is_token_whitelisted(token) { + return EthAssetMint { + nut_amount: amount_to_mint, + erc20_amount: token::Amount::zero(), + }; + } + + let supply = self.get_token_supply(token).unwrap_or_default(); + let cap = self.get_token_cap(token).unwrap_or_default(); + + if hints::unlikely(cap < supply) { + panic!( + "Namada's state is faulty! The Ethereum ERC20 asset {token} \ + has a higher minted supply than the configured token cap: \ + cap:{cap:?} < supply:{supply:?}" + ); + } + + if amount_to_mint + supply > cap { + let erc20_amount = cap - supply; + let nut_amount = amount_to_mint - erc20_amount; + + return EthAssetMint { + nut_amount, + erc20_amount, + }; + } + + EthAssetMint { + erc20_amount: amount_to_mint, + nut_amount: token::Amount::zero(), + } + } +} + +/// Number of tokens to mint after receiving a "transfer +/// to Namada" Ethereum event. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct EthAssetMint { + /// Amount of NUTs to mint. + pub nut_amount: token::Amount, + /// Amount of wrapped ERC20s to mint. + pub erc20_amount: token::Amount, } /// A handle to the Ethereum addresses of the set of consensus From 82320aa3673d5b36962d824f387d152b1676504f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 16:28:29 +0100 Subject: [PATCH 10/94] Add helper fn to whitelist tokens in tests --- ethereum_bridge/src/test_utils.rs | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/ethereum_bridge/src/test_utils.rs b/ethereum_bridge/src/test_utils.rs index e74c4803e0..f2a1ee0b8c 100644 --- a/ethereum_bridge/src/test_utils.rs +++ b/ethereum_bridge/src/test_utils.rs @@ -5,6 +5,7 @@ use std::num::NonZeroU64; use borsh::BorshSerialize; use namada_core::ledger::eth_bridge::storage::bridge_pool::get_key_from_hash; +use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::storage::mockdb::MockDBWriteBatch; use namada_core::ledger::storage::testing::{TestStorage, TestWlStorage}; use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; @@ -114,6 +115,45 @@ pub fn bootstrap_ethereum_bridge( config } +/// Whitelist metadata to pass to [`whitelist_tokens`]. +pub struct WhitelistMeta { + /// Token cap. + pub cap: token::Amount, + /// Token denomination. + pub denom: u8, +} + +/// Whitelist the given Ethereum tokens. +pub fn whitelist_tokens(wl_storage: &mut TestWlStorage, token_list: L) +where + L: Into>, +{ + for (asset, WhitelistMeta { cap, denom }) in token_list.into() { + let cap_key = whitelist::Key { + asset, + suffix: whitelist::KeyType::Cap, + } + .into(); + wl_storage.write(&cap_key, cap).expect("Test failed"); + + let whitelisted_key = whitelist::Key { + asset, + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + wl_storage + .write(&whitelisted_key, true) + .expect("Test failed"); + + let denom_key = whitelist::Key { + asset, + suffix: whitelist::KeyType::Denomination, + } + .into(); + wl_storage.write(&denom_key, denom).expect("Test failed"); + } +} + /// Returns the number of keys in `storage` which have values present. pub fn stored_keys_count(wl_storage: &TestWlStorage) -> usize { let root = Key { segments: vec![] }; From 1a70c4fc05fa9de94bf663131d533eda0d3176fa Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 16:35:22 +0100 Subject: [PATCH 11/94] Fix unit tests requiring token whitelist checks --- .../transactions/ethereum_events/events.rs | 10 ++++++++++ .../transactions/ethereum_events/mod.rs | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 05f374c64b..ace6d16f5d 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -797,6 +797,16 @@ mod tests { fn test_act_on_transfers_to_namada_mints_wdai() { let mut wl_storage = TestWlStorage::default(); test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + test_utils::whitelist_tokens( + &mut wl_storage, + [( + DAI_ERC20_ETH_ADDRESS, + test_utils::WhitelistMeta { + cap: Amount::max(), + denom: 18, + }, + )], + ); let initial_stored_keys_count = stored_keys_count(&wl_storage); let amount = Amount::from(100); diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs index d3cd32972d..c8bd21a4bf 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs @@ -330,6 +330,16 @@ mod tests { )]); let mut wl_storage = TestWlStorage::default(); test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + test_utils::whitelist_tokens( + &mut wl_storage, + [( + DAI_ERC20_ETH_ADDRESS, + test_utils::WhitelistMeta { + cap: Amount::max(), + denom: 18, + }, + )], + ); let changed_keys = apply_updates(&mut wl_storage, updates, voting_powers)?; @@ -405,6 +415,16 @@ mod tests { vec![(sole_validator.clone(), Amount::native_whole(100))], )); test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + test_utils::whitelist_tokens( + &mut wl_storage, + [( + DAI_ERC20_ETH_ADDRESS, + test_utils::WhitelistMeta { + cap: Amount::max(), + denom: 18, + }, + )], + ); let receiver = address::testing::established_address_1(); let event = EthereumEvent::TransfersToNamada { From 339e18aa54d09fddeaa11942cb531de668da9ef8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 21:06:38 +0100 Subject: [PATCH 12/94] Fix refunding and burning of NUTs --- core/src/types/eth_bridge_pool.rs | 16 ++++++++++++++++ .../transactions/ethereum_events/events.rs | 8 ++++---- wasm/wasm_source/src/tx_bridge_pool.rs | 4 ++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index aa471ce7d1..c7394af167 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -5,6 +5,7 @@ use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use ethabi::token::Token; use serde::{Deserialize, Serialize}; +use crate::ledger::eth_bridge::storage::wrapped_erc20s; use crate::types::address::Address; use crate::types::eth_abi::Encode; pub use crate::types::ethereum_events::TransferToEthereumKind; @@ -70,6 +71,21 @@ pub struct PendingTransfer { pub gas_fee: GasFee, } +impl PendingTransfer { + /// Get a token [`Address`] from this [`PendingTransfer`]. + #[inline] + pub fn token_address(&self) -> Address { + match &self.transfer.kind { + TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(&self.transfer.asset) + } + TransferToEthereumKind::Nut => { + wrapped_erc20s::nut(&self.transfer.asset) + } + } + } +} + impl From for ethbridge_structs::Erc20Transfer { fn from(pending: PendingTransfer) -> Self { Self { diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index ace6d16f5d..ea97caa72f 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -510,7 +510,7 @@ where ); (escrow_balance_key, sender_balance_key) } else { - let token = wrapped_erc20s::token(&transfer.transfer.asset); + let token = transfer.token_address(); let escrow_balance_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); let sender_balance_key = balance_key(&token, &transfer.transfer.sender); (escrow_balance_key, sender_balance_key) @@ -548,7 +548,7 @@ where return Ok(changed_keys); } - let token = wrapped_erc20s::token(&transfer.transfer.asset); + let token = transfer.token_address(); let escrow_balance_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); update::amount(wl_storage, &escrow_balance_key, |balance| { @@ -690,7 +690,7 @@ mod tests { ) .expect("Test failed"); } else { - let token = wrapped_erc20s::token(&transfer.transfer.asset); + let token = transfer.token_address(); let sender_key = balance_key(&token, &transfer.transfer.sender); let sender_balance = Amount::from(0); wl_storage @@ -1029,7 +1029,7 @@ mod tests { .expect("Test failed"); assert_eq!(escrow_balance, Amount::from(0)); } else { - let token = wrapped_erc20s::token(&transfer.transfer.asset); + let token = transfer.token_address(); let sender_key = balance_key(&token, &transfer.transfer.sender); let value = wl_storage.read_bytes(&sender_key).expect("Test failed"); diff --git a/wasm/wasm_source/src/tx_bridge_pool.rs b/wasm/wasm_source/src/tx_bridge_pool.rs index bf73e83f7d..c1a65403a7 100644 --- a/wasm/wasm_source/src/tx_bridge_pool.rs +++ b/wasm/wasm_source/src/tx_bridge_pool.rs @@ -1,7 +1,7 @@ //! A tx for adding a transfer request across the Ethereum bridge //! into the bridge pool. use borsh::{BorshDeserialize, BorshSerialize}; -use eth_bridge::storage::{bridge_pool, native_erc20_key, wrapped_erc20s}; +use eth_bridge::storage::{bridge_pool, native_erc20_key}; use eth_bridge_pool::{GasFee, PendingTransfer, TransferToEthereum}; use namada_tx_prelude::*; @@ -45,7 +45,7 @@ fn apply_tx(ctx: &mut Ctx, signed: Tx) -> TxResult { )?; } else { // Otherwise we escrow ERC20 tokens. - let token = wrapped_erc20s::token(&asset); + let token = transfer.token_address(); token::transfer( ctx, sender, From d115bb094232351dea8e0c9314b6260c4260af66 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 13 Jul 2023 11:14:08 +0100 Subject: [PATCH 13/94] Rework DAI minting unit tests to account for NUTs --- .../transactions/ethereum_events/events.rs | 142 +++++++++++++----- 1 file changed, 102 insertions(+), 40 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index ea97caa72f..e8cc2e723d 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -792,52 +792,114 @@ mod tests { ); } - #[test] - /// Test acting on a single transfer and minting the first ever wDAI - fn test_act_on_transfers_to_namada_mints_wdai() { - let mut wl_storage = TestWlStorage::default(); - test_utils::bootstrap_ethereum_bridge(&mut wl_storage); - test_utils::whitelist_tokens( - &mut wl_storage, - [( - DAI_ERC20_ETH_ADDRESS, - test_utils::WhitelistMeta { - cap: Amount::max(), - denom: 18, - }, - )], - ); - let initial_stored_keys_count = stored_keys_count(&wl_storage); + /// Parameters to test minting DAI in Namada. + struct TestMintDai { + /// The token cap of DAI. + /// + /// If the token is not whitelisted, this value + /// is not set. + dai_token_cap: Option, + /// The transferred amount of DAI. + transferred_amount: token::Amount, + } - let amount = Amount::from(100); - let receiver = address::testing::established_address_1(); - let transfers = vec![TransferToNamada { - amount, - asset: DAI_ERC20_ETH_ADDRESS, - receiver: receiver.clone(), - }]; + impl TestMintDai { + /// Execute a test with the given parameters. + fn run_test(self) { + let dai_token_cap = self.dai_token_cap.unwrap_or_default(); - update_transfers_to_namada_state( - &mut wl_storage, - &mut BTreeSet::new(), - &transfers, - ) - .unwrap(); + let (erc20_amount, nut_amount) = + if dai_token_cap > self.transferred_amount { + (self.transferred_amount, token::Amount::zero()) + } else { + (dai_token_cap, self.transferred_amount - dai_token_cap) + }; + assert_eq!(self.transferred_amount, nut_amount + erc20_amount); + + let mut wl_storage = TestWlStorage::default(); + test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + if !dai_token_cap.is_zero() { + test_utils::whitelist_tokens( + &mut wl_storage, + [( + DAI_ERC20_ETH_ADDRESS, + test_utils::WhitelistMeta { + cap: dai_token_cap, + denom: 18, + }, + )], + ); + } - let wdai = wrapped_erc20s::token(&DAI_ERC20_ETH_ADDRESS); - let receiver_balance_key = balance_key(&wdai, &receiver); - let wdai_supply_key = minted_balance_key(&wdai); + let receiver = address::testing::established_address_1(); + let transfers = vec![TransferToNamada { + amount: self.transferred_amount, + asset: DAI_ERC20_ETH_ADDRESS, + receiver: receiver.clone(), + }]; + + update_transfers_to_namada_state( + &mut wl_storage, + &mut BTreeSet::new(), + &transfers, + ) + .unwrap(); - assert_eq!( - stored_keys_count(&wl_storage), - initial_stored_keys_count + 2 - ); + for is_nut in [false, true] { + let wdai = if is_nut { + wrapped_erc20s::nut(&DAI_ERC20_ETH_ADDRESS) + } else { + wrapped_erc20s::token(&DAI_ERC20_ETH_ADDRESS) + }; + let expected_amount = + if is_nut { nut_amount } else { erc20_amount }; + + let receiver_balance_key = balance_key(&wdai, &receiver); + let wdai_supply_key = minted_balance_key(&wdai); - let expected_amount = amount.try_to_vec().unwrap(); - for key in vec![receiver_balance_key, wdai_supply_key] { - let value = wl_storage.read_bytes(&key).unwrap(); - assert_matches!(value, Some(bytes) if bytes == expected_amount); + for key in vec![receiver_balance_key, wdai_supply_key] { + let value: Option = + wl_storage.read(&key).unwrap(); + if expected_amount.is_zero() { + assert_matches!(value, None); + } else { + assert_matches!(value, Some(amount) if amount == expected_amount); + } + } + } + } + } + + /// Test that if DAI is never whitelisted, we only mint NUTs. + #[test] + fn test_minting_dai_when_not_whitelisted() { + TestMintDai { + dai_token_cap: None, + transferred_amount: Amount::from(100), + } + .run_test(); + } + + /// Test that overrunning the token caps results in minting DAI NUTs, + /// along with wDAI. + #[test] + fn test_minting_dai_on_cap_overrun() { + TestMintDai { + dai_token_cap: Some(Amount::from(80)), + transferred_amount: Amount::from(100), + } + .run_test(); + } + + /// Test acting on a single "transfer to Namada" Ethereum event + /// and minting the first ever wDAI. + #[test] + fn test_minting_dai_wrapped() { + TestMintDai { + dai_token_cap: Some(Amount::max()), + transferred_amount: Amount::from(100), } + .run_test(); } #[test] From d26e9838a3d5d987677b70b306928fdb9e208211 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 13 Jul 2023 13:53:09 +0100 Subject: [PATCH 14/94] Test burning NUTs --- .../transactions/ethereum_events/events.rs | 147 ++++++++++++++---- 1 file changed, 113 insertions(+), 34 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index e8cc2e723d..da967b85a0 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -613,21 +613,25 @@ mod tests { assets_transferred: A, ) -> Vec where - A: Into>, + A: Into< + BTreeSet<(EthAddress, eth_bridge_pool::TransferToEthereumKind)>, + >, { let sender = address::testing::established_address_1(); let payer = address::testing::established_address_2(); // set pending transfers let mut pending_transfers = vec![]; - for (i, asset) in assets_transferred.into().into_iter().enumerate() { + for (i, (asset, kind)) in + assets_transferred.into().into_iter().enumerate() + { let transfer = PendingTransfer { transfer: eth_bridge_pool::TransferToEthereum { asset, sender: sender.clone(), recipient: EthAddress([i as u8 + 1; 20]), amount: Amount::from(10), - kind: eth_bridge_pool::TransferToEthereumKind::Erc20, + kind, }, gas_fee: GasFee { amount: Amount::from(1), @@ -651,7 +655,18 @@ mod tests { ) -> Vec { init_bridge_pool_transfers( wl_storage, - (0..2).map(|i| EthAddress([i; 20])).collect::>(), + (0..2) + .map(|i| { + ( + EthAddress([i; 20]), + if i & 1 == 0 { + eth_bridge_pool::TransferToEthereumKind::Erc20 + } else { + eth_bridge_pool::TransferToEthereumKind::Nut + }, + ) + }) + .collect::>(), ) } @@ -914,10 +929,19 @@ mod tests { let native_erc20 = read_native_erc20_address(&wl_storage).expect("Test failed"); let random_erc20 = EthAddress([0xff; 20]); - let random_erc20_token = wrapped_erc20s::token(&random_erc20); + let random_erc20_token = wrapped_erc20s::nut(&random_erc20); + let random_erc20_2 = EthAddress([0xee; 20]); + let random_erc20_token_2 = wrapped_erc20s::token(&random_erc20_2); let pending_transfers = init_bridge_pool_transfers( &mut wl_storage, - [native_erc20, random_erc20], + [ + (native_erc20, eth_bridge_pool::TransferToEthereumKind::Erc20), + (random_erc20, eth_bridge_pool::TransferToEthereumKind::Nut), + ( + random_erc20_2, + eth_bridge_pool::TransferToEthereumKind::Erc20, + ), + ], ); init_balance(&mut wl_storage, &pending_transfers); let pending_keys: HashSet = @@ -959,7 +983,16 @@ mod tests { &BRIDGE_POOL_ADDRESS )) ); + assert!( + changed_keys.remove(&balance_key( + &random_erc20_token_2, + &BRIDGE_POOL_ADDRESS + )) + ); assert!(changed_keys.remove(&minted_balance_key(&random_erc20_token))); + assert!( + changed_keys.remove(&minted_balance_key(&random_erc20_token_2)) + ); assert!(changed_keys.remove(&payer_balance_key)); assert!(changed_keys.remove(&pool_balance_key)); assert!(changed_keys.remove(&get_nonce_key())); @@ -981,7 +1014,7 @@ mod tests { .expect("Test failed: no value in storage"), ) .expect("Test failed"); - assert_eq!(relayer_balance, Amount::from(2)); + assert_eq!(relayer_balance, Amount::from(3)); let bp_balance_post = Amount::try_from_slice( &wl_storage .read_bytes(&pool_balance_key) @@ -990,7 +1023,8 @@ mod tests { ) .expect("Test failed"); bp_balance_pre.spend(&bp_balance_post); - assert_eq!(bp_balance_pre, Amount::from(2)); + assert_eq!(bp_balance_pre, Amount::from(3)); + assert_eq!(bp_balance_post, Amount::from(0)); } #[test] @@ -1169,13 +1203,31 @@ mod tests { let pending_transfers = init_bridge_pool_transfers( &mut wl_storage, [ - native_erc20, - EthAddress([0xaa; 20]), - EthAddress([0xbb; 20]), - EthAddress([0xcc; 20]), - EthAddress([0xdd; 20]), - EthAddress([0xee; 20]), - EthAddress([0xff; 20]), + (native_erc20, eth_bridge_pool::TransferToEthereumKind::Nut), + ( + EthAddress([0xaa; 20]), + eth_bridge_pool::TransferToEthereumKind::Erc20, + ), + ( + EthAddress([0xbb; 20]), + eth_bridge_pool::TransferToEthereumKind::Nut, + ), + ( + EthAddress([0xcc; 20]), + eth_bridge_pool::TransferToEthereumKind::Erc20, + ), + ( + EthAddress([0xdd; 20]), + eth_bridge_pool::TransferToEthereumKind::Nut, + ), + ( + EthAddress([0xee; 20]), + eth_bridge_pool::TransferToEthereumKind::Erc20, + ), + ( + EthAddress([0xff; 20]), + eth_bridge_pool::TransferToEthereumKind::Nut, + ), ], ); init_balance(&mut wl_storage, &pending_transfers); @@ -1213,6 +1265,7 @@ mod tests { sent_amount: token::Amount, prev_balance: Option, prev_supply: Option, + kind: eth_bridge_pool::TransferToEthereumKind, } test_wrapped_erc20s_aux(|wl_storage, event| { @@ -1225,29 +1278,48 @@ mod tests { let native_erc20 = read_native_erc20_address(wl_storage).expect("Test failed"); let deltas = transfers - .filter_map(|TransferToEthereum { asset, amount, .. }| { - if asset == &native_erc20 { - return None; - } - let erc20_token = wrapped_erc20s::token(asset); - let prev_balance = wl_storage - .read(&balance_key(&erc20_token, &BRIDGE_POOL_ADDRESS)) - .expect("Test failed"); - let prev_supply = wl_storage - .read(&minted_balance_key(&erc20_token)) - .expect("Test failed"); - Some(Delta { - asset: *asset, - sent_amount: *amount, - prev_balance, - prev_supply, - }) - }) + .filter_map( + |TransferToEthereum { + kind, + asset, + amount, + .. + }| { + if asset == &native_erc20 { + return None; + } + let erc20_token = match kind { + eth_bridge_pool::TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(asset) + } + eth_bridge_pool::TransferToEthereumKind::Nut => { + wrapped_erc20s::nut(asset) + } + }; + let prev_balance = wl_storage + .read(&balance_key( + &erc20_token, + &BRIDGE_POOL_ADDRESS, + )) + .expect("Test failed"); + let prev_supply = wl_storage + .read(&minted_balance_key(&erc20_token)) + .expect("Test failed"); + Some(Delta { + kind: *kind, + asset: *asset, + sent_amount: *amount, + prev_balance, + prev_supply, + }) + }, + ) .collect::>(); _ = act_on(wl_storage, event).unwrap(); for Delta { + kind, ref asset, sent_amount, prev_balance, @@ -1263,7 +1335,14 @@ mod tests { .checked_sub(sent_amount) .expect("Test failed"); - let erc20_token = wrapped_erc20s::token(asset); + let erc20_token = match kind { + eth_bridge_pool::TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(asset) + } + eth_bridge_pool::TransferToEthereumKind::Nut => { + wrapped_erc20s::nut(asset) + } + }; let balance: token::Amount = wl_storage .read(&balance_key(&erc20_token, &BRIDGE_POOL_ADDRESS)) From 8de897da2fa68ffc1c76658c8f1b4f632ae2ef50 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 14 Jul 2023 09:16:27 +0100 Subject: [PATCH 15/94] Check NUT escrow in Bridge pool VP --- shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index df5696e60a..b6232e21fa 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -19,7 +19,6 @@ use namada_core::ledger::eth_bridge::storage::bridge_pool::{ }; use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; use namada_ethereum_bridge::parameters::read_native_erc20_address; -use namada_ethereum_bridge::storage::wrapped_erc20s; use crate::ledger::native_vp::ethereum_bridge::vp::check_balance_changes; use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader}; @@ -92,7 +91,7 @@ where transfer: &PendingTransfer, ) -> Result { // check that the assets to be transferred were escrowed - let token = wrapped_erc20s::token(&transfer.transfer.asset); + let token = transfer.token_address(); let owner_key = balance_key(&token, &transfer.transfer.sender); let escrow_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); if keys_changed.contains(&owner_key) @@ -347,6 +346,7 @@ mod test_bridge_pool_vp { use namada_ethereum_bridge::parameters::{ Contracts, EthereumBridgeConfig, UpgradeableContract, }; + use namada_ethereum_bridge::storage::wrapped_erc20s; use super::*; use crate::ledger::gas::VpGasMeter; From a83986679eff0494d9d51c7721b5a096a59f07cd Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 14 Jul 2023 15:57:55 +0100 Subject: [PATCH 16/94] Test NUTs in the Bridge pool --- .../ethereum_bridge/bridge_pool_vp.rs | 173 ++++++++++++++++-- 1 file changed, 159 insertions(+), 14 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index b6232e21fa..9632f4d7f3 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -370,23 +370,33 @@ mod test_bridge_pool_vp { const ASSET: EthAddress = EthAddress([0; 20]); const BERTHA_WEALTH: u64 = 1_000_000; const BERTHA_TOKENS: u64 = 10_000; + const DAES_NUTS: u64 = 10_000; + const DAEWONS_GAS: u64 = 1_000_000; const ESCROWED_AMOUNT: u64 = 1_000; const ESCROWED_TOKENS: u64 = 1_000; + const ESCROWED_NUTS: u64 = 1_000; const GAS_FEE: u64 = 100; const TOKENS: u64 = 100; /// A set of balances for an address struct Balance { + /// NUT or ERC20 Ethereum asset kind. + kind: TransferToEthereumKind, + /// The owner of the ERC20 assets. owner: Address, - balance: Amount, + /// The gas to escrow under the Bridge pool. + gas: Amount, + /// The tokens to be sent across the Ethereum bridge, + /// escrowed to the Bridge pool account. token: Amount, } impl Balance { - fn new(address: Address) -> Self { + fn new(kind: TransferToEthereumKind, address: Address) -> Self { Self { + kind, owner: address, - balance: 0.into(), + gas: 0.into(), token: 0.into(), } } @@ -398,6 +408,22 @@ mod test_bridge_pool_vp { .expect("The token address decoding shouldn't fail") } + /// An implicit user address for testing & development + #[allow(dead_code)] + pub fn daewon_address() -> Address { + use crate::types::key::*; + pub fn daewon_keypair() -> common::SecretKey { + let bytes = [ + 235, 250, 15, 1, 145, 250, 172, 218, 247, 27, 63, 212, 60, 47, + 164, 57, 187, 156, 182, 144, 107, 174, 38, 81, 37, 40, 19, 142, + 68, 135, 57, 50, + ]; + let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); + ed_sk.try_to_sk().unwrap() + } + (&daewon_keypair().ref_to()).into() + } + /// A sampled established address for tests pub fn established_address_1() -> Address { Address::decode("atest1v4ehgw36g56ngwpk8ppnzsf4xqeyvsf3xq6nxde5gseyys3nxgenvvfex5cnyd2rx9zrzwfctgx7sp") @@ -433,20 +459,32 @@ mod test_bridge_pool_vp { writelog .write(&get_pending_key(&transfer), transfer.try_to_vec().unwrap()) .expect("Test failed"); - // set up a user with a balance + // set up users with ERC20 and NUT balances update_balances( &mut writelog, - Balance::new(bertha_address()), + Balance::new(TransferToEthereumKind::Erc20, bertha_address()), SignedAmount::Positive(BERTHA_WEALTH.into()), SignedAmount::Positive(BERTHA_TOKENS.into()), ); + update_balances( + &mut writelog, + Balance::new(TransferToEthereumKind::Nut, daewon_address()), + SignedAmount::Positive(DAEWONS_GAS.into()), + SignedAmount::Positive(DAES_NUTS.into()), + ); // set up the initial balances of the bridge pool update_balances( &mut writelog, - Balance::new(BRIDGE_POOL_ADDRESS), + Balance::new(TransferToEthereumKind::Erc20, BRIDGE_POOL_ADDRESS), SignedAmount::Positive(ESCROWED_AMOUNT.into()), SignedAmount::Positive(ESCROWED_TOKENS.into()), ); + update_balances( + &mut writelog, + Balance::new(TransferToEthereumKind::Nut, BRIDGE_POOL_ADDRESS), + SignedAmount::Positive(ESCROWED_AMOUNT.into()), + SignedAmount::Positive(ESCROWED_NUTS.into()), + ); writelog.commit_tx(); writelog } @@ -460,14 +498,19 @@ mod test_bridge_pool_vp { token_delta: SignedAmount, ) -> BTreeSet { // get the balance keys - let token_key = - balance_key(&wrapped_erc20s::token(&ASSET), &balance.owner); + let token_key = balance_key( + &match balance.kind { + TransferToEthereumKind::Erc20 => wrapped_erc20s::token(&ASSET), + TransferToEthereumKind::Nut => wrapped_erc20s::nut(&ASSET), + }, + &balance.owner, + ); let account_key = balance_key(&nam(), &balance.owner); // update the balance of nam let new_balance = match gas_delta { - SignedAmount::Positive(amount) => balance.balance + amount, - SignedAmount::Negative(amount) => balance.balance - amount, + SignedAmount::Positive(amount) => balance.gas + amount, + SignedAmount::Negative(amount) => balance.gas - amount, } .try_to_vec() .expect("Test failed"); @@ -592,8 +635,9 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + kind: TransferToEthereumKind::Erc20, owner: bertha_address(), - balance: BERTHA_WEALTH.into(), + gas: BERTHA_WEALTH.into(), token: BERTHA_TOKENS.into(), }, payer_gas_delta, @@ -605,8 +649,9 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + kind: TransferToEthereumKind::Erc20, owner: BRIDGE_POOL_ADDRESS, - balance: ESCROWED_AMOUNT.into(), + gas: ESCROWED_AMOUNT.into(), token: ESCROWED_TOKENS.into(), }, gas_escrow_delta, @@ -931,8 +976,9 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + kind: TransferToEthereumKind::Erc20, owner: bertha_address(), - balance: BERTHA_WEALTH.into(), + gas: BERTHA_WEALTH.into(), token: BERTHA_TOKENS.into(), }, SignedAmount::Negative(GAS_FEE.into()), @@ -944,8 +990,9 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + kind: TransferToEthereumKind::Erc20, owner: BRIDGE_POOL_ADDRESS, - balance: ESCROWED_AMOUNT.into(), + gas: ESCROWED_AMOUNT.into(), token: ESCROWED_TOKENS.into(), }, SignedAmount::Positive(GAS_FEE.into()), @@ -1326,4 +1373,102 @@ mod test_bridge_pool_vp { .expect("Test failed"); assert!(!res); } + + /// Auxiliary function to test NUT functionality. + fn test_nut_aux(kind: TransferToEthereumKind, expect: Expect) { + // setup + let mut wl_storage = setup_storage(); + let tx = Tx::from_type(TxType::Raw); + + // the transfer to be added to the pool + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind, + asset: ASSET, + sender: daewon_address(), + recipient: EthAddress([1; 20]), + amount: TOKENS.into(), + }, + gas_fee: GasFee { + amount: GAS_FEE.into(), + payer: daewon_address(), + }, + }; + + // add transfer to pool + let mut keys_changed = { + wl_storage + .write_log + .write( + &get_pending_key(&transfer), + transfer.try_to_vec().unwrap(), + ) + .unwrap(); + BTreeSet::from([get_pending_key(&transfer)]) + }; + + // update Daewon's balances + let mut new_keys_changed = update_balances( + &mut wl_storage.write_log, + Balance { + kind, + owner: daewon_address(), + gas: DAEWONS_GAS.into(), + token: DAES_NUTS.into(), + }, + SignedAmount::Negative(GAS_FEE.into()), + SignedAmount::Negative(TOKENS.into()), + ); + keys_changed.append(&mut new_keys_changed); + + // change the bridge pool balances + let mut new_keys_changed = update_balances( + &mut wl_storage.write_log, + Balance { + kind, + owner: BRIDGE_POOL_ADDRESS, + gas: ESCROWED_AMOUNT.into(), + token: ESCROWED_NUTS.into(), + }, + SignedAmount::Positive(GAS_FEE.into()), + SignedAmount::Positive(TOKENS.into()), + ); + keys_changed.append(&mut new_keys_changed); + + // create the data to be given to the vp + let verifiers = BTreeSet::default(); + let vp = BridgePoolVp { + ctx: setup_ctx( + &tx, + &wl_storage.storage, + &wl_storage.write_log, + &keys_changed, + &verifiers, + ), + }; + + let mut tx = Tx::from_type(TxType::Raw); + tx.add_data(transfer); + + let res = vp.validate_tx(&tx, &keys_changed, &verifiers); + match expect { + Expect::True => assert!(res.expect("Test failed")), + Expect::False => assert!(!res.expect("Test failed")), + Expect::Error => assert!(res.is_err()), + } + } + + /// Test that the Bridge pool VP rejects a tx based on the fact + /// that an account might hold NUTs of some arbitrary Ethereum + /// asset, but not hold ERC20s. + #[test] + fn test_reject_no_erc20_balance_despite_nut_balance() { + test_nut_aux(TransferToEthereumKind::Erc20, Expect::False) + } + + /// Test the happy flow of escrowing NUTs. + #[test] + fn test_escrowing_nuts_happy_flow() { + test_nut_aux(TransferToEthereumKind::Nut, Expect::True) + } } From 180c986b9a75b82d409c54559b6a9561a2e1e96c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 22:42:49 +0100 Subject: [PATCH 17/94] Add NUT cmdline flag for Bridge pool transfers --- apps/src/lib/cli.rs | 8 ++++++++ shared/src/ledger/args.rs | 5 +++++ shared/src/ledger/eth_bridge/bridge_pool.rs | 9 ++++++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 1ed2275f2c..e7a34eba67 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2513,6 +2513,7 @@ pub mod args { pub const NET_ADDRESS: Arg = arg("net-address"); pub const NAMADA_START_TIME: ArgOpt = arg_opt("time"); pub const NO_CONVERSIONS: ArgFlag = flag("no-conversions"); + pub const NUT: ArgFlag = flag("nut"); pub const OUT_FILE_PATH_OPT: ArgOpt = arg_opt("out-file-path"); pub const OUTPUT_FOLDER_PATH: ArgOpt = arg_opt("output-folder-path"); @@ -2787,6 +2788,7 @@ pub mod args { impl CliToSdk> for EthereumBridgePool { fn to_sdk(self, ctx: &mut Context) -> EthereumBridgePool { EthereumBridgePool:: { + nut: self.nut, tx: self.tx.to_sdk(ctx), asset: self.asset, recipient: self.recipient, @@ -2809,6 +2811,7 @@ pub mod args { let fee_amount = FEE_AMOUNT.parse(matches).amount; let fee_payer = FEE_PAYER.parse(matches); let code_path = PathBuf::from(TX_BRIDGE_POOL_WASM); + let nut = NUT.parse(matches); Self { tx, asset, @@ -2818,6 +2821,7 @@ pub mod args { fee_amount, fee_payer, code_path, + nut, } } @@ -2852,6 +2856,10 @@ pub mod args { "The Namada address of the account paying the fee.", ), ) + .arg(NUT.def().help( + "Add Non Usable Tokens (NUTs) to the Bridge pool. These \ + are usually obtained from invalid transfers to Namada.", + )) } } diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 2d426d5510..8d884b4a5f 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -701,6 +701,11 @@ pub struct RecommendBatch { /// A transfer to be added to the Ethereum bridge pool. #[derive(Clone, Debug)] pub struct EthereumBridgePool { + /// Whether the transfer is for a NUT. + /// + /// By default, we add wrapped ERC20s onto the + /// Bridge pool. + pub nut: bool, /// The args for building a tx to the bridge pool pub tx: Tx, /// The type of token diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 596538bb29..940524d34f 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -39,6 +39,7 @@ pub async fn build_bridge_pool_tx( client: &C, args::EthereumBridgePool { tx: tx_args, + nut, asset, recipient, sender, @@ -60,9 +61,11 @@ pub async fn build_bridge_pool_tx( recipient, sender: sender.clone(), amount, - // TODO: dispatch on the transfer kind, based - // on CLI arguments - kind: TransferToEthereumKind::Erc20, + kind: if nut { + TransferToEthereumKind::Nut + } else { + TransferToEthereumKind::Erc20 + }, }, gas_fee: GasFee { amount: fee_amount, From 7732e249e354b1fce028aa4e3d94ae56a85bc792 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 13 Jul 2023 15:39:21 +0100 Subject: [PATCH 18/94] Update ethbridge-rs to v0.20.0 --- Cargo.lock | 24 ++++++++++++------------ apps/Cargo.toml | 6 +++--- core/Cargo.toml | 2 +- shared/Cargo.toml | 4 ++-- wasm/Cargo.lock | 20 ++++++++++---------- wasm_for_tests/wasm_source/Cargo.lock | 20 ++++++++++---------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4359495965..cb0660f4c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,8 +1999,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2010,8 +2010,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethbridge-structs", @@ -2021,8 +2021,8 @@ dependencies = [ [[package]] name = "ethbridge-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", @@ -2032,8 +2032,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2043,8 +2043,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethbridge-structs", @@ -2054,8 +2054,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethers", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index a5c4dec197..0fcade9cac 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -87,9 +87,9 @@ derivative.workspace = true directories.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} -ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} -ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} eyre.workspace = true fd-lock.workspace = true ferveo-common.workspace = true diff --git a/core/Cargo.toml b/core/Cargo.toml index 5da1f1f6d6..b596d1fb10 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -66,7 +66,7 @@ data-encoding.workspace = true derivative.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0" } +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0" } eyre.workspace = true ferveo = {optional = true, git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} ferveo-common = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} diff --git a/shared/Cargo.toml b/shared/Cargo.toml index d6c64d6e01..db949a9b7e 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -100,8 +100,8 @@ clru.workspace = true data-encoding.workspace = true derivation-path.workspace = true derivative.workspace = true -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} ethers.workspace = true eyre.workspace = true futures.workspace = true diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 2e8816fd54..9c180cd817 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethers", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 95d016ca57..cecb47a479 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethers", From 92bb87f51b8a7d062b15e55e32b6a309f967b284 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 13 Jul 2023 15:39:42 +0100 Subject: [PATCH 19/94] Remove whitelist Ethereum events from the ledger --- .../lib/node/ledger/ethereum_oracle/events.rs | 52 ++----------------- .../shell/vote_extensions/eth_events.rs | 5 -- core/src/types/ethereum_events.rs | 34 ------------ core/src/types/storage.rs | 1 - .../transactions/ethereum_events/events.rs | 4 -- 5 files changed, 5 insertions(+), 91 deletions(-) diff --git a/apps/src/lib/node/ledger/ethereum_oracle/events.rs b/apps/src/lib/node/ledger/ethereum_oracle/events.rs index 10837829de..645b87d1c4 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/events.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/events.rs @@ -7,15 +7,15 @@ pub mod eth_events { }; use ethbridge_events::{DynEventCodec, Events as RawEvents}; use ethbridge_governance_events::{ - GovernanceEvents, NewContractFilter, UpdateBridgeWhitelistFilter, - UpgradedContractFilter, ValidatorSetUpdateFilter, + GovernanceEvents, NewContractFilter, UpgradedContractFilter, + ValidatorSetUpdateFilter, }; use namada::core::types::ethereum_structs; use namada::eth_bridge::ethers::contract::EthEvent; use namada::types::address::Address; use namada::types::ethereum_events::{ - EthAddress, EthereumEvent, TokenWhitelist, TransferToEthereum, - TransferToEthereumKind, TransferToNamada, Uint, + EthAddress, EthereumEvent, TransferToEthereum, TransferToEthereumKind, + TransferToNamada, Uint, }; use namada::types::keccak::KeccakHash; use namada::types::token::Amount; @@ -106,31 +106,6 @@ pub mod eth_events { NewContractFilter::name().into(), )); } - RawEvents::Governance( - GovernanceEvents::UpdateBridgeWhitelistFilter( - UpdateBridgeWhitelistFilter { - nonce, - tokens, - token_cap, - }, - ), - ) => { - let mut whitelist = vec![]; - - for (token, cap) in - tokens.into_iter().zip(token_cap.into_iter()) - { - whitelist.push(TokenWhitelist { - token: token.parse_eth_address()?, - cap: cap.parse_amount()?, - }); - } - - EthereumEvent::UpdateBridgeWhitelist { - nonce: nonce.parse_uint256()?, - whitelist, - } - } RawEvents::Governance( GovernanceEvents::UpgradedContractFilter( UpgradedContractFilter { name: _, addr: _ }, @@ -342,7 +317,7 @@ pub mod eth_events { use ethabi::ethereum_types::{H160, U256}; use ethbridge_events::{ TRANSFER_TO_ERC_CODEC, TRANSFER_TO_NAMADA_CODEC, - UPDATE_BRIDGE_WHITELIST_CODEC, VALIDATOR_SET_UPDATE_CODEC, + VALIDATOR_SET_UPDATE_CODEC, }; use namada::eth_bridge::ethers::abi::AbiEncode; @@ -553,11 +528,6 @@ pub mod eth_events { bridge_validator_set_hash: [1; 32], governance_validator_set_hash: [2; 32], }; - let whitelist = UpdateBridgeWhitelistFilter { - nonce: 0u64.into(), - tokens: vec![H160([0; 20]); 2], - token_cap: vec![0u64.into(); 2], - }; assert_eq!( { let decoded: TransferToNamadaFilter = @@ -593,18 +563,6 @@ pub mod eth_events { }, update ); - assert_eq!( - { - let decoded: UpdateBridgeWhitelistFilter = - UPDATE_BRIDGE_WHITELIST_CODEC - .decode(&get_log(whitelist.clone().encode())) - .expect("Test failed") - .try_into() - .expect("Test failed"); - decoded - }, - whitelist - ); } /// Return an Ethereum events log, from the given encoded event diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs index a7006abecc..5282864e6d 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs @@ -276,11 +276,6 @@ where return Err(VoteExtensionError::InvalidNamNonce); } } - EthereumEvent::UpdateBridgeWhitelist { .. } => { - // TODO: check nonce of whitelist update; - // for this, we need to store the nonce of - // whitelist updates somewhere - } // consider other ethereum event kinds valid _ => {} } diff --git a/core/src/types/ethereum_events.rs b/core/src/types/ethereum_events.rs index 75bfc5f5cc..b896674a0c 100644 --- a/core/src/types/ethereum_events.rs +++ b/core/src/types/ethereum_events.rs @@ -334,16 +334,6 @@ pub enum EthereumEvent { #[allow(dead_code)] address: EthAddress, }, - /// Event indication a new Ethereum based token has been whitelisted for - /// transfer across the bridge - UpdateBridgeWhitelist { - /// Monotonically increasing nonce - #[allow(dead_code)] - nonce: Uint, - /// Tokens to be allowed to be transferred across the bridge - #[allow(dead_code)] - whitelist: Vec, - }, } impl EthereumEvent { @@ -462,30 +452,6 @@ pub struct TransferToEthereum { pub gas_payer: Address, } -/// struct for whitelisting a token from Ethereum. -/// Includes the address of issuing contract and -/// a cap on the max amount of this token allowed to be -/// held by the bridge. -#[derive( - Clone, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - BorshSerialize, - BorshDeserialize, - BorshSchema, -)] -#[allow(dead_code)] -pub struct TokenWhitelist { - /// Address of Ethereum smart contract issuing token - pub token: EthAddress, - /// Maximum amount of token allowed on the bridge - pub cap: Amount, -} - #[cfg(test)] pub mod tests { use std::str::FromStr; diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index 0f0a6032f0..a39089aa04 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -1254,7 +1254,6 @@ pub struct PrefixValue { pub struct EthEventsQueue { /// Queue of transfer to Namada events. pub transfers_to_namada: InnerEthEventsQueue, - // TODO: add queue of update whitelist events } /// A queue of confirmed Ethereum events of type `E`. diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index da967b85a0..3cfbf69fd1 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -752,10 +752,6 @@ mod tests { name: "bridge".to_string(), address: arbitrary_eth_address(), }, - EthereumEvent::UpdateBridgeWhitelist { - nonce: arbitrary_nonce(), - whitelist: vec![], - }, EthereumEvent::UpgradedContract { name: "bridge".to_string(), address: arbitrary_eth_address(), From a1213c7adde739978119e859710c8cc0494c9292 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 13 Jul 2023 16:47:32 +0100 Subject: [PATCH 20/94] Add whitelisted ERC20 tokens to the genesis file --- apps/src/lib/config/genesis.rs | 12 +- apps/src/lib/node/ledger/shell/mod.rs | 23 +-- ethereum_bridge/src/parameters.rs | 133 +++++++++++++++--- ethereum_bridge/src/test_utils.rs | 3 + .../ethereum_bridge/bridge_pool_vp.rs | 1 + .../ledger/native_vp/ethereum_bridge/vp.rs | 1 + tests/src/native_vp/eth_bridge_pool.rs | 1 + 7 files changed, 137 insertions(+), 37 deletions(-) diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index e9abf6c26d..2b920839f0 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -909,10 +909,13 @@ pub fn genesis( } #[cfg(any(test, feature = "dev"))] pub fn genesis(num_validators: u64) -> Genesis { - use namada::ledger::eth_bridge::{Contracts, UpgradeableContract}; + use namada::ledger::eth_bridge::{ + Contracts, Erc20WhitelistEntry, UpgradeableContract, + }; use namada::types::address::{ self, apfel, btc, dot, eth, kartoffel, nam, schnitzel, wnam, }; + use namada::types::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; use namada::types::ethereum_events::EthAddress; use crate::wallet; @@ -1115,6 +1118,13 @@ pub fn genesis(num_validators: u64) -> Genesis { gov_params: GovernanceParameters::default(), pgf_params: PgfParameters::default(), ethereum_bridge_params: Some(EthereumBridgeConfig { + erc20_whitelist: vec![Erc20WhitelistEntry { + token_address: DAI_ERC20_ETH_ADDRESS, + token_cap: token::DenominatedAmount { + amount: token::Amount::max(), + denom: 18.into(), + }, + }], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index c65fbb3f99..c5e89225e3 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -28,7 +28,7 @@ use std::rc::Rc; use borsh::{BorshDeserialize, BorshSerialize}; use namada::core::ledger::eth_bridge; -use namada::ledger::eth_bridge::{EthBridgeQueries, EthereumBridgeConfig}; +use namada::ledger::eth_bridge::{EthBridgeQueries, EthereumOracleConfig}; use namada::ledger::events::log::EventLog; use namada::ledger::events::Event; use namada::ledger::gas::BlockGasMeter; @@ -968,27 +968,16 @@ where ); return; } - let Some(config) = EthereumBridgeConfig::read(&self.wl_storage) else { - tracing::info!( - "Not starting oracle as the Ethereum bridge config couldn't be found in storage" - ); - return; - }; + let config = EthereumOracleConfig::read(&self.wl_storage).expect( + "The oracle config must be present in storage, since the \ + bridge is enabled", + ); let start_block = self .wl_storage .storage .ethereum_height .clone() - .unwrap_or_else(|| { - self.wl_storage - .read(ð_bridge::storage::eth_start_height_key()) - .expect( - "Failed to read Ethereum start height from storage", - ) - .expect( - "The Ethereum start height should be in storage", - ) - }); + .unwrap_or(config.eth_start_height); tracing::info!( ?start_block, "Found Ethereum height from which the Ethereum oracle should \ diff --git a/ethereum_bridge/src/parameters.rs b/ethereum_bridge/src/parameters.rs index 4f3b2f1bc5..e657167eca 100644 --- a/ethereum_bridge/src/parameters.rs +++ b/ethereum_bridge/src/parameters.rs @@ -3,6 +3,7 @@ use std::num::NonZeroU64; use borsh::{BorshDeserialize, BorshSerialize}; use eyre::{eyre, Result}; +use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::storage; use namada_core::ledger::storage::types::encode; use namada_core::ledger::storage::WlStorage; @@ -10,11 +11,33 @@ use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; use namada_core::types::ethereum_events::EthAddress; use namada_core::types::ethereum_structs; use namada_core::types::storage::Key; +use namada_core::types::token::DenominatedAmount; use serde::{Deserialize, Serialize}; -use crate::storage::eth_bridge_queries::{EthBridgeEnabled, EthBridgeStatus}; +use crate::storage::eth_bridge_queries::{ + EthBridgeEnabled, EthBridgeQueries, EthBridgeStatus, +}; use crate::{bridge_pool_vp, storage as bridge_storage, vp}; +/// An ERC20 token whitelist entry. +#[derive( + Clone, + Copy, + Eq, + PartialEq, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, +)] +pub struct Erc20WhitelistEntry { + /// The address of the whitelisted ERC20 token. + pub token_address: EthAddress, + /// The token cap of the whitelisted ERC20 token. + pub token_cap: DenominatedAmount, +} + /// Represents a configuration value for the minimum number of /// confirmations an Ethereum event must reach before it can be acted on. #[derive( @@ -135,6 +158,8 @@ pub struct EthereumBridgeConfig { /// Minimum number of confirmations needed to trust an Ethereum branch. /// This must be at least one. pub min_confirmations: MinimumConfirmations, + /// List of ERC20 token types whitelisted at genesis time. + pub erc20_whitelist: Vec, /// The addresses of the Ethereum contracts that need to be directly known /// by validators. pub contracts: Contracts, @@ -151,6 +176,7 @@ impl EthereumBridgeConfig { H: 'static + storage::traits::StorageHasher, { let Self { + erc20_whitelist, eth_start_height, min_confirmations, contracts: @@ -187,13 +213,71 @@ impl EthereumBridgeConfig { wl_storage .write_bytes(ð_start_height_key, encode(eth_start_height)) .unwrap(); + for Erc20WhitelistEntry { + token_address: addr, + token_cap: DenominatedAmount { amount: cap, denom }, + } in erc20_whitelist + { + let key = whitelist::Key { + asset: *addr, + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + wl_storage.write_bytes(&key, encode(&true)).unwrap(); + + let key = whitelist::Key { + asset: *addr, + suffix: whitelist::KeyType::Cap, + } + .into(); + wl_storage.write_bytes(&key, encode(cap)).unwrap(); + + let key = whitelist::Key { + asset: *addr, + suffix: whitelist::KeyType::Denomination, + } + .into(); + wl_storage.write_bytes(&key, encode(denom)).unwrap(); + } // Initialize the storage for the Ethereum Bridge VP. vp::init_storage(wl_storage); // Initialize the storage for the Bridge Pool VP. bridge_pool_vp::init_storage(wl_storage); } +} + +/// Subset of [`EthereumBridgeConfig`], containing only Ethereum +/// oracle specific parameters. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EthereumOracleConfig { + /// Initial Ethereum block height when events will first be extracted from. + pub eth_start_height: ethereum_structs::BlockHeight, + /// Minimum number of confirmations needed to trust an Ethereum branch. + /// This must be at least one. + pub min_confirmations: MinimumConfirmations, + /// The addresses of the Ethereum contracts that need to be directly known + /// by validators. + pub contracts: Contracts, +} + +impl From for EthereumOracleConfig { + fn from(config: EthereumBridgeConfig) -> Self { + let EthereumBridgeConfig { + eth_start_height, + min_confirmations, + contracts, + .. + } = config; + Self { + eth_start_height, + min_confirmations, + contracts, + } + } +} - /// Reads the latest [`EthereumBridgeConfig`] from storage. If it is not +impl EthereumOracleConfig { + /// Reads the latest [`EthereumOracleConfig`] from storage. If it is not /// present, `None` will be returned - this could be the case if the bridge /// has not been bootstrapped yet. Panics if the storage appears to be /// corrupt. @@ -202,25 +286,27 @@ impl EthereumBridgeConfig { DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, H: 'static + storage::traits::StorageHasher, { + // TODO(namada#1720): remove present key check; `is_bridge_active` + // should not panic, when the active status key has not been + // written to; simply return bridge disabled instead + let has_active_key = + wl_storage.has_key(&bridge_storage::active_key()).unwrap(); + + if !has_active_key || !wl_storage.ethbridge_queries().is_bridge_active() + { + return None; + } + let min_confirmations_key = bridge_storage::min_confirmations_key(); let native_erc20_key = bridge_storage::native_erc20_key(); let bridge_contract_key = bridge_storage::bridge_contract_key(); let governance_contract_key = bridge_storage::governance_contract_key(); let eth_start_height_key = bridge_storage::eth_start_height_key(); - let Some(min_confirmations) = StorageRead::read::( - wl_storage, - &min_confirmations_key, - ) - .unwrap_or_else(|err| { - panic!("Could not read {min_confirmations_key}: {err:?}") - }) else { - // The bridge has not been configured yet - return None; - }; - // These reads must succeed otherwise the storage is corrupt or a // read failed + let min_confirmations = + must_read_key(wl_storage, &min_confirmations_key); let native_erc20 = must_read_key(wl_storage, &native_erc20_key); let bridge_contract = must_read_key(wl_storage, &bridge_contract_key); let governance_contract = @@ -299,6 +385,7 @@ mod tests { #[test] fn test_round_trip_toml_serde() -> Result<()> { let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::default(), contracts: Contracts { @@ -324,6 +411,7 @@ mod tests { fn test_ethereum_bridge_config_read_write_storage() { let mut wl_storage = TestWlStorage::default(); let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::default(), contracts: Contracts { @@ -340,7 +428,8 @@ mod tests { }; config.init_storage(&mut wl_storage); - let read = EthereumBridgeConfig::read(&wl_storage).unwrap(); + let read = EthereumOracleConfig::read(&wl_storage).unwrap(); + let config = EthereumOracleConfig::from(config); assert_eq!(config, read); } @@ -348,7 +437,7 @@ mod tests { #[test] fn test_ethereum_bridge_config_uninitialized() { let wl_storage = TestWlStorage::default(); - let read = EthereumBridgeConfig::read(&wl_storage); + let read = EthereumOracleConfig::read(&wl_storage); assert!(read.is_none()); } @@ -358,6 +447,7 @@ mod tests { fn test_ethereum_bridge_config_storage_corrupt() { let mut wl_storage = TestWlStorage::default(); let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::default(), contracts: Contracts { @@ -379,7 +469,7 @@ mod tests { .unwrap(); // This should panic because the min_confirmations value is not valid - EthereumBridgeConfig::read(&wl_storage); + EthereumOracleConfig::read(&wl_storage); } #[test] @@ -388,16 +478,21 @@ mod tests { )] fn test_ethereum_bridge_config_storage_partially_configured() { let mut wl_storage = TestWlStorage::default(); + wl_storage + .write_bytes( + &bridge_storage::active_key(), + encode(&EthBridgeStatus::Enabled(EthBridgeEnabled::AtGenesis)), + ) + .unwrap(); // Write a valid min_confirmations value - let min_confirmations_key = bridge_storage::min_confirmations_key(); wl_storage .write_bytes( - &min_confirmations_key, + &bridge_storage::min_confirmations_key(), MinimumConfirmations::default().try_to_vec().unwrap(), ) .unwrap(); // This should panic as the other config values are not written - EthereumBridgeConfig::read(&wl_storage); + EthereumOracleConfig::read(&wl_storage); } } diff --git a/ethereum_bridge/src/test_utils.rs b/ethereum_bridge/src/test_utils.rs index f2a1ee0b8c..aec6463396 100644 --- a/ethereum_bridge/src/test_utils.rs +++ b/ethereum_bridge/src/test_utils.rs @@ -92,6 +92,8 @@ pub fn bootstrap_ethereum_bridge( wl_storage: &mut TestWlStorage, ) -> EthereumBridgeConfig { let config = EthereumBridgeConfig { + // start with empty erc20 whitelist + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::from(unsafe { // SAFETY: The only way the API contract of `NonZeroU64` can @@ -212,6 +214,7 @@ pub fn init_storage_with_validators( ) .expect("Test failed"); let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 9632f4d7f3..949df882b5 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -539,6 +539,7 @@ mod test_bridge_pool_vp { fn setup_storage() -> WlStorage { // a dummy config for testing let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { diff --git a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs index 5fd7aa6cd1..715b8ad616 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs @@ -393,6 +393,7 @@ mod tests { // a dummy config for testing let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index ee3c659591..f972215c71 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -63,6 +63,7 @@ mod test_bridge_pool_vp { ..Default::default() }; let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { From 477703871029dc324c6cddcf4ddad06301ed5c12 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Sat, 15 Jul 2023 13:15:25 +0100 Subject: [PATCH 21/94] Update ethbridge-rs to v0.21.0 --- Cargo.lock | 24 ++++++++++++------------ apps/Cargo.toml | 6 +++--- core/Cargo.toml | 2 +- shared/Cargo.toml | 4 ++-- wasm/Cargo.lock | 20 ++++++++++---------- wasm_for_tests/wasm_source/Cargo.lock | 20 ++++++++++---------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb0660f4c0..d8131f58e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,8 +1999,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2010,8 +2010,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -2021,8 +2021,8 @@ dependencies = [ [[package]] name = "ethbridge-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", @@ -2032,8 +2032,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2043,8 +2043,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -2054,8 +2054,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethers", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 0fcade9cac..0c8557b774 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -87,9 +87,9 @@ derivative.workspace = true directories.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} -ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} -ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} eyre.workspace = true fd-lock.workspace = true ferveo-common.workspace = true diff --git a/core/Cargo.toml b/core/Cargo.toml index b596d1fb10..195f419b39 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -66,7 +66,7 @@ data-encoding.workspace = true derivative.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0" } +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0" } eyre.workspace = true ferveo = {optional = true, git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} ferveo-common = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} diff --git a/shared/Cargo.toml b/shared/Cargo.toml index db949a9b7e..8ea2c055b5 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -100,8 +100,8 @@ clru.workspace = true data-encoding.workspace = true derivation-path.workspace = true derivative.workspace = true -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} ethers.workspace = true eyre.workspace = true futures.workspace = true diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 9c180cd817..a273155cd2 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethers", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index cecb47a479..6d5a227eb4 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethers", From cde95039d8013bfd4ae385b57fc93d5e9c23ff0b Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 31 Jul 2023 13:25:32 +0100 Subject: [PATCH 22/94] Move ethbridge-rs deps to the workspace --- Cargo.toml | 6 ++++++ apps/Cargo.toml | 6 +++--- core/Cargo.toml | 2 +- shared/Cargo.toml | 4 ++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d50daf0b6..d524d0c7a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,12 @@ directories = "4.0.1" ed25519-consensus = "1.2.0" escargot = "0.5.7" ethabi = "18.0.0" +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0" } ethers = "2.0.0" expectrl = "0.7.0" eyre = "0.6.5" diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 0c8557b774..16fe028be3 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -87,9 +87,9 @@ derivative.workspace = true directories.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-bridge-events.workspace = true +ethbridge-events.workspace = true +ethbridge-governance-events.workspace = true eyre.workspace = true fd-lock.workspace = true ferveo-common.workspace = true diff --git a/core/Cargo.toml b/core/Cargo.toml index 195f419b39..75ea0c64c2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -66,7 +66,7 @@ data-encoding.workspace = true derivative.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0" } +ethbridge-structs.workspace = true eyre.workspace = true ferveo = {optional = true, git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} ferveo-common = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 8ea2c055b5..35c5156cac 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -100,8 +100,8 @@ clru.workspace = true data-encoding.workspace = true derivation-path.workspace = true derivative.workspace = true -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-bridge-contract.workspace = true +ethbridge-governance-contract.workspace = true ethers.workspace = true eyre.workspace = true futures.workspace = true From 20044d015688eb8cd638b456d6d0d9fbf773f943 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 31 Jul 2023 13:40:24 +0100 Subject: [PATCH 23/94] Add changelog entry for the Ethereum token whitelist Closes #1290 --- .changelog/unreleased/features/1290-token-whitelist.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/1290-token-whitelist.md diff --git a/.changelog/unreleased/features/1290-token-whitelist.md b/.changelog/unreleased/features/1290-token-whitelist.md new file mode 100644 index 0000000000..57bf9e61bb --- /dev/null +++ b/.changelog/unreleased/features/1290-token-whitelist.md @@ -0,0 +1,2 @@ +- Implement Ethereum token whitelist. + ([\#1290](https://github.com/anoma/namada/issues/1290)) \ No newline at end of file From 2ae23fb132d3a0c6429ab2cf2d5f0e90765ea939 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 09:44:41 +0100 Subject: [PATCH 24/94] Add helper methods to EthAssetMint --- ethereum_bridge/src/storage/eth_bridge_queries.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index 0a738d96a4..08fdcab2fa 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -500,6 +500,20 @@ pub struct EthAssetMint { pub erc20_amount: token::Amount, } +impl EthAssetMint { + /// Check if NUTs should be minted. + #[inline] + pub fn should_mint_nuts(&self) -> bool { + !self.nut_amount.is_zero() + } + + /// Check if ERC20s should be minted. + #[inline] + pub fn should_mint_erc20s(&self) -> bool { + !self.erc20_amount.is_zero() + } +} + /// A handle to the Ethereum addresses of the set of consensus /// validators in Namada, at some given epoch. pub struct ConsensusEthAddresses<'db, D, H> From f1be3f97d0997184744bade64436eadfd0fb1ef9 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 10:14:33 +0100 Subject: [PATCH 25/94] Use EthAssetMint helper methods --- .../protocol/transactions/ethereum_events/events.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 3cfbf69fd1..df386f5c43 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -145,14 +145,14 @@ where // TODO: query denomination of the whitelisted token from storage, // and print this amount with the proper formatting; for now, use // NAM's formatting - if !asset_count.erc20_amount.is_zero() { + if asset_count.should_mint_erc20s() { tracing::info!( "Minted wrapped ERC20s - (asset - {asset}, receiver - \ {receiver}, amount - {})", asset_count.erc20_amount.to_string_native(), ); } - if !asset_count.nut_amount.is_zero() { + if asset_count.should_mint_nuts() { tracing::info!( "Minted NUTs - (asset - {asset}, receiver - {receiver}, \ amount - {})", @@ -254,10 +254,12 @@ where let assets_to_mint = [ // check if we should mint nuts - (!asset_count.nut_amount.is_zero()) + asset_count + .should_mint_nuts() .then(|| (wrapped_erc20s::nut(asset), asset_count.nut_amount)), // check if we should mint erc20s - (!asset_count.erc20_amount.is_zero()) + asset_count + .should_mint_erc20s() .then(|| (wrapped_erc20s::token(asset), asset_count.erc20_amount)), ] .into_iter() From 6ef086f5a2baf57bf1425bc1aafde54ebed9c972 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 10:04:09 +0100 Subject: [PATCH 26/94] Refactor redeem_native_token to use update::amount --- .../transactions/ethereum_events/events.rs | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index df386f5c43..c84b868285 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -183,45 +183,44 @@ where let receiver_native_token_balance_key = token::balance_key(&wl_storage.storage.native_token, receiver); - let eth_bridge_native_token_balance_pre: token::Amount = - StorageRead::read(wl_storage, ð_bridge_native_token_balance_key)? - .expect( - "Ethereum bridge must always have an explicit balance of the \ - native token", - ); - let receiver_native_token_balance_pre: token::Amount = - StorageRead::read(wl_storage, &receiver_native_token_balance_key)? - .unwrap_or_default(); - - let eth_bridge_native_token_balance_post = - eth_bridge_native_token_balance_pre - .checked_sub(*amount) - .expect( - "Ethereum bridge should always have enough native tokens to \ - redeem any confirmed transfers", - ); - let receiver_native_token_balance_post = receiver_native_token_balance_pre - .checked_add(*amount) - .expect("Receiver's balance is full"); - - StorageWrite::write( + update::amount( wl_storage, ð_bridge_native_token_balance_key, - eth_bridge_native_token_balance_post, + |balance| { + tracing::debug!( + %eth_bridge_native_token_balance_key, + ?balance, + "Existing value found", + ); + balance.spend(amount); + tracing::debug!( + %eth_bridge_native_token_balance_key, + ?balance, + "New value calculated", + ); + }, )?; - StorageWrite::write( + update::amount( wl_storage, &receiver_native_token_balance_key, - receiver_native_token_balance_post, + |balance| { + tracing::debug!( + %receiver_native_token_balance_key, + ?balance, + "Existing value found", + ); + balance.receive(amount); + tracing::debug!( + %receiver_native_token_balance_key, + ?balance, + "New value calculated", + ); + }, )?; tracing::info!( amount = %amount.to_string_native(), %receiver, - eth_bridge_native_token_balance_pre = %eth_bridge_native_token_balance_pre.to_string_native(), - eth_bridge_native_token_balance_post = %eth_bridge_native_token_balance_post.to_string_native(), - receiver_native_token_balance_pre = %receiver_native_token_balance_pre.to_string_native(), - receiver_native_token_balance_post = %receiver_native_token_balance_post.to_string_native(), "Redeemed native token for wrapped ERC20 token" ); Ok(BTreeSet::from([ From f1bd957593fa238c3dce2c9a13d16cfa643b0bb4 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 10:26:08 +0100 Subject: [PATCH 27/94] Vet Ethereum bridge config of the native token --- ethereum_bridge/src/parameters.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ethereum_bridge/src/parameters.rs b/ethereum_bridge/src/parameters.rs index e657167eca..6395dd6cab 100644 --- a/ethereum_bridge/src/parameters.rs +++ b/ethereum_bridge/src/parameters.rs @@ -11,7 +11,7 @@ use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; use namada_core::types::ethereum_events::EthAddress; use namada_core::types::ethereum_structs; use namada_core::types::storage::Key; -use namada_core::types::token::DenominatedAmount; +use namada_core::types::token::{DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES}; use serde::{Deserialize, Serialize}; use crate::storage::eth_bridge_queries::{ @@ -218,6 +218,15 @@ impl EthereumBridgeConfig { token_cap: DenominatedAmount { amount: cap, denom }, } in erc20_whitelist { + if addr == native_erc20 + && denom != &NATIVE_MAX_DECIMAL_PLACES.into() + { + panic!( + "Error writing Ethereum bridge config: The native token \ + should have {NATIVE_MAX_DECIMAL_PLACES} decimal places" + ); + } + let key = whitelist::Key { asset: *addr, suffix: whitelist::KeyType::Whitelisted, From 69037fbe27cdc6dfa7651f6dfd4d30dafb19f467 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 11:46:51 +0100 Subject: [PATCH 28/94] Harden Bridge pool VP against wNAM NUT transfers --- .../ethereum_bridge/bridge_pool_vp.rs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 949df882b5..c5a936d103 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -14,6 +14,7 @@ use std::collections::BTreeSet; use borsh::BorshDeserialize; use eyre::eyre; +use namada_core::hints; use namada_core::ledger::eth_bridge::storage::bridge_pool::{ get_pending_key, is_bridge_pool_key, BRIDGE_POOL_ADDRESS, }; @@ -26,7 +27,7 @@ use crate::ledger::storage::traits::StorageHasher; use crate::ledger::storage::{DBIter, DB}; use crate::proto::Tx; use crate::types::address::{Address, InternalAddress}; -use crate::types::eth_bridge_pool::PendingTransfer; +use crate::types::eth_bridge_pool::{PendingTransfer, TransferToEthereumKind}; use crate::types::ethereum_events::EthAddress; use crate::types::storage::Key; use crate::types::token::{balance_key, Amount}; @@ -316,6 +317,19 @@ where } // check the escrowed assets if transfer.transfer.asset == wnam_address { + if hints::unlikely(matches!( + &transfer.transfer.kind, + TransferToEthereumKind::Nut + )) { + // NB: this should never be possible: protocol tx state updates + // never result in wNAM NUTs being minted. in turn, this means + // that users should never hold wNAM NUTs. doesn't hurt to add + // the extra check to the vp, though + tracing::error!( + "Attempted to add a wNAM NUT transfer to the Bridge pool" + ); + return Ok(false); + } // if we are going to mint wNam on Ethereum, the appropriate // amount of Nam must be escrowed in the Ethereum bridge VP's // storage. @@ -357,9 +371,7 @@ mod test_bridge_pool_vp { use crate::ledger::storage_api::StorageWrite; use crate::types::address::{nam, wnam}; use crate::types::chain::ChainId; - use crate::types::eth_bridge_pool::{ - GasFee, TransferToEthereum, TransferToEthereumKind, - }; + use crate::types::eth_bridge_pool::{GasFee, TransferToEthereum}; use crate::types::hash::Hash; use crate::types::storage::TxIndex; use crate::types::transaction::TxType; From a26de50346fb96a52d071ed60d52fd1a0adc3fe8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 11:48:56 +0100 Subject: [PATCH 29/94] Update wNAM supplies when acting on Ethereum events --- .../transactions/ethereum_events/events.rs | 61 ++++++++++++++++--- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index c84b868285..3c92d3dcab 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -19,7 +19,9 @@ use namada_core::ledger::storage::traits::StorageHasher; use namada_core::ledger::storage::{DBIter, WlStorage, DB}; use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; use namada_core::types::address::Address; -use namada_core::types::eth_bridge_pool::PendingTransfer; +use namada_core::types::eth_bridge_pool::{ + PendingTransfer, TransferToEthereumKind, +}; use namada_core::types::ethereum_events::{ EthAddress, EthereumEvent, TransferToEthereum, TransferToNamada, TransfersToNamada, @@ -161,7 +163,12 @@ where } changed } else { - redeem_native_token(wl_storage, receiver, amount)? + redeem_native_token( + wl_storage, + &wrapped_native_erc20, + receiver, + amount, + )? }; changed_keys.append(&mut changed) } @@ -171,6 +178,7 @@ where /// Redeems `amount` of the native token for `receiver` from escrow. fn redeem_native_token( wl_storage: &mut WlStorage, + native_erc20: &EthAddress, receiver: &Address, amount: &token::Amount, ) -> Result> @@ -182,6 +190,8 @@ where token::balance_key(&wl_storage.storage.native_token, &BRIDGE_ADDRESS); let receiver_native_token_balance_key = token::balance_key(&wl_storage.storage.native_token, receiver); + let native_werc20_supply_key = + minted_balance_key(&wrapped_erc20s::token(native_erc20)); update::amount( wl_storage, @@ -217,6 +227,19 @@ where ); }, )?; + update::amount(wl_storage, &native_werc20_supply_key, |balance| { + tracing::debug!( + %native_werc20_supply_key, + ?balance, + "Existing value found", + ); + balance.spend(amount); + tracing::debug!( + %native_werc20_supply_key, + ?balance, + "New value calculated", + ); + })?; tracing::info!( amount = %amount.to_string_native(), @@ -226,6 +249,7 @@ where Ok(BTreeSet::from([ eth_bridge_native_token_balance_key, receiver_native_token_balance_key, + native_werc20_supply_key, ])) } @@ -357,7 +381,7 @@ where "Valid transfer to Ethereum detected, compensating the \ relayer and burning any Ethereum assets in Namada" ); - changed_keys.append(&mut burn_transferred_assets( + changed_keys.append(&mut update_transferred_asset_balances( wl_storage, &pending_transfer, )?); @@ -529,7 +553,9 @@ where Ok(changed_keys) } -fn burn_transferred_assets( +/// Burns any transferred ERC20s other than wNAM. If NAM is transferred, +/// update the wNAM supply key. +fn update_transferred_asset_balances( wl_storage: &mut WlStorage, transfer: &PendingTransfer, ) -> Result> @@ -544,12 +570,26 @@ where return Err(eyre::eyre!("Could not read wNam key from storage")); }; + let token = transfer.token_address(); + + // the wrapped NAM supply increases when we transfer to Ethereum if transfer.transfer.asset == native_erc20_addr { - tracing::debug!(?transfer, "Keeping wrapped NAM in escrow"); + if hints::unlikely(matches!( + &transfer.transfer.kind, + TransferToEthereumKind::Nut + )) { + unreachable!("Attempted to mint wNAM NUTs!"); + } + let supply_key = minted_balance_key(&token); + update::amount(wl_storage, &supply_key, |supply| { + supply.receive(&transfer.transfer.amount); + })?; + _ = changed_keys.insert(supply_key); + tracing::debug!(?transfer, "Updated wrapped NAM supply"); return Ok(changed_keys); } - let token = transfer.token_address(); + // other asset kinds must be burned let escrow_balance_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); update::amount(wl_storage, &escrow_balance_key, |balance| { @@ -1161,8 +1201,13 @@ mod tests { let receiver_native_token_balance_key = token::balance_key(&wl_storage.storage.native_token, &receiver); - let changed_keys = - redeem_native_token(&mut wl_storage, &receiver, &amount)?; + let native_erc20 = read_native_erc20_address(&wl_storage)?; + let changed_keys = redeem_native_token( + &mut wl_storage, + &native_erc20, + &receiver, + &amount, + )?; assert_eq!( changed_keys, From c67a65604ce009229179c21aeac260f153ad04a8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 15:27:19 +0100 Subject: [PATCH 30/94] Allow null pre-balances during Bridge pool escrow checks --- .../native_vp/ethereum_bridge/bridge_pool_vp.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index c5a936d103..8585e9a79b 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -67,10 +67,15 @@ where let account_key = balance_key(&self.ctx.storage.native_token, address); let before: Amount = (&self.ctx) .read_pre_value(&account_key) - .unwrap_or_else(|error| { + .map_err(|error| { tracing::warn!(?error, %account_key, "reading pre value"); - None - })?; + }) + .ok()? + // NB: the previous balance of the given account might + // have been null. this is valid if the account is + // being credited, such as when we escrow gas under + // the Bridge pool + .unwrap_or_default(); let after: Amount = (&self.ctx) .read_post_value(&account_key) .unwrap_or_else(|error| { From d3a4906d8a54d3f2335bfe6bfbf1bd8c052853c2 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 09:22:55 +0100 Subject: [PATCH 31/94] Flow control of NAM transferred to Ethereum --- .../ethereum_bridge/bridge_pool_vp.rs | 223 ++++++++++++++---- tests/src/native_vp/eth_bridge_pool.rs | 8 +- 2 files changed, 186 insertions(+), 45 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 8585e9a79b..5248f54847 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -18,6 +18,7 @@ use namada_core::hints; use namada_core::ledger::eth_bridge::storage::bridge_pool::{ get_pending_key, is_bridge_pool_key, BRIDGE_POOL_ADDRESS, }; +use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; use namada_ethereum_bridge::parameters::read_native_erc20_address; @@ -39,11 +40,32 @@ use crate::vm::WasmCacheAccess; pub struct Error(#[from] eyre::Error); /// A positive or negative amount +#[derive(Copy, Clone)] enum SignedAmount { Positive(Amount), Negative(Amount), } +/// An [`Amount`] that has been updated with some delta value. +#[derive(Copy, Clone)] +struct AmountDelta { + /// The base [`Amount`], before applying the delta. + base: Amount, + /// The delta to be applied to the base amount. + delta: SignedAmount, +} + +impl AmountDelta { + /// Resolve the updated amount by applying the delta value. + #[inline] + fn resolve(self) -> Amount { + match self.delta { + SignedAmount::Positive(delta) => self.base + delta, + SignedAmount::Negative(delta) => self.base - delta, + } + } +} + /// Validity predicate for the Ethereum bridge pub struct BridgePoolVp<'ctx, D, H, CA> where @@ -63,7 +85,7 @@ where { /// Get the change in the balance of an account /// associated with an address - fn account_balance_delta(&self, address: &Address) -> Option { + fn account_balance_delta(&self, address: &Address) -> Option { let account_key = balance_key(&self.ctx.storage.native_token, address); let before: Amount = (&self.ctx) .read_pre_value(&account_key) @@ -82,11 +104,14 @@ where tracing::warn!(?error, %account_key, "reading post value"); None })?; - if before > after { - Some(SignedAmount::Negative(before - after)) - } else { - Some(SignedAmount::Positive(after - before)) - } + Some(AmountDelta { + base: before, + delta: if before > after { + SignedAmount::Negative(before - after) + } else { + SignedAmount::Positive(after - before) + }, + }) } /// Check that the correct amount of erc20 assets were @@ -124,36 +149,74 @@ where /// Check that the correct amount of Nam was sent /// from the correct account into escrow + #[inline] fn check_nam_escrowed(&self, delta: EscrowDelta) -> Result { + self.check_nam_escrowed_balance(delta) + .map(|balance| balance.is_some()) + } + + /// Check that the correct amount of Nam was sent + /// from the correct account into escrow, and return + /// the updated escrow balance. + fn check_nam_escrowed_balance( + &self, + delta: EscrowDelta, + ) -> Result, Error> { let EscrowDelta { payer_account, escrow_account, expected_debit, expected_credit, } = delta; - let debited = self.account_balance_delta(payer_account); - let credited = self.account_balance_delta(escrow_account); + let debit = self.account_balance_delta(payer_account); + let credit = self.account_balance_delta(escrow_account); - match (debited, credited) { + match (debit, credit) { + // success case + ( + Some(AmountDelta { + delta: SignedAmount::Negative(debit), + .. + }), + Some( + escrow_balance @ AmountDelta { + delta: SignedAmount::Positive(credit), + .. + }, + ), + ) => Ok((debit == expected_debit && credit == expected_credit) + .then_some(escrow_balance)), + // user did not debit from their account ( - Some(SignedAmount::Negative(debit)), - Some(SignedAmount::Positive(credit)), - ) => Ok(debit == expected_debit && credit == expected_credit), - (Some(SignedAmount::Positive(_)), _) => { + Some(AmountDelta { + delta: SignedAmount::Positive(_), + .. + }), + _, + ) => { tracing::debug!( "The account {} was not debited.", payer_account ); - Ok(false) + Ok(None) } - (_, Some(SignedAmount::Negative(_))) => { + // user did not credit escrow account + ( + _, + Some(AmountDelta { + delta: SignedAmount::Negative(_), + .. + }), + ) => { tracing::debug!( "The Ethereum bridge pool's escrow was not credited from \ account {}.", payer_account ); - Ok(false) + Ok(None) } + // some other error occurred while calculating + // balance deltas (None, _) | (_, None) => Err(Error(eyre!( "Could not calculate the balance delta for {}", payer_account @@ -161,6 +224,74 @@ where } } + /// Validate a wrapped NAM transfer to Ethereum. + fn check_wnam_preconditions<'trans>( + &self, + &wnam_address: &EthAddress, + transfer: &'trans PendingTransfer, + escrow_checks: EscrowCheck<'trans>, + ) -> Result { + if hints::unlikely(matches!( + &transfer.transfer.kind, + TransferToEthereumKind::Nut + )) { + // NB: this should never be possible: protocol tx state updates + // never result in wNAM NUTs being minted. in turn, this means + // that users should never hold wNAM NUTs. doesn't hurt to add + // the extra check to the vp, though + tracing::error!( + "Attempted to add a wNAM NUT transfer to the Bridge pool" + ); + return Ok(false); + } + + let wnam_whitelisted = { + let key = whitelist::Key { + asset: wnam_address, + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + (&self.ctx).read_pre_value(&key)?.unwrap_or(false) + }; + if !wnam_whitelisted { + tracing::debug!( + ?transfer, + "Wrapped NAM transfers are currently disabled" + ); + return Ok(false); + } + + // if we are going to mint wNam on Ethereum, the appropriate + // amount of Nam must be escrowed in the Ethereum bridge VP's + // storage. + let escrowed_balance = + match self.check_nam_escrowed_balance(escrow_checks.token_check)? { + Some(balance) => balance.resolve(), + None => return Ok(false), + }; + + let wnam_cap = { + let key = whitelist::Key { + asset: wnam_address, + suffix: whitelist::KeyType::Cap, + } + .into(); + (&self.ctx).read_pre_value(&key)?.unwrap_or_default() + }; + if escrowed_balance > wnam_cap { + tracing::debug!( + ?transfer, + escrowed_nam = %escrowed_balance.to_string_native(), + wnam_cap = %wnam_cap.to_string_native(), + "The balance of the escrow account exceeds the amount \ + of NAM that is allowed to cross the Ethereum bridge" + ); + return Ok(false); + } + + Ok(true) + } + /// Deteremine the debit and credit amounts that should be checked. fn escrow_check<'trans>( &self, @@ -226,6 +357,7 @@ where /// Helper struct for handling the different escrow /// checking scenarios. +#[derive(Copy, Clone)] struct EscrowDelta<'a> { payer_account: &'a Address, escrow_account: &'a Address, @@ -236,6 +368,7 @@ struct EscrowDelta<'a> { /// There are two checks we must do when minting wNam. /// 1. Check that gas fees were escrowed. /// 2. Check that the Nam to back wNam was escrowed. +#[derive(Copy, Clone)] struct EscrowCheck<'a> { gas_check: EscrowDelta<'a>, token_check: EscrowDelta<'a>, @@ -322,36 +455,23 @@ where } // check the escrowed assets if transfer.transfer.asset == wnam_address { - if hints::unlikely(matches!( - &transfer.transfer.kind, - TransferToEthereumKind::Nut - )) { - // NB: this should never be possible: protocol tx state updates - // never result in wNAM NUTs being minted. in turn, this means - // that users should never hold wNAM NUTs. doesn't hurt to add - // the extra check to the vp, though - tracing::error!( - "Attempted to add a wNAM NUT transfer to the Bridge pool" - ); - return Ok(false); - } - // if we are going to mint wNam on Ethereum, the appropriate - // amount of Nam must be escrowed in the Ethereum bridge VP's - // storage. - self.check_nam_escrowed(escrow_checks.token_check) - .map(|ok| { - if ok { - tracing::info!( - "The Ethereum bridge pool VP accepted the \ - transfer {:?}.", - transfer - ); - } - ok - }) + self.check_wnam_preconditions( + &wnam_address, + &transfer, + escrow_checks, + ) } else { self.check_erc20s_escrowed(keys_changed, &transfer) } + .map(|ok| { + if ok { + tracing::info!( + "The Ethereum bridge pool VP accepted the transfer {:?}.", + transfer + ); + } + ok + }) } } @@ -476,6 +596,23 @@ mod test_bridge_pool_vp { writelog .write(&get_pending_key(&transfer), transfer.try_to_vec().unwrap()) .expect("Test failed"); + // whitelist wnam + let key = whitelist::Key { + asset: wnam(), + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + writelog + .write(&key, true.try_to_vec().unwrap()) + .expect("Test failed"); + let key = whitelist::Key { + asset: wnam(), + suffix: whitelist::KeyType::Cap, + } + .into(); + writelog + .write(&key, Amount::max().try_to_vec().unwrap()) + .expect("Test failed"); // set up users with ERC20 and NUT balances update_balances( &mut writelog, diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index f972215c71..034ca60ab8 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -5,7 +5,8 @@ mod test_bridge_pool_vp { use borsh::{BorshDeserialize, BorshSerialize}; use namada::core::ledger::eth_bridge::storage::bridge_pool::BRIDGE_POOL_ADDRESS; use namada::ledger::eth_bridge::{ - wrapped_erc20s, Contracts, EthereumBridgeConfig, UpgradeableContract, + wrapped_erc20s, Contracts, Erc20WhitelistEntry, EthereumBridgeConfig, + UpgradeableContract, }; use namada::ledger::native_vp::ethereum_bridge::bridge_pool_vp::BridgePoolVp; use namada::proto::Tx; @@ -63,7 +64,10 @@ mod test_bridge_pool_vp { ..Default::default() }; let config = EthereumBridgeConfig { - erc20_whitelist: vec![], + erc20_whitelist: vec![Erc20WhitelistEntry { + token_address: wnam(), + token_cap: Amount::max().native_denominated(), + }], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { From 99fe4770fe9069fa69fd46df8847676320e28701 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 10:30:17 +0100 Subject: [PATCH 32/94] Replace ERC20 supply RPC query with atomic read of all flow control data --- shared/src/ledger/queries/shell/eth_bridge.rs | 146 ++++++++++-------- 1 file changed, 80 insertions(+), 66 deletions(-) diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index f7c0731c70..8aa7dcfe4d 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::ledger::eth_bridge::storage::bridge_pool::get_key_from_hash; -use namada_core::ledger::eth_bridge::storage::wrapped_erc20s; +use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::storage::merkle_tree::StoreRef; use namada_core::ledger::storage::{DBIter, StorageHasher, StoreType, DB}; use namada_core::ledger::storage_api::{ @@ -17,7 +17,7 @@ use namada_core::types::ethereum_events::{ }; use namada_core::types::ethereum_structs::RelayProof; use namada_core::types::storage::{BlockHeight, DbKeySeg, Key}; -use namada_core::types::token::{minted_balance_key, Amount}; +use namada_core::types::token::Amount; use namada_core::types::vote_extensions::validator_set_update::{ ValidatorSetArgs, VotingPowersMap, }; @@ -42,6 +42,20 @@ use crate::types::keccak::KeccakHash; use crate::types::storage::Epoch; use crate::types::storage::MembershipProof::BridgePool; +/// Contains information about the flow control of some ERC20 +/// wrapped asset. +#[derive( + Debug, Copy, Clone, Eq, PartialEq, BorshSerialize, BorshDeserialize, +)] +pub struct Erc20FlowControl { + /// Whether the wrapped asset is whitelisted. + whitelisted: bool, + /// Total minted supply of some wrapped asset. + supply: Amount, + /// The token cap of some wrapped asset. + cap: Amount, +} + pub type RelayProofBytes = Vec; router! {ETH_BRIDGE, @@ -104,31 +118,48 @@ router! {ETH_BRIDGE, ( "voting_powers" / "epoch" / [epoch: Epoch] ) -> VotingPowersMap = voting_powers_at_epoch, - // Read the total supply of some wrapped ERC20 token in Namada. - ( "erc20" / "supply" / [asset: EthAddress] ) - -> Option = read_erc20_supply, + // Read the total supply and respective cap of some wrapped + // ERC20 token in Namada. + ( "erc20" / "flow_control" / [asset: EthAddress] ) + -> Erc20FlowControl = get_erc20_flow_control, } -/// Read the total supply of some wrapped ERC20 token in Namada. -fn read_erc20_supply( +/// Read the total supply and respective cap of some wrapped +/// ERC20 token in Namada. +fn get_erc20_flow_control( ctx: RequestCtx<'_, D, H>, asset: EthAddress, -) -> storage_api::Result> +) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - let Some(native_erc20) = ctx.wl_storage.read(&native_erc20_key())? else { - return Err(storage_api::Error::SimpleMessage( - "The Ethereum bridge storage is not initialized", - )); - }; - let token = if asset == native_erc20 { - ctx.wl_storage.storage.native_token.clone() - } else { - wrapped_erc20s::token(&asset) - }; - ctx.wl_storage.read(&minted_balance_key(&token)) + let key = whitelist::Key { + asset, + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + let whitelisted = ctx.wl_storage.read(&key)?.unwrap_or(false); + + let key = whitelist::Key { + asset, + suffix: whitelist::KeyType::WrappedSupply, + } + .into(); + let supply = ctx.wl_storage.read(&key)?.unwrap_or_default(); + + let key = whitelist::Key { + asset, + suffix: whitelist::KeyType::Cap, + } + .into(); + let cap = ctx.wl_storage.read(&key)?.unwrap_or_default(); + + Ok(Erc20FlowControl { + whitelisted, + supply, + cap, + }) } /// Helper function to read a smart contract from storage. @@ -577,7 +608,6 @@ mod test_ethbridge_router { use namada_core::types::voting_power::{ EthBridgeVotingPower, FractionalVotingPower, }; - use namada_ethereum_bridge::parameters::read_native_erc20_address; use namada_ethereum_bridge::protocol::transactions::validator_set_update::aggregate_votes; use namada_ethereum_bridge::storage::proof::BridgePoolRootProof; use namada_proof_of_stake::pos_queries::PosQueries; @@ -1372,45 +1402,9 @@ mod test_ethbridge_router { assert!(resp.is_err()); } - /// Test reading the wrapped NAM supply + /// Test reading the supply and cap of an ERC20 token. #[tokio::test] - async fn test_read_wnam_supply() { - let mut client = TestClient::new(RPC); - assert_eq!(client.wl_storage.storage.last_epoch.0, 0); - - // initialize storage - test_utils::init_default_storage(&mut client.wl_storage); - - let native_erc20 = - read_native_erc20_address(&client.wl_storage).expect("Test failed"); - - // write tokens to storage - let amount = Amount::native_whole(12345); - let token = &client.wl_storage.storage.native_token; - client - .wl_storage - .write(&minted_balance_key(token), amount) - .expect("Test failed"); - - // commit the changes - client - .wl_storage - .storage - .commit_block(MockDBWriteBatch) - .expect("Test failed"); - - // check that reading wrapped NAM fails - let result = RPC - .shell() - .eth_bridge() - .read_erc20_supply(&client, &native_erc20) - .await; - assert_matches!(result, Ok(Some(a)) if a == amount); - } - - /// Test reading the supply of an ERC20 token. - #[tokio::test] - async fn test_read_erc20_supply() { + async fn test_get_erc20_flow_control() { const ERC20_TOKEN: EthAddress = EthAddress([0; 20]); let mut client = TestClient::new(RPC); @@ -1419,29 +1413,49 @@ mod test_ethbridge_router { // initialize storage test_utils::init_default_storage(&mut client.wl_storage); - // check supply - should be None + // check supply - should be 0 let result = RPC .shell() .eth_bridge() - .read_erc20_supply(&client, &ERC20_TOKEN) + .get_erc20_flow_control(&client, &ERC20_TOKEN) .await; - assert_matches!(result, Ok(None)); + assert_matches!( + result, + Ok(f) if f.supply.is_zero() && f.cap.is_zero() + ); // write tokens to storage - let amount = Amount::native_whole(12345); - let token = wrapped_erc20s::token(&ERC20_TOKEN); + let supply_amount = Amount::native_whole(123); + let cap_amount = Amount::native_whole(12345); + let key = whitelist::Key { + asset: ERC20_TOKEN, + suffix: whitelist::KeyType::WrappedSupply, + } + .into(); client .wl_storage - .write(&minted_balance_key(&token), amount) + .write(&key, supply_amount) + .expect("Test failed"); + let key = whitelist::Key { + asset: ERC20_TOKEN, + suffix: whitelist::KeyType::Cap, + } + .into(); + client + .wl_storage + .write(&key, cap_amount) .expect("Test failed"); // check that the supply was updated let result = RPC .shell() .eth_bridge() - .read_erc20_supply(&client, &ERC20_TOKEN) + .get_erc20_flow_control(&client, &ERC20_TOKEN) .await; - assert_matches!(result, Ok(Some(a)) if a == amount); + assert_matches!( + result, + Ok(f) if f.supply == supply_amount && f.cap == cap_amount + ); } } From 2861e4680b61a6f9a5f8b85baad161fc22fe6911 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 10:36:24 +0100 Subject: [PATCH 33/94] Refactor get_erc20_flow_control to use EthBridgeQueries --- shared/src/ledger/queries/shell/eth_bridge.rs | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index 8aa7dcfe4d..44ad306789 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -5,7 +5,6 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::ledger::eth_bridge::storage::bridge_pool::get_key_from_hash; -use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::storage::merkle_tree::StoreRef; use namada_core::ledger::storage::{DBIter, StorageHasher, StoreType, DB}; use namada_core::ledger::storage_api::{ @@ -134,26 +133,13 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - let key = whitelist::Key { - asset, - suffix: whitelist::KeyType::Whitelisted, - } - .into(); - let whitelisted = ctx.wl_storage.read(&key)?.unwrap_or(false); - - let key = whitelist::Key { - asset, - suffix: whitelist::KeyType::WrappedSupply, - } - .into(); - let supply = ctx.wl_storage.read(&key)?.unwrap_or_default(); + let ethbridge_queries = ctx.wl_storage.ethbridge_queries(); - let key = whitelist::Key { - asset, - suffix: whitelist::KeyType::Cap, - } - .into(); - let cap = ctx.wl_storage.read(&key)?.unwrap_or_default(); + let whitelisted = ethbridge_queries.is_token_whitelisted(&asset); + let supply = ethbridge_queries + .get_token_supply(&asset) + .unwrap_or_default(); + let cap = ethbridge_queries.get_token_cap(&asset).unwrap_or_default(); Ok(Erc20FlowControl { whitelisted, @@ -597,6 +583,7 @@ mod test_ethbridge_router { use namada_core::ledger::eth_bridge::storage::bridge_pool::{ get_pending_key, get_signed_root_key, BridgePoolTree, }; + use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::storage::mockdb::MockDBWriteBatch; use namada_core::ledger::storage_api::StorageWrite; use namada_core::types::address::testing::established_address_1; From 14556d78d0d343d12264933af1df88a632b991bc Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 11:14:48 +0100 Subject: [PATCH 34/94] Add test_wnam_doesnt_mint_nuts() unit test --- .../transactions/ethereum_events/events.rs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 3c92d3dcab..3ee884c413 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -619,7 +619,7 @@ mod tests { use namada_core::ledger::storage::testing::TestWlStorage; use namada_core::ledger::storage::types::encode; use namada_core::types::address::testing::gen_implicit_address; - use namada_core::types::address::{gen_established_address, nam}; + use namada_core::types::address::{gen_established_address, nam, wnam}; use namada_core::types::eth_bridge_pool::GasFee; use namada_core::types::ethereum_events::testing::{ arbitrary_eth_address, arbitrary_keccak_hash, arbitrary_nonce, @@ -1458,4 +1458,30 @@ mod tests { assert_eq!(pre_escrowed_balance, post_escrowed_balance); }) } + + /// Test that the ledger appropriately panics when we try to mint + /// wrapped NAM NUTs. Under normal circumstances, this should never + /// happen. + #[test] + #[should_panic(expected = "Attempted to mint wNAM NUTs!")] + fn test_wnam_doesnt_mint_nuts() { + let mut wl_storage = TestWlStorage::default(); + test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + + let transfer = PendingTransfer { + transfer: eth_bridge_pool::TransferToEthereum { + asset: wnam(), + sender: address::testing::established_address_1(), + recipient: EthAddress([5; 20]), + amount: Amount::from(10), + kind: eth_bridge_pool::TransferToEthereumKind::Nut, + }, + gas_fee: GasFee { + amount: Amount::from(1), + payer: address::testing::established_address_1(), + }, + }; + + _ = update_transferred_asset_balances(&mut wl_storage, &transfer); + } } From 7efe3c2eaf4da88a300154bc9a15f48bebebd825 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 11:25:26 +0100 Subject: [PATCH 35/94] Fix test_wrapped_nam_not_burned() unit test --- .../transactions/ethereum_events/events.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 3ee884c413..d66d4adef8 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -1245,7 +1245,7 @@ mod tests { let pending_transfers = init_bridge_pool_transfers( &mut wl_storage, [ - (native_erc20, eth_bridge_pool::TransferToEthereumKind::Nut), + (native_erc20, eth_bridge_pool::TransferToEthereumKind::Erc20), ( EthAddress([0xaa; 20]), eth_bridge_pool::TransferToEthereumKind::Erc20, @@ -1435,18 +1435,20 @@ mod tests { _ = act_on(wl_storage, event).unwrap(); - // check post supply + // check post supply - the wNAM minted supply should increase + // by the transferred amount assert!( wl_storage .read_bytes(&balance_key(&wnam, &BRIDGE_POOL_ADDRESS)) .expect("Test failed") .is_none() ); - assert!( + assert_eq!( wl_storage - .read_bytes(&minted_balance_key(&wnam)) - .expect("Test failed") - .is_none() + .read::(&minted_balance_key(&wnam)) + .expect("Reading from storage should not fail") + .expect("The wNAM supply should have been updated"), + Amount::from_u64(10), ); // check post balance From a714fb3559e56ac25f2a1d156914f049045380ab Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 13:16:31 +0100 Subject: [PATCH 36/94] Fix test_redeem_native_token() unit test --- .../transactions/ethereum_events/events.rs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index d66d4adef8..739e1d1ce1 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -1193,27 +1193,30 @@ mod tests { &wl_storage.storage.native_token, &BRIDGE_ADDRESS, ); + let bridge_pool_native_erc20_supply_key = + minted_balance_key(&wrapped_erc20s::token(&wnam())); StorageWrite::write( &mut wl_storage, &bridge_pool_native_token_balance_key, bridge_pool_initial_balance, )?; + StorageWrite::write( + &mut wl_storage, + &bridge_pool_native_erc20_supply_key, + amount, + )?; let receiver_native_token_balance_key = token::balance_key(&wl_storage.storage.native_token, &receiver); - let native_erc20 = read_native_erc20_address(&wl_storage)?; - let changed_keys = redeem_native_token( - &mut wl_storage, - &native_erc20, - &receiver, - &amount, - )?; + let changed_keys = + redeem_native_token(&mut wl_storage, &wnam(), &receiver, &amount)?; assert_eq!( changed_keys, BTreeSet::from([ bridge_pool_native_token_balance_key.clone(), - receiver_native_token_balance_key.clone() + receiver_native_token_balance_key.clone(), + bridge_pool_native_erc20_supply_key.clone(), ]) ); assert_eq!( @@ -1227,6 +1230,13 @@ mod tests { StorageRead::read(&wl_storage, &receiver_native_token_balance_key)?, Some(amount) ); + assert_eq!( + StorageRead::read( + &wl_storage, + &bridge_pool_native_erc20_supply_key + )?, + Some(Amount::zero()) + ); Ok(()) } From 0655085220cf1a187f363127e47d26d53e4fb929 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 13:26:36 +0100 Subject: [PATCH 37/94] Test that wrapped NAM is never minted --- .../transactions/ethereum_events/events.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 739e1d1ce1..8d50401b11 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -1188,6 +1188,16 @@ mod tests { let receiver = address::testing::established_address_1(); let amount = Amount::from(100); + // pre wNAM balance - 0 + let receiver_wnam_balance_key = + token::balance_key(&wrapped_erc20s::token(&wnam()), &receiver); + assert!( + wl_storage + .read_bytes(&receiver_wnam_balance_key) + .unwrap() + .is_none() + ); + let bridge_pool_initial_balance = Amount::from(100_000_000); let bridge_pool_native_token_balance_key = token::balance_key( &wl_storage.storage.native_token, @@ -1238,6 +1248,16 @@ mod tests { Some(Amount::zero()) ); + // post wNAM balance - 0 + // + // wNAM is never minted, it's converted back to NAM + assert!( + wl_storage + .read_bytes(&receiver_wnam_balance_key) + .unwrap() + .is_none() + ); + Ok(()) } From 7a537326e11ab66ad3febd8dcf25ad330b862631 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 13:43:26 +0100 Subject: [PATCH 38/94] Fix test_act_on_changes_storage_for_transfers_to_eth() unit test --- .../protocol/transactions/ethereum_events/events.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 8d50401b11..d6408cc7cf 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -642,10 +642,8 @@ mod tests { update_epoch_parameter(wl_storage, &epoch_duration) .expect("Test failed"); // set native ERC20 token - let native_erc20_key = bridge_storage::native_erc20_key(); - let native_erc20 = EthAddress([0; 20]); wl_storage - .write_bytes(&native_erc20_key, encode(&native_erc20)) + .write_bytes(&bridge_storage::native_erc20_key(), encode(&wnam())) .expect("Test failed"); } @@ -727,7 +725,7 @@ mod tests { .expect("Test failed"); for transfer in pending_transfers { - if transfer.transfer.asset == EthAddress([0; 20]) { + if transfer.transfer.asset == wnam() { // native ERC20 let sender_key = balance_key(&nam(), &transfer.transfer.sender); let sender_balance = Amount::from(0); @@ -1026,6 +1024,10 @@ mod tests { &BRIDGE_POOL_ADDRESS )) ); + assert!( + changed_keys + .remove(&minted_balance_key(&wrapped_erc20s::token(&wnam()))) + ); assert!(changed_keys.remove(&minted_balance_key(&random_erc20_token))); assert!( changed_keys.remove(&minted_balance_key(&random_erc20_token_2)) @@ -1146,7 +1148,7 @@ mod tests { // Check the balances for transfer in pending_transfers { - if transfer.transfer.asset == EthAddress([0; 20]) { + if transfer.transfer.asset == wnam() { let sender_key = balance_key(&nam(), &transfer.transfer.sender); let value = wl_storage.read_bytes(&sender_key).expect("Test failed"); From c0b0ed580d5dde8a8feda7fcf21f8c64fd534c6e Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 15:20:39 +0100 Subject: [PATCH 39/94] Refactor Bridge pool tests to allow modifying pending transfer --- .../ethereum_bridge/bridge_pool_vp.rs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 5248f54847..df6a9b033b 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -762,14 +762,14 @@ mod test_bridge_pool_vp { insert_transfer: F, expect: Expect, ) where - F: FnOnce(PendingTransfer, &mut WriteLog) -> BTreeSet, + F: FnOnce(&mut PendingTransfer, &mut WriteLog) -> BTreeSet, { // setup let mut wl_storage = setup_storage(); let tx = Tx::from_type(TxType::Raw); // the transfer to be added to the pool - let transfer = PendingTransfer { + let mut transfer = PendingTransfer { transfer: TransferToEthereum { kind: TransferToEthereumKind::Erc20, asset: ASSET, @@ -784,7 +784,7 @@ mod test_bridge_pool_vp { }; // add transfer to pool let mut keys_changed = - insert_transfer(transfer.clone(), &mut wl_storage.write_log); + insert_transfer(&mut transfer, &mut wl_storage.write_log); // change Bertha's balances let mut new_keys_changed = update_balances( @@ -846,11 +846,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::True, ); @@ -867,11 +867,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -888,11 +888,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -909,11 +909,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -931,11 +931,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(10.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -952,11 +952,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(10.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -973,11 +973,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -994,11 +994,11 @@ mod test_bridge_pool_vp { SignedAmount::Negative(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -1013,7 +1013,7 @@ mod test_bridge_pool_vp { SignedAmount::Positive(GAS_FEE.into()), SignedAmount::Negative(TOKENS.into()), SignedAmount::Positive(TOKENS.into()), - |transfer, _| BTreeSet::from([get_pending_key(&transfer)]), + |transfer, _| BTreeSet::from([get_pending_key(transfer)]), Expect::Error, ); } @@ -1041,9 +1041,9 @@ mod test_bridge_pool_vp { payer: bertha_address(), }, }; - log.write(&get_pending_key(&transfer), t.try_to_vec().unwrap()) + log.write(&get_pending_key(transfer), t.try_to_vec().unwrap()) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -1074,7 +1074,7 @@ mod test_bridge_pool_vp { }; log.write(&get_pending_key(&t), transfer.try_to_vec().unwrap()) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::Error, ); @@ -1091,12 +1091,12 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); BTreeSet::from([ - get_pending_key(&transfer), + get_pending_key(transfer), get_signed_root_key(), ]) }, From d704d86e711622e72623d87a6761ec36e5310bee Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 13:35:58 +0100 Subject: [PATCH 40/94] Add `asset` field to Balance --- .../native_vp/ethereum_bridge/bridge_pool_vp.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index df6a9b033b..8f82d7ea81 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -517,6 +517,8 @@ mod test_bridge_pool_vp { /// A set of balances for an address struct Balance { + /// The address of the Ethereum asset. + asset: EthAddress, /// NUT or ERC20 Ethereum asset kind. kind: TransferToEthereumKind, /// The owner of the ERC20 assets. @@ -532,6 +534,7 @@ mod test_bridge_pool_vp { fn new(kind: TransferToEthereumKind, address: Address) -> Self { Self { kind, + asset: ASSET, owner: address, gas: 0.into(), token: 0.into(), @@ -654,8 +657,12 @@ mod test_bridge_pool_vp { // get the balance keys let token_key = balance_key( &match balance.kind { - TransferToEthereumKind::Erc20 => wrapped_erc20s::token(&ASSET), - TransferToEthereumKind::Nut => wrapped_erc20s::nut(&ASSET), + TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(&balance.asset) + } + TransferToEthereumKind::Nut => { + wrapped_erc20s::nut(&balance.asset) + } }, &balance.owner, ); @@ -790,6 +797,7 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + asset: transfer.transfer.asset, kind: TransferToEthereumKind::Erc20, owner: bertha_address(), gas: BERTHA_WEALTH.into(), @@ -804,6 +812,7 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + asset: transfer.transfer.asset, kind: TransferToEthereumKind::Erc20, owner: BRIDGE_POOL_ADDRESS, gas: ESCROWED_AMOUNT.into(), @@ -1131,6 +1140,7 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + asset: ASSET, kind: TransferToEthereumKind::Erc20, owner: bertha_address(), gas: BERTHA_WEALTH.into(), @@ -1145,6 +1155,7 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + asset: ASSET, kind: TransferToEthereumKind::Erc20, owner: BRIDGE_POOL_ADDRESS, gas: ESCROWED_AMOUNT.into(), @@ -1567,6 +1578,7 @@ mod test_bridge_pool_vp { &mut wl_storage.write_log, Balance { kind, + asset: ASSET, owner: daewon_address(), gas: DAEWONS_GAS.into(), token: DAES_NUTS.into(), @@ -1581,6 +1593,7 @@ mod test_bridge_pool_vp { &mut wl_storage.write_log, Balance { kind, + asset: ASSET, owner: BRIDGE_POOL_ADDRESS, gas: ESCROWED_AMOUNT.into(), token: ESCROWED_NUTS.into(), From d90a5eac58371fe41215883e897b15c4e4dd163f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 13:36:24 +0100 Subject: [PATCH 41/94] New Bridge pool VP unit tests for wrapped NAM --- .../ethereum_bridge/bridge_pool_vp.rs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 8f82d7ea81..4b15d0050e 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -1639,4 +1639,48 @@ mod test_bridge_pool_vp { fn test_escrowing_nuts_happy_flow() { test_nut_aux(TransferToEthereumKind::Nut, Expect::True) } + + /// Test that the Bridge pool VP rejects a wNAM NUT transfer. + #[test] + fn test_bridge_pool_vp_rejects_wnam_nut() { + assert_bridge_pool( + SignedAmount::Negative(GAS_FEE.into()), + SignedAmount::Positive(GAS_FEE.into()), + SignedAmount::Negative(TOKENS.into()), + SignedAmount::Positive(TOKENS.into()), + |transfer, log| { + transfer.transfer.kind = TransferToEthereumKind::Nut; + transfer.transfer.asset = wnam(); + log.write( + &get_pending_key(transfer), + transfer.try_to_vec().unwrap(), + ) + .unwrap(); + BTreeSet::from([get_pending_key(transfer)]) + }, + Expect::False, + ); + } + + /// Test that the Bridge pool VP accepts a wNAM ERC20 transfer. + #[test] + fn test_bridge_pool_vp_accepts_wnam_erc20() { + assert_bridge_pool( + SignedAmount::Negative(GAS_FEE.into()), + SignedAmount::Positive(GAS_FEE.into()), + SignedAmount::Negative(TOKENS.into()), + SignedAmount::Positive(TOKENS.into()), + |transfer, log| { + transfer.transfer.kind = TransferToEthereumKind::Erc20; + transfer.transfer.asset = wnam(); + log.write( + &get_pending_key(transfer), + transfer.try_to_vec().unwrap(), + ) + .unwrap(); + BTreeSet::from([get_pending_key(transfer)]) + }, + Expect::True, + ); + } } From 75cf448ed91a3b73968245e19a3e665bb2549136 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 13:20:45 +0100 Subject: [PATCH 42/94] Fix wNAM edge cases in Bridge pool VP unit tests --- .../ethereum_bridge/bridge_pool_vp.rs | 125 ++++++++++++------ 1 file changed, 83 insertions(+), 42 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 4b15d0050e..056f41573b 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -27,7 +27,7 @@ use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader}; use crate::ledger::storage::traits::StorageHasher; use crate::ledger::storage::{DBIter, DB}; use crate::proto::Tx; -use crate::types::address::{Address, InternalAddress}; +use crate::types::address::Address; use crate::types::eth_bridge_pool::{PendingTransfer, TransferToEthereumKind}; use crate::types::ethereum_events::EthAddress; use crate::types::storage::Key; @@ -324,9 +324,7 @@ where }, token_check: EscrowDelta { payer_account: &transfer.transfer.sender, - escrow_account: &Address::Internal( - InternalAddress::EthBridge, - ), + escrow_account: &BRIDGE_ADDRESS, expected_debit: debit, expected_credit: transfer.transfer.amount, }, @@ -494,7 +492,7 @@ mod test_bridge_pool_vp { use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{Storage, WlStorage}; use crate::ledger::storage_api::StorageWrite; - use crate::types::address::{nam, wnam}; + use crate::types::address::{nam, wnam, InternalAddress}; use crate::types::chain::ChainId; use crate::types::eth_bridge_pool::{GasFee, TransferToEthereum}; use crate::types::hash::Hash; @@ -642,6 +640,14 @@ mod test_bridge_pool_vp { SignedAmount::Positive(ESCROWED_AMOUNT.into()), SignedAmount::Positive(ESCROWED_NUTS.into()), ); + // set up the initial balances of the ethereum bridge account + update_balances( + &mut writelog, + Balance::new(TransferToEthereumKind::Erc20, BRIDGE_ADDRESS), + SignedAmount::Positive(ESCROWED_AMOUNT.into()), + // we only care about escrowing NAM + SignedAmount::Positive(0.into()), + ); writelog.commit_tx(); writelog } @@ -654,46 +660,79 @@ mod test_bridge_pool_vp { gas_delta: SignedAmount, token_delta: SignedAmount, ) -> BTreeSet { - // get the balance keys - let token_key = balance_key( - &match balance.kind { - TransferToEthereumKind::Erc20 => { - wrapped_erc20s::token(&balance.asset) - } - TransferToEthereumKind::Nut => { - wrapped_erc20s::nut(&balance.asset) - } - }, - &balance.owner, - ); - let account_key = balance_key(&nam(), &balance.owner); + // wnam is drawn from the same account + if balance.asset == wnam() + && !matches!(&balance.owner, Address::Internal(_)) + { + use SignedAmount::*; + + // update the balance of nam + let original_balance = std::cmp::max(balance.token, balance.gas); + let updated_balance = match (gas_delta, token_delta) { + (Negative(x), Negative(y)) => original_balance - x - y, + (Negative(x), Positive(y)) => original_balance - x + y, + (Positive(x), Negative(y)) => original_balance + x - y, + (Positive(x), Positive(y)) => original_balance + x + y, + }; - // update the balance of nam - let new_balance = match gas_delta { - SignedAmount::Positive(amount) => balance.gas + amount, - SignedAmount::Negative(amount) => balance.gas - amount, - } - .try_to_vec() - .expect("Test failed"); + // write the changes to the log + let account_key = balance_key(&nam(), &balance.owner); + write_log + .write( + &account_key, + updated_balance.try_to_vec().expect("Test failed"), + ) + .expect("Test failed"); - // update the balance of tokens - let new_token_balance = match token_delta { - SignedAmount::Positive(amount) => balance.token + amount, - SignedAmount::Negative(amount) => balance.token - amount, - } - .try_to_vec() - .expect("Test failed"); + // changed keys + [account_key].into() + } else { + // get the balance keys + let token_key = if balance.asset == wnam() { + // the match above guards against non-internal addresses, + // so the only logical owner here is the Ethereum bridge + // address, where we escrow NAM to, when minting wNAM on + // Ethereum + assert_eq!(balance.owner, BRIDGE_POOL_ADDRESS); + balance_key(&nam(), &BRIDGE_ADDRESS) + } else { + balance_key( + &match balance.kind { + TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(&balance.asset) + } + TransferToEthereumKind::Nut => { + wrapped_erc20s::nut(&balance.asset) + } + }, + &balance.owner, + ) + }; + let account_key = balance_key(&nam(), &balance.owner); - // write the changes to the log - write_log - .write(&account_key, new_balance) - .expect("Test failed"); - write_log - .write(&token_key, new_token_balance) - .expect("Test failed"); + // update the balance of nam + let new_gas_balance = match gas_delta { + SignedAmount::Positive(amount) => balance.gas + amount, + SignedAmount::Negative(amount) => balance.gas - amount, + }; - // return the keys changed - [account_key, token_key].into() + // update the balance of tokens + let new_token_balance = match token_delta { + SignedAmount::Positive(amount) => balance.token + amount, + SignedAmount::Negative(amount) => balance.token - amount, + }; + + // write the changes to the log + write_log + .write(&account_key, new_gas_balance.try_to_vec().unwrap()) + .expect("Test failed"); + write_log + .write(&token_key, new_token_balance.try_to_vec().unwrap()) + .expect("Test failed"); + + // return the keys changed + [account_key, token_key].into() + } } /// Initialize some dummy storage for testing @@ -1312,7 +1351,9 @@ mod test_bridge_pool_vp { .write_log .write( &eb_account_key, - Amount::from(100).try_to_vec().expect("Test failed"), + Amount::from(ESCROWED_AMOUNT + 100) + .try_to_vec() + .expect("Test failed"), ) .expect("Test failed"); From fd1bf4d89d81601fe2397e27e9695369f76561f2 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 17:09:42 +0100 Subject: [PATCH 43/94] Add invalidate_wnam_over_cap_tx() unit test --- tests/src/native_vp/eth_bridge_pool.rs | 36 ++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index 034ca60ab8..956b2a1b8c 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -30,6 +30,7 @@ mod test_bridge_pool_vp { const BERTHA_TOKENS: u64 = 10_000; const GAS_FEE: u64 = 100; const TOKENS: u64 = 10; + const TOKEN_CAP: u64 = TOKENS; /// A signing keypair for good old Bertha. fn bertha_keypair() -> common::SecretKey { @@ -66,7 +67,7 @@ mod test_bridge_pool_vp { let config = EthereumBridgeConfig { erc20_whitelist: vec![Erc20WhitelistEntry { token_address: wnam(), - token_cap: Amount::max().native_denominated(), + token_cap: Amount::from_u64(TOKEN_CAP).native_denominated(), }], eth_start_height: Default::default(), min_confirmations: Default::default(), @@ -96,16 +97,23 @@ mod test_bridge_pool_vp { env } - fn validate_tx(tx: Tx) { + fn run_vp(tx: Tx) -> bool { let env = setup_env(tx); tx_host_env::set(env); let mut tx_env = tx_host_env::take(); tx_env.execute_tx().expect("Test failed."); let vp_env = TestNativeVpEnv::from_tx_env(tx_env, BRIDGE_POOL_ADDRESS); - let result = vp_env + vp_env .validate_tx(|ctx| BridgePoolVp { ctx }) - .expect("Test failed"); - assert!(result); + .expect("Test failed") + } + + fn validate_tx(tx: Tx) { + assert!(run_vp(tx)); + } + + fn invalidate_tx(tx: Tx) { + assert!(!run_vp(tx)); } fn create_tx(transfer: PendingTransfer, keypair: &common::SecretKey) -> Tx { @@ -156,6 +164,24 @@ mod test_bridge_pool_vp { validate_tx(create_tx(transfer, &bertha_keypair())); } + #[test] + fn invalidate_wnam_over_cap_tx() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: wnam(), + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKEN_CAP + 1), + }, + gas_fee: GasFee { + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + invalidate_tx(create_tx(transfer, &bertha_keypair())); + } + #[test] fn validate_mint_wnam_different_sender_tx() { let transfer = PendingTransfer { From 161cf43190809b28448d1566f8b69063b9e50445 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 13:46:03 +0100 Subject: [PATCH 44/94] Add changelog for #1781 --- .changelog/unreleased/features/1781-cap-wnam.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/1781-cap-wnam.md diff --git a/.changelog/unreleased/features/1781-cap-wnam.md b/.changelog/unreleased/features/1781-cap-wnam.md new file mode 100644 index 0000000000..aeba012c6b --- /dev/null +++ b/.changelog/unreleased/features/1781-cap-wnam.md @@ -0,0 +1,2 @@ +- Control the flow of NAM over the Ethereum bridge + ([\#1781](https://github.com/anoma/namada/pull/1781)) \ No newline at end of file From b30a877460fc69d24303a0e744ef2322a882f013 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 16:12:00 +0100 Subject: [PATCH 45/94] Update ethbridge-rs to v0.22.0 --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 12 ++++++------ wasm/Cargo.lock | 20 ++++++++++---------- wasm_for_tests/wasm_source/Cargo.lock | 20 ++++++++++---------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8131f58e9..18e78ed504 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,8 +1999,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2010,8 +2010,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -2021,8 +2021,8 @@ dependencies = [ [[package]] name = "ethbridge-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", @@ -2032,8 +2032,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2043,8 +2043,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -2054,8 +2054,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethers", diff --git a/Cargo.toml b/Cargo.toml index d524d0c7a2..7375bb01e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,12 +66,12 @@ directories = "4.0.1" ed25519-consensus = "1.2.0" escargot = "0.5.7" ethabi = "18.0.0" -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0" } +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0" } ethers = "2.0.0" expectrl = "0.7.0" eyre = "0.6.5" diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index a273155cd2..0f4f1f4a39 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethers", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 6d5a227eb4..6405e7100e 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethers", From 1a63f01d71bff78b6064c266f2f2bee7b6da80ba Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 17:02:50 +0100 Subject: [PATCH 46/94] Fix compilation errors to support new `ethbridge-rs` --- .../lib/node/ledger/ethereum_oracle/events.rs | 80 ++++---- .../lib/node/ledger/ethereum_oracle/mod.rs | 15 +- .../lib/node/ledger/shell/finalize_block.rs | 31 +-- .../shell/vote_extensions/eth_events.rs | 39 +--- core/src/types/eth_bridge_pool.rs | 183 +++++++++++++++--- core/src/types/ethereum_events.rs | 91 +++------ .../transactions/ethereum_events/events.rs | 62 +++--- .../src/storage/eth_bridge_queries.rs | 32 ++- shared/src/ledger/eth_bridge/bridge_pool.rs | 66 ++++--- shared/src/ledger/queries/mod.rs | 3 + shared/src/ledger/queries/shell.rs | 2 +- shared/src/ledger/queries/shell/eth_bridge.rs | 156 ++++++++++----- 12 files changed, 456 insertions(+), 304 deletions(-) diff --git a/apps/src/lib/node/ledger/ethereum_oracle/events.rs b/apps/src/lib/node/ledger/ethereum_oracle/events.rs index 645b87d1c4..3472fcb601 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/events.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/events.rs @@ -14,9 +14,9 @@ pub mod eth_events { use namada::eth_bridge::ethers::contract::EthEvent; use namada::types::address::Address; use namada::types::ethereum_events::{ - EthAddress, EthereumEvent, TransferToEthereum, TransferToEthereumKind, - TransferToNamada, Uint, + EthAddress, EthereumEvent, TransferToEthereum, TransferToNamada, Uint, }; + use namada::types::hash::Hash; use namada::types::keccak::KeccakHash; use namada::types::token::Amount; use num256::Uint256; @@ -153,32 +153,33 @@ pub mod eth_events { }; } - /// Trait to add parsing methods to foreign types. - trait Parse: Sized { - parse_method! { parse_eth_transfer_kind -> TransferToEthereumKind } - parse_method! { parse_eth_address -> EthAddress } - parse_method! { parse_address -> Address } - parse_method! { parse_amount -> Amount } - parse_method! { parse_u32 -> u32 } - parse_method! { parse_uint256 -> Uint } - parse_method! { parse_bool -> bool } - parse_method! { parse_string -> String } - parse_method! { parse_keccak -> KeccakHash } - parse_method! { parse_amount_array -> Vec } - parse_method! { parse_eth_address_array -> Vec } - parse_method! { parse_address_array -> Vec
} - parse_method! { parse_string_array -> Vec } - parse_method! { parse_transfer_to_namada_array -> Vec } - parse_method! { parse_transfer_to_namada -> TransferToNamada } - parse_method! { parse_transfer_to_eth_array -> Vec } - parse_method! { parse_transfer_to_eth -> TransferToEthereum } + macro_rules! trait_parse_def { + ($($name:ident -> $type:ty;)*) => { + /// Trait to add parsing methods to foreign types. + trait Parse: Sized { + $( parse_method!($name -> $type); )* + } + } } - impl Parse for u8 { - fn parse_eth_transfer_kind(self) -> Result { - self.try_into() - .map_err(|err| Error::Decode(format!("{:?}", err))) - } + trait_parse_def! { + parse_address -> Address; + parse_address_array -> Vec
; + parse_amount -> Amount; + parse_amount_array -> Vec; + parse_bool -> bool; + parse_eth_address -> EthAddress; + parse_eth_address_array -> Vec; + parse_hash -> Hash; + parse_keccak -> KeccakHash; + parse_string -> String; + parse_string_array -> Vec; + parse_transfer_to_eth -> TransferToEthereum; + parse_transfer_to_eth_array -> Vec; + parse_transfer_to_namada -> TransferToNamada; + parse_transfer_to_namada_array -> Vec; + parse_u32 -> u32; + parse_uint256 -> Uint; } impl Parse for ethabi::Address { @@ -200,7 +201,13 @@ pub mod eth_events { impl Parse for ethabi::Uint { fn parse_amount(self) -> Result { - Ok(Amount::from(self.as_u64())) + let uint = { + use namada::core::types::uint::Uint as NamadaUint; + let mut num_buf = [0; 32]; + self.to_little_endian(&mut num_buf); + NamadaUint::from_little_endian(&num_buf) + }; + Amount::from_uint(uint, 0).map_err(|e| Error::Decode(e.to_string())) } fn parse_u32(self) -> Result { @@ -222,6 +229,10 @@ pub mod eth_events { fn parse_keccak(self) -> Result { Ok(KeccakHash(self)) } + + fn parse_hash(self) -> Result { + Ok(Hash(self)) + } } impl Parse for Vec { @@ -279,21 +290,15 @@ pub mod eth_events { impl Parse for ethereum_structs::Erc20Transfer { fn parse_transfer_to_eth(self) -> Result { - let kind = self.kind.parse_eth_transfer_kind()?; let asset = self.from.parse_eth_address()?; let receiver = self.to.parse_eth_address()?; - let sender = self.sender.parse_address()?; let amount = self.amount.parse_amount()?; - let gas_payer = self.fee_from.parse_address()?; - let gas_amount = self.fee.parse_amount()?; + let checksum = self.namada_data_digest.parse_hash()?; Ok(TransferToEthereum { - kind, asset, amount, - sender, receiver, - gas_amount, - gas_payer, + checksum, }) } } @@ -509,13 +514,10 @@ pub mod eth_events { let eth_transfers = TransferToErcFilter { transfers: vec![ ethereum_structs::Erc20Transfer { - kind: TransferToEthereumKind::Erc20 as u8, from: H160([1; 20]), to: H160([2; 20]), - sender: address.clone(), amount: 0u64.into(), - fee_from: address.clone(), - fee: 0u64.into(), + namada_data_digest: [0; 32], }; 2 ], diff --git a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs index 05486aa11e..fc9ae9f0d1 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs @@ -569,9 +569,8 @@ mod test_oracle { use namada::eth_bridge::ethers::types::H160; use namada::eth_bridge::structs::Erc20Transfer; use namada::types::address::testing::gen_established_address; - use namada::types::ethereum_events::{ - EthAddress, TransferToEthereum, TransferToEthereumKind, - }; + use namada::types::ethereum_events::{EthAddress, TransferToEthereum}; + use namada::types::hash::Hash; use tokio::sync::oneshot::channel; use tokio::time::timeout; @@ -828,13 +827,10 @@ mod test_oracle { let gas_payer = gen_established_address(); let second_event = TransferToErcFilter { transfers: vec![Erc20Transfer { - kind: TransferToEthereumKind::Erc20 as u8, amount: 0.into(), from: H160([0; 20]), - sender: gas_payer.to_string(), to: H160([1; 20]), - fee: 0.into(), - fee_from: gas_payer.to_string(), + namada_data_digest: [0; 32], }], valid_map: vec![true], relayer_address: gas_payer.to_string(), @@ -898,13 +894,10 @@ mod test_oracle { assert_eq!( transfer, TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: Default::default(), asset: EthAddress([0; 20]), - sender: gas_payer.clone(), receiver: EthAddress([1; 20]), - gas_amount: Default::default(), - gas_payer: gas_payer.clone(), + checksum: Hash::default(), } ); } else { diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index f8fb4fb0a4..f79c5cf421 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1038,9 +1038,7 @@ mod test_finalize_block { }; use namada::proto::{Code, Data, Section, Signature}; use namada::types::dec::POS_DECIMAL_PRECISION; - use namada::types::ethereum_events::{ - EthAddress, TransferToEthereum, TransferToEthereumKind, Uint as ethUint, - }; + use namada::types::ethereum_events::{EthAddress, Uint as ethUint}; use namada::types::hash::Hash; use namada::types::keccak::KeccakHash; use namada::types::key::tm_consensus_key_raw_hash; @@ -1695,17 +1693,24 @@ mod test_finalize_block { } // write transfer to storage let transfer = { - use namada::core::types::eth_bridge_pool::PendingTransfer; - let transfer = TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - amount: 10u64.into(), - asset, - receiver, - gas_amount: 10u64.into(), - sender: bertha.clone(), - gas_payer: bertha.clone(), + use namada::core::types::eth_bridge_pool::{ + GasFee, PendingTransfer, TransferToEthereum, + TransferToEthereumKind, + }; + let pending = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + amount: 10u64.into(), + asset, + recipient: receiver, + sender: bertha.clone(), + }, + gas_fee: GasFee { + amount: 10u64.into(), + payer: bertha.clone(), + }, }; - let pending = PendingTransfer::from(&transfer); + let transfer = (&pending).into(); shell .wl_storage .write(&bridge_pool::get_pending_key(&pending), pending) diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs index 5282864e6d..cec4158940 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs @@ -461,9 +461,9 @@ mod test_vote_extensions { #[cfg(feature = "abcipp")] use namada::types::eth_abi::Encode; use namada::types::ethereum_events::{ - EthAddress, EthereumEvent, TransferToEthereum, TransferToEthereumKind, - Uint, + EthAddress, EthereumEvent, TransferToEthereum, Uint, }; + use namada::types::hash::Hash; #[cfg(feature = "abcipp")] use namada::types::keccak::keccak_hash; #[cfg(feature = "abcipp")] @@ -594,13 +594,10 @@ mod test_vote_extensions { let event_1 = EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), asset: EthAddress([1; 20]), - sender: gen_established_address(), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -608,13 +605,10 @@ mod test_vote_extensions { let event_2 = EthereumEvent::TransfersToEthereum { nonce: 1.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), asset: EthAddress([1; 20]), - sender: gen_established_address(), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -660,13 +654,10 @@ mod test_vote_extensions { let event_1 = EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), asset: EthAddress([1; 20]), - sender: gen_established_address(), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -723,13 +714,10 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -818,13 +806,10 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -896,13 +881,10 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -979,13 +961,10 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index c7394af167..4f12eec619 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -1,6 +1,8 @@ //! The necessary type definitions for the contents of the //! Ethereum bridge pool +use std::borrow::Cow; + use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use ethabi::token::Token; use serde::{Deserialize, Serialize}; @@ -8,16 +10,108 @@ use serde::{Deserialize, Serialize}; use crate::ledger::eth_bridge::storage::wrapped_erc20s; use crate::types::address::Address; use crate::types::eth_abi::Encode; -pub use crate::types::ethereum_events::TransferToEthereumKind; use crate::types::ethereum_events::{ EthAddress, TransferToEthereum as TransferToEthereumEvent, }; +use crate::types::hash::Hash as HashDigest; use crate::types::storage::{DbKeySeg, Key}; use crate::types::token::Amount; +/// A version used in our Ethereuem smart contracts +const VERSION: u8 = 1; + /// A namespace used in our Ethereuem smart contracts const NAMESPACE: &str = "transfer"; +/// Transfer to Ethereum kinds. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, +)] +pub enum TransferToEthereumKind { + /// Transfer ERC20 assets from Namada to Ethereum. + /// + /// These transfers burn wrapped ERC20 assets in Namada, once + /// they have been confirmed. + Erc20, + /// Refund non-usable tokens. + /// + /// These Bridge pool transfers should be crafted for assets + /// that have been transferred to Namada, that had either not + /// been whitelisted or whose token caps had been exceeded in + /// Namada at the time of the transfer. + Nut, +} + +/// Additional data appended to a [`TransferToEthereumEvent`] to +/// construct a [`PendingTransfer`]. +#[derive( + Debug, + Clone, + Hash, + PartialOrd, + PartialEq, + Ord, + Eq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + BorshSchema, +)] +pub struct PendingTransferAppendix<'transfer> { + /// The kind of the pending transfer to Ethereum. + pub kind: Cow<'transfer, TransferToEthereumKind>, + /// The sender of the transfer. + pub sender: Cow<'transfer, Address>, + /// The amount of gas fees (in NAM) + /// paid by the user sending this transfer + pub gas_fee: Cow<'transfer, GasFee>, +} + +impl From for PendingTransferAppendix<'static> { + #[inline] + fn from(pending: PendingTransfer) -> Self { + Self { + kind: Cow::Owned(pending.transfer.kind), + sender: Cow::Owned(pending.transfer.sender), + gas_fee: Cow::Owned(pending.gas_fee), + } + } +} + +impl<'t> From<&'t PendingTransfer> for PendingTransferAppendix<'t> { + #[inline] + fn from(pending: &'t PendingTransfer) -> Self { + Self { + kind: Cow::Borrowed(&pending.transfer.kind), + sender: Cow::Borrowed(&pending.transfer.sender), + gas_fee: Cow::Borrowed(&pending.gas_fee), + } + } +} + +impl<'transfer> PendingTransferAppendix<'transfer> { + /// Calculate the checksum of this [`PendingTransferAppendix`]. + pub fn checksum(&self) -> HashDigest { + let serialized = self + .try_to_vec() + .expect("Serializing a PendingTransferAppendix should not fail"); + HashDigest::sha256(serialized) + } +} + /// A transfer message to be submitted to Ethereum /// to move assets from Namada across the bridge. #[derive( @@ -84,51 +178,84 @@ impl PendingTransfer { } } } + + /// Retrieve a reference to the appendix of this [`PendingTransfer`]. + #[inline] + pub fn appendix(&self) -> PendingTransferAppendix<'_> { + self.into() + } + + /// Retrieve the owned appendix of this [`PendingTransfer`]. + #[inline] + pub fn into_appendix(self) -> PendingTransferAppendix<'static> { + self.into() + } + + /// Craft a [`PendingTransfer`] from its constituents. + pub fn from_parts( + event: &TransferToEthereumEvent, + appendix: PendingTransferAppendix<'_>, + ) -> Self { + let transfer = TransferToEthereum { + kind: *appendix.kind, + asset: event.asset, + recipient: event.receiver, + sender: (*appendix.sender).clone(), + amount: event.amount, + }; + let gas_fee = (*appendix.gas_fee).clone(); + Self { transfer, gas_fee } + } } -impl From for ethbridge_structs::Erc20Transfer { - fn from(pending: PendingTransfer) -> Self { +impl From<&PendingTransfer> for ethbridge_structs::Erc20Transfer { + fn from(pending: &PendingTransfer) -> Self { + let HashDigest(namada_data_digest) = pending.appendix().checksum(); Self { - kind: pending.transfer.kind as u8, from: pending.transfer.asset.0.into(), to: pending.transfer.recipient.0.into(), amount: pending.transfer.amount.into(), - fee_from: pending.gas_fee.payer.to_string(), - fee: pending.gas_fee.amount.into(), - sender: pending.transfer.sender.to_string(), + namada_data_digest, + } + } +} + +impl From<&PendingTransfer> for TransferToEthereumEvent { + fn from(pending: &PendingTransfer) -> Self { + Self { + amount: pending.transfer.amount, + asset: pending.transfer.asset, + receiver: pending.transfer.recipient, + checksum: pending.appendix().checksum(), } } } -impl Encode<8> for PendingTransfer { - fn tokenize(&self) -> [Token; 8] { +impl Encode<6> for PendingTransfer { + fn tokenize(&self) -> [Token; 6] { // TODO: This version should be looked up from storage - let version = Token::Uint(1.into()); + let version = Token::Uint(VERSION.into()); let namespace = Token::String(NAMESPACE.into()); let from = Token::Address(self.transfer.asset.0.into()); - let fee = Token::Uint(self.gas_fee.amount.into()); let to = Token::Address(self.transfer.recipient.0.into()); let amount = Token::Uint(self.transfer.amount.into()); - let fee_from = Token::String(self.gas_fee.payer.to_string()); - let sender = Token::String(self.transfer.sender.to_string()); - [version, namespace, from, to, amount, fee_from, fee, sender] + let checksum = Token::FixedBytes(self.appendix().checksum().0.into()); + [version, namespace, from, to, amount, checksum] } } -impl From<&TransferToEthereumEvent> for PendingTransfer { - fn from(event: &TransferToEthereumEvent) -> Self { - let transfer = TransferToEthereum { - kind: event.kind, - asset: event.asset, - recipient: event.receiver, - sender: event.sender.clone(), - amount: event.amount, - }; - let gas_fee = GasFee { - amount: event.gas_amount, - payer: event.gas_payer.clone(), - }; - Self { transfer, gas_fee } +// TODO: test that encode for `PendingTransfer` and +// `TransferToEthereumEvent` yield the same keccak hash +impl Encode<6> for TransferToEthereumEvent { + fn tokenize(&self) -> [Token; 6] { + // TODO: This version should be looked up from storage + let version = Token::Uint(VERSION.into()); + let namespace = Token::String(NAMESPACE.into()); + let from = Token::Address(self.asset.0.into()); + let to = Token::Address(self.receiver.0.into()); + let amount = Token::Uint(self.amount.into()); + let checksum = Token::FixedBytes(self.checksum.0.into()); + [version, namespace, from, to, amount, checksum] } } diff --git a/core/src/types/ethereum_events.rs b/core/src/types/ethereum_events.rs index b896674a0c..f7097e9749 100644 --- a/core/src/types/ethereum_events.rs +++ b/core/src/types/ethereum_events.rs @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize}; use crate::types::address::Address; use crate::types::eth_abi::Encode; +use crate::types::ethereum_structs::Erc20Transfer; use crate::types::hash::Hash; use crate::types::keccak::KeccakHash; use crate::types::storage::{DbKeySeg, KeySeg}; @@ -366,60 +367,6 @@ pub struct TransferToNamada { pub receiver: Address, } -/// Transfer to Ethereum kinds. -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - BorshSerialize, - BorshDeserialize, - BorshSchema, - Serialize, - Deserialize, -)] -#[repr(u8)] -pub enum TransferToEthereumKind { - /// Transfer ERC20 assets from Namada to Ethereum. - /// - /// These transfers burn wrapped ERC20 assets in Namada, once - /// they have been confirmed. - Erc20 = Self::KIND_ERC20, - /// Refund non-usable tokens. - /// - /// These Bridge pool transfers should be crafted for assets - /// that have been transferred to Namada, that had either not - /// been whitelisted or whose token caps had been exceeded in - /// Namada at the time of the transfer. - Nut = Self::KIND_NUT, -} - -// XXX: keep these values in sync with the smart contracts -impl TransferToEthereumKind { - const KIND_ERC20: u8 = 0; - const KIND_NUT: u8 = 1; -} - -impl TryFrom for TransferToEthereumKind { - type Error = eyre::Error; - - fn try_from(kind: u8) -> Result { - match kind { - Self::KIND_ERC20 => Ok(Self::Erc20), - Self::KIND_NUT => Ok(Self::Nut), - _ => Err(eyre!( - "Only valid kinds are {} (ERC20) and {} (NUT)", - Self::KIND_ERC20, - Self::KIND_NUT - )), - } - } -} - /// An event transferring some kind of value from Namada to Ethereum #[derive( Clone, @@ -436,20 +383,40 @@ impl TryFrom for TransferToEthereumKind { Deserialize, )] pub struct TransferToEthereum { - /// The kind of transfer to Ethereum. - pub kind: TransferToEthereumKind, /// Quantity of wrapped Asset in the transfer pub amount: Amount, /// Address of the smart contract issuing the token pub asset: EthAddress, /// The address receiving assets on Ethereum pub receiver: EthAddress, - /// The amount of fees (in NAM) - pub gas_amount: Amount, - /// The address sending assets to Ethereum. - pub sender: Address, - /// The account of fee payer. - pub gas_payer: Address, + /// Checksum of all Namada specific fields, including, + /// but not limited to, whether it is a NUT transfer, + /// the address of the sender, etc + /// + /// It serves to uniquely identify an event stored under + /// the Bridge pool, in Namada + pub checksum: Hash, +} + +impl From for TransferToEthereum { + #[inline] + fn from(transfer: Erc20Transfer) -> Self { + Self { + amount: { + let uint = { + use crate::types::uint::Uint as NamadaUint; + let mut num_buf = [0; 32]; + transfer.amount.to_little_endian(&mut num_buf); + NamadaUint::from_little_endian(&num_buf) + }; + // this is infallible for a denom of 0 + Amount::from_uint(uint, 0).unwrap() + }, + asset: EthAddress(transfer.from.0), + receiver: EthAddress(transfer.to.0), + checksum: Hash(transfer.namada_data_digest), + } + } } #[cfg(test)] diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index d6408cc7cf..65c8f832ca 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -7,8 +7,7 @@ use borsh::BorshDeserialize; use eyre::{Result, WrapErr}; use namada_core::hints; use namada_core::ledger::eth_bridge::storage::bridge_pool::{ - get_nonce_key, get_pending_key, is_pending_transfer_key, - BRIDGE_POOL_ADDRESS, + get_nonce_key, is_pending_transfer_key, BRIDGE_POOL_ADDRESS, }; use namada_core::ledger::eth_bridge::storage::{ self as bridge_storage, wrapped_erc20s, @@ -370,11 +369,14 @@ where for (event, is_valid) in transfers.iter().zip(valid_transfers.iter().copied()) { - let pending_transfer = event.into(); - let key = get_pending_key(&pending_transfer); - if hints::unlikely(!wl_storage.has_key(&key)?) { + let (pending_transfer, key) = if let Some((pending, key)) = + wl_storage.ethbridge_queries().lookup_transfer_to_eth(event) + { + (pending, key) + } else { + hints::cold(); unreachable!("The transfer should exist in the bridge pool"); - } + }; if hints::likely(is_valid) { tracing::debug!( ?pending_transfer, @@ -612,6 +614,7 @@ mod tests { use assert_matches::assert_matches; use borsh::BorshSerialize; use eyre::Result; + use namada_core::ledger::eth_bridge::storage::bridge_pool::get_pending_key; use namada_core::ledger::parameters::{ update_epoch_parameter, EpochDuration, }; @@ -982,19 +985,10 @@ mod tests { let pending_keys: HashSet = pending_transfers.iter().map(get_pending_key).collect(); let relayer = gen_established_address("random"); - let mut transfers = vec![]; - for transfer in pending_transfers { - let transfer_to_eth = TransferToEthereum { - kind: transfer.transfer.kind, - amount: transfer.transfer.amount, - asset: transfer.transfer.asset, - receiver: transfer.transfer.recipient, - gas_amount: transfer.gas_fee.amount, - gas_payer: transfer.gas_fee.payer, - sender: transfer.transfer.sender, - }; - transfers.push(transfer_to_eth); - } + let transfers: Vec<_> = pending_transfers + .iter() + .map(TransferToEthereum::from) + .collect(); let event = EthereumEvent::TransfersToEthereum { nonce: arbitrary_nonce(), valid_transfers_map: transfers.iter().map(|_| true).collect(), @@ -1307,16 +1301,8 @@ mod tests { init_balance(&mut wl_storage, &pending_transfers); let (transfers, valid_transfers_map) = pending_transfers .into_iter() - .map(|transfer| { - let transfer_to_eth = TransferToEthereum { - kind: transfer.transfer.kind, - amount: transfer.transfer.amount, - asset: transfer.transfer.asset, - receiver: transfer.transfer.recipient, - gas_amount: transfer.gas_fee.amount, - gas_payer: transfer.gas_fee.payer, - sender: transfer.transfer.sender, - }; + .map(|ref transfer| { + let transfer_to_eth: TransferToEthereum = transfer.into(); (transfer_to_eth, true) }) .unzip(); @@ -1353,16 +1339,18 @@ mod tests { read_native_erc20_address(wl_storage).expect("Test failed"); let deltas = transfers .filter_map( - |TransferToEthereum { - kind, - asset, - amount, - .. - }| { + |event @ TransferToEthereum { asset, amount, .. }| { if asset == &native_erc20 { return None; } - let erc20_token = match kind { + let kind = { + let (pending, _) = wl_storage + .ethbridge_queries() + .lookup_transfer_to_eth(event) + .expect("Test failed"); + pending.transfer.kind + }; + let erc20_token = match &kind { eth_bridge_pool::TransferToEthereumKind::Erc20 => { wrapped_erc20s::token(asset) } @@ -1380,7 +1368,7 @@ mod tests { .read(&minted_balance_key(&erc20_token)) .expect("Test failed"); Some(Delta { - kind: *kind, + kind, asset: *asset, sent_amount: *amount, prev_balance, diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index 08fdcab2fa..c30de9a536 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -1,16 +1,19 @@ use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::hints; -use namada_core::ledger::eth_bridge::storage::bridge_pool::{ - get_nonce_key, get_signed_root_key, +use namada_core::ledger::eth_bridge::storage::{ + active_key, bridge_pool, whitelist, }; -use namada_core::ledger::eth_bridge::storage::{active_key, whitelist}; use namada_core::ledger::storage; use namada_core::ledger::storage::{StoreType, WlStorage}; use namada_core::ledger::storage_api::StorageRead; use namada_core::types::address::Address; -use namada_core::types::ethereum_events::{EthAddress, GetEventNonce, Uint}; +use namada_core::types::eth_abi::Encode; +use namada_core::types::eth_bridge_pool::PendingTransfer; +use namada_core::types::ethereum_events::{ + EthAddress, GetEventNonce, TransferToEthereum, Uint, +}; use namada_core::types::keccak::KeccakHash; -use namada_core::types::storage::{BlockHeight, Epoch}; +use namada_core::types::storage::{BlockHeight, Epoch, Key as StorageKey}; use namada_core::types::token; use namada_core::types::vote_extensions::validator_set_update::{ EthAddrBook, ValidatorSetArgs, VotingPowersMap, VotingPowersMapExt, @@ -175,7 +178,7 @@ where &self .wl_storage .storage - .read(&get_nonce_key()) + .read(&bridge_pool::get_nonce_key()) .expect("Reading Bridge pool nonce shouldn't fail.") .0 .expect("Reading Bridge pool nonce shouldn't fail."), @@ -191,7 +194,7 @@ where .storage .db .read_subspace_val_with_height( - &get_nonce_key(), + &bridge_pool::get_nonce_key(), height, self.wl_storage.storage.get_last_block_height(), ) @@ -225,7 +228,7 @@ where self, ) -> Option<(BridgePoolRootProof, BlockHeight)> { self.wl_storage - .read_bytes(&get_signed_root_key()) + .read_bytes(&bridge_pool::get_signed_root_key()) .expect("Reading signed Bridge pool root shouldn't fail.") .map(|bytes| { BorshDeserialize::try_from_slice(&bytes).expect( @@ -488,6 +491,19 @@ where nut_amount: token::Amount::zero(), } } + + /// Given a [`TransferToEthereum`] event, look-up the corresponding + /// [`PendingTransfer`]. + pub fn lookup_transfer_to_eth( + self, + transfer: &TransferToEthereum, + ) -> Option<(PendingTransfer, StorageKey)> { + let pending_key = bridge_pool::get_key_from_hash(&transfer.keccak256()); + self.wl_storage + .read(&pending_key) + .expect("Reading from storage should not fail") + .zip(Some(pending_key)) + } } /// Number of tokens to mint after receiving a "transfer diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 940524d34f..9576b70baf 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -1,5 +1,6 @@ //! Bridge pool SDK functionality. +use std::borrow::Cow; use std::cmp::Ordering; use std::collections::HashMap; use std::io::Write; @@ -17,7 +18,9 @@ use super::{block_on_eth_sync, eth_sync_or_exit, BlockOnEthSync}; use crate::eth_bridge::ethers::abi::AbiDecode; use crate::eth_bridge::structs::RelayProof; use crate::ledger::args; -use crate::ledger::queries::{Client, RPC}; +use crate::ledger::queries::{ + Client, GenBridgePoolProofReq, GenBridgePoolProofRsp, RPC, +}; use crate::ledger::rpc::{query_wasm_code_hash, validate_amount}; use crate::ledger::tx::{prepare_tx, Error}; use crate::proto::Tx; @@ -179,9 +182,8 @@ where /// bridge pool. async fn construct_bridge_pool_proof( client: &C, - transfers: &[KeccakHash], - relayer: Address, -) -> Halt> + args: GenBridgePoolProofReq<'_, '_>, +) -> Halt where C: Client + Sync, { @@ -196,8 +198,8 @@ where .into_iter() .filter_map(|(ref transfer, voting_power)| { if voting_power > FractionalVotingPower::ONE_THIRD { - let hash = PendingTransfer::from(transfer).keccak256(); - transfers.contains(&hash).then_some(hash) + let hash = transfer.keccak256(); + args.transfers.contains(&hash).then_some(hash) } else { None } @@ -234,7 +236,7 @@ where } } - let data = (transfers, relayer).try_to_vec().unwrap(); + let data = args.try_to_vec().unwrap(); let response = RPC .shell() .eth_bridge() @@ -242,7 +244,7 @@ where .await; response.map(|response| response.data).try_halt(|e| { - println!("Encountered error constructing proof:\n{:?}", e); + println!("Encountered error constructing proof:\n{e}"); }) } @@ -265,25 +267,29 @@ pub async fn construct_proof( where C: Client + Sync, { - let bp_proof_bytes = construct_bridge_pool_proof( + let GenBridgePoolProofRsp { + abi_encoded_proof: bp_proof_bytes, + appendices, + } = construct_bridge_pool_proof( client, - &args.transfers, - args.relayer.clone(), + GenBridgePoolProofReq { + transfers: args.transfers.as_slice().into(), + relayer: Cow::Borrowed(&args.relayer), + with_appendix: true, + }, ) .await?; - let bp_proof: RelayProof = - AbiDecode::decode(&bp_proof_bytes).try_halt(|error| { - println!("Unable to decode the generated proof: {:?}", error); - })?; let resp = BridgePoolProofResponse { hashes: args.transfers, relayer_address: args.relayer, - total_fees: bp_proof - .transfers - .iter() - .map(|t| t.fee.as_u64()) - .sum::() - .into(), + total_fees: appendices + .map(|appendices| { + appendices + .into_iter() + .map(|app| app.gas_fee.amount) + .sum::() + }) + .unwrap_or(Amount::zero()), abi_encoded_proof: bp_proof_bytes, }; println!("{}", serde_json::to_string(&resp).unwrap()); @@ -316,9 +322,18 @@ where eth_sync_or_exit(&*eth_client).await?; } - let bp_proof = - construct_bridge_pool_proof(nam_client, &args.transfers, args.relayer) - .await?; + let GenBridgePoolProofRsp { + abi_encoded_proof: bp_proof, + .. + } = construct_bridge_pool_proof( + nam_client, + GenBridgePoolProofReq { + transfers: Cow::Owned(args.transfers), + relayer: Cow::Owned(args.relayer), + with_appendix: false, + }, + ) + .await?; let bridge = match RPC .shell() .eth_bridge() @@ -468,8 +483,7 @@ mod recommendations { .transfer_to_ethereum_progress(client) .await .unwrap() - .keys() - .map(PendingTransfer::from) + .into_keys() .collect::>(); // get the signed bridge pool root so we can analyze the signatures diff --git a/shared/src/ledger/queries/mod.rs b/shared/src/ledger/queries/mod.rs index ce689e6325..bcd9fc8c27 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -12,6 +12,9 @@ pub use types::{ }; use vp::{Vp, VP}; +pub use self::shell::eth_bridge::{ + Erc20FlowControl, GenBridgePoolProofReq, GenBridgePoolProofRsp, +}; use super::storage::traits::StorageHasher; use super::storage::{DBIter, DB}; use super::storage_api; diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 94412f1a15..b1e243f74b 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -1,4 +1,4 @@ -mod eth_bridge; +pub(super) mod eth_bridge; use borsh::{BorshDeserialize, BorshSerialize}; use masp_primitives::asset_type::AssetType; diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index 44ad306789..0dbd76b376 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -1,5 +1,6 @@ //! Ethereum bridge related shell queries. +use std::borrow::Cow; use std::collections::HashMap; use std::str::FromStr; @@ -11,6 +12,7 @@ use namada_core::ledger::storage_api::{ self, CustomError, ResultExt, StorageRead, }; use namada_core::types::address::Address; +use namada_core::types::eth_bridge_pool::PendingTransferAppendix; use namada_core::types::ethereum_events::{ EthAddress, EthereumEvent, TransferToEthereum, }; @@ -34,6 +36,7 @@ use namada_ethereum_bridge::storage::{ }; use namada_proof_of_stake::pos_queries::PosQueries; +use crate::eth_bridge::ethers::abi::AbiDecode; use crate::ledger::queries::{EncodedResponseQuery, RequestCtx, RequestQuery}; use crate::types::eth_abi::{Encode, EncodeCell}; use crate::types::eth_bridge_pool::PendingTransfer; @@ -55,7 +58,39 @@ pub struct Erc20FlowControl { cap: Amount, } -pub type RelayProofBytes = Vec; +/// Request data to pass to `generate_bridge_pool_proof`. +#[derive(Debug, Clone, Eq, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct GenBridgePoolProofReq<'transfers, 'relayer> { + /// The hashes of the transfers to be relayed. + pub transfers: Cow<'transfers, [KeccakHash]>, + /// The address of the relayer to compensate. + pub relayer: Cow<'relayer, Address>, + /// Whether to return the appendix of a [`PendingTransfer`]. + pub with_appendix: bool, +} + +/// Response data returned by `generate_bridge_pool_proof`. +#[derive(Debug, Clone, Eq, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct GenBridgePoolProofRsp { + /// Ethereum ABI encoded [`RelayProof`]. + pub abi_encoded_proof: Vec, + /// Appendix data of all requested pending transfers. + pub appendices: Option>>, +} + +impl GenBridgePoolProofRsp { + /// Retrieve all [`PendingTransfer`] instances returned from the RPC server. + pub fn pending_transfers(self) -> impl Iterator { + RelayProof::decode(&self.abi_encoded_proof) + .into_iter() + .flat_map(|proof| proof.transfers) + .zip(self.appendices.into_iter().flatten()) + .map(|(event, appendix)| { + let event: TransferToEthereum = event.into(); + PendingTransfer::from_parts(&event, appendix) + }) + } +} router! {ETH_BRIDGE, // Get the current contents of the Ethereum bridge pool @@ -70,12 +105,12 @@ router! {ETH_BRIDGE, // Generate a merkle proof for the inclusion of requested // transfers in the Ethereum bridge pool ( "pool" / "proof" ) - -> RelayProofBytes = (with_options generate_bridge_pool_proof), + -> GenBridgePoolProofRsp = (with_options generate_bridge_pool_proof), // Iterates over all ethereum events and returns the amount of // voting power backing each `TransferToEthereum` event. ( "pool" / "transfer_to_eth_progress" ) - -> HashMap + -> HashMap = transfer_to_ethereum_progress, // Request a proof of a validator set signed off for @@ -290,8 +325,11 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - if let Ok((transfer_hashes, relayer)) = - <(Vec, Address)>::try_from_slice(request.data.as_slice()) + if let Ok(GenBridgePoolProofReq { + transfers: transfer_hashes, + relayer, + with_appendix, + }) = BorshDeserialize::try_from_slice(request.data.as_slice()) { // get the latest signed merkle root of the Ethereum bridge pool let (signed_root, height) = ctx @@ -335,14 +373,19 @@ where .into(), ))); } - let transfers = values - .iter() - .map(|bytes| { - PendingTransfer::try_from_slice(bytes) - .expect("Deserializing storage shouldn't fail") - .into() - }) - .collect(); + let (transfers, appendices) = values.iter().fold( + (vec![], vec![]), + |(mut transfers, mut appendices), bytes| { + let pending = PendingTransfer::try_from_slice(bytes) + .expect("Deserializing storage shouldn't fail"); + let eth_transfer = (&pending).into(); + if with_appendix { + appendices.push(pending.into_appendix()); + } + transfers.push(eth_transfer); + (transfers, appendices) + }, + ); // get the membership proof match tree.get_sub_tree_existence_proof( &keys, @@ -353,7 +396,7 @@ where .wl_storage .ethbridge_queries() .get_validator_set_args(None); - let data = RelayProof { + let relay_proof = RelayProof { validator_set_args: validator_args.into(), signatures: sort_sigs( &voting_powers, @@ -366,9 +409,13 @@ where batch_nonce: signed_root.data.1.into(), relayer_address: relayer.to_string(), }; - let data = ethers::abi::AbiEncode::encode(data) - .try_to_vec() - .expect("Serializing a relay proof should not fail."); + let rsp = GenBridgePoolProofRsp { + abi_encoded_proof: ethers::abi::AbiEncode::encode( + relay_proof, + ), + appendices: with_appendix.then_some(appendices), + }; + let data = rsp.try_to_vec().into_storage_result()?; Ok(EncodedResponseQuery { data, ..Default::default() @@ -389,7 +436,7 @@ where /// backing each `TransferToEthereum` event. fn transfer_to_ethereum_progress( ctx: RequestCtx<'_, D, H>, -) -> storage_api::Result> +) -> storage_api::Result> where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -444,6 +491,12 @@ where ) .average_voting_power(ctx.wl_storage); for transfer in transfers { + let key = get_key_from_hash(&transfer.keccak256()); + let transfer = ctx + .wl_storage + .read::(&key) + .into_storage_result()? + .expect("The transfer must be present in storage"); pending_events.insert(transfer, voting_power); } } @@ -974,9 +1027,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - (vec![transfer.keccak256()], bertha_address()) - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -999,7 +1056,7 @@ mod test_ethbridge_router { let data = RelayProof { validator_set_args: validator_args.into(), signatures: sort_sigs(&voting_powers, &signed_root.signatures), - transfers: vec![transfer.into()], + transfers: vec![(&transfer).into()], pool_root: signed_root.data.0.0, proof: proof.proof.into_iter().map(|hash| hash.0).collect(), proof_flags: proof.flags, @@ -1007,12 +1064,11 @@ mod test_ethbridge_router { relayer_address: bertha_address().to_string(), }; let proof = ethers::abi::AbiEncode::encode(data); - assert_eq!(proof, resp.data); + assert_eq!(proof, resp.data.abi_encoded_proof); } - /// Test if the merkle tree including a transfer - /// has had its root signed, then we cannot generate - /// a proof. + /// Test if the merkle tree including a transfer has not had its + /// root signed, then we cannot generate a proof. #[tokio::test] async fn test_cannot_get_proof() { let mut client = TestClient::new(RPC); @@ -1090,9 +1146,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - (vec![transfer2.keccak256()], bertha_address()) - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer2.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -1204,16 +1264,8 @@ mod test_ethbridge_router { ) .expect("Test failed"); - let event_transfer = - namada_core::types::ethereum_events::TransferToEthereum { - kind: transfer.transfer.kind, - asset: transfer.transfer.asset, - receiver: transfer.transfer.recipient, - amount: transfer.transfer.amount, - gas_payer: transfer.gas_fee.payer.clone(), - gas_amount: transfer.gas_fee.amount, - sender: transfer.transfer.sender.clone(), - }; + let event_transfer: namada_core::types::ethereum_events::TransferToEthereum + = (&transfer).into(); let eth_event = EthereumEvent::TransfersToEthereum { nonce: Default::default(), transfers: vec![event_transfer.clone()], @@ -1274,10 +1326,8 @@ mod test_ethbridge_router { .transfer_to_ethereum_progress(&client) .await .unwrap(); - let expected: HashMap< - namada_core::types::ethereum_events::TransferToEthereum, - FractionalVotingPower, - > = [(event_transfer, voting_power)].into_iter().collect(); + let expected: HashMap = + [(transfer, voting_power)].into_iter().collect(); assert_eq!(expected, resp); } @@ -1354,9 +1404,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - vec![(transfer.keccak256(), bertha_address())] - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -1377,9 +1431,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - vec![transfer.keccak256()] - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, From 91b2fef6e48940d3e29a8618005ffe7cc7c88f47 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 7 Aug 2023 14:32:08 +0100 Subject: [PATCH 47/94] Test pending transfers and events have the same ABI encoding --- core/src/types/eth_bridge_pool.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index 4f12eec619..3a9b185e9f 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -244,8 +244,6 @@ impl Encode<6> for PendingTransfer { } } -// TODO: test that encode for `PendingTransfer` and -// `TransferToEthereumEvent` yield the same keccak hash impl Encode<6> for TransferToEthereumEvent { fn tokenize(&self) -> [Token; 6] { // TODO: This version should be looked up from storage @@ -292,3 +290,30 @@ pub struct GasFee { /// The account of fee payer. pub payer: Address, } + +#[cfg(test)] +mod test_eth_bridge_pool_types { + use super::*; + use crate::types::address::testing::established_address_1; + + /// Test that [`PendingTransfer`] and [`TransferToEthereum`] + /// have the same keccak hash, after being ABI encoded. + #[test] + fn test_same_keccak_hash() { + let pending = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + amount: 10u64.into(), + asset: EthAddress([0xaa; 20]), + recipient: EthAddress([0xbb; 20]), + sender: established_address_1(), + }, + gas_fee: GasFee { + amount: 10u64.into(), + payer: established_address_1(), + }, + }; + let event: TransferToEthereumEvent = (&pending).into(); + assert_eq!(pending.keccak256(), event.keccak256()); + } +} From 5678ee7cabd4d27ce94ef3ef62b1fa5e9a8445be Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 7 Aug 2023 14:48:17 +0100 Subject: [PATCH 48/94] Changelog for #1789 --- .changelog/unreleased/features/1789-update-ethbridge-rs.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/1789-update-ethbridge-rs.md diff --git a/.changelog/unreleased/features/1789-update-ethbridge-rs.md b/.changelog/unreleased/features/1789-update-ethbridge-rs.md new file mode 100644 index 0000000000..4fe862e522 --- /dev/null +++ b/.changelog/unreleased/features/1789-update-ethbridge-rs.md @@ -0,0 +1,2 @@ +- Update ethbridge-rs to v0.22.0 + ([\#1789](https://github.com/anoma/namada/pull/1789)) \ No newline at end of file From 28dad92e41db3b104953e1d70f197f3f8ccf3130 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 09:26:06 +0100 Subject: [PATCH 49/94] Move `check_balance_changes` to Bridge pool VP module --- .../ethereum_bridge/bridge_pool_vp.rs | 105 ++++++++++++++++- .../ledger/native_vp/ethereum_bridge/vp.rs | 106 +----------------- 2 files changed, 105 insertions(+), 106 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 056f41573b..f3ee00e3c9 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -22,7 +22,6 @@ use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; use namada_ethereum_bridge::parameters::read_native_erc20_address; -use crate::ledger::native_vp::ethereum_bridge::vp::check_balance_changes; use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader}; use crate::ledger::storage::traits::StorageHasher; use crate::ledger::storage::{DBIter, DB}; @@ -31,7 +30,7 @@ use crate::types::address::Address; use crate::types::eth_bridge_pool::{PendingTransfer, TransferToEthereumKind}; use crate::types::ethereum_events::EthAddress; use crate::types::storage::Key; -use crate::types::token::{balance_key, Amount}; +use crate::types::token::{balance_key, Amount, Change}; use crate::vm::WasmCacheAccess; #[derive(thiserror::Error, Debug)] @@ -473,6 +472,108 @@ where } } +/// Checks that the balances at both `sender` and `receiver` have changed by +/// some amount, and that the changes balance each other out. If the balance +/// changes are invalid, the reason is logged and a `None` is returned. +/// Otherwise, return the `Amount` of the transfer i.e. by how much the sender's +/// balance decreased, or equivalently by how much the receiver's balance +/// increased +fn check_balance_changes( + reader: impl StorageReader, + sender: &Key, + receiver: &Key, +) -> Result, Error> { + let sender_balance_pre = reader + .read_pre_value::(sender)? + .unwrap_or_default() + .change(); + let sender_balance_post = match reader.read_post_value::(sender)? { + Some(value) => value, + None => { + return Err(Error(eyre!( + "Rejecting transaction as could not read_post balance key {}", + sender, + ))); + } + } + .change(); + let receiver_balance_pre = reader + .read_pre_value::(receiver)? + .unwrap_or_default() + .change(); + let receiver_balance_post = match reader + .read_post_value::(receiver)? + { + Some(value) => value, + None => { + return Err(Error(eyre!( + "Rejecting transaction as could not read_post balance key {}", + receiver, + ))); + } + } + .change(); + + let sender_balance_delta = + calculate_delta(sender_balance_pre, sender_balance_post)?; + let receiver_balance_delta = + calculate_delta(receiver_balance_pre, receiver_balance_post)?; + if receiver_balance_delta != -sender_balance_delta { + tracing::debug!( + ?sender_balance_pre, + ?receiver_balance_pre, + ?sender_balance_post, + ?receiver_balance_post, + ?sender_balance_delta, + ?receiver_balance_delta, + "Rejecting transaction as balance changes do not match" + ); + return Ok(None); + } + if sender_balance_delta.is_zero() || sender_balance_delta > Change::zero() { + assert!( + receiver_balance_delta.is_zero() + || receiver_balance_delta < Change::zero() + ); + tracing::debug!( + "Rejecting transaction as no balance change or invalid change" + ); + return Ok(None); + } + if sender_balance_post < Change::zero() { + tracing::debug!( + ?sender_balance_post, + "Rejecting transaction as balance is negative" + ); + return Ok(None); + } + if receiver_balance_post < Change::zero() { + tracing::debug!( + ?receiver_balance_post, + "Rejecting transaction as balance is negative" + ); + return Ok(None); + } + + Ok(Some(Amount::from_change(receiver_balance_delta))) +} + +/// Return the delta between `balance_pre` and `balance_post`, erroring if there +/// is an underflow +fn calculate_delta( + balance_pre: Change, + balance_post: Change, +) -> Result { + match balance_post.checked_sub(&balance_pre) { + Some(result) => Ok(result), + None => Err(Error(eyre!( + "Underflow while calculating delta: {} - {}", + balance_post, + balance_pre + ))), + } +} + #[cfg(test)] mod test_bridge_pool_vp { use std::env::temp_dir; diff --git a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs index 715b8ad616..6a18554692 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs @@ -11,9 +11,9 @@ use namada_core::ledger::storage::traits::StorageHasher; use namada_core::ledger::{eth_bridge, storage as ledger_storage}; use namada_core::types::address::Address; use namada_core::types::storage::Key; -use namada_core::types::token::{balance_key, Amount, Change}; +use namada_core::types::token::{balance_key, Amount}; -use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader, VpEnv}; +use crate::ledger::native_vp::{Ctx, NativeVp, VpEnv}; use crate::proto::Tx; use crate::vm::WasmCacheAccess; @@ -224,108 +224,6 @@ fn determine_check_type( Ok(Some(CheckType::Erc20Transfer)) } -/// Checks that the balances at both `sender` and `receiver` have changed by -/// some amount, and that the changes balance each other out. If the balance -/// changes are invalid, the reason is logged and a `None` is returned. -/// Otherwise, return the `Amount` of the transfer i.e. by how much the sender's -/// balance decreased, or equivalently by how much the receiver's balance -/// increased -pub(super) fn check_balance_changes( - reader: impl StorageReader, - sender: &Key, - receiver: &Key, -) -> Result> { - let sender_balance_pre = reader - .read_pre_value::(sender)? - .unwrap_or_default() - .change(); - let sender_balance_post = match reader.read_post_value::(sender)? { - Some(value) => value, - None => { - return Err(eyre!( - "Rejecting transaction as could not read_post balance key {}", - sender, - )); - } - } - .change(); - let receiver_balance_pre = reader - .read_pre_value::(receiver)? - .unwrap_or_default() - .change(); - let receiver_balance_post = match reader - .read_post_value::(receiver)? - { - Some(value) => value, - None => { - return Err(eyre!( - "Rejecting transaction as could not read_post balance key {}", - receiver, - )); - } - } - .change(); - - let sender_balance_delta = - calculate_delta(sender_balance_pre, sender_balance_post)?; - let receiver_balance_delta = - calculate_delta(receiver_balance_pre, receiver_balance_post)?; - if receiver_balance_delta != -sender_balance_delta { - tracing::debug!( - ?sender_balance_pre, - ?receiver_balance_pre, - ?sender_balance_post, - ?receiver_balance_post, - ?sender_balance_delta, - ?receiver_balance_delta, - "Rejecting transaction as balance changes do not match" - ); - return Ok(None); - } - if sender_balance_delta.is_zero() || sender_balance_delta > Change::zero() { - assert!( - receiver_balance_delta.is_zero() - || receiver_balance_delta < Change::zero() - ); - tracing::debug!( - "Rejecting transaction as no balance change or invalid change" - ); - return Ok(None); - } - if sender_balance_post < Change::zero() { - tracing::debug!( - ?sender_balance_post, - "Rejecting transaction as balance is negative" - ); - return Ok(None); - } - if receiver_balance_post < Change::zero() { - tracing::debug!( - ?receiver_balance_post, - "Rejecting transaction as balance is negative" - ); - return Ok(None); - } - - Ok(Some(Amount::from_change(receiver_balance_delta))) -} - -/// Return the delta between `balance_pre` and `balance_post`, erroring if there -/// is an underflow -fn calculate_delta( - balance_pre: Change, - balance_post: Change, -) -> Result { - match balance_post.checked_sub(&balance_pre) { - Some(result) => Ok(result), - None => Err(eyre!( - "Underflow while calculating delta: {} - {}", - balance_post, - balance_pre - )), - } -} - #[cfg(test)] mod tests { use std::default::Default; From e102affebec318b4fb70b5459d62c03cd95acb82 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 09:41:45 +0100 Subject: [PATCH 50/94] Add token addr field to Bridge pool gas fees --- core/src/types/eth_bridge_pool.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index 3a9b185e9f..4a7e11cc06 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -75,8 +75,8 @@ pub struct PendingTransferAppendix<'transfer> { pub kind: Cow<'transfer, TransferToEthereumKind>, /// The sender of the transfer. pub sender: Cow<'transfer, Address>, - /// The amount of gas fees (in NAM) - /// paid by the user sending this transfer + /// The amount of gas fees paid by the user + /// sending this transfer. pub gas_fee: Cow<'transfer, GasFee>, } @@ -158,10 +158,10 @@ pub struct TransferToEthereum { BorshSchema, )] pub struct PendingTransfer { - /// The message to send to Ethereum to + /// Transfer to Ethereum data. pub transfer: TransferToEthereum, - /// The amount of gas fees (in NAM) - /// paid by the user sending this transfer + /// Amount of gas fees paid by the user + /// sending the transfer. pub gas_fee: GasFee, } @@ -267,9 +267,9 @@ impl From<&PendingTransfer> for Key { } } -/// The amount of NAM to be payed to the relayer of -/// a transfer across the Ethereum Bridge to compensate -/// for Ethereum gas fees. +/// The amount of fees to be payed, in Namada, to the relayer +/// of a transfer across the Ethereum Bridge, compensating +/// for Ethereum gas costs. #[derive( Debug, Clone, @@ -285,10 +285,13 @@ impl From<&PendingTransfer> for Key { BorshSchema, )] pub struct GasFee { - /// The amount of fees (in NAM) + /// The amount of fees. pub amount: Amount, /// The account of fee payer. pub payer: Address, + /// The address of the fungible token to draw + /// gas fees from. + pub token: Address, } #[cfg(test)] From b30fbeb6171c9fff71f9860bd45a175adfe3312e Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 10:37:27 +0100 Subject: [PATCH 51/94] Fix compilation errors from prev commit --- .../lib/node/ledger/shell/finalize_block.rs | 3 ++- .../ledger/eth_bridge/storage/bridge_pool.rs | 18 ++++++++++++++++++ core/src/types/eth_bridge_pool.rs | 2 ++ .../transactions/ethereum_events/events.rs | 3 +++ shared/src/ledger/eth_bridge/bridge_pool.rs | 3 ++- .../ethereum_bridge/bridge_pool_vp.rs | 9 +++++++++ shared/src/ledger/queries/shell/eth_bridge.rs | 8 ++++++++ tests/src/native_vp/eth_bridge_pool.rs | 4 ++++ wasm/wasm_source/src/tx_bridge_pool.rs | 10 +++++++--- 9 files changed, 55 insertions(+), 5 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index f79c5cf421..55a1c63314 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1656,6 +1656,7 @@ mod test_finalize_block { /// /// Sets the validity of the transfer on Ethereum's side. fn test_bp_nonce_is_incremented_aux(valid_transfer: bool) { + use crate::node::ledger::shell::address::nam; test_bp(|shell: &mut TestShell| { let asset = EthAddress([0xff; 20]); let receiver = EthAddress([0xaa; 20]); @@ -1680,7 +1681,6 @@ mod test_finalize_block { } // add bertha's gas fees the pool { - use crate::node::ledger::shell::address::nam; let amt: Amount = 999_999_u64.into(); let pool_balance_key = token::balance_key( &nam(), @@ -1706,6 +1706,7 @@ mod test_finalize_block { sender: bertha.clone(), }, gas_fee: GasFee { + token: nam(), amount: 10u64.into(), payer: bertha.clone(), }, diff --git a/core/src/ledger/eth_bridge/storage/bridge_pool.rs b/core/src/ledger/eth_bridge/storage/bridge_pool.rs index 0c20c50ff7..167e23e779 100644 --- a/core/src/ledger/eth_bridge/storage/bridge_pool.rs +++ b/core/src/ledger/eth_bridge/storage/bridge_pool.rs @@ -415,6 +415,7 @@ mod test_bridge_pool_tree { use proptest::prelude::*; use super::*; + use crate::types::address::nam; use crate::types::eth_bridge_pool::{ GasFee, TransferToEthereum, TransferToEthereumKind, }; @@ -441,6 +442,7 @@ mod test_bridge_pool_tree { amount: 1.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -468,6 +470,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -496,6 +499,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -534,6 +538,7 @@ mod test_bridge_pool_tree { amount: 1.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -562,6 +567,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -593,6 +599,7 @@ mod test_bridge_pool_tree { amount: 1u64.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -617,6 +624,7 @@ mod test_bridge_pool_tree { amount: 1u64.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -653,6 +661,7 @@ mod test_bridge_pool_tree { amount: 1.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -672,6 +681,7 @@ mod test_bridge_pool_tree { amount: 1u64.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -704,6 +714,7 @@ mod test_bridge_pool_tree { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -733,6 +744,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -763,6 +775,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -793,6 +806,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -821,6 +835,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -849,6 +864,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -877,6 +893,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -909,6 +926,7 @@ mod test_bridge_pool_tree { amount: Default::default(), }, gas_fee: GasFee { + token: nam(), amount: Default::default(), payer: bertha_address(), }, diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index 4a7e11cc06..86b0815509 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -297,6 +297,7 @@ pub struct GasFee { #[cfg(test)] mod test_eth_bridge_pool_types { use super::*; + use crate::types::address::nam; use crate::types::address::testing::established_address_1; /// Test that [`PendingTransfer`] and [`TransferToEthereum`] @@ -312,6 +313,7 @@ mod test_eth_bridge_pool_types { sender: established_address_1(), }, gas_fee: GasFee { + token: nam(), amount: 10u64.into(), payer: established_address_1(), }, diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 65c8f832ca..32c9a16e4b 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -676,6 +676,7 @@ mod tests { kind, }, gas_fee: GasFee { + token: nam(), amount: Amount::from(1), payer: payer.clone(), }, @@ -1087,6 +1088,7 @@ mod tests { kind: eth_bridge_pool::TransferToEthereumKind::Erc20, }, gas_fee: GasFee { + token: nam(), amount: Amount::from(1), payer: address::testing::established_address_1(), }, @@ -1499,6 +1501,7 @@ mod tests { kind: eth_bridge_pool::TransferToEthereumKind::Nut, }, gas_fee: GasFee { + token: nam(), amount: Amount::from(1), payer: address::testing::established_address_1(), }, diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 9576b70baf..4b362caf4f 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -62,7 +62,7 @@ pub async fn build_bridge_pool_tx( transfer: TransferToEthereum { asset, recipient, - sender: sender.clone(), + sender, amount, kind: if nut { TransferToEthereumKind::Nut @@ -699,6 +699,7 @@ mod recommendations { amount: Default::default(), }, gas_fee: GasFee { + token: namada_core::types::address::nam(), amount: gas_amount.into(), payer: bertha_address(), }, diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index f3ee00e3c9..9e7bdb1bed 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -680,6 +680,7 @@ mod test_bridge_pool_vp { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -925,6 +926,7 @@ mod test_bridge_pool_vp { amount: TOKENS.into(), }, gas_fee: GasFee { + token: nam(), amount: GAS_FEE.into(), payer: bertha_address(), }, @@ -1186,6 +1188,7 @@ mod test_bridge_pool_vp { amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: GAS_FEE.into(), payer: bertha_address(), }, @@ -1217,6 +1220,7 @@ mod test_bridge_pool_vp { amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: GAS_FEE.into(), payer: bertha_address(), }, @@ -1343,6 +1347,7 @@ mod test_bridge_pool_vp { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1410,6 +1415,7 @@ mod test_bridge_pool_vp { amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: 100.into(), payer: bertha_address(), }, @@ -1500,6 +1506,7 @@ mod test_bridge_pool_vp { amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: 100.into(), payer: bertha_address(), }, @@ -1607,6 +1614,7 @@ mod test_bridge_pool_vp { amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: 100.into(), payer: established_address_1(), }, @@ -1698,6 +1706,7 @@ mod test_bridge_pool_vp { amount: TOKENS.into(), }, gas_fee: GasFee { + token: nam(), amount: GAS_FEE.into(), payer: daewon_address(), }, diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index 0dbd76b376..bd9115f48a 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -656,6 +656,7 @@ mod test_ethbridge_router { use super::*; use crate::ledger::queries::testing::TestClient; use crate::ledger::queries::RPC; + use crate::types::address::nam; use crate::types::eth_abi::Encode; use crate::types::eth_bridge_pool::{ GasFee, PendingTransfer, TransferToEthereum, TransferToEthereumKind, @@ -865,6 +866,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -907,6 +909,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -968,6 +971,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1081,6 +1085,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1176,6 +1181,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1248,6 +1254,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1350,6 +1357,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index 956b2a1b8c..a452e9b82e 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -139,6 +139,7 @@ mod test_bridge_pool_vp { amount: Amount::from(TOKENS), }, gas_fee: GasFee { + token: nam(), amount: Amount::from(GAS_FEE), payer: bertha_address(), }, @@ -157,6 +158,7 @@ mod test_bridge_pool_vp { amount: Amount::from(TOKENS), }, gas_fee: GasFee { + token: nam(), amount: Amount::from(GAS_FEE), payer: bertha_address(), }, @@ -175,6 +177,7 @@ mod test_bridge_pool_vp { amount: Amount::from(TOKEN_CAP + 1), }, gas_fee: GasFee { + token: nam(), amount: Amount::from(GAS_FEE), payer: bertha_address(), }, @@ -193,6 +196,7 @@ mod test_bridge_pool_vp { amount: Amount::from(TOKENS), }, gas_fee: GasFee { + token: nam(), amount: Amount::from(GAS_FEE), payer: albert_address(), }, diff --git a/wasm/wasm_source/src/tx_bridge_pool.rs b/wasm/wasm_source/src/tx_bridge_pool.rs index c1a65403a7..765029d647 100644 --- a/wasm/wasm_source/src/tx_bridge_pool.rs +++ b/wasm/wasm_source/src/tx_bridge_pool.rs @@ -12,13 +12,16 @@ fn apply_tx(ctx: &mut Ctx, signed: Tx) -> TxResult { .map_err(|e| Error::wrap("Error deserializing PendingTransfer", e))?; log_string("Received transfer to add to pool."); // pay the gas fees - let GasFee { amount, ref payer } = transfer.gas_fee; - let nam_addr = ctx.get_native_token().unwrap(); + let GasFee { + token: ref fee_token_addr, + amount, + ref payer, + } = transfer.gas_fee; token::transfer( ctx, payer, &bridge_pool::BRIDGE_POOL_ADDRESS, - &nam_addr, + fee_token_addr, amount.native_denominated(), &None, &None, @@ -33,6 +36,7 @@ fn apply_tx(ctx: &mut Ctx, signed: Tx) -> TxResult { } = transfer.transfer; // if minting wNam, escrow the correct amount if asset == native_erc20_address(ctx)? { + let nam_addr = ctx.get_native_token()?; token::transfer( ctx, sender, From 1850b0742e5e7ea183793acf3ac6753a03ca2def Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 11:14:48 +0100 Subject: [PATCH 52/94] Check correct gas fee token in Bridge pool transfers --- .../transactions/ethereum_events/events.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 32c9a16e4b..08605308be 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -361,10 +361,6 @@ where }) .filter(is_pending_transfer_key) .collect(); - let pool_balance_key = - balance_key(&wl_storage.storage.native_token, &BRIDGE_POOL_ADDRESS); - let relayer_rewards_key = - balance_key(&wl_storage.storage.native_token, relayer); // Remove the completed transfers from the bridge pool for (event, is_valid) in transfers.iter().zip(valid_transfers.iter().copied()) @@ -398,6 +394,10 @@ where &pending_transfer, )?); } + let pool_balance_key = + balance_key(&pending_transfer.gas_fee.token, &BRIDGE_POOL_ADDRESS); + let relayer_rewards_key = + balance_key(&pending_transfer.gas_fee.token, relayer); // give the relayer the gas fee for this transfer. update::amount(wl_storage, &relayer_rewards_key, |balance| { balance.receive(&pending_transfer.gas_fee.amount); @@ -409,10 +409,8 @@ where wl_storage.delete(&key)?; _ = pending_keys.remove(&key); _ = changed_keys.insert(key); - } - if !transfers.is_empty() { - changed_keys.insert(relayer_rewards_key); - changed_keys.insert(pool_balance_key); + _ = changed_keys.insert(pool_balance_key); + _ = changed_keys.insert(relayer_rewards_key); } if pending_keys.is_empty() { @@ -494,9 +492,9 @@ where let mut changed_keys = BTreeSet::default(); let payer_balance_key = - balance_key(&wl_storage.storage.native_token, &transfer.gas_fee.payer); + balance_key(&transfer.gas_fee.token, &transfer.gas_fee.payer); let pool_balance_key = - balance_key(&wl_storage.storage.native_token, &BRIDGE_POOL_ADDRESS); + balance_key(&transfer.gas_fee.token, &BRIDGE_POOL_ADDRESS); update::amount(wl_storage, &payer_balance_key, |balance| { balance.receive(&transfer.gas_fee.amount); })?; From bbff3cc61f4cfa90dfe06018d115a739d4468f6a Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 14:11:59 +0100 Subject: [PATCH 53/94] Validate different gas fee tokens in Bridge pool VP --- .../ethereum_bridge/bridge_pool_vp.rs | 382 ++++++++---------- 1 file changed, 178 insertions(+), 204 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 9e7bdb1bed..312cdf493d 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -10,7 +10,10 @@ //! correctly. This means that the appropriate data is //! added to the pool and gas fees are submitted appropriately //! and that tokens to be transferred are escrowed. + +use std::borrow::Cow; use std::collections::BTreeSet; +use std::marker::PhantomData; use borsh::BorshDeserialize; use eyre::eyre; @@ -21,16 +24,17 @@ use namada_core::ledger::eth_bridge::storage::bridge_pool::{ use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; use namada_ethereum_bridge::parameters::read_native_erc20_address; +use namada_ethereum_bridge::storage::wrapped_erc20s; use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader}; use crate::ledger::storage::traits::StorageHasher; use crate::ledger::storage::{DBIter, DB}; use crate::proto::Tx; -use crate::types::address::Address; +use crate::types::address::{Address, InternalAddress}; use crate::types::eth_bridge_pool::{PendingTransfer, TransferToEthereumKind}; use crate::types::ethereum_events::EthAddress; use crate::types::storage::Key; -use crate::types::token::{balance_key, Amount, Change}; +use crate::types::token::{balance_key, Amount}; use crate::vm::WasmCacheAccess; #[derive(thiserror::Error, Debug)] @@ -84,8 +88,12 @@ where { /// Get the change in the balance of an account /// associated with an address - fn account_balance_delta(&self, address: &Address) -> Option { - let account_key = balance_key(&self.ctx.storage.native_token, address); + fn account_balance_delta( + &self, + token: &Address, + address: &Address, + ) -> Option { + let account_key = balance_key(token, address); let before: Amount = (&self.ctx) .read_pre_value(&account_key) .map_err(|error| { @@ -113,62 +121,34 @@ where }) } - /// Check that the correct amount of erc20 assets were - /// sent from the correct account into escrow. - fn check_erc20s_escrowed( + /// Check that the correct amount of tokens were sent + /// from the correct account into escrow. + #[inline] + fn check_escrowed_toks( &self, - keys_changed: &BTreeSet, - transfer: &PendingTransfer, + delta: EscrowDelta, ) -> Result { - // check that the assets to be transferred were escrowed - let token = transfer.token_address(); - let owner_key = balance_key(&token, &transfer.transfer.sender); - let escrow_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); - if keys_changed.contains(&owner_key) - && keys_changed.contains(&escrow_key) - { - match check_balance_changes(&self.ctx, &owner_key, &escrow_key)? { - Some(amount) if amount == transfer.transfer.amount => Ok(true), - _ => { - tracing::debug!( - "The assets of the transfer were not properly \ - escrowed into the Ethereum bridge pool" - ); - Ok(false) - } - } - } else { - tracing::debug!( - "The assets of the transfer were not properly escrowed into \ - the Ethereum bridge pool." - ); - Ok(false) - } - } - - /// Check that the correct amount of Nam was sent - /// from the correct account into escrow - #[inline] - fn check_nam_escrowed(&self, delta: EscrowDelta) -> Result { - self.check_nam_escrowed_balance(delta) + self.check_escrowed_toks_balance(delta) .map(|balance| balance.is_some()) } - /// Check that the correct amount of Nam was sent + /// Check that the correct amount of tokens were sent /// from the correct account into escrow, and return /// the updated escrow balance. - fn check_nam_escrowed_balance( + fn check_escrowed_toks_balance( &self, - delta: EscrowDelta, + delta: EscrowDelta, ) -> Result, Error> { let EscrowDelta { + token, payer_account, escrow_account, expected_debit, expected_credit, + .. } = delta; - let debit = self.account_balance_delta(payer_account); - let credit = self.account_balance_delta(escrow_account); + let debit = self.account_balance_delta(&token, payer_account); + let credit = self.account_balance_delta(&token, escrow_account); match (debit, credit) { // success case @@ -223,12 +203,51 @@ where } } + /// Check that the gas was correctly escrow. + fn check_gas_escrow( + &self, + wnam_address: &EthAddress, + transfer: &PendingTransfer, + gas_check: EscrowDelta<'_, GasCheck>, + ) -> Result { + if hints::unlikely( + *gas_check.token == wrapped_erc20s::token(wnam_address), + ) { + // NB: this should never be possible: protocol tx state updates + // never result in wNAM ERC20s being minted + tracing::error!( + ?transfer, + "Attempted to pay Bridge pool fees with wrapped NAM." + ); + return Ok(false); + } + if matches!( + &*gas_check.token, + Address::Internal(InternalAddress::Nut(_)) + ) { + tracing::debug!( + ?transfer, + "The gas fees of the transfer cannot be paid in NUTs." + ); + return Ok(false); + } + if !self.check_escrowed_toks(gas_check)? { + tracing::debug!( + ?transfer, + "The gas fees of the transfer were not properly escrowed into \ + the Ethereum bridge pool." + ); + return Ok(false); + } + Ok(true) + } + /// Validate a wrapped NAM transfer to Ethereum. - fn check_wnam_preconditions<'trans>( + fn check_wnam_escrow( &self, &wnam_address: &EthAddress, - transfer: &'trans PendingTransfer, - escrow_checks: EscrowCheck<'trans>, + transfer: &PendingTransfer, + token_check: EscrowDelta<'_, TokenCheck>, ) -> Result { if hints::unlikely(matches!( &transfer.transfer.kind, @@ -239,6 +258,7 @@ where // that users should never hold wNAM NUTs. doesn't hurt to add // the extra check to the vp, though tracing::error!( + ?transfer, "Attempted to add a wNAM NUT transfer to the Bridge pool" ); return Ok(false); @@ -264,7 +284,7 @@ where // amount of Nam must be escrowed in the Ethereum bridge VP's // storage. let escrowed_balance = - match self.check_nam_escrowed_balance(escrow_checks.token_check)? { + match self.check_escrowed_toks_balance(token_check)? { Some(balance) => balance.resolve(), None => return Ok(false), }; @@ -292,18 +312,28 @@ where } /// Deteremine the debit and credit amounts that should be checked. - fn escrow_check<'trans>( - &self, + fn determine_escrow_checks<'trans, 'this: 'trans>( + &'this self, wnam_address: &EthAddress, transfer: &'trans PendingTransfer, ) -> Result, Error> { - let is_native_asset = &transfer.transfer.asset == wnam_address; - // there is a corner case where the gas fees and escrowed Nam - // are debited from the same address when mint wNam. - Ok( - if transfer.gas_fee.payer == transfer.transfer.sender - && is_native_asset - { + let tok_is_native_asset = &transfer.transfer.asset == wnam_address; + let gas_is_native_asset = + transfer.gas_fee.token == self.ctx.storage.native_token; + + let (expected_gas_debit, expected_token_debit) = { + let same_sender_and_fee_payer = + transfer.gas_fee.payer == transfer.transfer.sender; + let same_tokens_and_gas_asset = (tok_is_native_asset + && gas_is_native_asset) + || transfer.token_address() == transfer.gas_fee.token; + + // there is a corner case where the gas fees and escrowed tokens + // are debited from the same address + let same_debited_address = + same_sender_and_fee_payer && same_tokens_and_gas_asset; + + if same_debited_address { let debit = transfer .gas_fee .amount @@ -314,63 +344,92 @@ where amount." )) })?; - EscrowCheck { - gas_check: EscrowDelta { - payer_account: &transfer.gas_fee.payer, - escrow_account: &BRIDGE_POOL_ADDRESS, - expected_debit: debit, - expected_credit: transfer.gas_fee.amount, - }, - token_check: EscrowDelta { - payer_account: &transfer.transfer.sender, - escrow_account: &BRIDGE_ADDRESS, - expected_debit: debit, - expected_credit: transfer.transfer.amount, - }, - } + (debit, debit) } else { - EscrowCheck { - gas_check: EscrowDelta { - payer_account: &transfer.gas_fee.payer, - escrow_account: &BRIDGE_POOL_ADDRESS, - expected_debit: transfer.gas_fee.amount, - expected_credit: transfer.gas_fee.amount, - }, - token_check: EscrowDelta { - payer_account: &transfer.transfer.sender, - escrow_account: if is_native_asset { - &BRIDGE_ADDRESS - } else { - &BRIDGE_POOL_ADDRESS - }, - expected_debit: transfer.transfer.amount, - expected_credit: transfer.transfer.amount, - }, - } + (transfer.gas_fee.amount, transfer.transfer.amount) + } + }; + Ok(EscrowCheck { + gas_check: EscrowDelta { + token: if gas_is_native_asset { + Cow::Borrowed(&self.ctx.storage.native_token) + } else { + Cow::Borrowed(&transfer.gas_fee.token) + }, + payer_account: &transfer.gas_fee.payer, + escrow_account: &BRIDGE_POOL_ADDRESS, + expected_debit: expected_gas_debit, + expected_credit: transfer.gas_fee.amount, + _kind: PhantomData, }, - ) + token_check: EscrowDelta { + token: if tok_is_native_asset { + Cow::Borrowed(&self.ctx.storage.native_token) + } else { + Cow::Owned(transfer.token_address()) + }, + payer_account: &transfer.transfer.sender, + escrow_account: if tok_is_native_asset { + &BRIDGE_ADDRESS + } else { + &BRIDGE_POOL_ADDRESS + }, + expected_debit: expected_token_debit, + expected_credit: transfer.transfer.amount, + _kind: PhantomData, + }, + }) } } /// Helper struct for handling the different escrow /// checking scenarios. -#[derive(Copy, Clone)] -struct EscrowDelta<'a> { +struct EscrowDelta<'a, KIND> { + token: Cow<'a, Address>, payer_account: &'a Address, escrow_account: &'a Address, expected_debit: Amount, expected_credit: Amount, + _kind: PhantomData<*const KIND>, +} + +impl EscrowDelta<'_, KIND> { + fn validate_changed_keys(&self, changed_keys: &BTreeSet) -> bool { + let EscrowDelta { + token, + payer_account, + escrow_account, + .. + } = self; + let owner_key = balance_key(token, payer_account); + let escrow_key = balance_key(token, escrow_account); + changed_keys.contains(&owner_key) && changed_keys.contains(&escrow_key) + } } /// There are two checks we must do when minting wNam. +/// /// 1. Check that gas fees were escrowed. /// 2. Check that the Nam to back wNam was escrowed. -#[derive(Copy, Clone)] struct EscrowCheck<'a> { - gas_check: EscrowDelta<'a>, - token_check: EscrowDelta<'a>, + gas_check: EscrowDelta<'a, GasCheck>, + token_check: EscrowDelta<'a, TokenCheck>, +} + +impl EscrowCheck<'_> { + #[inline] + fn validate_changed_keys(&self, changed_keys: &BTreeSet) -> bool { + self.gas_check.validate_changed_keys(changed_keys) + && self.token_check.validate_changed_keys(changed_keys) + } } +/// Perform a gas check. +enum GasCheck {} + +/// Perform a token check. +enum TokenCheck {} + impl<'a, D, H, CA> NativeVp for BridgePoolVp<'a, D, H, CA> where D: 'static + DB + for<'iter> DBIter<'iter>, @@ -445,20 +504,32 @@ where } // The deltas in the escrowed amounts we must check. let wnam_address = read_native_erc20_address(&self.ctx.pre())?; - let escrow_checks = self.escrow_check(&wnam_address, &transfer)?; + let escrow_checks = + self.determine_escrow_checks(&wnam_address, &transfer)?; + if !escrow_checks.validate_changed_keys(keys_changed) { + tracing::debug!( + ?transfer, + "Missing storage modifications in the Bridge pool" + ); + return Ok(false); + } // check that gas was correctly escrowed. - if !self.check_nam_escrowed(escrow_checks.gas_check)? { + if !self.check_gas_escrow( + &wnam_address, + &transfer, + escrow_checks.gas_check, + )? { return Ok(false); } // check the escrowed assets if transfer.transfer.asset == wnam_address { - self.check_wnam_preconditions( + self.check_wnam_escrow( &wnam_address, &transfer, - escrow_checks, + escrow_checks.token_check, ) } else { - self.check_erc20s_escrowed(keys_changed, &transfer) + self.check_escrowed_toks(escrow_checks.token_check) } .map(|ok| { if ok { @@ -466,114 +537,18 @@ where "The Ethereum bridge pool VP accepted the transfer {:?}.", transfer ); + } else { + tracing::debug!( + ?transfer, + "The assets of the transfer were not properly escrowed \ + into the Ethereum bridge pool." + ); } ok }) } } -/// Checks that the balances at both `sender` and `receiver` have changed by -/// some amount, and that the changes balance each other out. If the balance -/// changes are invalid, the reason is logged and a `None` is returned. -/// Otherwise, return the `Amount` of the transfer i.e. by how much the sender's -/// balance decreased, or equivalently by how much the receiver's balance -/// increased -fn check_balance_changes( - reader: impl StorageReader, - sender: &Key, - receiver: &Key, -) -> Result, Error> { - let sender_balance_pre = reader - .read_pre_value::(sender)? - .unwrap_or_default() - .change(); - let sender_balance_post = match reader.read_post_value::(sender)? { - Some(value) => value, - None => { - return Err(Error(eyre!( - "Rejecting transaction as could not read_post balance key {}", - sender, - ))); - } - } - .change(); - let receiver_balance_pre = reader - .read_pre_value::(receiver)? - .unwrap_or_default() - .change(); - let receiver_balance_post = match reader - .read_post_value::(receiver)? - { - Some(value) => value, - None => { - return Err(Error(eyre!( - "Rejecting transaction as could not read_post balance key {}", - receiver, - ))); - } - } - .change(); - - let sender_balance_delta = - calculate_delta(sender_balance_pre, sender_balance_post)?; - let receiver_balance_delta = - calculate_delta(receiver_balance_pre, receiver_balance_post)?; - if receiver_balance_delta != -sender_balance_delta { - tracing::debug!( - ?sender_balance_pre, - ?receiver_balance_pre, - ?sender_balance_post, - ?receiver_balance_post, - ?sender_balance_delta, - ?receiver_balance_delta, - "Rejecting transaction as balance changes do not match" - ); - return Ok(None); - } - if sender_balance_delta.is_zero() || sender_balance_delta > Change::zero() { - assert!( - receiver_balance_delta.is_zero() - || receiver_balance_delta < Change::zero() - ); - tracing::debug!( - "Rejecting transaction as no balance change or invalid change" - ); - return Ok(None); - } - if sender_balance_post < Change::zero() { - tracing::debug!( - ?sender_balance_post, - "Rejecting transaction as balance is negative" - ); - return Ok(None); - } - if receiver_balance_post < Change::zero() { - tracing::debug!( - ?receiver_balance_post, - "Rejecting transaction as balance is negative" - ); - return Ok(None); - } - - Ok(Some(Amount::from_change(receiver_balance_delta))) -} - -/// Return the delta between `balance_pre` and `balance_post`, erroring if there -/// is an underflow -fn calculate_delta( - balance_pre: Change, - balance_post: Change, -) -> Result { - match balance_post.checked_sub(&balance_pre) { - Some(result) => Ok(result), - None => Err(Error(eyre!( - "Underflow while calculating delta: {} - {}", - balance_post, - balance_pre - ))), - } -} - #[cfg(test)] mod test_bridge_pool_vp { use std::env::temp_dir; @@ -584,7 +559,6 @@ mod test_bridge_pool_vp { use namada_ethereum_bridge::parameters::{ Contracts, EthereumBridgeConfig, UpgradeableContract, }; - use namada_ethereum_bridge::storage::wrapped_erc20s; use super::*; use crate::ledger::gas::VpGasMeter; From fdf00a821fb8776ce35aed0ff5a6b7ee943b6b5c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 15:18:16 +0100 Subject: [PATCH 54/94] Fix test_minting_wnam() unit test --- .../src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 312cdf493d..98e2c2f793 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -1372,7 +1372,7 @@ mod test_bridge_pool_vp { /// Test that we can escrow Nam if we /// want to mint wNam on Ethereum. #[test] - fn test_mint_wnam() { + fn test_minting_wnam() { // setup let mut wl_storage = setup_storage(); let eb_account_key = @@ -1396,7 +1396,7 @@ mod test_bridge_pool_vp { }; // add transfer to pool - let keys_changed = { + let mut keys_changed = { wl_storage .write_log .write( @@ -1418,6 +1418,7 @@ mod test_bridge_pool_vp { .expect("Test failed"), ) .expect("Test failed"); + assert!(keys_changed.insert(account_key)); let bp_account_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); wl_storage .write_log @@ -1428,6 +1429,7 @@ mod test_bridge_pool_vp { .expect("Test failed"), ) .expect("Test failed"); + assert!(keys_changed.insert(bp_account_key)); wl_storage .write_log .write( @@ -1437,6 +1439,7 @@ mod test_bridge_pool_vp { .expect("Test failed"), ) .expect("Test failed"); + assert!(keys_changed.insert(eb_account_key)); let verifiers = BTreeSet::default(); // create the data to be given to the vp From 7ec76d952b29ffa31c42409ca6214bb27cee3b12 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 9 Aug 2023 11:09:49 +0100 Subject: [PATCH 55/94] Add misc Bridge pool VP tests --- tests/src/native_vp/eth_bridge_pool.rs | 87 ++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index a452e9b82e..22584e1464 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -94,6 +94,17 @@ mod test_bridge_pool_vp { // Bertha has ERC20 tokens too. let token = wrapped_erc20s::token(&ASSET); env.credit_tokens(&bertha_address(), &token, BERTHA_TOKENS.into()); + // Bertha has... NUTs? :D + let nuts = wrapped_erc20s::nut(&ASSET); + env.credit_tokens(&bertha_address(), &nuts, BERTHA_TOKENS.into()); + // give Bertha some wNAM. technically this is impossible to mint, + // but we're testing invalid protocol paths... + let wnam_tok_addr = wrapped_erc20s::token(&wnam()); + env.credit_tokens( + &bertha_address(), + &wnam_tok_addr, + BERTHA_TOKENS.into(), + ); env } @@ -203,4 +214,80 @@ mod test_bridge_pool_vp { }; validate_tx(create_tx(transfer, &bertha_keypair())); } + + #[test] + fn invalidate_fees_paid_in_nuts() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: wnam(), + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKENS), + }, + gas_fee: GasFee { + token: wrapped_erc20s::nut(&ASSET), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + invalidate_tx(create_tx(transfer, &bertha_keypair())); + } + + #[test] + fn invalidate_fees_paid_in_wnam() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: wnam(), + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKENS), + }, + gas_fee: GasFee { + token: wrapped_erc20s::token(&wnam()), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + invalidate_tx(create_tx(transfer, &bertha_keypair())); + } + + #[test] + fn validate_erc20_tx_with_same_gas_token() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: ASSET, + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKENS), + }, + gas_fee: GasFee { + token: wrapped_erc20s::token(&ASSET), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + validate_tx(create_tx(transfer, &bertha_keypair())); + } + + #[test] + fn validate_wnam_tx_with_diff_gas_token() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: wnam(), + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKENS), + }, + gas_fee: GasFee { + token: wrapped_erc20s::token(&ASSET), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + validate_tx(create_tx(transfer, &bertha_keypair())); + } } From 5c8a030ec3901fd01d2e6551f0a467d872316de3 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 9 Aug 2023 13:58:36 +0100 Subject: [PATCH 56/94] Fix bug in `determine_escrow_checks` and document corner cases --- .../ethereum_bridge/bridge_pool_vp.rs | 107 ++++++++++++------ 1 file changed, 71 insertions(+), 36 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 98e2c2f793..9899ce8238 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -318,64 +318,83 @@ where transfer: &'trans PendingTransfer, ) -> Result, Error> { let tok_is_native_asset = &transfer.transfer.asset == wnam_address; - let gas_is_native_asset = - transfer.gas_fee.token == self.ctx.storage.native_token; + + // NB: this comparison is not enough to check + // if NAM is being used for both tokens and gas + // fees, since wrapped NAM will have a different + // token address + let same_token_and_gas_erc20 = + transfer.token_address() == transfer.gas_fee.token; let (expected_gas_debit, expected_token_debit) = { + // NB: there is a corner case where the gas fees and escrowed + // tokens are debited from the same address, when the gas fee + // payer and token sender are the same, and the underlying + // transferred assets are the same let same_sender_and_fee_payer = transfer.gas_fee.payer == transfer.transfer.sender; - let same_tokens_and_gas_asset = (tok_is_native_asset - && gas_is_native_asset) - || transfer.token_address() == transfer.gas_fee.token; - - // there is a corner case where the gas fees and escrowed tokens - // are debited from the same address + let gas_is_native_asset = + transfer.gas_fee.token == self.ctx.storage.native_token; + let gas_and_token_is_native_asset = + gas_is_native_asset && tok_is_native_asset; + let same_token_and_gas_asset = + gas_and_token_is_native_asset || same_token_and_gas_erc20; let same_debited_address = - same_sender_and_fee_payer && same_tokens_and_gas_asset; + same_sender_and_fee_payer && same_token_and_gas_asset; if same_debited_address { - let debit = transfer - .gas_fee - .amount - .checked_add(transfer.transfer.amount) - .ok_or_else(|| { - Error(eyre!( - "Addition oveflowed adding gas fee + transfer \ - amount." - )) - })?; + let debit = sum_gas_and_token_amounts(transfer)?; (debit, debit) } else { (transfer.gas_fee.amount, transfer.transfer.amount) } }; + let (expected_gas_credit, expected_token_credit) = { + // NB: there is a corner case where the gas fees and escrowed + // tokens are credited to the same address, when the underlying + // transferred assets are the same (unless the asset is NAM) + let same_credited_address = same_token_and_gas_erc20; + + if same_credited_address { + let credit = sum_gas_and_token_amounts(transfer)?; + (credit, credit) + } else { + (transfer.gas_fee.amount, transfer.transfer.amount) + } + }; + let (token_check_addr, token_check_escrow_acc) = if tok_is_native_asset + { + // when minting wrapped NAM on Ethereum, escrow to the Ethereum + // bridge address, and draw from NAM token accounts + let token = Cow::Borrowed(&self.ctx.storage.native_token); + let escrow_account = &BRIDGE_ADDRESS; + (token, escrow_account) + } else { + // otherwise, draw from ERC20/NUT wrapped asset token accounts, + // and escrow to the Bridge pool address + let token = Cow::Owned(transfer.token_address()); + let escrow_account = &BRIDGE_POOL_ADDRESS; + (token, escrow_account) + }; + Ok(EscrowCheck { gas_check: EscrowDelta { - token: if gas_is_native_asset { - Cow::Borrowed(&self.ctx.storage.native_token) - } else { - Cow::Borrowed(&transfer.gas_fee.token) - }, + // NB: it's fine to not check for wrapped NAM here, + // as users won't hold wrapped NAM tokens in practice, + // anyway + token: Cow::Borrowed(&transfer.gas_fee.token), payer_account: &transfer.gas_fee.payer, escrow_account: &BRIDGE_POOL_ADDRESS, expected_debit: expected_gas_debit, - expected_credit: transfer.gas_fee.amount, + expected_credit: expected_gas_credit, _kind: PhantomData, }, token_check: EscrowDelta { - token: if tok_is_native_asset { - Cow::Borrowed(&self.ctx.storage.native_token) - } else { - Cow::Owned(transfer.token_address()) - }, + token: token_check_addr, payer_account: &transfer.transfer.sender, - escrow_account: if tok_is_native_asset { - &BRIDGE_ADDRESS - } else { - &BRIDGE_POOL_ADDRESS - }, + escrow_account: token_check_escrow_acc, expected_debit: expected_token_debit, - expected_credit: transfer.transfer.amount, + expected_credit: expected_token_credit, _kind: PhantomData, }, }) @@ -430,6 +449,22 @@ enum GasCheck {} /// Perform a token check. enum TokenCheck {} +/// Sum gas and token amounts on a pending transfer, checking for overflows. +#[inline] +fn sum_gas_and_token_amounts( + transfer: &PendingTransfer, +) -> Result { + transfer + .gas_fee + .amount + .checked_add(transfer.transfer.amount) + .ok_or_else(|| { + Error(eyre!( + "Addition oveflowed adding gas fee + transfer amount." + )) + }) +} + impl<'a, D, H, CA> NativeVp for BridgePoolVp<'a, D, H, CA> where D: 'static + DB + for<'iter> DBIter<'iter>, From bad6a7c39de3c310d802e97ab5f44ee4049a6ebb Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 10 Aug 2023 09:14:59 +0100 Subject: [PATCH 57/94] Arbitrary gas fees in Ethereum events `init_balances` --- .../transactions/ethereum_events/events.rs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 08605308be..d0bfdfc668 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -715,18 +715,25 @@ mod tests { wl_storage: &mut TestWlStorage, pending_transfers: &Vec, ) { - // Gas payer - let payer = address::testing::established_address_2(); - let payer_key = balance_key(&nam(), &payer); - let payer_balance = Amount::from(0); - wl_storage - .write_bytes( - &payer_key, - payer_balance.try_to_vec().expect("Test failed"), - ) + for transfer in pending_transfers { + // Gas + let payer = address::testing::established_address_2(); + let payer_key = balance_key(&transfer.gas_fee.token, &payer); + let payer_balance = Amount::from(0); + wl_storage + .write_bytes( + &payer_key, + payer_balance.try_to_vec().expect("Test failed"), + ) + .expect("Test failed"); + let escrow_key = + balance_key(&transfer.gas_fee.token, &BRIDGE_POOL_ADDRESS); + update::amount(wl_storage, &escrow_key, |balance| { + let gas_fee = Amount::from_u64(1); + balance.receive(&gas_fee); + }) .expect("Test failed"); - for transfer in pending_transfers { if transfer.transfer.asset == wnam() { // native ERC20 let sender_key = balance_key(&nam(), &transfer.transfer.sender); @@ -772,12 +779,6 @@ mod tests { ) .expect("Test failed"); }; - let gas_fee = Amount::from(1); - let escrow_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); - update::amount(wl_storage, &escrow_key, |balance| { - balance.receive(&gas_fee); - }) - .expect("Test failed"); } } From 87455467e2e3936ad55f4b56ab932a6a8c5be108 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 10 Aug 2023 10:09:33 +0100 Subject: [PATCH 58/94] Test paying Bridge pool gas fees in ERC20 tokens --- .../transactions/ethereum_events/events.rs | 230 ++++++++++++++---- 1 file changed, 179 insertions(+), 51 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index d0bfdfc668..48aac0b8de 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -609,6 +609,8 @@ where #[cfg(test)] mod tests { + use std::collections::HashMap; + use assert_matches::assert_matches; use borsh::BorshSerialize; use eyre::Result; @@ -648,21 +650,88 @@ mod tests { .expect("Test failed"); } + /// Helper data structure to feed to [`init_bridge_pool_transfers`]. + struct TransferData { + kind: eth_bridge_pool::TransferToEthereumKind, + gas_token: Address, + } + + impl Default for TransferData { + fn default() -> Self { + Self { + kind: eth_bridge_pool::TransferToEthereumKind::Erc20, + gas_token: nam(), + } + } + } + + /// Build [`TransferData`] values. + struct TransferDataBuilder { + kind: Option, + gas_token: Option
, + } + + #[allow(dead_code)] + impl TransferDataBuilder { + fn new() -> Self { + Self { + kind: None, + gas_token: None, + } + } + + fn kind( + mut self, + kind: eth_bridge_pool::TransferToEthereumKind, + ) -> Self { + self.kind = Some(kind); + self + } + + fn kind_erc20(self) -> Self { + self.kind(eth_bridge_pool::TransferToEthereumKind::Erc20) + } + + fn kind_nut(self) -> Self { + self.kind(eth_bridge_pool::TransferToEthereumKind::Nut) + } + + fn gas_token(mut self, address: Address) -> Self { + self.gas_token = Some(address); + self + } + + fn gas_erc20(self, address: &EthAddress) -> Self { + self.gas_token(wrapped_erc20s::token(address)) + } + + fn gas_nut(self, address: &EthAddress) -> Self { + self.gas_token(wrapped_erc20s::nut(address)) + } + + fn build(self) -> TransferData { + TransferData { + kind: self.kind.unwrap_or_else(|| TransferData::default().kind), + gas_token: self + .gas_token + .unwrap_or_else(|| TransferData::default().gas_token), + } + } + } + fn init_bridge_pool_transfers( wl_storage: &mut TestWlStorage, assets_transferred: A, ) -> Vec where - A: Into< - BTreeSet<(EthAddress, eth_bridge_pool::TransferToEthereumKind)>, - >, + A: Into>, { let sender = address::testing::established_address_1(); let payer = address::testing::established_address_2(); // set pending transfers let mut pending_transfers = vec![]; - for (i, (asset, kind)) in + for (i, (asset, TransferData { kind, gas_token })) in assets_transferred.into().into_iter().enumerate() { let transfer = PendingTransfer { @@ -674,7 +743,7 @@ mod tests { kind, }, gas_fee: GasFee { - token: nam(), + token: gas_token, amount: Amount::from(1), payer: payer.clone(), }, @@ -700,14 +769,16 @@ mod tests { .map(|i| { ( EthAddress([i; 20]), - if i & 1 == 0 { - eth_bridge_pool::TransferToEthereumKind::Erc20 - } else { - eth_bridge_pool::TransferToEthereumKind::Nut - }, + TransferDataBuilder::new() + .kind(if i & 1 == 0 { + eth_bridge_pool::TransferToEthereumKind::Erc20 + } else { + eth_bridge_pool::TransferToEthereumKind::Nut + }) + .build(), ) }) - .collect::>(), + .collect::>(), ) } @@ -970,14 +1041,36 @@ mod tests { let random_erc20_token = wrapped_erc20s::nut(&random_erc20); let random_erc20_2 = EthAddress([0xee; 20]); let random_erc20_token_2 = wrapped_erc20s::token(&random_erc20_2); + let random_erc20_3 = EthAddress([0xdd; 20]); + let random_erc20_token_3 = wrapped_erc20s::token(&random_erc20_3); + let random_erc20_4 = EthAddress([0xcc; 20]); + let random_erc20_token_4 = wrapped_erc20s::nut(&random_erc20_4); + let erc20_gas_addr = EthAddress([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, + ]); let pending_transfers = init_bridge_pool_transfers( &mut wl_storage, [ - (native_erc20, eth_bridge_pool::TransferToEthereumKind::Erc20), - (random_erc20, eth_bridge_pool::TransferToEthereumKind::Nut), + (native_erc20, TransferData::default()), + (random_erc20, TransferDataBuilder::new().kind_nut().build()), ( random_erc20_2, - eth_bridge_pool::TransferToEthereumKind::Erc20, + TransferDataBuilder::new().kind_erc20().build(), + ), + ( + random_erc20_3, + TransferDataBuilder::new() + .kind_erc20() + .gas_erc20(&erc20_gas_addr) + .build(), + ), + ( + random_erc20_4, + TransferDataBuilder::new() + .kind_nut() + .gas_erc20(&erc20_gas_addr) + .build(), ), ], ); @@ -995,39 +1088,53 @@ mod tests { transfers, relayer: relayer.clone(), }; - let payer_balance_key = balance_key(&nam(), &relayer); - let pool_balance_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); - let mut bp_balance_pre = Amount::try_from_slice( + let payer_nam_balance_key = balance_key(&nam(), &relayer); + let payer_erc_balance_key = + balance_key(&wrapped_erc20s::token(&erc20_gas_addr), &relayer); + let pool_nam_balance_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); + let pool_erc_balance_key = balance_key( + &wrapped_erc20s::token(&erc20_gas_addr), + &BRIDGE_POOL_ADDRESS, + ); + let mut bp_nam_balance_pre = Amount::try_from_slice( + &wl_storage + .read_bytes(&pool_nam_balance_key) + .expect("Test failed") + .expect("Test failed"), + ) + .expect("Test failed"); + let mut bp_erc_balance_pre = Amount::try_from_slice( &wl_storage - .read_bytes(&pool_balance_key) + .read_bytes(&pool_erc_balance_key) .expect("Test failed") .expect("Test failed"), ) .expect("Test failed"); let mut changed_keys = act_on(&mut wl_storage, event).unwrap(); - assert!( - changed_keys.remove(&balance_key( - &random_erc20_token, - &BRIDGE_POOL_ADDRESS - )) - ); - assert!( - changed_keys.remove(&balance_key( - &random_erc20_token_2, - &BRIDGE_POOL_ADDRESS - )) - ); + for erc20 in [ + random_erc20_token, + random_erc20_token_2, + random_erc20_token_3, + random_erc20_token_4, + ] { + assert!( + changed_keys.remove(&balance_key(&erc20, &BRIDGE_POOL_ADDRESS)), + "Expected {erc20:?} Bridge pool balance to change" + ); + assert!( + changed_keys.remove(&minted_balance_key(&erc20)), + "Expected {erc20:?} minted supply to change" + ); + } assert!( changed_keys .remove(&minted_balance_key(&wrapped_erc20s::token(&wnam()))) ); - assert!(changed_keys.remove(&minted_balance_key(&random_erc20_token))); - assert!( - changed_keys.remove(&minted_balance_key(&random_erc20_token_2)) - ); - assert!(changed_keys.remove(&payer_balance_key)); - assert!(changed_keys.remove(&pool_balance_key)); + assert!(changed_keys.remove(&payer_nam_balance_key)); + assert!(changed_keys.remove(&payer_erc_balance_key)); + assert!(changed_keys.remove(&pool_nam_balance_key)); + assert!(changed_keys.remove(&pool_erc_balance_key)); assert!(changed_keys.remove(&get_nonce_key())); assert!(changed_keys.iter().all(|k| pending_keys.contains(k))); @@ -1040,24 +1147,45 @@ mod tests { // NOTE: we should have one write -- the bridge pool nonce update 1 ); - let relayer_balance = Amount::try_from_slice( + let relayer_nam_balance = Amount::try_from_slice( + &wl_storage + .read_bytes(&payer_nam_balance_key) + .expect("Test failed: read error") + .expect("Test failed: no value in storage"), + ) + .expect("Test failed"); + assert_eq!(relayer_nam_balance, Amount::from(3)); + let relayer_erc_balance = Amount::try_from_slice( &wl_storage - .read_bytes(&payer_balance_key) + .read_bytes(&payer_erc_balance_key) .expect("Test failed: read error") .expect("Test failed: no value in storage"), ) .expect("Test failed"); - assert_eq!(relayer_balance, Amount::from(3)); - let bp_balance_post = Amount::try_from_slice( + assert_eq!(relayer_erc_balance, Amount::from(2)); + + let bp_nam_balance_post = Amount::try_from_slice( + &wl_storage + .read_bytes(&pool_nam_balance_key) + .expect("Test failed: read error") + .expect("Test failed: no value in storage"), + ) + .expect("Test failed"); + let bp_erc_balance_post = Amount::try_from_slice( &wl_storage - .read_bytes(&pool_balance_key) + .read_bytes(&pool_erc_balance_key) .expect("Test failed: read error") .expect("Test failed: no value in storage"), ) .expect("Test failed"); - bp_balance_pre.spend(&bp_balance_post); - assert_eq!(bp_balance_pre, Amount::from(3)); - assert_eq!(bp_balance_post, Amount::from(0)); + + bp_nam_balance_pre.spend(&bp_nam_balance_post); + assert_eq!(bp_nam_balance_pre, Amount::from(3)); + assert_eq!(bp_nam_balance_post, Amount::from(0)); + + bp_erc_balance_pre.spend(&bp_erc_balance_post); + assert_eq!(bp_erc_balance_pre, Amount::from(2)); + assert_eq!(bp_erc_balance_post, Amount::from(0)); } #[test] @@ -1272,30 +1400,30 @@ mod tests { let pending_transfers = init_bridge_pool_transfers( &mut wl_storage, [ - (native_erc20, eth_bridge_pool::TransferToEthereumKind::Erc20), + (native_erc20, TransferData::default()), ( EthAddress([0xaa; 20]), - eth_bridge_pool::TransferToEthereumKind::Erc20, + TransferDataBuilder::new().kind_erc20().build(), ), ( EthAddress([0xbb; 20]), - eth_bridge_pool::TransferToEthereumKind::Nut, + TransferDataBuilder::new().kind_nut().build(), ), ( EthAddress([0xcc; 20]), - eth_bridge_pool::TransferToEthereumKind::Erc20, + TransferDataBuilder::new().kind_erc20().build(), ), ( EthAddress([0xdd; 20]), - eth_bridge_pool::TransferToEthereumKind::Nut, + TransferDataBuilder::new().kind_nut().build(), ), ( EthAddress([0xee; 20]), - eth_bridge_pool::TransferToEthereumKind::Erc20, + TransferDataBuilder::new().kind_erc20().build(), ), ( EthAddress([0xff; 20]), - eth_bridge_pool::TransferToEthereumKind::Nut, + TransferDataBuilder::new().kind_nut().build(), ), ], ); From 9c3fbcc20dd324d0b9b7ef348c2fc3f7f1c31794 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 21:46:41 +0100 Subject: [PATCH 59/94] Update CLI args for Bridge pool transfers --- apps/src/lib/cli.rs | 41 ++++++++++++++------- shared/src/ledger/args.rs | 2 + shared/src/ledger/eth_bridge/bridge_pool.rs | 2 + 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index e7a34eba67..bd67eaf454 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2421,6 +2421,20 @@ pub mod args { ); pub const BLOCK_HEIGHT: Arg = arg("block-height"); // pub const BLOCK_HEIGHT_OPT: ArgOpt = arg_opt("height"); + pub const BRIDGE_POOL_GAS_AMOUNT: ArgDefault = + arg_default( + "pool-gas-amount", + DefaultFn(|| token::DenominatedAmount { + amount: token::Amount::default(), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }), + ); + pub const BRIDGE_POOL_GAS_PAYER: Arg = arg("pool-gas-payer"); + pub const BRIDGE_POOL_GAS_TOKEN: ArgDefaultFromCtx = + arg_default_from_ctx( + "pool-gas-token", + DefaultFn(|| "NAM".parse().unwrap()), + ); pub const BROADCAST_ONLY: ArgFlag = flag("broadcast-only"); pub const CHAIN_ID: Arg = arg("chain-id"); pub const CHAIN_ID_OPT: ArgOpt = CHAIN_ID.opt(); @@ -2475,14 +2489,6 @@ pub mod args { ); pub const GAS_TOKEN: ArgDefaultFromCtx = arg_default_from_ctx("gas-token", DefaultFn(|| "NAM".parse().unwrap())); - pub const FEE_PAYER: Arg = arg("fee-payer"); - pub const FEE_AMOUNT: ArgDefault = arg_default( - "fee-amount", - DefaultFn(|| token::DenominatedAmount { - amount: token::Amount::default(), - denom: NATIVE_MAX_DECIMAL_PLACES.into(), - }), - ); pub const GENESIS_PATH: Arg = arg("genesis-path"); pub const GENESIS_VALIDATOR: ArgOpt = arg("genesis-validator").opt(); @@ -2796,6 +2802,7 @@ pub mod args { amount: self.amount, fee_amount: self.fee_amount, fee_payer: ctx.get(&self.fee_payer), + fee_token: ctx.get(&self.fee_token), code_path: self.code_path, } } @@ -2808,8 +2815,9 @@ pub mod args { let recipient = ETH_ADDRESS.parse(matches); let sender = ADDRESS.parse(matches); let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); - let fee_amount = FEE_AMOUNT.parse(matches).amount; - let fee_payer = FEE_PAYER.parse(matches); + let fee_amount = BRIDGE_POOL_GAS_AMOUNT.parse(matches).amount; + let fee_payer = BRIDGE_POOL_GAS_PAYER.parse(matches); + let fee_token = BRIDGE_POOL_GAS_TOKEN.parse(matches); let code_path = PathBuf::from(TX_BRIDGE_POOL_WASM); let nut = NUT.parse(matches); Self { @@ -2820,6 +2828,7 @@ pub mod args { amount, fee_amount, fee_payer, + fee_token, code_path, nut, } @@ -2847,15 +2856,19 @@ pub mod args { "The amount of tokens being sent across the bridge.", ), ) - .arg(FEE_AMOUNT.def().help( - "The amount of NAM you wish to pay to have this transfer \ + .arg(BRIDGE_POOL_GAS_AMOUNT.def().help( + "The amount of gas you wish to pay to have this transfer \ relayed to Ethereum.", )) .arg( - FEE_PAYER.def().help( - "The Namada address of the account paying the fee.", + BRIDGE_POOL_GAS_PAYER.def().help( + "The Namada address of the account paying the gas.", ), ) + .arg(BRIDGE_POOL_GAS_TOKEN.def().help( + "The token for paying the Bridge pool gas fees. Defaults \ + to NAM.", + )) .arg(NUT.def().help( "Add Non Usable Tokens (NUTs) to the Bridge pool. These \ are usually obtained from invalid transfers to Namada.", diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 8d884b4a5f..7a0939d98d 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -720,6 +720,8 @@ pub struct EthereumBridgePool { pub fee_amount: token::Amount, /// The account of fee payer. pub fee_payer: C::Address, + /// The token in which the gas is being paid + pub fee_token: C::Address, /// Path to the tx WASM code file pub code_path: PathBuf, } diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 4b362caf4f..2e9358502a 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -49,6 +49,7 @@ pub async fn build_bridge_pool_tx( amount, fee_amount, fee_payer, + fee_token, code_path, }: args::EthereumBridgePool, gas_payer: common::PublicKey, @@ -71,6 +72,7 @@ pub async fn build_bridge_pool_tx( }, }, gas_fee: GasFee { + token: fee_token, amount: fee_amount, payer: fee_payer, }, From 4091b555d12d54457cdb329748585ef0f4052096 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 22:13:41 +0100 Subject: [PATCH 60/94] Standardize Bridge pool transfer CLI args --- apps/src/lib/cli.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index bd67eaf454..37ab19b30a 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2435,6 +2435,7 @@ pub mod args { "pool-gas-token", DefaultFn(|| "NAM".parse().unwrap()), ); + pub const BRIDGE_POOL_TARGET: Arg = arg("target"); pub const BROADCAST_ONLY: ArgFlag = flag("broadcast-only"); pub const CHAIN_ID: Arg = arg("chain-id"); pub const CHAIN_ID_OPT: ArgOpt = CHAIN_ID.opt(); @@ -2464,7 +2465,7 @@ pub mod args { pub const ETH_GAS: ArgOpt = arg_opt("eth-gas"); pub const ETH_GAS_PRICE: ArgOpt = arg_opt("eth-gas-price"); pub const ETH_ADDRESS: Arg = arg("ethereum-address"); - pub const ETH_ADDRESS_OPT: ArgOpt = arg_opt("ethereum-address"); + pub const ETH_ADDRESS_OPT: ArgOpt = ETH_ADDRESS.opt(); pub const ETH_RPC_ENDPOINT: ArgDefault = arg_default( "eth-rpc-endpoint", DefaultFn(|| "http://localhost:8545".into()), @@ -2812,8 +2813,8 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let asset = ERC20.parse(matches); - let recipient = ETH_ADDRESS.parse(matches); - let sender = ADDRESS.parse(matches); + let recipient = BRIDGE_POOL_TARGET.parse(matches); + let sender = SOURCE.parse(matches); let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); let fee_amount = BRIDGE_POOL_GAS_AMOUNT.parse(matches).amount; let fee_payer = BRIDGE_POOL_GAS_PAYER.parse(matches); @@ -2842,14 +2843,12 @@ pub mod args { .help("The Ethereum address of the ERC20 token."), ) .arg( - ETH_ADDRESS + BRIDGE_POOL_TARGET .def() .help("The Ethereum address receiving the tokens."), ) .arg( - ADDRESS - .def() - .help("The Namada address sending the tokens."), + SOURCE.def().help("The Namada address sending the tokens."), ) .arg( AMOUNT.def().help( From 5342193e1d8d00a7665842cf963477f8b1a82e58 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 22:36:21 +0100 Subject: [PATCH 61/94] Make Bridge pool gas payer CLI arg optional --- apps/src/lib/cli.rs | 14 +++++++------- shared/src/ledger/args.rs | 4 +++- shared/src/ledger/eth_bridge/bridge_pool.rs | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 37ab19b30a..cc0e81fc30 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2429,7 +2429,8 @@ pub mod args { denom: NATIVE_MAX_DECIMAL_PLACES.into(), }), ); - pub const BRIDGE_POOL_GAS_PAYER: Arg = arg("pool-gas-payer"); + pub const BRIDGE_POOL_GAS_PAYER: ArgOpt = + arg_opt("pool-gas-payer"); pub const BRIDGE_POOL_GAS_TOKEN: ArgDefaultFromCtx = arg_default_from_ctx( "pool-gas-token", @@ -2802,7 +2803,7 @@ pub mod args { sender: ctx.get(&self.sender), amount: self.amount, fee_amount: self.fee_amount, - fee_payer: ctx.get(&self.fee_payer), + fee_payer: self.fee_payer.map(|fee_payer| ctx.get(&fee_payer)), fee_token: ctx.get(&self.fee_token), code_path: self.code_path, } @@ -2859,11 +2860,10 @@ pub mod args { "The amount of gas you wish to pay to have this transfer \ relayed to Ethereum.", )) - .arg( - BRIDGE_POOL_GAS_PAYER.def().help( - "The Namada address of the account paying the gas.", - ), - ) + .arg(BRIDGE_POOL_GAS_PAYER.def().help( + "The Namada address of the account paying the gas. By \ + default, it is the same as the source.", + )) .arg(BRIDGE_POOL_GAS_TOKEN.def().help( "The token for paying the Bridge pool gas fees. Defaults \ to NAM.", diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 7a0939d98d..9aed6793e6 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -719,7 +719,9 @@ pub struct EthereumBridgePool { /// The amount of fees (in NAM) pub fee_amount: token::Amount, /// The account of fee payer. - pub fee_payer: C::Address, + /// + /// If unset, it is the same as the sender. + pub fee_payer: Option, /// The token in which the gas is being paid pub fee_token: C::Address, /// Path to the tx WASM code file diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 2e9358502a..d3f875c76c 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -54,6 +54,7 @@ pub async fn build_bridge_pool_tx( }: args::EthereumBridgePool, gas_payer: common::PublicKey, ) -> Result { + let fee_payer = fee_payer.unwrap_or_else(|| sender.clone()); let DenominatedAmount { amount, .. } = validate_amount(client, amount, &BRIDGE_ADDRESS, tx_args.force) .await From ce21ebd79516086b934ab9500b7ecfe803c85f04 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 11 Aug 2023 09:55:13 +0100 Subject: [PATCH 62/94] Fix docstring Co-authored-by: Jacob Turner --- shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 9899ce8238..3ae649e4be 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -203,7 +203,7 @@ where } } - /// Check that the gas was correctly escrow. + /// Check that the gas was correctly escrowed. fn check_gas_escrow( &self, wnam_address: &EthAddress, From ba741a5174cfe1715f66324bd47d5cc94df51e2f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 11 Aug 2023 13:47:26 +0100 Subject: [PATCH 63/94] Add `DenominatedAmount::is_zero` --- core/src/types/token.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 6056495cd1..a72bce448b 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -309,6 +309,12 @@ impl DenominatedAmount { } } + /// Check if the inner [`Amount`] is zero. + #[inline] + pub fn is_zero(&self) -> bool { + self.amount.is_zero() + } + /// A precise string representation. The number of /// decimal places in this string gives the denomination. /// This not true of the string produced by the `Display` From 27f07c80e96c11f47c20bd024085c6bce9a84aa6 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 11 Aug 2023 13:47:44 +0100 Subject: [PATCH 64/94] Fix the denomination of Bridge pool gas fees --- apps/src/lib/cli.rs | 3 ++- shared/src/ledger/args.rs | 4 ++-- shared/src/ledger/eth_bridge/bridge_pool.rs | 22 +++++++++++++++------ shared/src/ledger/rpc.rs | 1 + 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index cc0e81fc30..93de834af3 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2817,7 +2817,8 @@ pub mod args { let recipient = BRIDGE_POOL_TARGET.parse(matches); let sender = SOURCE.parse(matches); let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); - let fee_amount = BRIDGE_POOL_GAS_AMOUNT.parse(matches).amount; + let fee_amount = + InputAmount::Unvalidated(BRIDGE_POOL_GAS_AMOUNT.parse(matches)); let fee_payer = BRIDGE_POOL_GAS_PAYER.parse(matches); let fee_token = BRIDGE_POOL_GAS_TOKEN.parse(matches); let code_path = PathBuf::from(TX_BRIDGE_POOL_WASM); diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 9aed6793e6..642d75fbf0 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -716,8 +716,8 @@ pub struct EthereumBridgePool { pub sender: C::Address, /// The amount to be transferred pub amount: InputAmount, - /// The amount of fees (in NAM) - pub fee_amount: token::Amount, + /// The amount of gas fees + pub fee_amount: InputAmount, /// The account of fee payer. /// /// If unset, it is the same as the sender. diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index d3f875c76c..db575bf6b1 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use borsh::BorshSerialize; use ethbridge_bridge_contract::Bridge; use ethers::providers::Middleware; -use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; +use namada_core::ledger::eth_bridge::storage::wrapped_erc20s; use namada_core::types::key::common; use owo_colors::OwoColorize; use serde::{Deserialize, Serialize}; @@ -55,11 +55,21 @@ pub async fn build_bridge_pool_tx( gas_payer: common::PublicKey, ) -> Result { let fee_payer = fee_payer.unwrap_or_else(|| sender.clone()); - let DenominatedAmount { amount, .. } = - validate_amount(client, amount, &BRIDGE_ADDRESS, tx_args.force) - .await - .expect("Failed to validate amount"); - + let DenominatedAmount { amount, .. } = validate_amount( + client, + amount, + &wrapped_erc20s::token(&asset), + tx_args.force, + ) + .await + .ok_or_else(|| Error::Other("Failed to validate amount".into()))?; + let DenominatedAmount { + amount: fee_amount, .. + } = validate_amount(client, fee_amount, &fee_token, tx_args.force) + .await + .ok_or_else(|| { + Error::Other("Failed to validate Bridge pool fee amount".into()) + })?; let transfer = PendingTransfer { transfer: TransferToEthereum { asset, diff --git a/shared/src/ledger/rpc.rs b/shared/src/ledger/rpc.rs index d0fad687be..a4f24e666f 100644 --- a/shared/src/ledger/rpc.rs +++ b/shared/src/ledger/rpc.rs @@ -983,6 +983,7 @@ pub async fn validate_amount( force: bool, ) -> Option { let input_amount = match amount { + InputAmount::Unvalidated(amt) if amt.is_zero() => return Some(amt), InputAmount::Unvalidated(amt) => amt.canonical(), InputAmount::Validated(amt) => return Some(amt), }; From 968f1a6d5663e3485b39d7e090b67d6969487160 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 9 Aug 2023 16:51:30 +0100 Subject: [PATCH 65/94] Changelog for #1795 --- .changelog/unreleased/features/1795-bridge-pool-fees.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/1795-bridge-pool-fees.md diff --git a/.changelog/unreleased/features/1795-bridge-pool-fees.md b/.changelog/unreleased/features/1795-bridge-pool-fees.md new file mode 100644 index 0000000000..36a9a73c1c --- /dev/null +++ b/.changelog/unreleased/features/1795-bridge-pool-fees.md @@ -0,0 +1,2 @@ +- Allow Bridge pool transfer fees to be paid in arbitrary token types (except + NUTs) ([\#1795](https://github.com/anoma/namada/pull/1795)) \ No newline at end of file From ee8cdb21eb8d1e108c815ed4fce404d7d00595b9 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 11 Aug 2023 14:34:45 +0100 Subject: [PATCH 66/94] Retrieve the inner raw string on wrapped context values --- apps/src/lib/cli/context.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index ee3a4a8dfa..106b3d00fc 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -239,6 +239,10 @@ impl FromContext { phantom: PhantomData, } } + + pub fn into_raw(self) -> String { + self.raw + } } impl FromContext { From 7a56f841f10a7c7b76e0e4d20eb5559b1911e7ae Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 11 Aug 2023 14:35:18 +0100 Subject: [PATCH 67/94] Bridge pool conversion rates table --- apps/src/lib/cli.rs | 37 ++++++++++++++++++++++++++++++++----- shared/src/ledger/args.rs | 20 ++++++++++++++++++-- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 93de834af3..ed8f11f141 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2449,6 +2449,7 @@ pub mod args { "consensus-timeout-commit", DefaultFn(|| Timeout::from_str("1s").unwrap()), ); + pub const CONVERSION_TABLE: Arg = arg("conversion-table"); pub const DAEMON_MODE: ArgFlag = flag("daemon"); pub const DAEMON_MODE_RETRY_DUR: ArgOpt = arg_opt("retry-sleep"); pub const DAEMON_MODE_SUCCESS_DUR: ArgOpt = @@ -2517,7 +2518,6 @@ pub mod args { arg("max-commission-rate-change"); pub const MAX_ETH_GAS: ArgOpt = arg_opt("max_eth-gas"); pub const MODE: ArgOpt = arg_opt("mode"); - pub const NAM_PER_ETH: Arg = arg("nam-per-eth"); pub const NET_ADDRESS: Arg = arg("net-address"); pub const NAMADA_START_TIME: ArgOpt = arg_opt("time"); pub const NO_CONVERSIONS: ArgFlag = flag("no-conversions"); @@ -2877,12 +2877,38 @@ pub mod args { } impl CliToSdkCtxless> for RecommendBatch { - fn to_sdk_ctxless(self) -> RecommendBatch { + fn to_sdk(self, ctx: &mut Context) -> RecommendBatch { RecommendBatch:: { query: self.query.to_sdk_ctxless(), max_gas: self.max_gas, gas: self.gas, - nam_per_eth: self.nam_per_eth, + conversion_table: { + let file = std::io::BufReader::new( + std::fs::File::open(self.conversion_table).expect( + "Failed to open the provided file to the \ + conversion table", + ), + ); + let table: HashMap = + serde_json::from_reader(file) + .expect("Failed to parse conversion table"); + table + .into_iter() + .map(|(token, conversion_rate)| { + let token_from_ctx = + FromContext::
::new(token); + let address = ctx.get(&token_from_ctx); + let alias = token_from_ctx.into_raw(); + ( + address, + BpConversionTableEntry { + alias, + conversion_rate, + }, + ) + }) + .collect() + }, } } } @@ -2892,12 +2918,12 @@ pub mod args { let query = Query::parse(matches); let max_gas = MAX_ETH_GAS.parse(matches); let gas = ETH_GAS.parse(matches); - let nam_to_eth = NAM_PER_ETH.parse(matches); + let conversion_table = CONVERSION_TABLE.parse(matches); Self { query, max_gas, gas, - nam_per_eth: nam_to_eth, + conversion_table, } } @@ -4589,6 +4615,7 @@ pub mod args { impl NamadaTypes for CliTypes { type Address = WalletAddress; type BalanceOwner = WalletBalanceOwner; + type BpConversionTable = PathBuf; type Data = PathBuf; type EthereumAddress = String; type Keypair = WalletKeypair; diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 642d75fbf0..a63f6fba55 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -1,5 +1,6 @@ //! Structures encapsulating SDK arguments +use std::collections::HashMap; use std::path::PathBuf; use std::time::Duration as StdDuration; @@ -7,6 +8,7 @@ use namada_core::types::chain::ChainId; use namada_core::types::dec::Dec; use namada_core::types::ethereum_events::EthAddress; use namada_core::types::time::DateTimeUtc; +use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; use crate::ibc::core::ics24_host::identifier::{ChannelId, PortId}; @@ -57,15 +59,29 @@ pub trait NamadaTypes: Clone + std::fmt::Debug { type TransferTarget: Clone + std::fmt::Debug; /// Represents some data that is used in a transaction type Data: Clone + std::fmt::Debug; + /// Bridge pool recommendations conversion rates table. + type BpConversionTable: Clone + std::fmt::Debug; } /// The concrete types being used in Namada SDK #[derive(Clone, Debug)] pub struct SdkTypes; +/// An entry in the Bridge pool recommendations conversion +/// rates table. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BpConversionTableEntry { + /// An alias for the token, or the string representation + /// of its address if none is available. + pub alias: String, + /// Conversion rate from the given token to gwei. + pub conversion_rate: f64, +} + impl NamadaTypes for SdkTypes { type Address = Address; type BalanceOwner = namada_core::types::masp::BalanceOwner; + type BpConversionTable = HashMap; type Data = Vec; type EthereumAddress = (); type Keypair = namada_core::types::key::common::SecretKey; @@ -694,8 +710,8 @@ pub struct RecommendBatch { /// An optional parameter indicating how much net /// gas the relayer is willing to pay. pub gas: Option, - /// Estimate of amount of NAM a single ETH is worth. - pub nam_per_eth: f64, + /// Estimate amount of gwei a certain token type is worth. + pub conversion_table: HashMap, } /// A transfer to be added to the Ethereum bridge pool. From 3a1587650a43b41e09b170c22db9ba682ca4e7e9 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 11 Aug 2023 15:04:52 +0100 Subject: [PATCH 68/94] Convert gwei to tokens in Bridge pool conversion table --- apps/src/lib/cli.rs | 9 +-- shared/src/ledger/args.rs | 4 +- shared/src/ledger/eth_bridge/bridge_pool.rs | 75 ++++++++++++++++----- 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index ed8f11f141..719b6900a5 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2358,6 +2358,7 @@ pub mod cmds { } pub mod args { + use std::collections::HashMap; use std::convert::TryFrom; use std::env; use std::net::SocketAddr; @@ -2876,7 +2877,7 @@ pub mod args { } } - impl CliToSdkCtxless> for RecommendBatch { + impl CliToSdk> for RecommendBatch { fn to_sdk(self, ctx: &mut Context) -> RecommendBatch { RecommendBatch:: { query: self.query.to_sdk_ctxless(), @@ -2940,9 +2941,9 @@ pub mod args { costs as close to the given value as possible without \ exceeding it.", )) - .arg(NAM_PER_ETH.def().help( - "The amount of NAM that one ETH is worth, represented as \ - a decimal number.", + .arg(CONVERSION_TABLE.def().help( + "Path to a JSON object containing a mapping between token \ + aliases (or addresses) and their conversion rates in gwei", )) } } diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index a63f6fba55..bc7e0c0342 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -710,8 +710,8 @@ pub struct RecommendBatch { /// An optional parameter indicating how much net /// gas the relayer is willing to pay. pub gas: Option, - /// Estimate amount of gwei a certain token type is worth. - pub conversion_table: HashMap, + /// Bridge pool recommendations conversion rates table. + pub conversion_table: C::BpConversionTable, } /// A transfer to be added to the Ethereum bridge pool. diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index db575bf6b1..c058c6f6f8 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -532,26 +532,43 @@ mod recommendations { let validator_gas = signature_fee() * signature_checks(voting_powers, &bp_root.signatures) + valset_fee() * valset_size; - // This is the amount of gwei a single name is worth - let gwei_per_nam = Uint::from_u64( - (10u64.pow(9) as f64 / args.nam_per_eth).floor() as u64, - ); // we don't recommend transfers that have already been relayed let mut contents: Vec<(String, I256, PendingTransfer)> = query_signed_bridge_pool(client) .await? .into_iter() - .filter_map(|(k, v)| { - if !in_progress.contains(&v) { + .filter_map(|(pending_hash, pending)| { + if !in_progress.contains(&pending) { + let conversion_rate = + if let Some(entry) = args + .conversion_table + .get(&pending.gas_fee.token) + { + let rate = entry.conversion_rate; + if rate <= 0.0f64 { + eprintln!( + "Ignoring token with an invalid conversion rate: {}", + pending.gas_fee.token, + ); + return None; + } + rate + } else { + return None; + }; + // This is the amount of gwei a single gas token is worth + let gwei_per_gas_token = Uint::from_u64( + (10u64.pow(9) as f64 / conversion_rate).floor() as u64, + ); Some(( - k, - I256::try_from(v.gas_fee.amount * gwei_per_nam) + pending_hash, + I256::try_from(pending.gas_fee.amount * gwei_per_gas_token) .map(|cost| transfer_fee() - cost) .try_halt(|err| { tracing::debug!(%err, "Failed to convert value to I256"); }), - v, + pending, )) } else { None @@ -568,7 +585,13 @@ mod recommendations { let max_gas = args.max_gas.map(Uint::from_u64).unwrap_or(uint::MAX_VALUE); let max_cost = args.gas.map(I256::from).unwrap_or_default(); - generate(contents, validator_gas, max_gas, max_cost)?; + generate( + contents, + &args.conversion_table, + validator_gas, + max_gas, + max_cost, + )?; control_flow::proceed(()) } @@ -611,6 +634,7 @@ mod recommendations { /// input parameters. fn generate( contents: Vec<(String, I256, PendingTransfer)>, + conversion_table: &HashMap, validator_gas: Uint, max_gas: Uint, max_cost: I256, @@ -630,13 +654,11 @@ mod recommendations { let mut total_cost = I256::try_from(validator_gas).try_halt(|err| { tracing::debug!(%err, "Failed to convert value to I256"); })?; - let mut total_fees = uint::ZERO; + let mut total_fees = HashMap::new(); let mut recommendation = vec![]; for (hash, cost, transfer) in contents.into_iter() { let next_total_gas = total_gas + unsigned_transfer_fee(); let next_total_cost = total_cost + cost; - let next_total_fees = - total_fees + Uint::from(transfer.gas_fee.amount); if cost.is_negative() { if next_total_gas <= max_gas && next_total_cost <= max_cost { state.feasible_region = true; @@ -661,7 +683,7 @@ mod recommendations { } total_cost = next_total_cost; total_gas = next_total_gas; - total_fees = next_total_fees; + update_total_fees(&mut total_fees, transfer, conversion_table); } control_flow::proceed( @@ -672,7 +694,7 @@ mod recommendations { total_gas ); println!("Estimated net profit (in gwei): {}", -total_cost); - println!("Total fees (in NAM): {}", total_fees); + println!("Total fees: {total_fees:#?}"); Some(recommendation) } else { println!( @@ -684,6 +706,23 @@ mod recommendations { ) } + fn update_total_fees( + total_fees: &mut HashMap, + transfer: PendingTransfer, + conversion_table: &HashMap, + ) { + let GasFee { token, amount, .. } = transfer.gas_fee; + let fees = total_fees + .entry( + conversion_table + .get(&token) + .map(|entry| entry.alias.clone()) + .unwrap_or_else(|| token.to_string()), + ) + .or_insert(uint::ZERO); + *fees += Uint::from(amount); + } + #[cfg(test)] mod test_recommendations { use namada_core::types::address::Address; @@ -783,6 +822,7 @@ mod recommendations { let expected = vec![hash; 17]; let recommendation = generate( process_transfers(profitable), + &Default::default(), Uint::from_u64(800_000), uint::MAX_VALUE, I256::zero(), @@ -800,6 +840,7 @@ mod recommendations { let expected: Vec<_> = vec![hash; 17]; let recommendation = generate( process_transfers(transfers), + &Default::default(), Uint::from_u64(800_000), uint::MAX_VALUE, I256::zero(), @@ -816,6 +857,7 @@ mod recommendations { let expected = vec![hash; 2]; let recommendation = generate( process_transfers(transfers), + &Default::default(), Uint::from_u64(50_000), Uint::from_u64(150_000), I256(uint::MAX_SIGNED_VALUE), @@ -836,6 +878,7 @@ mod recommendations { .collect(); let recommendation = generate( process_transfers(transfers), + &Default::default(), Uint::from_u64(150_000), uint::MAX_VALUE, I256::from(20_000), @@ -853,6 +896,7 @@ mod recommendations { transfers.extend([transfer(17_500), transfer(17_500)]); let recommendation = generate( process_transfers(transfers), + &Default::default(), Uint::from_u64(150_000), Uint::from_u64(330_000), I256::from(20_000), @@ -867,6 +911,7 @@ mod recommendations { let transfers = vec![transfer(75_000); 4]; let recommendation = generate( process_transfers(transfers), + &Default::default(), Uint::from_u64(300_000), uint::MAX_VALUE, I256::from(20_000), From d1ac06459a0ea355d795a0db0485e343a8b3652e Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 14 Aug 2023 09:44:08 +0100 Subject: [PATCH 69/94] Fix Bridge pool `construct_proof` --- shared/src/ledger/eth_bridge/bridge_pool.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index c058c6f6f8..ae3186a668 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -266,7 +266,7 @@ where struct BridgePoolProofResponse { hashes: Vec, relayer_address: Address, - total_fees: Amount, + total_fees: HashMap, abi_encoded_proof: Vec, } @@ -297,12 +297,20 @@ where relayer_address: args.relayer, total_fees: appendices .map(|appendices| { - appendices - .into_iter() - .map(|app| app.gas_fee.amount) - .sum::() + appendices.into_iter().fold( + HashMap::new(), + |mut total_fees, app| { + let GasFee { token, amount, .. } = + app.gas_fee.into_owned(); + let fees = total_fees + .entry(token) + .or_insert_with(Amount::zero); + fees.receive(&amount); + total_fees + }, + ) }) - .unwrap_or(Amount::zero()), + .unwrap_or_default(), abi_encoded_proof: bp_proof_bytes, }; println!("{}", serde_json::to_string(&resp).unwrap()); From 11aef0cf4330a3e2e6aa6b82a7e2c1ba096c9fbc Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 14 Aug 2023 14:09:42 +0100 Subject: [PATCH 70/94] Add context to CLI Bridge pool cmds --- apps/src/bin/namada-relayer/main.rs | 2 +- apps/src/lib/cli.rs | 131 +++++++++++++----- apps/src/lib/cli/relayer.rs | 76 ++++++---- .../lib/node/ledger/shell/testing/client.rs | 19 ++- 4 files changed, 164 insertions(+), 64 deletions(-) diff --git a/apps/src/bin/namada-relayer/main.rs b/apps/src/bin/namada-relayer/main.rs index 0b314cb9fa..52c15192dc 100644 --- a/apps/src/bin/namada-relayer/main.rs +++ b/apps/src/bin/namada-relayer/main.rs @@ -12,7 +12,7 @@ async fn main() -> Result<()> { // init logging logging::init_from_env_or(LevelFilter::INFO)?; - let (cmd, _) = cli::namada_relayer_cli()?; + let cmd = cli::namada_relayer_cli()?; // run the CLI CliApi::<()>::handle_relayer_command::(None, cmd).await } diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 719b6900a5..08c60ec3b1 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1971,46 +1971,63 @@ pub mod cmds { /// Used as sub-commands (`SubCmd` instance) in `namadar` binary. #[derive(Clone, Debug)] pub enum EthBridgePool { + /// The [`super::Context`] provides access to the wallet and the + /// config. It will generate a new wallet and config, if they + /// don't exist. + WithContext(EthBridgePoolWithCtx), + /// Utils don't have [`super::Context`], only the global arguments. + WithoutContext(EthBridgePoolWithoutCtx), + } + + /// Ethereum Bridge pool commands requiring [`super::Context`]. + #[derive(Clone, Debug)] + pub enum EthBridgePoolWithCtx { /// Get a recommendation on a batch of transfers /// to relay. - RecommendBatch(args::RecommendBatch), + RecommendBatch(RecommendBatch), + } + + /// Ethereum Bridge pool commands not requiring [`super::Context`]. + #[derive(Clone, Debug)] + pub enum EthBridgePoolWithoutCtx { /// Construct a proof that a set of transfers is in the pool. /// This can be used to relay transfers across the /// bridge to Ethereum. - ConstructProof(args::BridgePoolProof), + ConstructProof(ConstructProof), /// Construct and relay a bridge pool proof to /// Ethereum directly. - RelayProof(args::RelayBridgePoolProof), + RelayProof(RelayProof), /// Query the contents of the pool. - QueryPool(args::Query), + QueryPool(QueryEthBridgePool), /// Query to provable contents of the pool. - QuerySigned(args::Query), + QuerySigned(QuerySignedBridgePool), /// Check the confirmation status of `TransferToEthereum` /// events. - QueryRelays(args::Query), + QueryRelays(QueryRelayProgress), } impl Cmd for EthBridgePool { fn add_sub(app: App) -> App { - app.subcommand(ConstructProof::def().display_order(1)) + app.subcommand(RecommendBatch::def().display_order(1)) + .subcommand(ConstructProof::def().display_order(1)) + .subcommand(RelayProof::def().display_order(1)) .subcommand(QueryEthBridgePool::def().display_order(1)) .subcommand(QuerySignedBridgePool::def().display_order(1)) .subcommand(QueryRelayProgress::def().display_order(1)) } fn parse(matches: &ArgMatches) -> Option { - let recommend = RecommendBatch::parse(matches) - .map(|query| Self::RecommendBatch(query.0)); - let construct_proof = ConstructProof::parse(matches) - .map(|proof| Self::ConstructProof(proof.0)); - let relay_proof = RelayProof::parse(matches) - .map(|proof| Self::RelayProof(proof.0)); - let query_pool = QueryEthBridgePool::parse(matches) - .map(|q| Self::QueryPool(q.0)); - let query_signed = QuerySignedBridgePool::parse(matches) - .map(|q| Self::QuerySigned(q.0)); - let query_relays = QueryRelayProgress::parse(matches) - .map(|q| Self::QueryRelays(q.0)); + use EthBridgePoolWithCtx::*; + use EthBridgePoolWithoutCtx::*; + + let recommend = Self::parse_with_ctx(matches, RecommendBatch); + let construct_proof = + Self::parse_without_ctx(matches, ConstructProof); + let relay_proof = Self::parse_without_ctx(matches, RelayProof); + let query_pool = Self::parse_without_ctx(matches, QueryPool); + let query_signed = Self::parse_without_ctx(matches, QuerySigned); + let query_relays = Self::parse_without_ctx(matches, QueryRelays); + construct_proof .or(recommend) .or(relay_proof) @@ -2020,6 +2037,24 @@ pub mod cmds { } } + impl EthBridgePool { + /// A helper method to parse sub cmds with context + fn parse_with_ctx( + matches: &ArgMatches, + sub_to_self: impl Fn(T) -> EthBridgePoolWithCtx, + ) -> Option { + T::parse(matches).map(|sub| Self::WithContext(sub_to_self(sub))) + } + + /// A helper method to parse sub cmds without context + fn parse_without_ctx( + matches: &ArgMatches, + sub_to_self: impl Fn(T) -> EthBridgePoolWithoutCtx, + ) -> Option { + T::parse(matches).map(|sub| Self::WithoutContext(sub_to_self(sub))) + } + } + impl SubCmd for EthBridgePool { const CMD: &'static str = "ethereum-bridge-pool"; @@ -2134,7 +2169,7 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct QueryEthBridgePool(args::Query); + pub struct QueryEthBridgePool(pub args::Query); impl SubCmd for QueryEthBridgePool { const CMD: &'static str = "query"; @@ -2153,7 +2188,7 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct QuerySignedBridgePool(args::Query); + pub struct QuerySignedBridgePool(pub args::Query); impl SubCmd for QuerySignedBridgePool { const CMD: &'static str = "query-signed"; @@ -2175,7 +2210,7 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct QueryRelayProgress(args::Query); + pub struct QueryRelayProgress(pub args::Query); impl SubCmd for QueryRelayProgress { const CMD: &'static str = "query-relayed"; @@ -2199,14 +2234,14 @@ pub mod cmds { /// Query an Ethereum ABI encoding of the consensus validator /// set in Namada, at the given epoch, or the latest /// one, if none is provided. - ConsensusValidatorSet(args::ConsensusValidatorSet), + ConsensusValidatorSet(ConsensusValidatorSet), /// Query an Ethereum ABI encoding of a proof of the consensus /// validator set in Namada, at the given epoch, or the next /// one, if none is provided. - ValidatorSetProof(args::ValidatorSetProof), + ValidatorSetProof(ValidatorSetProof), /// Relay a validator set update to Namada's Ethereum bridge /// smart contracts. - ValidatorSetUpdateRelay(args::ValidatorSetUpdateRelay), + ValidatorSetUpdateRelay(ValidatorSetUpdateRelay), } impl SubCmd for ValidatorSet { @@ -2216,11 +2251,11 @@ pub mod cmds { matches.subcommand_matches(Self::CMD).and_then(|matches| { let consensus_validator_set = ConsensusValidatorSet::parse(matches) - .map(|args| Self::ConsensusValidatorSet(args.0)); + .map(Self::ConsensusValidatorSet); let validator_set_proof = ValidatorSetProof::parse(matches) - .map(|args| Self::ValidatorSetProof(args.0)); + .map(Self::ValidatorSetProof); let relay = ValidatorSetUpdateRelay::parse(matches) - .map(|args| Self::ValidatorSetUpdateRelay(args.0)); + .map(Self::ValidatorSetUpdateRelay); consensus_validator_set.or(validator_set_proof).or(relay) }) } @@ -2241,7 +2276,7 @@ pub mod cmds { #[derive(Clone, Debug)] pub struct ConsensusValidatorSet( - args::ConsensusValidatorSet, + pub args::ConsensusValidatorSet, ); impl SubCmd for ConsensusValidatorSet { @@ -2265,7 +2300,7 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct ValidatorSetProof(args::ValidatorSetProof); + pub struct ValidatorSetProof(pub args::ValidatorSetProof); impl SubCmd for ValidatorSetProof { const CMD: &'static str = "proof"; @@ -2289,7 +2324,7 @@ pub mod cmds { #[derive(Clone, Debug)] pub struct ValidatorSetUpdateRelay( - args::ValidatorSetUpdateRelay, + pub args::ValidatorSetUpdateRelay, ); impl SubCmd for ValidatorSetUpdateRelay { @@ -5467,9 +5502,39 @@ pub fn namada_wallet_cli() -> Result<(cmds::NamadaWallet, Context)> { cmds::NamadaWallet::parse_or_print_help(app) } -pub fn namada_relayer_cli() -> Result<(cmds::NamadaRelayer, Context)> { +pub enum NamadaRelayer { + EthBridgePoolWithCtx(Box<(cmds::EthBridgePoolWithCtx, Context)>), + EthBridgePoolWithoutCtx(cmds::EthBridgePoolWithoutCtx), + ValidatorSet(cmds::ValidatorSet), +} + +pub fn namada_relayer_cli() -> Result { let app = namada_relayer_app(); - cmds::NamadaRelayer::parse_or_print_help(app) + let matches = app.clone().get_matches(); + match Cmd::parse(&matches) { + Some(cmd) => match cmd { + cmds::NamadaRelayer::EthBridgePool( + cmds::EthBridgePool::WithContext(sub_cmd), + ) => { + let global_args = args::Global::parse(&matches); + let context = Context::new(global_args)?; + Ok(NamadaRelayer::EthBridgePoolWithCtx(Box::new(( + sub_cmd, context, + )))) + } + cmds::NamadaRelayer::EthBridgePool( + cmds::EthBridgePool::WithoutContext(sub_cmd), + ) => Ok(NamadaRelayer::EthBridgePoolWithoutCtx(sub_cmd)), + cmds::NamadaRelayer::ValidatorSet(sub_cmd) => { + Ok(NamadaRelayer::ValidatorSet(sub_cmd)) + } + }, + None => { + let mut app = app; + app.print_help().unwrap(); + safe_exit(2); + } + } } fn namada_app() -> App { diff --git a/apps/src/lib/cli/relayer.rs b/apps/src/lib/cli/relayer.rs index 531051d27a..242a9ff061 100644 --- a/apps/src/lib/cli/relayer.rs +++ b/apps/src/lib/cli/relayer.rs @@ -5,9 +5,10 @@ use namada::eth_bridge::ethers::providers::{Http, Provider}; use namada::ledger::eth_bridge::{bridge_pool, validator_set}; use namada::types::control_flow::ProceedOrElse; +use crate::cli; use crate::cli::api::{CliApi, CliClient}; -use crate::cli::args::CliToSdkCtxless; -use crate::cli::cmds; +use crate::cli::args::{CliToSdk, CliToSdkCtxless}; +use crate::cli::cmds::*; fn error() -> Report { eyre!("Fatal error") @@ -16,29 +17,38 @@ fn error() -> Report { impl CliApi { pub async fn handle_relayer_command( client: Option, - cmd: cmds::NamadaRelayer, + cmd: cli::NamadaRelayer, ) -> Result<()> where C: CliClient, { match cmd { - cmds::NamadaRelayer::EthBridgePool(sub) => match sub { - cmds::EthBridgePool::RecommendBatch(mut args) => { - let client = client.unwrap_or_else(|| { - C::from_tendermint_address( - &mut args.query.ledger_address, - ) - }); - client - .wait_until_node_is_synced() - .await - .proceed_or_else(error)?; - let args = args.to_sdk_ctxless(); - bridge_pool::recommend_batch(&client, args) - .await - .proceed_or_else(error)?; + cli::NamadaRelayer::EthBridgePoolWithCtx(boxed) => { + let (sub, mut ctx) = *boxed; + match sub { + EthBridgePoolWithCtx::RecommendBatch(RecommendBatch( + mut args, + )) => { + let client = client.unwrap_or_else(|| { + C::from_tendermint_address( + &mut args.query.ledger_address, + ) + }); + client + .wait_until_node_is_synced() + .await + .proceed_or_else(error)?; + let args = args.to_sdk(&mut ctx); + bridge_pool::recommend_batch(&client, args) + .await + .proceed_or_else(error)?; + } } - cmds::EthBridgePool::ConstructProof(mut args) => { + } + cli::NamadaRelayer::EthBridgePoolWithoutCtx(sub) => match sub { + EthBridgePoolWithoutCtx::ConstructProof(ConstructProof( + mut args, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, @@ -53,7 +63,7 @@ impl CliApi { .await .proceed_or_else(error)?; } - cmds::EthBridgePool::RelayProof(mut args) => { + EthBridgePoolWithoutCtx::RelayProof(RelayProof(mut args)) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, @@ -74,7 +84,9 @@ impl CliApi { .await .proceed_or_else(error)?; } - cmds::EthBridgePool::QueryPool(mut query) => { + EthBridgePoolWithoutCtx::QueryPool(QueryEthBridgePool( + mut query, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address(&mut query.ledger_address) }); @@ -84,7 +96,9 @@ impl CliApi { .proceed_or_else(error)?; bridge_pool::query_bridge_pool(&client).await; } - cmds::EthBridgePool::QuerySigned(mut query) => { + EthBridgePoolWithoutCtx::QuerySigned( + QuerySignedBridgePool(mut query), + ) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address(&mut query.ledger_address) }); @@ -96,7 +110,9 @@ impl CliApi { .await .proceed_or_else(error)?; } - cmds::EthBridgePool::QueryRelays(mut query) => { + EthBridgePoolWithoutCtx::QueryRelays(QueryRelayProgress( + mut query, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address(&mut query.ledger_address) }); @@ -107,8 +123,10 @@ impl CliApi { bridge_pool::query_relay_progress(&client).await; } }, - cmds::NamadaRelayer::ValidatorSet(sub) => match sub { - cmds::ValidatorSet::ConsensusValidatorSet(mut args) => { + cli::NamadaRelayer::ValidatorSet(sub) => match sub { + ValidatorSet::ConsensusValidatorSet(ConsensusValidatorSet( + mut args, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, @@ -122,7 +140,9 @@ impl CliApi { validator_set::query_validator_set_args(&client, args) .await; } - cmds::ValidatorSet::ValidatorSetProof(mut args) => { + ValidatorSet::ValidatorSetProof(ValidatorSetProof( + mut args, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, @@ -138,7 +158,9 @@ impl CliApi { ) .await; } - cmds::ValidatorSet::ValidatorSetUpdateRelay(mut args) => { + ValidatorSet::ValidatorSetUpdateRelay( + ValidatorSetUpdateRelay(mut args), + ) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, diff --git a/apps/src/lib/node/ledger/shell/testing/client.rs b/apps/src/lib/node/ledger/shell/testing/client.rs index 164e4a03ea..c07735d0b9 100644 --- a/apps/src/lib/node/ledger/shell/testing/client.rs +++ b/apps/src/lib/node/ledger/shell/testing/client.rs @@ -8,7 +8,7 @@ use tendermint_config::net::Address as TendermintAddress; use super::node::MockNode; use crate::cli::api::{CliApi, CliClient}; use crate::cli::args::Global; -use crate::cli::{args, cmds, Cmd, Context, NamadaClient}; +use crate::cli::{args, cmds, Cmd, Context, NamadaClient, NamadaRelayer}; use crate::node::ledger::shell::testing::utils::Bin; pub fn run( @@ -63,8 +63,21 @@ pub fn run( let app = App::new("test"); let app = cmds::NamadaRelayer::add_sub(args::Global::def(app)); let matches = app.get_matches_from(args.clone()); - let cmd = cmds::NamadaRelayer::parse(&matches) - .expect("Could not parse wallet command"); + let cmd = match cmds::NamadaRelayer::parse(&matches) + .expect("Could not parse relayer command") + { + cmds::NamadaRelayer::EthBridgePool( + cmds::EthBridgePool::WithContext(sub_cmd), + ) => NamadaRelayer::EthBridgePoolWithCtx(Box::new(( + sub_cmd, ctx, + ))), + cmds::NamadaRelayer::EthBridgePool( + cmds::EthBridgePool::WithoutContext(sub_cmd), + ) => NamadaRelayer::EthBridgePoolWithoutCtx(sub_cmd), + cmds::NamadaRelayer::ValidatorSet(sub_cmd) => { + NamadaRelayer::ValidatorSet(sub_cmd) + } + }; rt.block_on(CliApi::<()>::handle_relayer_command(Some(node), cmd)) } } From 5898fb37b593d9b8bf7270e24c391b31e033d4ed Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 10:19:31 +0100 Subject: [PATCH 71/94] Refactor recommendations to use EligibleRecommendation --- shared/src/ledger/eth_bridge/bridge_pool.rs | 47 +++++++++++++++------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index ae3186a668..ed47df5d1d 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -486,6 +486,22 @@ mod recommendations { Generous, } + /// Transfer to Ethereum that is eligible to be recommended + /// for a relay operation, generating a profit. + /// + /// This means that the underlying Ethereum event has not + /// been "seen" yet, and that the user provided appropriate + /// conversion rates to gwei for the gas fee token in + /// the transfer. + struct EligibleRecommendation { + /// Pending transfer to Ethereum. + pending_transfer: PendingTransfer, + /// Hash of the [`PendingTransfer`]. + transfer_hash: String, + /// Cost of relaying the transfer, in gwei. + cost: I256, + } + /// Recommend the most economical batch of transfers to relay based /// on a conversion rate estimates from NAM to ETH and gas usage /// heuristics. @@ -542,7 +558,7 @@ mod recommendations { + valset_fee() * valset_size; // we don't recommend transfers that have already been relayed - let mut contents: Vec<(String, I256, PendingTransfer)> = + let mut contents: Vec = query_signed_bridge_pool(client) .await? .into_iter() @@ -583,12 +599,16 @@ mod recommendations { } }) .try_fold(Vec::new(), |mut accum, (hash, cost, transf)| { - accum.push((hash, cost?, transf)); + accum.push(EligibleRecommendation { + cost: cost?, + transfer_hash: hash, + pending_transfer: transf, + }); control_flow::proceed(accum) })?; // sort transfers in decreasing amounts of profitability - contents.sort_by_key(|(_, cost, _)| *cost); + contents.sort_by_key(|EligibleRecommendation { cost, .. }| *cost); let max_gas = args.max_gas.map(Uint::from_u64).unwrap_or(uint::MAX_VALUE); @@ -641,7 +661,7 @@ mod recommendations { /// Generates the actual recommendation from restrictions given by the /// input parameters. fn generate( - contents: Vec<(String, I256, PendingTransfer)>, + contents: Vec, conversion_table: &HashMap, validator_gas: Uint, max_gas: Uint, @@ -664,7 +684,12 @@ mod recommendations { })?; let mut total_fees = HashMap::new(); let mut recommendation = vec![]; - for (hash, cost, transfer) in contents.into_iter() { + for EligibleRecommendation { + cost, + transfer_hash: hash, + pending_transfer: transfer, + } in contents.into_iter() + { let next_total_gas = total_gas + unsigned_transfer_fee(); let next_total_cost = total_cost + cost; if cost.is_negative() { @@ -770,15 +795,13 @@ mod recommendations { /// understands. fn process_transfers( transfers: Vec, - ) -> Vec<(String, I256, PendingTransfer)> { + ) -> Vec { transfers .into_iter() - .map(|t| { - ( - t.keccak256().to_string(), - transfer_fee() - t.gas_fee.amount.change(), - t, - ) + .map(|t| EligibleRecommendation { + cost: transfer_fee() - t.gas_fee.amount.change(), + transfer_hash: t.keccak256().to_string(), + pending_transfer: t, }) .collect() } From 4eae7389e25ccc67599754cef2b7bc26f6cbbff1 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 10:36:34 +0100 Subject: [PATCH 72/94] Rename `generate` to `generate_recommendations` --- shared/src/ledger/eth_bridge/bridge_pool.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index ed47df5d1d..6836662469 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -613,7 +613,7 @@ mod recommendations { let max_gas = args.max_gas.map(Uint::from_u64).unwrap_or(uint::MAX_VALUE); let max_cost = args.gas.map(I256::from).unwrap_or_default(); - generate( + generate_recommendations( contents, &args.conversion_table, validator_gas, @@ -660,7 +660,7 @@ mod recommendations { /// Generates the actual recommendation from restrictions given by the /// input parameters. - fn generate( + fn generate_recommendations( contents: Vec, conversion_table: &HashMap, validator_gas: Uint, @@ -791,8 +791,8 @@ mod recommendations { } } - /// Convert transfers into a format that the `generate` function - /// understands. + /// Convert transfers into a format that the + /// [`generate_recommendations`] function understands. fn process_transfers( transfers: Vec, ) -> Vec { @@ -851,7 +851,7 @@ mod recommendations { let profitable = vec![transfer(100_000); 17]; let hash = profitable[0].keccak256().to_string(); let expected = vec![hash; 17]; - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(profitable), &Default::default(), Uint::from_u64(800_000), @@ -869,7 +869,7 @@ mod recommendations { let hash = transfers[0].keccak256().to_string(); transfers.push(transfer(0)); let expected: Vec<_> = vec![hash; 17]; - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), &Default::default(), Uint::from_u64(800_000), @@ -886,7 +886,7 @@ mod recommendations { let transfers = vec![transfer(75_000); 4]; let hash = transfers[0].keccak256().to_string(); let expected = vec![hash; 2]; - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), &Default::default(), Uint::from_u64(50_000), @@ -907,7 +907,7 @@ mod recommendations { .map(|t| t.keccak256().to_string()) .take(5) .collect(); - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), &Default::default(), Uint::from_u64(150_000), @@ -925,7 +925,7 @@ mod recommendations { let hash = transfers[0].keccak256().to_string(); let expected = vec![hash; 4]; transfers.extend([transfer(17_500), transfer(17_500)]); - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), &Default::default(), Uint::from_u64(150_000), @@ -940,7 +940,7 @@ mod recommendations { #[test] fn test_wholly_infeasible() { let transfers = vec![transfer(75_000); 4]; - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), &Default::default(), Uint::from_u64(300_000), From 0fc3e5dc935f9f0ca9e7a24a2a27676fa63f3fdc Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 10:42:32 +0100 Subject: [PATCH 73/94] Fix Bridge pool proof warning voting power --- shared/src/ledger/eth_bridge/bridge_pool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 6836662469..5119fa4e2c 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -210,7 +210,7 @@ where let warnings: Vec<_> = in_progress .into_iter() .filter_map(|(ref transfer, voting_power)| { - if voting_power > FractionalVotingPower::ONE_THIRD { + if voting_power >= FractionalVotingPower::ONE_THIRD { let hash = transfer.keccak256(); args.transfers.contains(&hash).then_some(hash) } else { From ed3b5a3a75800b625a629b3efbc5c6cd13b338b7 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 11:49:30 +0100 Subject: [PATCH 74/94] Factor out `generate_eligible()` --- shared/src/ledger/eth_bridge/bridge_pool.rs | 124 +++++++++++--------- 1 file changed, 71 insertions(+), 53 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 5119fa4e2c..7302dbbb14 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -436,6 +436,8 @@ where } mod recommendations { + use std::collections::BTreeSet; + use borsh::BorshDeserialize; use namada_core::types::uint::{self, Uint, I256}; @@ -521,7 +523,8 @@ mod recommendations { .await .unwrap() .into_keys() - .collect::>(); + .map(|pending| pending.keccak256().to_string()) + .collect::>(); // get the signed bridge pool root so we can analyze the signatures // the estimate the gas cost of verifying them. @@ -558,63 +561,17 @@ mod recommendations { + valset_fee() * valset_size; // we don't recommend transfers that have already been relayed - let mut contents: Vec = - query_signed_bridge_pool(client) - .await? - .into_iter() - .filter_map(|(pending_hash, pending)| { - if !in_progress.contains(&pending) { - let conversion_rate = - if let Some(entry) = args - .conversion_table - .get(&pending.gas_fee.token) - { - let rate = entry.conversion_rate; - if rate <= 0.0f64 { - eprintln!( - "Ignoring token with an invalid conversion rate: {}", - pending.gas_fee.token, - ); - return None; - } - rate - } else { - return None; - }; - // This is the amount of gwei a single gas token is worth - let gwei_per_gas_token = Uint::from_u64( - (10u64.pow(9) as f64 / conversion_rate).floor() as u64, - ); - Some(( - pending_hash, - I256::try_from(pending.gas_fee.amount * gwei_per_gas_token) - .map(|cost| transfer_fee() - cost) - .try_halt(|err| { - tracing::debug!(%err, "Failed to convert value to I256"); - }), - pending, - )) - } else { - None - } - }) - .try_fold(Vec::new(), |mut accum, (hash, cost, transf)| { - accum.push(EligibleRecommendation { - cost: cost?, - transfer_hash: hash, - pending_transfer: transf, - }); - control_flow::proceed(accum) - })?; - - // sort transfers in decreasing amounts of profitability - contents.sort_by_key(|EligibleRecommendation { cost, .. }| *cost); + let eligible = generate_eligible( + &args.conversion_table, + &in_progress, + query_signed_bridge_pool(client).await?, + )?; let max_gas = args.max_gas.map(Uint::from_u64).unwrap_or(uint::MAX_VALUE); let max_cost = args.gas.map(I256::from).unwrap_or_default(); generate_recommendations( - contents, + eligible, &args.conversion_table, validator_gas, max_gas, @@ -658,6 +615,67 @@ mod recommendations { ) } + /// Generate eligible recommendations. + fn generate_eligible( + conversion_table: &HashMap, + in_progress: &BTreeSet, + signed_pool: HashMap, + ) -> Halt> { + let mut eligible: Vec<_> = signed_pool + .into_iter() + .filter_map(|(pending_hash, pending)| { + if in_progress.contains(&pending_hash) { + return None; + } + + let conversion_rate = if let Some(entry) = + conversion_table.get(&pending.gas_fee.token) + { + let rate = entry.conversion_rate; + if rate <= 0.0f64 { + eprintln!( + "Ignoring token with an invalid conversion rate: \ + {}", + pending.gas_fee.token, + ); + return None; + } + rate + } else { + return None; + }; + + // This is the amount of gwei a single gas token is worth + let gwei_per_gas_token = Uint::from_u64( + (10u64.pow(9) as f64 / conversion_rate).floor() as u64, + ); + + Some( + I256::try_from(pending.gas_fee.amount * gwei_per_gas_token) + .map_err(|err| err.to_string()) + .and_then(|cost| { + transfer_fee().checked_sub(&cost).ok_or_else(|| { + "Underflowed calculating relaying cost".into() + }) + }) + .map(|cost| EligibleRecommendation { + cost, + pending_transfer: pending, + transfer_hash: pending_hash, + }), + ) + }) + .collect::, _>>() + .try_halt(|err| { + tracing::debug!(%err, "Failed to calculate relaying cost"); + })?; + + // sort transfers in increasing amounts of profitability + eligible.sort_by_key(|EligibleRecommendation { cost, .. }| *cost); + + control_flow::proceed(eligible) + } + /// Generates the actual recommendation from restrictions given by the /// input parameters. fn generate_recommendations( From fb89bff2fab0cebd9e759018fbc942b9a2c687d6 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 11:59:46 +0100 Subject: [PATCH 75/94] Improve conversion rate query --- shared/src/ledger/eth_bridge/bridge_pool.rs | 30 ++++++++++----------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 7302dbbb14..4b0fdd57c7 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -628,22 +628,20 @@ mod recommendations { return None; } - let conversion_rate = if let Some(entry) = - conversion_table.get(&pending.gas_fee.token) - { - let rate = entry.conversion_rate; - if rate <= 0.0f64 { - eprintln!( - "Ignoring token with an invalid conversion rate: \ - {}", - pending.gas_fee.token, - ); - return None; - } - rate - } else { - return None; - }; + let conversion_rate = conversion_table + .get(&pending.gas_fee.token) + .and_then(|entry| { + let rate = entry.conversion_rate; + if rate <= 0.0f64 { + eprintln!( + "Ignoring token with an invalid conversion \ + rate: {}", + pending.gas_fee.token, + ); + return None; + } + Some(rate) + })?; // This is the amount of gwei a single gas token is worth let gwei_per_gas_token = Uint::from_u64( From ad57fabb7115b371a50fcbf5ae7ad6d8cd5163a8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 16:09:53 +0100 Subject: [PATCH 76/94] Avoid overflowing when calculating earned gwei amount --- shared/src/ledger/eth_bridge/bridge_pool.rs | 23 ++++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 4b0fdd57c7..f6b13e7752 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -644,17 +644,24 @@ mod recommendations { })?; // This is the amount of gwei a single gas token is worth - let gwei_per_gas_token = Uint::from_u64( - (10u64.pow(9) as f64 / conversion_rate).floor() as u64, - ); + let gwei_per_gas_token = + Uint::from_u64((1e9 / conversion_rate).floor() as u64); Some( - I256::try_from(pending.gas_fee.amount * gwei_per_gas_token) + Uint::from(pending.gas_fee.amount) + .checked_mul(gwei_per_gas_token) + .ok_or_else(|| { + "Overflowed calculating earned gwei".into() + }) + .and_then(I256::try_from) .map_err(|err| err.to_string()) - .and_then(|cost| { - transfer_fee().checked_sub(&cost).ok_or_else(|| { - "Underflowed calculating relaying cost".into() - }) + .and_then(|amt_of_earned_gwei| { + transfer_fee() + .checked_sub(&amt_of_earned_gwei) + .ok_or_else(|| { + "Underflowed calculating relaying cost" + .into() + }) }) .map(|cost| EligibleRecommendation { cost, From 3701ff71845121dd0b5768d36b31a35f1da168ec Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 16:10:39 +0100 Subject: [PATCH 77/94] Add test_generate_eligible_happy_path() unit test --- shared/src/ledger/eth_bridge/bridge_pool.rs | 48 +++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index f6b13e7752..37adf25bd5 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -495,6 +495,7 @@ mod recommendations { /// been "seen" yet, and that the user provided appropriate /// conversion rates to gwei for the gas fee token in /// the transfer. + #[derive(Debug, Eq, PartialEq)] struct EligibleRecommendation { /// Pending transfer to Ethereum. pending_transfer: PendingTransfer, @@ -836,6 +837,53 @@ mod recommendations { } } + /// Test the happy path of generating eligible recommendations + /// for Bridge pool relayed transfers. + #[test] + fn test_generate_eligible_happy_path() { + let pending = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: EthAddress([1; 20]), + recipient: EthAddress([2; 20]), + sender: bertha_address(), + amount: Default::default(), + }, + gas_fee: GasFee { + token: namada_core::types::address::eth(), + amount: 1_000_000_000_u64.into(), // 1 GWEI + payer: bertha_address(), + }, + }; + let conversion_table = { + let mut table = HashMap::new(); + table.insert( + namada_core::types::address::eth(), + args::BpConversionTableEntry { + alias: "ETH".into(), + conversion_rate: 1e9, // 1 ETH = 1e9 GWEI + }, + ); + table + }; + let in_progress = BTreeSet::new(); + let signed_pool = HashMap::from([( + pending.keccak256().to_string(), + pending.clone(), + )]); + let eligible = + generate_eligible(&conversion_table, &in_progress, signed_pool) + .proceed(); + let expected = vec![EligibleRecommendation { + transfer_hash: pending.keccak256().to_string(), + cost: transfer_fee() + - I256::try_from(pending.gas_fee.amount) + .expect("Test failed"), + pending_transfer: pending, + }]; + assert_eq!(eligible, expected); + } + #[test] fn test_signature_count() { let voting_powers = VotingPowersMap::from([ From bf1a3beee6b8af77f65f4e0f8db9af01ecad596c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 16:20:50 +0100 Subject: [PATCH 78/94] Abstract `generate_eligible` unit tests with a helper fn --- shared/src/ledger/eth_bridge/bridge_pool.rs | 58 +++++++++++++++------ 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 37adf25bd5..6c7f1933a1 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -837,10 +837,20 @@ mod recommendations { } } - /// Test the happy path of generating eligible recommendations - /// for Bridge pool relayed transfers. - #[test] - fn test_generate_eligible_happy_path() { + /// Data to pass to the [`test_generate_eligible_aux`] callback. + struct TestGenerateEligible<'a> { + pending: &'a PendingTransfer, + #[allow(dead_code)] + in_progress: &'a mut BTreeSet, + signed_pool: &'a mut HashMap, + expected_eligible: &'a mut Vec, + } + + /// Helper function to test [`generate_eligible`]. + fn test_generate_eligible_aux(mut callback: F) + where + F: FnMut(TestGenerateEligible<'_>), + { let pending = PendingTransfer { transfer: TransferToEthereum { kind: TransferToEthereumKind::Erc20, @@ -866,24 +876,40 @@ mod recommendations { ); table }; - let in_progress = BTreeSet::new(); - let signed_pool = HashMap::from([( - pending.keccak256().to_string(), - pending.clone(), - )]); + let mut in_progress = BTreeSet::new(); + let mut signed_pool = HashMap::new(); + let mut expected = vec![]; + callback(TestGenerateEligible { + pending: &pending, + in_progress: &mut in_progress, + signed_pool: &mut signed_pool, + expected_eligible: &mut expected, + }); let eligible = generate_eligible(&conversion_table, &in_progress, signed_pool) .proceed(); - let expected = vec![EligibleRecommendation { - transfer_hash: pending.keccak256().to_string(), - cost: transfer_fee() - - I256::try_from(pending.gas_fee.amount) - .expect("Test failed"), - pending_transfer: pending, - }]; assert_eq!(eligible, expected); } + /// Test the happy path of generating eligible recommendations + /// for Bridge pool relayed transfers. + #[test] + fn test_generate_eligible_happy_path() { + test_generate_eligible_aux(|ctx| { + ctx.signed_pool.insert( + ctx.pending.keccak256().to_string(), + ctx.pending.clone(), + ); + ctx.expected_eligible.push(EligibleRecommendation { + transfer_hash: ctx.pending.keccak256().to_string(), + cost: transfer_fee() + - I256::try_from(ctx.pending.gas_fee.amount) + .expect("Test failed"), + pending_transfer: ctx.pending.clone(), + }); + }); + } + #[test] fn test_signature_count() { let voting_powers = VotingPowersMap::from([ From e7fa5eea8c8741be1dc2976ad40f40cd09c0fb03 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 16:46:31 +0100 Subject: [PATCH 79/94] More conversion table unit tests --- shared/src/ledger/eth_bridge/bridge_pool.rs | 62 ++++++++++++++++----- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 6c7f1933a1..caadf38a9e 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -840,12 +840,26 @@ mod recommendations { /// Data to pass to the [`test_generate_eligible_aux`] callback. struct TestGenerateEligible<'a> { pending: &'a PendingTransfer, - #[allow(dead_code)] + conversion_table: + &'a mut HashMap, in_progress: &'a mut BTreeSet, signed_pool: &'a mut HashMap, expected_eligible: &'a mut Vec, } + impl TestGenerateEligible<'_> { + /// Add ETH to a conversion table. + fn add_eth_to_conversion_table(&mut self) { + self.conversion_table.insert( + namada_core::types::address::eth(), + args::BpConversionTableEntry { + alias: "ETH".into(), + conversion_rate: 1e9, // 1 ETH = 1e9 GWEI + }, + ); + } + } + /// Helper function to test [`generate_eligible`]. fn test_generate_eligible_aux(mut callback: F) where @@ -865,29 +879,19 @@ mod recommendations { payer: bertha_address(), }, }; - let conversion_table = { - let mut table = HashMap::new(); - table.insert( - namada_core::types::address::eth(), - args::BpConversionTableEntry { - alias: "ETH".into(), - conversion_rate: 1e9, // 1 ETH = 1e9 GWEI - }, - ); - table - }; + let mut table = HashMap::new(); let mut in_progress = BTreeSet::new(); let mut signed_pool = HashMap::new(); let mut expected = vec![]; callback(TestGenerateEligible { pending: &pending, + conversion_table: &mut table, in_progress: &mut in_progress, signed_pool: &mut signed_pool, expected_eligible: &mut expected, }); let eligible = - generate_eligible(&conversion_table, &in_progress, signed_pool) - .proceed(); + generate_eligible(&table, &in_progress, signed_pool).proceed(); assert_eq!(eligible, expected); } @@ -895,7 +899,8 @@ mod recommendations { /// for Bridge pool relayed transfers. #[test] fn test_generate_eligible_happy_path() { - test_generate_eligible_aux(|ctx| { + test_generate_eligible_aux(|mut ctx| { + ctx.add_eth_to_conversion_table(); ctx.signed_pool.insert( ctx.pending.keccak256().to_string(), ctx.pending.clone(), @@ -910,6 +915,33 @@ mod recommendations { }); } + /// Test that a transfer is not recommended if it + /// is in the process of being relayed (has >0 voting + /// power behind it). + #[test] + fn test_generate_eligible_with_in_progress() { + test_generate_eligible_aux(|mut ctx| { + ctx.add_eth_to_conversion_table(); + ctx.signed_pool.insert( + ctx.pending.keccak256().to_string(), + ctx.pending.clone(), + ); + ctx.in_progress.insert(ctx.pending.keccak256().to_string()); + }); + } + + /// Test that a transfer is not recommended if its gas + /// token is not found in the conversion table. + #[test] + fn test_generate_eligible_no_gas_token() { + test_generate_eligible_aux(|ctx| { + ctx.signed_pool.insert( + ctx.pending.keccak256().to_string(), + ctx.pending.clone(), + ); + }); + } + #[test] fn test_signature_count() { let voting_powers = VotingPowersMap::from([ From 1b0dfee78bcb46bf5b14114734576e6bdc7bb189 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 20:35:04 +0100 Subject: [PATCH 80/94] Prohibit generating BP proofs when the signed root is outdated --- shared/src/ledger/queries/shell/eth_bridge.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index bd9115f48a..2588ef33bd 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -342,6 +342,21 @@ where )) .into_storage_result()?; + // make sure a relay attempt won't happen before the new signed + // root has had time to be generated + let latest_bp_nonce = + ctx.wl_storage.ethbridge_queries().get_bridge_pool_nonce(); + if latest_bp_nonce != signed_root.data.1 { + return Err(storage_api::Error::Custom(CustomError( + format!( + "Mismatch between the nonce in the Bridge pool root proof \ + ({}) and the latest Bridge pool nonce in storage ({})", + signed_root.data.1, latest_bp_nonce, + ) + .into(), + ))); + } + // get the merkle tree corresponding to the above root. let tree = ctx .wl_storage From a0f5852ab12efaa02060bb8a6fb6d1dc4044b24d Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 17 Aug 2023 08:57:03 +0100 Subject: [PATCH 81/94] Fail recommending BP batch of txs if nonce is out of date --- shared/src/ledger/eth_bridge/bridge_pool.rs | 37 ++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index caadf38a9e..97e28eb3b2 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -442,8 +442,11 @@ mod recommendations { use namada_core::types::uint::{self, Uint, I256}; use super::*; - use crate::eth_bridge::storage::bridge_pool::get_signed_root_key; + use crate::eth_bridge::storage::bridge_pool::{ + get_nonce_key, get_signed_root_key, + }; use crate::eth_bridge::storage::proof::BridgePoolRootProof; + use crate::types::ethereum_events::Uint as EthUint; use crate::types::storage::BlockHeight; use crate::types::vote_extensions::validator_set_update::{ EthAddrBook, VotingPowersMap, VotingPowersMapExt, @@ -535,15 +538,41 @@ mod recommendations { .storage_value( client, None, - Some(0.into()), + None, false, &get_signed_root_key(), ) .await - .unwrap() + .try_halt(|err| { + eprintln!("Failed to query Bridge pool proof: {err}"); + })? .data, ) - .unwrap(); + .try_halt(|err| { + eprintln!("Failed to decode Bridge pool proof: {err}"); + })?; + + // get the latest bridge pool nonce + let latest_bp_nonce = EthUint::try_from_slice( + &RPC.shell() + .storage_value(client, None, None, false, &get_nonce_key()) + .await + .try_halt(|err| { + eprintln!("Failed to query Bridge pool nonce: {err}"); + })? + .data, + ) + .try_halt(|err| { + eprintln!("Failed to decode Bridge pool nonce: {err}"); + })?; + + if latest_bp_nonce != bp_root.data.1 { + eprintln!( + "The signed Bridge pool nonce is not up to date, repeat this \ + query at a later time" + ); + return control_flow::halt(); + } // Get the voting powers of each of validator who signed // the above root. From 371b1feb256a6c1c0981f11bd8da5b69cec2a0a5 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 17 Aug 2023 10:35:49 +0100 Subject: [PATCH 82/94] Store height when BP root was signed --- ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs | 3 ++- ethereum_bridge/src/storage/eth_bridge_queries.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs index 7262c20a31..b512798ce8 100644 --- a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs +++ b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs @@ -45,6 +45,7 @@ where bridge pool root and nonce." ); let voting_powers = utils::get_voting_powers(wl_storage, &vext)?; + let root_height = vext.iter().next().unwrap().data.block_height; let (partial_proof, seen_by) = parse_vexts(wl_storage, vext); // apply updates to the bridge pool root. @@ -65,7 +66,7 @@ where wl_storage .write_bytes( &get_signed_root_key(), - (proof, wl_storage.storage.get_last_block_height()) + (proof, root_height) .try_to_vec() .expect("Serializing a Bridge pool root shouldn't fail."), ) diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index c30de9a536..5d068d2e38 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -220,7 +220,7 @@ where /// root and nonce. /// /// Also returns the block height at which the - /// a quorum of signatures was collected. + /// Bridge pool root was originally signed. /// /// No value exists when the bridge if first /// started. From 7b29efd4d24e939873dd400e8e9951e321b7e346 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 17 Aug 2023 10:36:41 +0100 Subject: [PATCH 83/94] Check BP root's height when vote extension was signed --- .../shell/vote_extensions/bridge_pool_vext.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs b/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs index edd83cdb3a..4decd44127 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs @@ -134,19 +134,16 @@ where VoteExtensionError::VerifySigFailed })?; - let bp_root = if cfg!(feature = "abcipp") { - self.wl_storage.ethbridge_queries().get_bridge_pool_root().0 - } else { - self.wl_storage - .ethbridge_queries() - .get_bridge_pool_root_at_height(ext.data.block_height) - .expect("We asserted that the queried height is correct") - .0 - }; + let bp_root = self + .wl_storage + .ethbridge_queries() + .get_bridge_pool_root_at_height(ext.data.block_height) + .expect("We asserted that the queried height is correct") + .0; let nonce = self .wl_storage .ethbridge_queries() - .get_bridge_pool_nonce() + .get_bridge_pool_nonce_at_height(ext.data.block_height) .to_bytes(); let signed = Signed::<_, SignableEthMessage>::new_from( keccak_hash([bp_root, nonce].concat()), From 00a859ac3ce742f1821edefd6875f2190d3ac474 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 17 Aug 2023 14:08:04 +0100 Subject: [PATCH 84/94] Test the height of the BP proof stored in the db --- .../transactions/bridge_pool_roots.rs | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs index b512798ce8..8571881453 100644 --- a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs +++ b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs @@ -196,11 +196,17 @@ mod test_apply_bp_roots_to_storage { use namada_core::ledger::storage_api::StorageRead; use namada_core::proto::{SignableEthMessage, Signed}; use namada_core::types::address; + use namada_core::types::dec::Dec; use namada_core::types::ethereum_events::Uint; use namada_core::types::keccak::{keccak_hash, KeccakHash}; + use namada_core::types::key::RefTo; use namada_core::types::storage::Key; use namada_core::types::token::Amount; use namada_core::types::vote_extensions::bridge_pool_roots; + use namada_proof_of_stake::parameters::PosParams; + use namada_proof_of_stake::{ + become_validator, bond_tokens, write_pos_params, BecomeValidator, + }; use super::*; use crate::protocol::transactions::votes::{ @@ -688,4 +694,127 @@ mod test_apply_bp_roots_to_storage { assert_eq!(proof.signatures, expected.signatures); assert_eq!(proof.data, expected.data); } + + /// Test that when we acquire a complete BP roots proof, + /// the block height stored in storage is that of the + /// tree root that was decided. + #[test] + fn test_bp_roots_across_epoch_boundaries() { + // the validators that will vote in the tally + let validator_1 = address::testing::established_address_1(); + let validator_1_stake = Amount::native_whole(100); + + let validator_2 = address::testing::established_address_2(); + let validator_2_stake = Amount::native_whole(100); + + let validator_3 = address::testing::established_address_3(); + let validator_3_stake = Amount::native_whole(100); + + // start epoch 0 with validator 1 + let (mut wl_storage, keys) = test_utils::setup_storage_with_validators( + HashMap::from([(validator_1.clone(), validator_1_stake)]), + ); + + // update the pos params + let params = PosParams { + pipeline_len: 1, + ..Default::default() + }; + write_pos_params(&mut wl_storage, params.clone()).expect("Test failed"); + + // insert validators 2 and 3 at epoch 1 + for (validator, stake) in [ + (&validator_2, validator_2_stake), + (&validator_3, validator_3_stake), + ] { + let keys = test_utils::TestValidatorKeys::generate(); + let consensus_key = &keys.consensus.ref_to(); + let eth_cold_key = &keys.eth_gov.ref_to(); + let eth_hot_key = &keys.eth_bridge.ref_to(); + become_validator(BecomeValidator { + storage: &mut wl_storage, + params: ¶ms, + address: validator, + consensus_key, + eth_cold_key, + eth_hot_key, + current_epoch: 0.into(), + commission_rate: Dec::new(5, 2).unwrap(), + max_commission_rate_change: Dec::new(1, 2).unwrap(), + }) + .expect("Test failed"); + bond_tokens(&mut wl_storage, None, validator, stake, 0.into()) + .expect("Test failed"); + } + + // query validators to make sure they were inserted correctly + macro_rules! query_validators { + () => { + |epoch: u64| { + wl_storage + .pos_queries() + .get_consensus_validators(Some(epoch.into())) + .iter() + .map(|validator| { + (validator.address, validator.bonded_stake) + }) + .collect::>() + } + }; + } + let query_validators = query_validators!(); + let epoch_0_validators = query_validators(0); + let epoch_1_validators = query_validators(1); + _ = query_validators; + assert_eq!( + epoch_0_validators, + HashMap::from([(validator_1.clone(), validator_1_stake)]) + ); + assert_eq!( + epoch_1_validators, + HashMap::from([ + (validator_1.clone(), validator_1_stake), + (validator_2, validator_2_stake), + (validator_3, validator_3_stake), + ]) + ); + + // set up the bridge pool's storage + bridge_pool_vp::init_storage(&mut wl_storage); + test_utils::commit_bridge_pool_root_at_height( + &mut wl_storage.storage, + &KeccakHash([1; 32]), + 3.into(), + ); + + // construct proof + let root = wl_storage.ethbridge_queries().get_bridge_pool_root(); + let nonce = wl_storage.ethbridge_queries().get_bridge_pool_nonce(); + let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); + let hot_key = &keys[&validator_1].eth_bridge; + let vext = bridge_pool_roots::Vext { + validator_addr: validator_1.clone(), + block_height: 3.into(), + sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig, + } + .sign(&keys[&validator_1].protocol); + + _ = apply_derived_tx(&mut wl_storage, vext.into()) + .expect("Test failed"); + + // query validator set of the proof + // (should be the one from epoch 0) + let (_, root_height) = wl_storage + .ethbridge_queries() + .get_signed_bridge_pool_root() + .expect("Test failed"); + let root_epoch = wl_storage + .pos_queries() + .get_epoch(root_height) + .expect("Test failed"); + + let query_validators = query_validators!(); + let root_epoch_validators = query_validators(root_epoch.0); + assert_eq!(epoch_0_validators, root_epoch_validators); + } } From ac5d3acf5cd92e9cb1cee11545a263a8dcb519a1 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 18 Aug 2023 09:07:14 +0100 Subject: [PATCH 85/94] Return recommendation data in `generate_recommendations` --- shared/src/ledger/eth_bridge/bridge_pool.rs | 77 ++++++++++++++++----- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 97e28eb3b2..0f3f5d3597 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -508,6 +508,23 @@ mod recommendations { cost: I256, } + /// Batch of recommended transfers to Ethereum that generate + /// a profit after a relay operation. + #[derive(Debug, Eq, PartialEq)] + struct RecommendedBatch { + /// Hashes of the recommended transfers to be relayed. + transfer_hashes: Vec, + /// Estimate of the total fees, measured in gwei, that will be paid + /// on Ethereum. + ethereum_gas_fees: Uint, + /// Net profitt in gwei, based on the conversion rates provided + /// to the algorithm. + net_profit: I256, + /// Gas fees paid by the transfers considered for relaying, + /// paid in various token types. + bridge_pool_gas_fees: HashMap, + } + /// Recommend the most economical batch of transfers to relay based /// on a conversion rate estimates from NAM to ETH and gas usage /// heuristics. @@ -600,13 +617,36 @@ mod recommendations { let max_gas = args.max_gas.map(Uint::from_u64).unwrap_or(uint::MAX_VALUE); let max_cost = args.gas.map(I256::from).unwrap_or_default(); + generate_recommendations( eligible, &args.conversion_table, validator_gas, max_gas, max_cost, - )?; + )? + .map( + |RecommendedBatch { + transfer_hashes, + ethereum_gas_fees, + net_profit, + bridge_pool_gas_fees, + }| { + println!("Recommended batch: {transfer_hashes:#?}"); + println!( + "Estimated Ethereum transaction gas (in gwei): \ + {ethereum_gas_fees}", + ); + println!("Estimated net profit (in gwei): {net_profit}"); + println!("Total fees: {bridge_pool_gas_fees:#?}"); + }, + ) + .unwrap_or_else(|| { + println!( + "Unable to find a recommendation satisfying the input \ + parameters." + ); + }); control_flow::proceed(()) } @@ -719,7 +759,7 @@ mod recommendations { validator_gas: Uint, max_gas: Uint, max_cost: I256, - ) -> Halt>> { + ) -> Halt> { let mut state = AlgorithState { profitable: true, feasible_region: false, @@ -774,19 +814,13 @@ mod recommendations { control_flow::proceed( if state.feasible_region && !recommendation.is_empty() { - println!("Recommended batch: {:#?}", recommendation); - println!( - "Estimated Ethereum transaction gas (in gwei): {}", - total_gas - ); - println!("Estimated net profit (in gwei): {}", -total_cost); - println!("Total fees: {total_fees:#?}"); - Some(recommendation) + Some(RecommendedBatch { + transfer_hashes: recommendation, + ethereum_gas_fees: total_gas, + net_profit: -total_cost, + bridge_pool_gas_fees: total_fees, + }) } else { - println!( - "Unable to find a recommendation satisfying the input \ - parameters." - ); None }, ) @@ -1017,7 +1051,8 @@ mod recommendations { I256::zero(), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } @@ -1035,7 +1070,8 @@ mod recommendations { I256::zero(), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } @@ -1052,7 +1088,8 @@ mod recommendations { I256(uint::MAX_SIGNED_VALUE), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } @@ -1073,7 +1110,8 @@ mod recommendations { I256::from(20_000), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } @@ -1091,7 +1129,8 @@ mod recommendations { I256::from(20_000), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } From b2a1e9520a70f055ec1f1e9d5e9a28d759828e1c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 18 Aug 2023 14:45:21 +0100 Subject: [PATCH 86/94] Proper Debug impl for I256 values --- core/src/types/uint.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index f7c390e3c4..3ba8754713 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -208,7 +208,6 @@ const MINUS_ZERO: Uint = Uint([0u64, 0u64, 0u64, 9223372036854775808]); #[derive( Copy, Clone, - Debug, Default, PartialEq, Eq, @@ -219,6 +218,13 @@ const MINUS_ZERO: Uint = Uint([0u64, 0u64, 0u64, 9223372036854775808]); )] pub struct I256(pub Uint); +impl fmt::Debug for I256 { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(self, f) + } +} + impl fmt::Display for I256 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_negative() { From eff97857d6e67870830581340edca33668d8e15a Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 18 Aug 2023 15:22:45 +0100 Subject: [PATCH 87/94] Add test_conversion_table_profit_margin() unit test --- shared/src/ledger/eth_bridge/bridge_pool.rs | 96 ++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 0f3f5d3597..2dcdc028f5 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -924,7 +924,9 @@ mod recommendations { } /// Helper function to test [`generate_eligible`]. - fn test_generate_eligible_aux(mut callback: F) + fn test_generate_eligible_aux( + mut callback: F, + ) -> Vec where F: FnMut(TestGenerateEligible<'_>), { @@ -956,6 +958,7 @@ mod recommendations { let eligible = generate_eligible(&table, &in_progress, signed_pool).proceed(); assert_eq!(eligible, expected); + eligible } /// Test the happy path of generating eligible recommendations @@ -1147,6 +1150,97 @@ mod recommendations { .proceed(); assert!(recommendation.is_none()) } + + /// Test the profit margin obtained from relaying two + /// Bridge pool transfers with two distinct token types, + /// whose relation is 1:2 in value. + #[test] + fn test_conversion_table_profit_margin() { + // apfel is worth twice as much as schnitzel + const APF_RATE: f64 = 5e8; + const SCH_RATE: f64 = 1e9; + const APFEL: &str = "APF"; + const SCHNITZEL: &str = "SCH"; + + let conversion_table = { + let mut t = HashMap::new(); + t.insert( + namada_core::types::address::apfel(), + args::BpConversionTableEntry { + alias: APFEL.into(), + conversion_rate: APF_RATE, + }, + ); + t.insert( + namada_core::types::address::schnitzel(), + args::BpConversionTableEntry { + alias: SCHNITZEL.into(), + conversion_rate: SCH_RATE, + }, + ); + t + }; + + let eligible = test_generate_eligible_aux(|ctx| { + ctx.conversion_table.clone_from(&conversion_table); + // tune the pending transfer provided by the ctx + let transfer_paid_in_apfel = { + let mut pending = ctx.pending.clone(); + pending.transfer.amount = 1.into(); + pending.gas_fee.token = + namada_core::types::address::apfel(); + pending + }; + let transfer_paid_in_schnitzel = { + let mut pending = ctx.pending.clone(); + pending.transfer.amount = 2.into(); + pending.gas_fee.token = + namada_core::types::address::schnitzel(); + pending + }; + // add the transfers to the pool, and expect them to + // be eligible transfers + for (pending, rate) in [ + (transfer_paid_in_apfel, APF_RATE), + (transfer_paid_in_schnitzel, SCH_RATE), + ] { + ctx.signed_pool.insert( + pending.keccak256().to_string(), + pending.clone(), + ); + ctx.expected_eligible.push(EligibleRecommendation { + transfer_hash: pending.keccak256().to_string(), + cost: transfer_fee() + - I256::from((1e9 / rate).floor() as u64) + * I256::try_from(pending.gas_fee.amount) + .expect("Test failed"), + pending_transfer: pending, + }); + } + }); + + const VALIDATOR_GAS_FEE: Uint = Uint::from_u64(100_000); + + let recommended_batch = generate_recommendations( + eligible, + &conversion_table, + // gas spent by validator signature checks + VALIDATOR_GAS_FEE, + // unlimited amount of gas + uint::MAX_VALUE, + // only profitable + I256::zero(), + ) + .proceed() + .expect("Test failed"); + + assert_eq!( + recommended_batch.net_profit, + I256::from(1_000_000_000_u64) + I256::from(2_000_000_000_u64) + - I256(VALIDATOR_GAS_FEE) + - transfer_fee() * I256::from(2_u64) + ); + } } } From 225c885aa44971b2ae98d6399d5ffae2f198817b Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 18 Aug 2023 15:30:11 +0100 Subject: [PATCH 88/94] Ignore invalid conversion rates --- shared/src/ledger/eth_bridge/bridge_pool.rs | 27 +++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 2dcdc028f5..829c868e27 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -700,17 +700,30 @@ mod recommendations { let conversion_rate = conversion_table .get(&pending.gas_fee.token) - .and_then(|entry| { - let rate = entry.conversion_rate; - if rate <= 0.0f64 { + .and_then(|entry| match entry.conversion_rate { + r if r == 0.0f64 => { eprintln!( - "Ignoring token with an invalid conversion \ - rate: {}", + "{}: Ignoring null conversion rate", pending.gas_fee.token, ); - return None; + None } - Some(rate) + r if r < 0.0f64 => { + eprintln!( + "{}: Ignoring negative conversion rate: {r:.1}", + pending.gas_fee.token, + ); + None + } + r if r > 1e9 => { + eprintln!( + "{}: Ignoring high conversion rate: {r:.1} > \ + 10^9", + pending.gas_fee.token, + ); + None + } + r => Some(r), })?; // This is the amount of gwei a single gas token is worth From c4e4819d6210f46fd66931643e0abd63b284e0de Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 14 Aug 2023 14:18:51 +0100 Subject: [PATCH 89/94] Add changelog for #1811 --- .../unreleased/improvements/1811-fix-bp-recommendations.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/1811-fix-bp-recommendations.md diff --git a/.changelog/unreleased/improvements/1811-fix-bp-recommendations.md b/.changelog/unreleased/improvements/1811-fix-bp-recommendations.md new file mode 100644 index 0000000000..4c718ef920 --- /dev/null +++ b/.changelog/unreleased/improvements/1811-fix-bp-recommendations.md @@ -0,0 +1,2 @@ +- Added various fee types to the output of the Bridge pool recommendations RPC + ([\#1811](https://github.com/anoma/namada/pull/1811)) \ No newline at end of file From 655601c64c848ce7c2a21ea4dc89d309693537f8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 25 Aug 2023 11:01:26 +0100 Subject: [PATCH 90/94] Split Bridge pool transfer hashes on all whitespace toks --- apps/src/lib/cli.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 08c60ec3b1..a5550f0481 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1994,7 +1994,7 @@ pub mod cmds { /// This can be used to relay transfers across the /// bridge to Ethereum. ConstructProof(ConstructProof), - /// Construct and relay a bridge pool proof to + /// Construct and relay a Bridge pool proof to /// Ethereum directly. RelayProof(RelayProof), /// Query the contents of the pool. @@ -2093,7 +2093,7 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) - .about("Add a new transfer to the Ethereum bridge pool.") + .about("Add a new transfer to the Ethereum Bridge pool.") .arg_required_else_help(true) .add_args::>() } @@ -2182,7 +2182,7 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) - .about("Get the contents of the Ethereum bridge pool.") + .about("Get the contents of the Ethereum Bridge pool.") .add_args::>() } } @@ -2202,7 +2202,7 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about( - "Get the contents of the Ethereum bridge pool with a \ + "Get the contents of the Ethereum Bridge pool with a \ signed Merkle root.", ) .add_args::>() @@ -3001,7 +3001,7 @@ pub mod args { Self { query, transfers: hashes - .split(' ') + .split_whitespace() .map(|hash| { KeccakHash::try_from(hash).unwrap_or_else(|_| { tracing::info!( @@ -3019,7 +3019,8 @@ pub mod args { fn def(app: App) -> App { app.add_args::>() .arg(HASH_LIST.def().help( - "List of Keccak hashes of transfers in the bridge pool.", + "Whitespace separated Keccak hash list of transfers in \ + the Bridge pool.", )) .arg( RELAYER @@ -3092,7 +3093,8 @@ pub mod args { ensure Ethereum transfers aren't canceled midway through.", )) .arg(HASH_LIST.def().help( - "List of Keccak hashes of transfers in the bridge pool.", + "Whitespace separated Keccak hash list of transfers in \ + the Bridge pool.", )) .arg( RELAYER From 9a5988cac7b9033ee8ea8cb709cc89731dde0f79 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 29 Aug 2023 09:41:43 +0100 Subject: [PATCH 91/94] Changelog for #1851 --- .changelog/unreleased/improvements/1851-bridge-pool-hashlist.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/1851-bridge-pool-hashlist.md diff --git a/.changelog/unreleased/improvements/1851-bridge-pool-hashlist.md b/.changelog/unreleased/improvements/1851-bridge-pool-hashlist.md new file mode 100644 index 0000000000..4ff7002035 --- /dev/null +++ b/.changelog/unreleased/improvements/1851-bridge-pool-hashlist.md @@ -0,0 +1,2 @@ +- Split Bridge pool transfer hashes on all whitespace toks + ([\#1851](https://github.com/anoma/namada/pull/1851)) \ No newline at end of file From ecc5e863b4119e6135d3918b1a049943952d2b90 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 22 Aug 2023 15:47:11 +0100 Subject: [PATCH 92/94] Update ethbridge-rs to v0.23.0 --- Cargo.lock | 25 ++++++++++++------------- Cargo.toml | 12 ++++++------ wasm/Cargo.lock | 20 ++++++++++---------- wasm_for_tests/wasm_source/Cargo.lock | 20 ++++++++++---------- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18e78ed504..2f2b3197b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,8 +1999,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2010,8 +2010,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -2021,19 +2021,18 @@ dependencies = [ [[package]] name = "ethbridge-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", "ethers", - "smallvec", ] [[package]] name = "ethbridge-governance-contract" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2043,8 +2042,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -2054,8 +2053,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethers", diff --git a/Cargo.toml b/Cargo.toml index 7375bb01e1..2398902368 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,12 +66,12 @@ directories = "4.0.1" ed25519-consensus = "1.2.0" escargot = "0.5.7" ethabi = "18.0.0" -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} -ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} -ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0" } +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0" } ethers = "2.0.0" expectrl = "0.7.0" eyre = "0.6.5" diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 0f4f1f4a39..2e083c3f46 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethers", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 6405e7100e..108e62cfad 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethers", From 305598e5a41f6dcef97e694e41f79e05839395a9 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 28 Aug 2023 15:17:10 +0100 Subject: [PATCH 93/94] Fix Ethereum oracle tests --- .../lib/node/ledger/ethereum_oracle/events.rs | 26 +++---- .../lib/node/ledger/ethereum_oracle/mod.rs | 18 ++--- .../ledger/ethereum_oracle/test_tools/mod.rs | 76 ++++++++++++++++--- 3 files changed, 85 insertions(+), 35 deletions(-) diff --git a/apps/src/lib/node/ledger/ethereum_oracle/events.rs b/apps/src/lib/node/ledger/ethereum_oracle/events.rs index 3472fcb601..bd11cf09e8 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/events.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/events.rs @@ -324,9 +324,9 @@ pub mod eth_events { TRANSFER_TO_ERC_CODEC, TRANSFER_TO_NAMADA_CODEC, VALIDATOR_SET_UPDATE_CODEC, }; - use namada::eth_bridge::ethers::abi::AbiEncode; use super::*; + use crate::node::ledger::ethereum_oracle::test_tools::event_log::GetLog; /// Test that for Ethereum events for which a custom number of /// confirmations may be specified, if a value lower than the @@ -350,7 +350,7 @@ pub mod eth_events { let pending_event = PendingEvent::decode( codec, arbitrary_block_height, - &get_log(event.encode()), + &event.get_log(), min_confirmations.clone(), )?; @@ -397,7 +397,10 @@ pub mod eth_events { ]; let raw: TransferToNamadaFilter = TRANSFER_TO_NAMADA_CODEC - .decode(&get_log(data)) + .decode(ðabi::RawLog { + topics: vec![TransferToNamadaFilter::signature()], + data, + }) .expect("Test failed") .try_into() .expect("Test failed"); @@ -433,7 +436,7 @@ pub mod eth_events { let pending_event = PendingEvent::decode( codec, arbitrary_block_height, - &get_log(event.encode()), + &event.get_log(), min_confirmations, ) .unwrap(); @@ -534,7 +537,7 @@ pub mod eth_events { { let decoded: TransferToNamadaFilter = TRANSFER_TO_NAMADA_CODEC - .decode(&get_log(nam_transfers.clone().encode())) + .decode(&nam_transfers.clone().get_log()) .expect("Test failed") .try_into() .expect("Test failed"); @@ -545,7 +548,7 @@ pub mod eth_events { assert_eq!( { let decoded: TransferToErcFilter = TRANSFER_TO_ERC_CODEC - .decode(&get_log(eth_transfers.clone().encode())) + .decode(ð_transfers.clone().get_log()) .expect("Test failed") .try_into() .expect("Test failed"); @@ -557,7 +560,7 @@ pub mod eth_events { { let decoded: ValidatorSetUpdateFilter = VALIDATOR_SET_UPDATE_CODEC - .decode(&get_log(update.clone().encode())) + .decode(&update.clone().get_log()) .expect("Test failed") .try_into() .expect("Test failed"); @@ -566,15 +569,6 @@ pub mod eth_events { update ); } - - /// Return an Ethereum events log, from the given encoded event - /// data. - fn get_log(data: Vec) -> ethabi::RawLog { - ethabi::RawLog { - data, - topics: vec![], - } - } } } diff --git a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs index fc9ae9f0d1..a8db91831c 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs @@ -565,7 +565,6 @@ mod test_oracle { use ethbridge_bridge_events::{ TransferToErcFilter, TransferToNamadaFilter, }; - use namada::eth_bridge::ethers::abi::AbiEncode; use namada::eth_bridge::ethers::types::H160; use namada::eth_bridge::structs::Erc20Transfer; use namada::types::address::testing::gen_established_address; @@ -575,6 +574,7 @@ mod test_oracle { use tokio::time::timeout; use super::*; + use crate::node::ledger::ethereum_oracle::test_tools::event_log::GetLog; use crate::node::ledger::ethereum_oracle::test_tools::mock_web3_client::{ event_signature, TestCmd, TestOracle, Web3Client, Web3Controller, }; @@ -716,11 +716,11 @@ mod test_oracle { valid_map: vec![], confirmations: 100.into(), } - .encode(); + .get_log(); let (sender, _) = channel(); controller.apply_cmd(TestCmd::NewEvent { event_type: event_signature::(), - data: new_event, + log: new_event, height: 101, seen: sender, }); @@ -766,11 +766,11 @@ mod test_oracle { valid_map: vec![], confirmations: 100.into(), } - .encode(); + .get_log(); let (sender, mut seen) = channel(); controller.apply_cmd(TestCmd::NewEvent { event_type: event_signature::(), - data: new_event, + log: new_event, height: 150, seen: sender, }); @@ -821,7 +821,7 @@ mod test_oracle { valid_map: vec![], confirmations: 100.into(), } - .encode(); + .get_log(); // confirmed after 125 blocks let gas_payer = gen_established_address(); @@ -836,20 +836,20 @@ mod test_oracle { relayer_address: gas_payer.to_string(), nonce: 0.into(), } - .encode(); + .get_log(); // send in the events to the logs let (sender, seen_second) = channel(); controller.apply_cmd(TestCmd::NewEvent { event_type: event_signature::(), - data: second_event, + log: second_event, height: 125, seen: sender, }); let (sender, _recv) = channel(); controller.apply_cmd(TestCmd::NewEvent { event_type: event_signature::(), - data: first_event, + log: first_event, height: 100, seen: sender, }); diff --git a/apps/src/lib/node/ledger/ethereum_oracle/test_tools/mod.rs b/apps/src/lib/node/ledger/ethereum_oracle/test_tools/mod.rs index dd948a4032..672a1546f8 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/test_tools/mod.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/test_tools/mod.rs @@ -1,5 +1,64 @@ pub mod events_endpoint; +#[cfg(test)] +pub mod event_log { + // praise be unto thee whom'st've read and understand this code + // p.s.: https://medium.com/mycrypto/understanding-event-logs-on-the-ethereum-blockchain-f4ae7ba50378 + + use ethbridge_bridge_events::{ + TransferToErcFilter, TransferToNamadaFilter, + }; + use ethbridge_governance_events::ValidatorSetUpdateFilter; + use namada::eth_bridge::ethers::abi::AbiEncode; + use namada::eth_bridge::ethers::contract::EthEvent; + + /// Get an [`ethabi::RawLog`] from a given Ethereum event. + pub trait GetLog { + /// Return an [`ethabi::RawLog`]. + fn get_log(self) -> ethabi::RawLog; + } + + impl GetLog for TransferToNamadaFilter { + fn get_log(self) -> ethabi::RawLog { + ethabi::RawLog { + topics: vec![Self::signature()], + data: self.encode(), + } + } + } + + impl GetLog for TransferToErcFilter { + fn get_log(self) -> ethabi::RawLog { + ethabi::RawLog { + topics: vec![Self::signature(), { + let mut buf = [0; 32]; + self.nonce.to_big_endian(&mut buf); + ethabi::ethereum_types::H256(buf) + }], + data: (self.transfers, self.valid_map, self.relayer_address) + .encode(), + } + } + } + + impl GetLog for ValidatorSetUpdateFilter { + fn get_log(self) -> ethabi::RawLog { + ethabi::RawLog { + topics: vec![Self::signature(), { + let mut buf = [0; 32]; + self.validator_set_nonce.to_big_endian(&mut buf); + ethabi::ethereum_types::H256(buf) + }], + data: ( + self.bridge_validator_set_hash, + self.governance_validator_set_hash, + ) + .encode(), + } + } + } +} + #[cfg(test)] pub mod mock_web3_client { use std::borrow::Cow; @@ -32,7 +91,7 @@ pub mod mock_web3_client { NewHeight(Uint256), NewEvent { event_type: MockEventType, - data: Vec, + log: ethabi::RawLog, height: u32, seen: Sender<()>, }, @@ -60,10 +119,10 @@ pub mod mock_web3_client { } TestCmd::NewEvent { event_type: ty, - data, + log, height, seen, - } => oracle.events.push((ty, data, height, seen)), + } => oracle.events.push((ty, log, height, seen)), } } } @@ -82,7 +141,7 @@ pub mod mock_web3_client { pub struct Web3ClientInner { active: bool, latest_block_height: Uint256, - events: Vec<(MockEventType, Vec, u32, Sender<()>)>, + events: Vec<(MockEventType, ethabi::RawLog, u32, Sender<()>)>, blocks_processed: UnboundedSender, last_block_processed: Option, } @@ -116,16 +175,13 @@ pub mod mock_web3_client { let mut logs = vec![]; let mut events = vec![]; std::mem::swap(&mut client.events, &mut events); - for (event_ty, data, height, seen) in events.into_iter() { + for (event_ty, log, height, seen) in events.into_iter() { if event_ty == ty && block_to_check >= Uint256::from(height) { seen.send(()).unwrap(); - logs.push(ethabi::RawLog { - data, - topics: vec![], - }); + logs.push(log); } else { - client.events.push((event_ty, data, height, seen)); + client.events.push((event_ty, log, height, seen)); } } if client.last_block_processed.as_ref() < Some(&block_to_check) From 9a0320c9fbced3d2a16a583e072ca651e7ca4ba0 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 29 Aug 2023 10:47:05 +0100 Subject: [PATCH 94/94] Changelog for #1852 --- .changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md diff --git a/.changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md b/.changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md new file mode 100644 index 0000000000..820642bd7a --- /dev/null +++ b/.changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md @@ -0,0 +1,2 @@ +- Fix the decoding of events observed by the Ethereum oracle + ([\#1852](https://github.com/anoma/namada/pull/1852)) \ No newline at end of file