diff --git a/.changelog/unreleased/features/1290-token-whitelist.md b/.changelog/unreleased/features/1290-token-whitelist.md new file mode 100644 index 00000000000..57bf9e61bb1 --- /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 diff --git a/.changelog/unreleased/features/1781-cap-wnam.md b/.changelog/unreleased/features/1781-cap-wnam.md new file mode 100644 index 00000000000..aeba012c6b4 --- /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 diff --git a/Cargo.lock b/Cargo.lock index 3a069b0956e..d8131f58e9b 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.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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethers", diff --git a/Cargo.toml b/Cargo.toml index 2d50daf0b64..d524d0c7a24 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 992abbef332..16fe028be33 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.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/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 1ed2275f2c5..e7a34eba67a 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/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index e9abf6c26d9..2b920839f00 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/ethereum_oracle/events.rs b/apps/src/lib/node/ledger/ethereum_oracle/events.rs index 28cd61f7439..645b87d1c41 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/events.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/events.rs @@ -7,14 +7,14 @@ 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, + EthAddress, EthereumEvent, TransferToEthereum, TransferToEthereumKind, TransferToNamada, Uint, }; use namada::types::keccak::KeccakHash; @@ -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: _ }, @@ -180,6 +155,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 +174,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 +279,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 +287,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, @@ -332,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; @@ -524,6 +509,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(), @@ -542,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 = @@ -582,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/ethereum_oracle/mod.rs b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs index 1e967b12b44..05486aa11e7 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 8af2d1a7461..f8fb4fb0a45 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/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index c65fbb3f99f..c5e89225e37 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/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs index 891a403f90d..5282864e6d0 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 _ => {} } @@ -466,7 +461,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 +594,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 +608,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 +660,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 +723,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 +818,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 +896,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 +979,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/Cargo.toml b/core/Cargo.toml index 7c5addad7cc..75ea0c64c27 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.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/core/src/ledger/eth_bridge/storage/bridge_pool.rs b/core/src/ledger/eth_bridge/storage/bridge_pool.rs index 5134094f3f5..0c20c50ff78 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/ledger/eth_bridge/storage/mod.rs b/core/src/ledger/eth_bridge/storage/mod.rs index 0906be3d5da..b777caa2654 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 00000000000..77b0860a8c5 --- /dev/null +++ b/core/src/ledger/eth_bridge/storage/whitelist.rs @@ -0,0 +1,164 @@ +//! 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, + } +} + +#[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)); + } +} diff --git a/core/src/ledger/eth_bridge/storage/wrapped_erc20s.rs b/core/src/ledger/eth_bridge/storage/wrapped_erc20s.rs index 0062dd50c90..36ce04141ba 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 d587d202804..4f300c76ed9 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/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index d70c55ab783..c7394af167f 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -5,8 +5,10 @@ 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; use crate::types::ethereum_events::{ EthAddress, TransferToEthereum as TransferToEthereumEvent, }; @@ -33,6 +35,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 @@ -67,9 +71,25 @@ 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 { + 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 +118,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 bab4d46bd2b..b896674a0cd 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 { @@ -376,8 +366,9 @@ pub struct TransferToNamada { pub receiver: Address, } -/// An event transferring some kind of value from Namada to Ethereum +/// Transfer to Ethereum kinds. #[derive( + Copy, Clone, Debug, PartialEq, @@ -391,25 +382,45 @@ pub struct TransferToNamada { Serialize, Deserialize, )] -pub struct TransferToEthereum { - /// 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, +#[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; } -/// 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. +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, Debug, @@ -421,13 +432,24 @@ pub struct TransferToEthereum { BorshSerialize, BorshDeserialize, BorshSchema, + Serialize, + Deserialize, )] -#[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, +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, } #[cfg(test)] diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index 0f0a6032f00..a39089aa04e 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/parameters.rs b/ethereum_bridge/src/parameters.rs index 4f3b2f1bc52..6395dd6cabc 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, NATIVE_MAX_DECIMAL_PLACES}; 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,80 @@ 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 + { + 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, + } + .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 +295,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 +394,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 +420,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 +437,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 +446,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 +456,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 +478,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 +487,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/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 0052fb01b1b..d6408cc7cf7 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, @@ -30,7 +32,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,18 +142,33 @@ 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.should_mint_erc20s() { + tracing::info!( + "Minted wrapped ERC20s - (asset - {asset}, receiver - \ + {receiver}, amount - {})", + asset_count.erc20_amount.to_string_native(), + ); + } + if asset_count.should_mint_nuts() { + 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)? + redeem_native_token( + wl_storage, + &wrapped_native_erc20, + receiver, + amount, + )? }; changed_keys.append(&mut changed) } @@ -161,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> @@ -172,103 +190,141 @@ 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)); - 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", + ); + }, )?; + 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(), %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([ eth_bridge_native_token_balance_key, receiver_native_token_balance_key, + native_werc20_supply_key, ])) } +/// 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 + .should_mint_nuts() + .then(|| (wrapped_erc20s::nut(asset), asset_count.nut_amount)), + // check if we should mint erc20s + asset_count + .should_mint_erc20s() + .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( @@ -325,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, )?); @@ -479,7 +535,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) @@ -497,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> @@ -512,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 = wrapped_erc20s::token(&transfer.transfer.asset); + // other asset kinds must be burned let escrow_balance_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); update::amount(wl_storage, &escrow_balance_key, |balance| { @@ -547,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, @@ -570,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"); } @@ -582,20 +652,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, }, gas_fee: GasFee { amount: Amount::from(1), @@ -619,7 +694,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::>(), ) } @@ -639,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); @@ -658,7 +744,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 @@ -705,10 +791,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(), @@ -760,44 +842,116 @@ 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); - 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] /// When we act on an [`EthereumEvent::TransfersToEthereum`], test /// that pending transfers are deleted from the Bridge pool, the @@ -810,10 +964,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 = @@ -822,6 +985,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, @@ -854,7 +1018,20 @@ 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(&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(&get_nonce_key())); @@ -876,7 +1053,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) @@ -885,7 +1062,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] @@ -912,6 +1090,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), @@ -969,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"); @@ -985,7 +1164,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"); @@ -1011,27 +1190,45 @@ 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, &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 changed_keys = - redeem_native_token(&mut wl_storage, &receiver, &amount)?; + 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!( @@ -1045,6 +1242,23 @@ 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()) + ); + + // 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(()) } @@ -1063,13 +1277,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::Erc20), + ( + 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); @@ -1077,6 +1309,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, @@ -1106,6 +1339,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| { @@ -1118,29 +1352,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, @@ -1156,7 +1409,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)) @@ -1207,18 +1467,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 @@ -1230,4 +1492,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); + } } diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs index d3cd32972d6..c8bd21a4bf3 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 { diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index 827745def0c..08fdcab2fa1 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,126 @@ 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") + } + + /// 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, +} + +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 diff --git a/ethereum_bridge/src/test_utils.rs b/ethereum_bridge/src/test_utils.rs index e74c4803e03..aec64633964 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}; @@ -91,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 @@ -114,6 +117,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![] }; @@ -172,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/Cargo.toml b/shared/Cargo.toml index 28711a0329e..35c5156cac1 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.workspace = true +ethbridge-governance-contract.workspace = true ethers.workspace = true eyre.workspace = true futures.workspace = true diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 2d426d55103..8d884b4a5fe 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 5b8a8eaa5e7..940524d34f8 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}; @@ -39,6 +39,7 @@ pub async fn build_bridge_pool_tx( client: &C, args::EthereumBridgePool { tx: tx_args, + nut, asset, recipient, sender, @@ -60,6 +61,11 @@ pub async fn build_bridge_pool_tx( recipient, sender: sender.clone(), amount, + kind: if nut { + TransferToEthereumKind::Nut + } else { + TransferToEthereumKind::Erc20 + }, }, gas_fee: GasFee { amount: fee_amount, @@ -672,6 +678,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 323633b563f..056f41573bb 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,20 +14,21 @@ 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, }; +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::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}; use crate::proto::Tx; -use crate::types::address::{Address, InternalAddress}; -use crate::types::eth_bridge_pool::PendingTransfer; +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}; @@ -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,25 +85,33 @@ 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) - .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| { 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 @@ -92,7 +122,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) @@ -119,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(SignedAmount::Negative(debit)), - Some(SignedAmount::Positive(credit)), - ) => Ok(debit == expected_debit && credit == expected_credit), - (Some(SignedAmount::Positive(_)), _) => { + 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(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 @@ -156,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, @@ -188,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, }, @@ -221,6 +355,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, @@ -231,6 +366,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>, @@ -317,23 +453,23 @@ where } // check the escrowed assets if transfer.transfer.asset == wnam_address { - // 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 + }) } } @@ -347,6 +483,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; @@ -355,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; @@ -368,23 +505,36 @@ 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 { + /// The address of the Ethereum asset. + asset: EthAddress, + /// 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, + asset: ASSET, owner: address, - balance: 0.into(), + gas: 0.into(), token: 0.into(), } } @@ -396,6 +546,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") @@ -406,6 +572,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]), @@ -430,20 +597,57 @@ 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 + // 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, - 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()), + ); + // 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 } @@ -456,43 +660,86 @@ mod test_bridge_pool_vp { gas_delta: SignedAmount, token_delta: SignedAmount, ) -> BTreeSet { - // get the balance keys - let token_key = - balance_key(&wrapped_erc20s::token(&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, - } - .try_to_vec() - .expect("Test failed"); + // 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, + }; + + // 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, + // 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); + + // update the balance of nam + let new_gas_balance = match gas_delta { + SignedAmount::Positive(amount) => balance.gas + amount, + SignedAmount::Negative(amount) => balance.gas - amount, + }; + + // 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() } - .try_to_vec() - .expect("Test failed"); - - // 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"); - - // return the keys changed - [account_key, token_key].into() } /// Initialize some dummy storage for testing 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 { @@ -561,15 +808,16 @@ 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, sender: bertha_address(), recipient: EthAddress([1; 20]), @@ -582,14 +830,16 @@ 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( &mut wl_storage.write_log, Balance { + asset: transfer.transfer.asset, + kind: TransferToEthereumKind::Erc20, owner: bertha_address(), - balance: BERTHA_WEALTH.into(), + gas: BERTHA_WEALTH.into(), token: BERTHA_TOKENS.into(), }, payer_gas_delta, @@ -601,8 +851,10 @@ 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, - balance: ESCROWED_AMOUNT.into(), + gas: ESCROWED_AMOUNT.into(), token: ESCROWED_TOKENS.into(), }, gas_escrow_delta, @@ -642,11 +894,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, ); @@ -663,11 +915,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, ); @@ -684,11 +936,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, ); @@ -705,11 +957,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, ); @@ -727,11 +979,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, ); @@ -748,11 +1000,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, ); @@ -769,11 +1021,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, ); @@ -790,11 +1042,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, ); @@ -809,7 +1061,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, ); } @@ -826,6 +1078,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]), @@ -836,9 +1089,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, ); @@ -856,6 +1109,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]), @@ -868,7 +1122,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, ); @@ -885,12 +1139,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(), ]) }, @@ -925,8 +1179,10 @@ 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(), - balance: BERTHA_WEALTH.into(), + gas: BERTHA_WEALTH.into(), token: BERTHA_TOKENS.into(), }, SignedAmount::Negative(GAS_FEE.into()), @@ -938,8 +1194,10 @@ 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, - balance: ESCROWED_AMOUNT.into(), + gas: ESCROWED_AMOUNT.into(), token: ESCROWED_TOKENS.into(), }, SignedAmount::Positive(GAS_FEE.into()), @@ -977,6 +1235,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 +1302,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]), @@ -1091,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"); @@ -1130,6 +1392,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 +1499,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]), @@ -1316,4 +1580,148 @@ 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, + asset: ASSET, + 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, + asset: ASSET, + 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) + } + + /// 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, + ); + } } diff --git a/shared/src/ledger/native_vp/ethereum_bridge/mod.rs b/shared/src/ledger/native_vp/ethereum_bridge/mod.rs index 85df785e790..250d51d1b5d 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 00000000000..afea1da1d40 --- /dev/null +++ b/shared/src/ledger/native_vp/ethereum_bridge/nut.rs @@ -0,0 +1,239 @@ +//! 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) + } +} + +#[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)); + } + } +} diff --git a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs index 5fd7aa6cd18..715b8ad6161 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/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index 7a0244932a0..f3002ca7231 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,6 +588,15 @@ 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(_) => { // The address should be a part of a multitoken key diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index d7d0ed249e9..44ad306789d 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::wrapped_erc20s; 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 +16,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 +41,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 +117,35 @@ 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 ethbridge_queries = ctx.wl_storage.ethbridge_queries(); + + 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, + supply, + cap, + }) } /// Helper function to read a smart contract from storage. @@ -566,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; @@ -577,7 +595,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; @@ -588,7 +605,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 +805,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 +847,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 +908,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 +1018,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 +1109,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 +1181,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 +1206,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 +1293,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(), @@ -1364,45 +1389,9 @@ mod test_ethbridge_router { assert!(resp.is_err()); } - /// Test reading the wrapped NAM supply - #[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. + /// Test reading the supply and cap 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); @@ -1411,29 +1400,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 + ); } } diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index 544889b2d14..956b2a1b8cd 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -5,14 +5,15 @@ 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; 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}; @@ -29,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 { @@ -63,6 +65,10 @@ mod test_bridge_pool_vp { ..Default::default() }; let config = EthereumBridgeConfig { + erc20_whitelist: vec![Erc20WhitelistEntry { + token_address: wnam(), + token_cap: Amount::from_u64(TOKEN_CAP).native_denominated(), + }], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { @@ -91,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 { @@ -119,6 +132,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 +150,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(), @@ -149,10 +164,29 @@ 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 { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: wnam(), recipient: EthAddress([0; 20]), sender: bertha_address(), diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index be65562b6d4..a273155cd25 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.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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethers", diff --git a/wasm/wasm_source/src/tx_bridge_pool.rs b/wasm/wasm_source/src/tx_bridge_pool.rs index bf73e83f7d4..c1a65403a75 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, diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 77b2082a4e1..6d5a227eb47 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.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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethers",