diff --git a/.changelog/unreleased/features/1290-token-whitelist.md b/.changelog/unreleased/features/1290-token-whitelist.md new file mode 100644 index 0000000000..57bf9e61bb --- /dev/null +++ b/.changelog/unreleased/features/1290-token-whitelist.md @@ -0,0 +1,2 @@ +- Implement Ethereum token whitelist. + ([\#1290](https://github.com/anoma/namada/issues/1290)) \ No newline at end of file diff --git a/.changelog/unreleased/features/1781-cap-wnam.md b/.changelog/unreleased/features/1781-cap-wnam.md new file mode 100644 index 0000000000..aeba012c6b --- /dev/null +++ b/.changelog/unreleased/features/1781-cap-wnam.md @@ -0,0 +1,2 @@ +- Control the flow of NAM over the Ethereum bridge + ([\#1781](https://github.com/anoma/namada/pull/1781)) \ No newline at end of file diff --git a/.changelog/unreleased/features/1789-update-ethbridge-rs.md b/.changelog/unreleased/features/1789-update-ethbridge-rs.md new file mode 100644 index 0000000000..4fe862e522 --- /dev/null +++ b/.changelog/unreleased/features/1789-update-ethbridge-rs.md @@ -0,0 +1,2 @@ +- Update ethbridge-rs to v0.22.0 + ([\#1789](https://github.com/anoma/namada/pull/1789)) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3a069b0956..18e78ed504 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.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2010,8 +2010,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -2021,8 +2021,8 @@ dependencies = [ [[package]] name = "ethbridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", @@ -2032,8 +2032,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2043,8 +2043,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -2054,8 +2054,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethers", diff --git a/Cargo.toml b/Cargo.toml index 2d50daf0b6..7375bb01e1 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.22.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0" } ethers = "2.0.0" expectrl = "0.7.0" eyre = "0.6.5" diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 992abbef33..16fe028be3 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -87,9 +87,9 @@ derivative.workspace = true directories.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.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 1ed2275f2c..e7a34eba67 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2513,6 +2513,7 @@ pub mod args { pub const NET_ADDRESS: Arg = arg("net-address"); pub const NAMADA_START_TIME: ArgOpt = arg_opt("time"); pub const NO_CONVERSIONS: ArgFlag = flag("no-conversions"); + pub const NUT: ArgFlag = flag("nut"); pub const OUT_FILE_PATH_OPT: ArgOpt = arg_opt("out-file-path"); pub const OUTPUT_FOLDER_PATH: ArgOpt = arg_opt("output-folder-path"); @@ -2787,6 +2788,7 @@ pub mod args { impl CliToSdk> for EthereumBridgePool { fn to_sdk(self, ctx: &mut Context) -> EthereumBridgePool { EthereumBridgePool:: { + nut: self.nut, tx: self.tx.to_sdk(ctx), asset: self.asset, recipient: self.recipient, @@ -2809,6 +2811,7 @@ pub mod args { let fee_amount = FEE_AMOUNT.parse(matches).amount; let fee_payer = FEE_PAYER.parse(matches); let code_path = PathBuf::from(TX_BRIDGE_POOL_WASM); + let nut = NUT.parse(matches); Self { tx, asset, @@ -2818,6 +2821,7 @@ pub mod args { fee_amount, fee_payer, code_path, + nut, } } @@ -2852,6 +2856,10 @@ pub mod args { "The Namada address of the account paying the fee.", ), ) + .arg(NUT.def().help( + "Add Non Usable Tokens (NUTs) to the Bridge pool. These \ + are usually obtained from invalid transfers to Namada.", + )) } } diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index e9abf6c26d..2b920839f0 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -909,10 +909,13 @@ pub fn genesis( } #[cfg(any(test, feature = "dev"))] pub fn genesis(num_validators: u64) -> Genesis { - use namada::ledger::eth_bridge::{Contracts, UpgradeableContract}; + use namada::ledger::eth_bridge::{ + Contracts, Erc20WhitelistEntry, UpgradeableContract, + }; use namada::types::address::{ self, apfel, btc, dot, eth, kartoffel, nam, schnitzel, wnam, }; + use namada::types::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; use namada::types::ethereum_events::EthAddress; use crate::wallet; @@ -1115,6 +1118,13 @@ pub fn genesis(num_validators: u64) -> Genesis { gov_params: GovernanceParameters::default(), pgf_params: PgfParameters::default(), ethereum_bridge_params: Some(EthereumBridgeConfig { + erc20_whitelist: vec![Erc20WhitelistEntry { + token_address: DAI_ERC20_ETH_ADDRESS, + token_cap: token::DenominatedAmount { + amount: token::Amount::max(), + denom: 18.into(), + }, + }], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { diff --git a/apps/src/lib/node/ledger/ethereum_oracle/events.rs b/apps/src/lib/node/ledger/ethereum_oracle/events.rs index 28cd61f743..3472fcb601 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/events.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/events.rs @@ -7,16 +7,16 @@ 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, - TransferToNamada, Uint, + EthAddress, EthereumEvent, TransferToEthereum, TransferToNamada, Uint, }; + use namada::types::hash::Hash; use namada::types::keccak::KeccakHash; use namada::types::token::Amount; use num256::Uint256; @@ -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: _ }, @@ -178,24 +153,33 @@ pub mod eth_events { }; } - /// Trait to add parsing methods to foreign types. - trait Parse: Sized { - parse_method! { parse_eth_address -> EthAddress } - parse_method! { parse_address -> Address } - parse_method! { parse_amount -> Amount } - parse_method! { parse_u32 -> u32 } - parse_method! { parse_uint256 -> Uint } - parse_method! { parse_bool -> bool } - parse_method! { parse_string -> String } - parse_method! { parse_keccak -> KeccakHash } - parse_method! { parse_amount_array -> Vec } - parse_method! { parse_eth_address_array -> Vec } - parse_method! { parse_address_array -> Vec
} - parse_method! { parse_string_array -> Vec } - parse_method! { parse_transfer_to_namada_array -> Vec } - parse_method! { parse_transfer_to_namada -> TransferToNamada } - parse_method! { parse_transfer_to_eth_array -> Vec } - parse_method! { parse_transfer_to_eth -> TransferToEthereum } + macro_rules! trait_parse_def { + ($($name:ident -> $type:ty;)*) => { + /// Trait to add parsing methods to foreign types. + trait Parse: Sized { + $( parse_method!($name -> $type); )* + } + } + } + + trait_parse_def! { + parse_address -> Address; + parse_address_array -> Vec
; + parse_amount -> Amount; + parse_amount_array -> Vec; + parse_bool -> bool; + parse_eth_address -> EthAddress; + parse_eth_address_array -> Vec; + parse_hash -> Hash; + parse_keccak -> KeccakHash; + parse_string -> String; + parse_string_array -> Vec; + parse_transfer_to_eth -> TransferToEthereum; + parse_transfer_to_eth_array -> Vec; + parse_transfer_to_namada -> TransferToNamada; + parse_transfer_to_namada_array -> Vec; + parse_u32 -> u32; + parse_uint256 -> Uint; } impl Parse for ethabi::Address { @@ -217,7 +201,13 @@ pub mod eth_events { impl Parse for ethabi::Uint { fn parse_amount(self) -> Result { - Ok(Amount::from(self.as_u64())) + let uint = { + use namada::core::types::uint::Uint as NamadaUint; + let mut num_buf = [0; 32]; + self.to_little_endian(&mut num_buf); + NamadaUint::from_little_endian(&num_buf) + }; + Amount::from_uint(uint, 0).map_err(|e| Error::Decode(e.to_string())) } fn parse_u32(self) -> Result { @@ -239,6 +229,10 @@ pub mod eth_events { fn parse_keccak(self) -> Result { Ok(KeccakHash(self)) } + + fn parse_hash(self) -> Result { + Ok(Hash(self)) + } } impl Parse for Vec { @@ -298,17 +292,13 @@ pub mod eth_events { fn parse_transfer_to_eth(self) -> Result { let asset = self.from.parse_eth_address()?; let receiver = self.to.parse_eth_address()?; - let sender = self.sender.parse_address()?; let amount = self.amount.parse_amount()?; - let gas_payer = self.fee_from.parse_address()?; - let gas_amount = self.fee.parse_amount()?; + let checksum = self.namada_data_digest.parse_hash()?; Ok(TransferToEthereum { asset, amount, - sender, receiver, - gas_amount, - gas_payer, + checksum, }) } } @@ -332,7 +322,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; @@ -526,10 +516,8 @@ pub mod eth_events { ethereum_structs::Erc20Transfer { from: H160([1; 20]), to: H160([2; 20]), - sender: address.clone(), amount: 0u64.into(), - fee_from: address.clone(), - fee: 0u64.into(), + namada_data_digest: [0; 32], }; 2 ], @@ -542,11 +530,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 +565,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 1e967b12b4..fc9ae9f0d1 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs @@ -570,6 +570,7 @@ mod test_oracle { use namada::eth_bridge::structs::Erc20Transfer; use namada::types::address::testing::gen_established_address; use namada::types::ethereum_events::{EthAddress, TransferToEthereum}; + use namada::types::hash::Hash; use tokio::sync::oneshot::channel; use tokio::time::timeout; @@ -828,10 +829,8 @@ mod test_oracle { transfers: vec![Erc20Transfer { amount: 0.into(), from: H160([0; 20]), - sender: gas_payer.to_string(), to: H160([1; 20]), - fee: 0.into(), - fee_from: gas_payer.to_string(), + namada_data_digest: [0; 32], }], valid_map: vec![true], relayer_address: gas_payer.to_string(), @@ -897,10 +896,8 @@ mod test_oracle { TransferToEthereum { amount: Default::default(), asset: EthAddress([0; 20]), - sender: gas_payer.clone(), receiver: EthAddress([1; 20]), - gas_amount: Default::default(), - gas_payer: gas_payer.clone(), + checksum: Hash::default(), } ); } else { diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 8af2d1a746..f79c5cf421 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1038,9 +1038,7 @@ mod test_finalize_block { }; use namada::proto::{Code, Data, Section, Signature}; use namada::types::dec::POS_DECIMAL_PRECISION; - use namada::types::ethereum_events::{ - EthAddress, TransferToEthereum, Uint as ethUint, - }; + use namada::types::ethereum_events::{EthAddress, Uint as ethUint}; use namada::types::hash::Hash; use namada::types::keccak::KeccakHash; use namada::types::key::tm_consensus_key_raw_hash; @@ -1695,16 +1693,24 @@ mod test_finalize_block { } // write transfer to storage let transfer = { - use namada::core::types::eth_bridge_pool::PendingTransfer; - let transfer = TransferToEthereum { - amount: 10u64.into(), - asset, - receiver, - gas_amount: 10u64.into(), - sender: bertha.clone(), - gas_payer: bertha.clone(), + use namada::core::types::eth_bridge_pool::{ + GasFee, PendingTransfer, TransferToEthereum, + TransferToEthereumKind, + }; + let pending = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + amount: 10u64.into(), + asset, + recipient: receiver, + sender: bertha.clone(), + }, + gas_fee: GasFee { + amount: 10u64.into(), + payer: bertha.clone(), + }, }; - let pending = PendingTransfer::from(&transfer); + let transfer = (&pending).into(); shell .wl_storage .write(&bridge_pool::get_pending_key(&pending), pending) diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index c65fbb3f99..c5e89225e3 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -28,7 +28,7 @@ use std::rc::Rc; use borsh::{BorshDeserialize, BorshSerialize}; use namada::core::ledger::eth_bridge; -use namada::ledger::eth_bridge::{EthBridgeQueries, EthereumBridgeConfig}; +use namada::ledger::eth_bridge::{EthBridgeQueries, EthereumOracleConfig}; use namada::ledger::events::log::EventLog; use namada::ledger::events::Event; use namada::ledger::gas::BlockGasMeter; @@ -968,27 +968,16 @@ where ); return; } - let Some(config) = EthereumBridgeConfig::read(&self.wl_storage) else { - tracing::info!( - "Not starting oracle as the Ethereum bridge config couldn't be found in storage" - ); - return; - }; + let config = EthereumOracleConfig::read(&self.wl_storage).expect( + "The oracle config must be present in storage, since the \ + bridge is enabled", + ); let start_block = self .wl_storage .storage .ethereum_height .clone() - .unwrap_or_else(|| { - self.wl_storage - .read(ð_bridge::storage::eth_start_height_key()) - .expect( - "Failed to read Ethereum start height from storage", - ) - .expect( - "The Ethereum start height should be in storage", - ) - }); + .unwrap_or(config.eth_start_height); tracing::info!( ?start_block, "Found Ethereum height from which the Ethereum oracle should \ diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs index 891a403f90..cec4158940 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs @@ -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 _ => {} } @@ -468,6 +463,7 @@ mod test_vote_extensions { use namada::types::ethereum_events::{ EthAddress, EthereumEvent, TransferToEthereum, Uint, }; + use namada::types::hash::Hash; #[cfg(feature = "abcipp")] use namada::types::keccak::keccak_hash; #[cfg(feature = "abcipp")] @@ -600,10 +596,8 @@ mod test_vote_extensions { transfers: vec![TransferToEthereum { amount: 100.into(), asset: EthAddress([1; 20]), - sender: gen_established_address(), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -613,10 +607,8 @@ mod test_vote_extensions { transfers: vec![TransferToEthereum { amount: 100.into(), asset: EthAddress([1; 20]), - sender: gen_established_address(), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -664,10 +656,8 @@ mod test_vote_extensions { transfers: vec![TransferToEthereum { amount: 100.into(), asset: EthAddress([1; 20]), - sender: gen_established_address(), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -725,11 +715,9 @@ mod test_vote_extensions { nonce: 0.into(), transfers: vec![TransferToEthereum { amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -819,11 +807,9 @@ mod test_vote_extensions { nonce: 0.into(), transfers: vec![TransferToEthereum { amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -896,11 +882,9 @@ mod test_vote_extensions { nonce: 0.into(), transfers: vec![TransferToEthereum { amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -978,11 +962,9 @@ mod test_vote_extensions { nonce: 0.into(), transfers: vec![TransferToEthereum { amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), diff --git a/core/Cargo.toml b/core/Cargo.toml index 7c5addad7c..75ea0c64c2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -66,7 +66,7 @@ data-encoding.workspace = true derivative.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.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 5134094f3f..0c20c50ff7 100644 --- a/core/src/ledger/eth_bridge/storage/bridge_pool.rs +++ b/core/src/ledger/eth_bridge/storage/bridge_pool.rs @@ -415,7 +415,9 @@ mod test_bridge_pool_tree { use proptest::prelude::*; use super::*; - use crate::types::eth_bridge_pool::{GasFee, TransferToEthereum}; + use crate::types::eth_bridge_pool::{ + GasFee, TransferToEthereum, TransferToEthereumKind, + }; use crate::types::ethereum_events::EthAddress; /// An established user address for testing & development @@ -432,6 +434,7 @@ mod test_bridge_pool_tree { assert_eq!(tree.root().0, [0; 32]); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -458,6 +461,7 @@ mod test_bridge_pool_tree { for i in 0..2 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -485,6 +489,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -522,6 +527,7 @@ mod test_bridge_pool_tree { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -549,6 +555,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -579,6 +586,7 @@ mod test_bridge_pool_tree { fn test_parse_key() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -602,6 +610,7 @@ mod test_bridge_pool_tree { fn test_key_multiple_segments() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -637,6 +646,7 @@ mod test_bridge_pool_tree { let mut tree = BridgePoolTree::default(); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -655,6 +665,7 @@ mod test_bridge_pool_tree { ); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([0; 20]), @@ -686,6 +697,7 @@ mod test_bridge_pool_tree { fn test_single_leaf() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), sender: bertha_address(), recipient: EthAddress([0; 20]), @@ -714,6 +726,7 @@ mod test_bridge_pool_tree { for i in 0..2 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -743,6 +756,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -772,6 +786,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -799,6 +814,7 @@ mod test_bridge_pool_tree { for i in 0..2 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -826,6 +842,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -853,6 +870,7 @@ mod test_bridge_pool_tree { for i in 0..5 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -884,6 +902,7 @@ mod test_bridge_pool_tree { .into_iter() .map(|addr| PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress(addr), sender: bertha_address(), recipient: EthAddress(addr), diff --git a/core/src/ledger/eth_bridge/storage/mod.rs b/core/src/ledger/eth_bridge/storage/mod.rs index 0906be3d5d..b777caa265 100644 --- a/core/src/ledger/eth_bridge/storage/mod.rs +++ b/core/src/ledger/eth_bridge/storage/mod.rs @@ -1,5 +1,6 @@ //! Functionality for accessing the storage subspace pub mod bridge_pool; +pub mod whitelist; pub mod wrapped_erc20s; use super::ADDRESS; diff --git a/core/src/ledger/eth_bridge/storage/whitelist.rs b/core/src/ledger/eth_bridge/storage/whitelist.rs new file mode 100644 index 0000000000..77b0860a8c --- /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 0062dd50c9..36ce04141b 100644 --- a/core/src/ledger/eth_bridge/storage/wrapped_erc20s.rs +++ b/core/src/ledger/eth_bridge/storage/wrapped_erc20s.rs @@ -14,6 +14,11 @@ pub fn token(address: &EthAddress) -> Address { Address::Internal(InternalAddress::Erc20(*address)) } +/// Construct a NUT token address from an ERC20 address. +pub fn nut(address: &EthAddress) -> Address { + Address::Internal(InternalAddress::Nut(*address)) +} + /// Represents the type of a key relating to a wrapped ERC20 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] pub enum KeyType { diff --git a/core/src/types/address.rs b/core/src/types/address.rs index d587d20280..4f300c76ed 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -99,6 +99,8 @@ const PREFIX_INTERNAL: &str = "ano"; const PREFIX_IBC: &str = "ibc"; /// Fixed-length address strings prefix for Ethereum addresses. const PREFIX_ETH: &str = "eth"; +/// Fixed-length address strings prefix for Non-Usable-Token addresses. +const PREFIX_NUT: &str = "nut"; #[allow(missing_docs)] #[derive(Error, Debug)] @@ -234,6 +236,11 @@ impl Address { eth_addr.to_canonical().replace("0x", ""); format!("{}::{}", PREFIX_ETH, eth_addr) } + InternalAddress::Nut(eth_addr) => { + let eth_addr = + eth_addr.to_canonical().replace("0x", ""); + format!("{PREFIX_NUT}::{eth_addr}") + } InternalAddress::ReplayProtection => { internal::REPLAY_PROTECTION.to_string() } @@ -330,12 +337,18 @@ impl Address { "Invalid IBC internal address", )), }, - Some((PREFIX_ETH, raw)) => match string { + Some((prefix @ (PREFIX_ETH | PREFIX_NUT), raw)) => match string { _ if raw.len() == HASH_HEX_LEN => { match EthAddress::from_str(&format!("0x{}", raw)) { - Ok(eth_addr) => Ok(Address::Internal( - InternalAddress::Erc20(eth_addr), - )), + Ok(eth_addr) => Ok(match prefix { + PREFIX_ETH => Address::Internal( + InternalAddress::Erc20(eth_addr), + ), + PREFIX_NUT => Address::Internal( + InternalAddress::Nut(eth_addr), + ), + _ => unreachable!(), + }), Err(e) => Err(Error::new( ErrorKind::InvalidData, e.to_string(), @@ -543,6 +556,8 @@ pub enum InternalAddress { EthBridgePool, /// ERC20 token for Ethereum bridge Erc20(EthAddress), + /// Non-usable ERC20 tokens + Nut(EthAddress), /// Replay protection contains transactions' hash ReplayProtection, /// Multitoken @@ -566,6 +581,7 @@ impl Display for InternalAddress { Self::EthBridge => "EthBridge".to_string(), Self::EthBridgePool => "EthBridgePool".to_string(), Self::Erc20(eth_addr) => format!("Erc20: {}", eth_addr), + Self::Nut(eth_addr) => format!("Non-usable token: {eth_addr}"), Self::ReplayProtection => "ReplayProtection".to_string(), Self::Multitoken => "Multitoken".to_string(), Self::Pgf => "PublicGoodFundings".to_string(), @@ -861,6 +877,7 @@ pub mod testing { InternalAddress::EthBridge => {} InternalAddress::EthBridgePool => {} InternalAddress::Erc20(_) => {} + InternalAddress::Nut(_) => {} InternalAddress::ReplayProtection => {} InternalAddress::Pgf => {} InternalAddress::Multitoken => {} /* Add new addresses in the @@ -876,6 +893,7 @@ pub mod testing { Just(InternalAddress::EthBridge), Just(InternalAddress::EthBridgePool), Just(arb_erc20()), + Just(arb_nut()), Just(InternalAddress::ReplayProtection), Just(InternalAddress::Multitoken), Just(InternalAddress::Pgf), @@ -900,6 +918,13 @@ pub mod testing { fn arb_erc20() -> InternalAddress { use crate::types::ethereum_events::testing::arbitrary_eth_address; + // TODO: generate random erc20 addr data InternalAddress::Erc20(arbitrary_eth_address()) } + + fn arb_nut() -> InternalAddress { + use crate::types::ethereum_events::testing::arbitrary_eth_address; + // TODO: generate random erc20 addr data + InternalAddress::Nut(arbitrary_eth_address()) + } } diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index d70c55ab78..3a9b185e9f 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -1,21 +1,117 @@ //! The necessary type definitions for the contents of the //! Ethereum bridge pool +use std::borrow::Cow; + use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use ethabi::token::Token; use serde::{Deserialize, Serialize}; +use crate::ledger::eth_bridge::storage::wrapped_erc20s; use crate::types::address::Address; use crate::types::eth_abi::Encode; use crate::types::ethereum_events::{ EthAddress, TransferToEthereum as TransferToEthereumEvent, }; +use crate::types::hash::Hash as HashDigest; use crate::types::storage::{DbKeySeg, Key}; use crate::types::token::Amount; +/// A version used in our Ethereuem smart contracts +const VERSION: u8 = 1; + /// A namespace used in our Ethereuem smart contracts const NAMESPACE: &str = "transfer"; +/// Transfer to Ethereum kinds. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, +)] +pub enum TransferToEthereumKind { + /// Transfer ERC20 assets from Namada to Ethereum. + /// + /// These transfers burn wrapped ERC20 assets in Namada, once + /// they have been confirmed. + Erc20, + /// Refund non-usable tokens. + /// + /// These Bridge pool transfers should be crafted for assets + /// that have been transferred to Namada, that had either not + /// been whitelisted or whose token caps had been exceeded in + /// Namada at the time of the transfer. + Nut, +} + +/// Additional data appended to a [`TransferToEthereumEvent`] to +/// construct a [`PendingTransfer`]. +#[derive( + Debug, + Clone, + Hash, + PartialOrd, + PartialEq, + Ord, + Eq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + BorshSchema, +)] +pub struct PendingTransferAppendix<'transfer> { + /// The kind of the pending transfer to Ethereum. + pub kind: Cow<'transfer, TransferToEthereumKind>, + /// The sender of the transfer. + pub sender: Cow<'transfer, Address>, + /// The amount of gas fees (in NAM) + /// paid by the user sending this transfer + pub gas_fee: Cow<'transfer, GasFee>, +} + +impl From for PendingTransferAppendix<'static> { + #[inline] + fn from(pending: PendingTransfer) -> Self { + Self { + kind: Cow::Owned(pending.transfer.kind), + sender: Cow::Owned(pending.transfer.sender), + gas_fee: Cow::Owned(pending.gas_fee), + } + } +} + +impl<'t> From<&'t PendingTransfer> for PendingTransferAppendix<'t> { + #[inline] + fn from(pending: &'t PendingTransfer) -> Self { + Self { + kind: Cow::Borrowed(&pending.transfer.kind), + sender: Cow::Borrowed(&pending.transfer.sender), + gas_fee: Cow::Borrowed(&pending.gas_fee), + } + } +} + +impl<'transfer> PendingTransferAppendix<'transfer> { + /// Calculate the checksum of this [`PendingTransferAppendix`]. + pub fn checksum(&self) -> HashDigest { + let serialized = self + .try_to_vec() + .expect("Serializing a PendingTransferAppendix should not fail"); + HashDigest::sha256(serialized) + } +} + /// A transfer message to be submitted to Ethereum /// to move assets from Namada across the bridge. #[derive( @@ -33,6 +129,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,47 +165,95 @@ pub struct PendingTransfer { pub gas_fee: GasFee, } -impl From for ethbridge_structs::Erc20Transfer { - fn from(pending: PendingTransfer) -> Self { +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) + } + } + } + + /// Retrieve a reference to the appendix of this [`PendingTransfer`]. + #[inline] + pub fn appendix(&self) -> PendingTransferAppendix<'_> { + self.into() + } + + /// Retrieve the owned appendix of this [`PendingTransfer`]. + #[inline] + pub fn into_appendix(self) -> PendingTransferAppendix<'static> { + self.into() + } + + /// Craft a [`PendingTransfer`] from its constituents. + pub fn from_parts( + event: &TransferToEthereumEvent, + appendix: PendingTransferAppendix<'_>, + ) -> Self { + let transfer = TransferToEthereum { + kind: *appendix.kind, + asset: event.asset, + recipient: event.receiver, + sender: (*appendix.sender).clone(), + amount: event.amount, + }; + let gas_fee = (*appendix.gas_fee).clone(); + Self { transfer, gas_fee } + } +} + +impl From<&PendingTransfer> for ethbridge_structs::Erc20Transfer { + fn from(pending: &PendingTransfer) -> Self { + let HashDigest(namada_data_digest) = pending.appendix().checksum(); Self { from: pending.transfer.asset.0.into(), to: pending.transfer.recipient.0.into(), amount: pending.transfer.amount.into(), - fee_from: pending.gas_fee.payer.to_string(), - fee: pending.gas_fee.amount.into(), - sender: pending.transfer.sender.to_string(), + namada_data_digest, } } } -impl Encode<8> for PendingTransfer { - fn tokenize(&self) -> [Token; 8] { +impl From<&PendingTransfer> for TransferToEthereumEvent { + fn from(pending: &PendingTransfer) -> Self { + Self { + amount: pending.transfer.amount, + asset: pending.transfer.asset, + receiver: pending.transfer.recipient, + checksum: pending.appendix().checksum(), + } + } +} + +impl Encode<6> for PendingTransfer { + fn tokenize(&self) -> [Token; 6] { // TODO: This version should be looked up from storage - let version = Token::Uint(1.into()); + let version = Token::Uint(VERSION.into()); let namespace = Token::String(NAMESPACE.into()); let from = Token::Address(self.transfer.asset.0.into()); - let fee = Token::Uint(self.gas_fee.amount.into()); let to = Token::Address(self.transfer.recipient.0.into()); let amount = Token::Uint(self.transfer.amount.into()); - let fee_from = Token::String(self.gas_fee.payer.to_string()); - let sender = Token::String(self.transfer.sender.to_string()); - [version, namespace, from, to, amount, fee_from, fee, sender] + let checksum = Token::FixedBytes(self.appendix().checksum().0.into()); + [version, namespace, from, to, amount, checksum] } } -impl From<&TransferToEthereumEvent> for PendingTransfer { - fn from(event: &TransferToEthereumEvent) -> Self { - let transfer = TransferToEthereum { - asset: event.asset, - recipient: event.receiver, - sender: event.sender.clone(), - amount: event.amount, - }; - let gas_fee = GasFee { - amount: event.gas_amount, - payer: event.gas_payer.clone(), - }; - Self { transfer, gas_fee } +impl Encode<6> for TransferToEthereumEvent { + fn tokenize(&self) -> [Token; 6] { + // TODO: This version should be looked up from storage + let version = Token::Uint(VERSION.into()); + let namespace = Token::String(NAMESPACE.into()); + let from = Token::Address(self.asset.0.into()); + let to = Token::Address(self.receiver.0.into()); + let amount = Token::Uint(self.amount.into()); + let checksum = Token::FixedBytes(self.checksum.0.into()); + [version, namespace, from, to, amount, checksum] } } @@ -144,3 +290,30 @@ pub struct GasFee { /// The account of fee payer. pub payer: Address, } + +#[cfg(test)] +mod test_eth_bridge_pool_types { + use super::*; + use crate::types::address::testing::established_address_1; + + /// Test that [`PendingTransfer`] and [`TransferToEthereum`] + /// have the same keccak hash, after being ABI encoded. + #[test] + fn test_same_keccak_hash() { + let pending = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + amount: 10u64.into(), + asset: EthAddress([0xaa; 20]), + recipient: EthAddress([0xbb; 20]), + sender: established_address_1(), + }, + gas_fee: GasFee { + amount: 10u64.into(), + payer: established_address_1(), + }, + }; + let event: TransferToEthereumEvent = (&pending).into(); + assert_eq!(pending.keccak256(), event.keccak256()); + } +} diff --git a/core/src/types/ethereum_events.rs b/core/src/types/ethereum_events.rs index bab4d46bd2..f7097e9749 100644 --- a/core/src/types/ethereum_events.rs +++ b/core/src/types/ethereum_events.rs @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize}; use crate::types::address::Address; use crate::types::eth_abi::Encode; +use crate::types::ethereum_structs::Erc20Transfer; use crate::types::hash::Hash; use crate::types::keccak::KeccakHash; use crate::types::storage::{DbKeySeg, KeySeg}; @@ -334,16 +335,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 { @@ -398,36 +389,34 @@ pub struct TransferToEthereum { pub asset: EthAddress, /// The address receiving assets on Ethereum pub receiver: EthAddress, - /// The amount of fees (in NAM) - pub gas_amount: Amount, - /// The address sending assets to Ethereum. - pub sender: Address, - /// The account of fee payer. - pub gas_payer: Address, + /// Checksum of all Namada specific fields, including, + /// but not limited to, whether it is a NUT transfer, + /// the address of the sender, etc + /// + /// It serves to uniquely identify an event stored under + /// the Bridge pool, in Namada + pub checksum: Hash, } -/// struct for whitelisting a token from Ethereum. -/// Includes the address of issuing contract and -/// a cap on the max amount of this token allowed to be -/// held by the bridge. -#[derive( - Clone, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - BorshSerialize, - BorshDeserialize, - BorshSchema, -)] -#[allow(dead_code)] -pub struct TokenWhitelist { - /// Address of Ethereum smart contract issuing token - pub token: EthAddress, - /// Maximum amount of token allowed on the bridge - pub cap: Amount, +impl From for TransferToEthereum { + #[inline] + fn from(transfer: Erc20Transfer) -> Self { + Self { + amount: { + let uint = { + use crate::types::uint::Uint as NamadaUint; + let mut num_buf = [0; 32]; + transfer.amount.to_little_endian(&mut num_buf); + NamadaUint::from_little_endian(&num_buf) + }; + // this is infallible for a denom of 0 + Amount::from_uint(uint, 0).unwrap() + }, + asset: EthAddress(transfer.from.0), + receiver: EthAddress(transfer.to.0), + checksum: Hash(transfer.namada_data_digest), + } + } } #[cfg(test)] diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index 0f0a6032f0..a39089aa04 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -1254,7 +1254,6 @@ pub struct PrefixValue { pub struct EthEventsQueue { /// Queue of transfer to Namada events. pub transfers_to_namada: InnerEthEventsQueue, - // TODO: add queue of update whitelist events } /// A queue of confirmed Ethereum events of type `E`. diff --git a/ethereum_bridge/src/parameters.rs b/ethereum_bridge/src/parameters.rs index 4f3b2f1bc5..6395dd6cab 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 0052fb01b1..65c8f832ca 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -7,8 +7,7 @@ use borsh::BorshDeserialize; use eyre::{Result, WrapErr}; use namada_core::hints; use namada_core::ledger::eth_bridge::storage::bridge_pool::{ - get_nonce_key, get_pending_key, is_pending_transfer_key, - BRIDGE_POOL_ADDRESS, + get_nonce_key, is_pending_transfer_key, BRIDGE_POOL_ADDRESS, }; use namada_core::ledger::eth_bridge::storage::{ self as bridge_storage, wrapped_erc20s, @@ -19,7 +18,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 +31,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 +141,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 +177,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 +189,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( @@ -314,18 +369,21 @@ where for (event, is_valid) in transfers.iter().zip(valid_transfers.iter().copied()) { - let pending_transfer = event.into(); - let key = get_pending_key(&pending_transfer); - if hints::unlikely(!wl_storage.has_key(&key)?) { + let (pending_transfer, key) = if let Some((pending, key)) = + wl_storage.ethbridge_queries().lookup_transfer_to_eth(event) + { + (pending, key) + } else { + hints::cold(); unreachable!("The transfer should exist in the bridge pool"); - } + }; if hints::likely(is_valid) { tracing::debug!( ?pending_transfer, "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 +537,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 +555,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 +572,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| { @@ -540,6 +614,7 @@ mod tests { use assert_matches::assert_matches; use borsh::BorshSerialize; use eyre::Result; + use namada_core::ledger::eth_bridge::storage::bridge_pool::get_pending_key; use namada_core::ledger::parameters::{ update_epoch_parameter, EpochDuration, }; @@ -547,7 +622,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 +645,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 +655,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 +697,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 +728,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 +747,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 +794,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,42 +845,114 @@ mod tests { ); } - #[test] - /// Test acting on a single transfer and minting the first ever wDAI - fn test_act_on_transfers_to_namada_mints_wdai() { - let mut wl_storage = TestWlStorage::default(); - test_utils::bootstrap_ethereum_bridge(&mut wl_storage); - 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); + + 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(); + } - 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); + /// 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] @@ -810,27 +967,28 @@ 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 = pending_transfers.iter().map(get_pending_key).collect(); let relayer = gen_established_address("random"); - let mut transfers = vec![]; - for transfer in pending_transfers { - let transfer_to_eth = TransferToEthereum { - amount: transfer.transfer.amount, - asset: transfer.transfer.asset, - receiver: transfer.transfer.recipient, - gas_amount: transfer.gas_fee.amount, - gas_payer: transfer.gas_fee.payer, - sender: transfer.transfer.sender, - }; - transfers.push(transfer_to_eth); - } + let transfers: Vec<_> = pending_transfers + .iter() + .map(TransferToEthereum::from) + .collect(); let event = EthereumEvent::TransfersToEthereum { nonce: arbitrary_nonce(), valid_transfers_map: transfers.iter().map(|_| true).collect(), @@ -854,7 +1012,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 +1047,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 +1056,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 +1084,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 +1142,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 +1158,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 +1184,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 +1236,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,27 +1271,38 @@ 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); let (transfers, valid_transfers_map) = pending_transfers .into_iter() - .map(|transfer| { - let transfer_to_eth = TransferToEthereum { - amount: transfer.transfer.amount, - asset: transfer.transfer.asset, - receiver: transfer.transfer.recipient, - gas_amount: transfer.gas_fee.amount, - gas_payer: transfer.gas_fee.payer, - sender: transfer.transfer.sender, - }; + .map(|ref transfer| { + let transfer_to_eth: TransferToEthereum = transfer.into(); (transfer_to_eth, true) }) .unzip(); @@ -1106,6 +1325,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 +1338,50 @@ 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( + |event @ TransferToEthereum { asset, amount, .. }| { + if asset == &native_erc20 { + return None; + } + let kind = { + let (pending, _) = wl_storage + .ethbridge_queries() + .lookup_transfer_to_eth(event) + .expect("Test failed"); + pending.transfer.kind + }; + let erc20_token = match &kind { + eth_bridge_pool::TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(asset) + } + 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, + 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 +1397,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 +1455,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 +1480,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 d3cd32972d..c8bd21a4bf 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs @@ -330,6 +330,16 @@ mod tests { )]); let mut wl_storage = TestWlStorage::default(); test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + test_utils::whitelist_tokens( + &mut wl_storage, + [( + DAI_ERC20_ETH_ADDRESS, + test_utils::WhitelistMeta { + cap: Amount::max(), + denom: 18, + }, + )], + ); let changed_keys = apply_updates(&mut wl_storage, updates, voting_powers)?; @@ -405,6 +415,16 @@ mod tests { vec![(sole_validator.clone(), Amount::native_whole(100))], )); test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + test_utils::whitelist_tokens( + &mut wl_storage, + [( + DAI_ERC20_ETH_ADDRESS, + test_utils::WhitelistMeta { + cap: Amount::max(), + denom: 18, + }, + )], + ); let receiver = address::testing::established_address_1(); let event = EthereumEvent::TransfersToNamada { diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index 827745def0..c30de9a536 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -1,16 +1,19 @@ use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::hints; -use namada_core::ledger::eth_bridge::storage::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, bridge_pool, whitelist, }; use namada_core::ledger::storage; use namada_core::ledger::storage::{StoreType, WlStorage}; use namada_core::ledger::storage_api::StorageRead; use namada_core::types::address::Address; -use namada_core::types::ethereum_events::{EthAddress, GetEventNonce, Uint}; +use namada_core::types::eth_abi::Encode; +use namada_core::types::eth_bridge_pool::PendingTransfer; +use namada_core::types::ethereum_events::{ + EthAddress, GetEventNonce, TransferToEthereum, Uint, +}; use namada_core::types::keccak::KeccakHash; -use namada_core::types::storage::{BlockHeight, Epoch}; +use namada_core::types::storage::{BlockHeight, Epoch, Key as StorageKey}; use namada_core::types::token; use namada_core::types::vote_extensions::validator_set_update::{ EthAddrBook, ValidatorSetArgs, VotingPowersMap, VotingPowersMapExt, @@ -175,7 +178,7 @@ where &self .wl_storage .storage - .read(&get_nonce_key()) + .read(&bridge_pool::get_nonce_key()) .expect("Reading Bridge pool nonce shouldn't fail.") .0 .expect("Reading Bridge pool nonce shouldn't fail."), @@ -191,7 +194,7 @@ where .storage .db .read_subspace_val_with_height( - &get_nonce_key(), + &bridge_pool::get_nonce_key(), height, self.wl_storage.storage.get_last_block_height(), ) @@ -225,7 +228,7 @@ where self, ) -> Option<(BridgePoolRootProof, BlockHeight)> { self.wl_storage - .read_bytes(&get_signed_root_key()) + .read_bytes(&bridge_pool::get_signed_root_key()) .expect("Reading signed Bridge pool root shouldn't fail.") .map(|bytes| { BorshDeserialize::try_from_slice(&bytes).expect( @@ -392,6 +395,139 @@ 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(), + } + } + + /// Given a [`TransferToEthereum`] event, look-up the corresponding + /// [`PendingTransfer`]. + pub fn lookup_transfer_to_eth( + self, + transfer: &TransferToEthereum, + ) -> Option<(PendingTransfer, StorageKey)> { + let pending_key = bridge_pool::get_key_from_hash(&transfer.keccak256()); + self.wl_storage + .read(&pending_key) + .expect("Reading from storage should not fail") + .zip(Some(pending_key)) + } +} + +/// Number of tokens to mint after receiving a "transfer +/// 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 e74c4803e0..aec6463396 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 28711a0329..35c5156cac 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -100,8 +100,8 @@ clru.workspace = true data-encoding.workspace = true derivation-path.workspace = true derivative.workspace = true -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.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 2d426d5510..8d884b4a5f 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -701,6 +701,11 @@ pub struct RecommendBatch { /// A transfer to be added to the Ethereum bridge pool. #[derive(Clone, Debug)] pub struct EthereumBridgePool { + /// Whether the transfer is for a NUT. + /// + /// By default, we add wrapped ERC20s onto the + /// Bridge pool. + pub nut: bool, /// The args for building a tx to the bridge pool pub tx: Tx, /// The type of token diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 5b8a8eaa5e..9576b70baf 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -1,5 +1,6 @@ //! Bridge pool SDK functionality. +use std::borrow::Cow; use std::cmp::Ordering; use std::collections::HashMap; use std::io::Write; @@ -17,7 +18,9 @@ use super::{block_on_eth_sync, eth_sync_or_exit, BlockOnEthSync}; use crate::eth_bridge::ethers::abi::AbiDecode; use crate::eth_bridge::structs::RelayProof; use crate::ledger::args; -use crate::ledger::queries::{Client, RPC}; +use crate::ledger::queries::{ + Client, GenBridgePoolProofReq, GenBridgePoolProofRsp, RPC, +}; use crate::ledger::rpc::{query_wasm_code_hash, validate_amount}; use crate::ledger::tx::{prepare_tx, Error}; use crate::proto::Tx; @@ -28,7 +31,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 +42,7 @@ pub async fn build_bridge_pool_tx( client: &C, args::EthereumBridgePool { tx: tx_args, + nut, asset, recipient, sender, @@ -60,6 +64,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, @@ -173,9 +182,8 @@ where /// bridge pool. async fn construct_bridge_pool_proof( client: &C, - transfers: &[KeccakHash], - relayer: Address, -) -> Halt> + args: GenBridgePoolProofReq<'_, '_>, +) -> Halt where C: Client + Sync, { @@ -190,8 +198,8 @@ where .into_iter() .filter_map(|(ref transfer, voting_power)| { if voting_power > FractionalVotingPower::ONE_THIRD { - let hash = PendingTransfer::from(transfer).keccak256(); - transfers.contains(&hash).then_some(hash) + let hash = transfer.keccak256(); + args.transfers.contains(&hash).then_some(hash) } else { None } @@ -228,7 +236,7 @@ where } } - let data = (transfers, relayer).try_to_vec().unwrap(); + let data = args.try_to_vec().unwrap(); let response = RPC .shell() .eth_bridge() @@ -236,7 +244,7 @@ where .await; response.map(|response| response.data).try_halt(|e| { - println!("Encountered error constructing proof:\n{:?}", e); + println!("Encountered error constructing proof:\n{e}"); }) } @@ -259,25 +267,29 @@ pub async fn construct_proof( where C: Client + Sync, { - let bp_proof_bytes = construct_bridge_pool_proof( + let GenBridgePoolProofRsp { + abi_encoded_proof: bp_proof_bytes, + appendices, + } = construct_bridge_pool_proof( client, - &args.transfers, - args.relayer.clone(), + GenBridgePoolProofReq { + transfers: args.transfers.as_slice().into(), + relayer: Cow::Borrowed(&args.relayer), + with_appendix: true, + }, ) .await?; - let bp_proof: RelayProof = - AbiDecode::decode(&bp_proof_bytes).try_halt(|error| { - println!("Unable to decode the generated proof: {:?}", error); - })?; let resp = BridgePoolProofResponse { hashes: args.transfers, relayer_address: args.relayer, - total_fees: bp_proof - .transfers - .iter() - .map(|t| t.fee.as_u64()) - .sum::() - .into(), + total_fees: appendices + .map(|appendices| { + appendices + .into_iter() + .map(|app| app.gas_fee.amount) + .sum::() + }) + .unwrap_or(Amount::zero()), abi_encoded_proof: bp_proof_bytes, }; println!("{}", serde_json::to_string(&resp).unwrap()); @@ -310,9 +322,18 @@ where eth_sync_or_exit(&*eth_client).await?; } - let bp_proof = - construct_bridge_pool_proof(nam_client, &args.transfers, args.relayer) - .await?; + let GenBridgePoolProofRsp { + abi_encoded_proof: bp_proof, + .. + } = construct_bridge_pool_proof( + nam_client, + GenBridgePoolProofReq { + transfers: Cow::Owned(args.transfers), + relayer: Cow::Owned(args.relayer), + with_appendix: false, + }, + ) + .await?; let bridge = match RPC .shell() .eth_bridge() @@ -462,8 +483,7 @@ mod recommendations { .transfer_to_ethereum_progress(client) .await .unwrap() - .keys() - .map(PendingTransfer::from) + .into_keys() .collect::>(); // get the signed bridge pool root so we can analyze the signatures @@ -672,6 +692,7 @@ mod recommendations { pub fn transfer(gas_amount: u64) -> PendingTransfer { PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), recipient: EthAddress([2; 20]), sender: bertha_address(), diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 323633b563..056f41573b 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -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 85df785e79..250d51d1b5 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/mod.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/mod.rs @@ -3,4 +3,5 @@ //! pool. pub mod bridge_pool_vp; +pub mod nut; pub mod vp; diff --git a/shared/src/ledger/native_vp/ethereum_bridge/nut.rs b/shared/src/ledger/native_vp/ethereum_bridge/nut.rs new file mode 100644 index 0000000000..afea1da1d4 --- /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 5fd7aa6cd1..715b8ad616 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs @@ -393,6 +393,7 @@ mod tests { // a dummy config for testing let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index 7a0244932a..f3002ca723 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -11,6 +11,7 @@ use crate::ledger::gas::{self, BlockGasMeter, VpGasMeter}; use crate::ledger::governance::GovernanceVp; use crate::ledger::ibc::vp::Ibc; use crate::ledger::native_vp::ethereum_bridge::bridge_pool_vp::BridgePoolVp; +use crate::ledger::native_vp::ethereum_bridge::nut::NonUsableTokens; use crate::ledger::native_vp::ethereum_bridge::vp::EthBridge; use crate::ledger::native_vp::multitoken::MultitokenVp; use crate::ledger::native_vp::parameters::{self, ParametersVp}; @@ -72,6 +73,8 @@ pub enum Error { ReplayProtectionNativeVpError( crate::ledger::native_vp::replay_protection::Error, ), + #[error("Non usable tokens native VP error: {0}")] + NutNativeVpError(native_vp::ethereum_bridge::nut::Error), #[error("Access to an internal address {0} is forbidden")] AccessForbidden(InternalAddress), } @@ -585,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/mod.rs b/shared/src/ledger/queries/mod.rs index ce689e6325..bcd9fc8c27 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -12,6 +12,9 @@ pub use types::{ }; use vp::{Vp, VP}; +pub use self::shell::eth_bridge::{ + Erc20FlowControl, GenBridgePoolProofReq, GenBridgePoolProofRsp, +}; use super::storage::traits::StorageHasher; use super::storage::{DBIter, DB}; use super::storage_api; diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 94412f1a15..b1e243f74b 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -1,4 +1,4 @@ -mod eth_bridge; +pub(super) mod eth_bridge; use borsh::{BorshDeserialize, BorshSerialize}; use masp_primitives::asset_type::AssetType; diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index d7d0ed249e..0dbd76b376 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -1,23 +1,24 @@ //! Ethereum bridge related shell queries. +use std::borrow::Cow; use std::collections::HashMap; 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::{ self, CustomError, ResultExt, StorageRead, }; use namada_core::types::address::Address; +use namada_core::types::eth_bridge_pool::PendingTransferAppendix; use namada_core::types::ethereum_events::{ EthAddress, EthereumEvent, TransferToEthereum, }; 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, }; @@ -35,6 +36,7 @@ use namada_ethereum_bridge::storage::{ }; use namada_proof_of_stake::pos_queries::PosQueries; +use crate::eth_bridge::ethers::abi::AbiDecode; use crate::ledger::queries::{EncodedResponseQuery, RequestCtx, RequestQuery}; use crate::types::eth_abi::{Encode, EncodeCell}; use crate::types::eth_bridge_pool::PendingTransfer; @@ -42,7 +44,53 @@ use crate::types::keccak::KeccakHash; use crate::types::storage::Epoch; use crate::types::storage::MembershipProof::BridgePool; -pub type RelayProofBytes = Vec; +/// 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, +} + +/// Request data to pass to `generate_bridge_pool_proof`. +#[derive(Debug, Clone, Eq, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct GenBridgePoolProofReq<'transfers, 'relayer> { + /// The hashes of the transfers to be relayed. + pub transfers: Cow<'transfers, [KeccakHash]>, + /// The address of the relayer to compensate. + pub relayer: Cow<'relayer, Address>, + /// Whether to return the appendix of a [`PendingTransfer`]. + pub with_appendix: bool, +} + +/// Response data returned by `generate_bridge_pool_proof`. +#[derive(Debug, Clone, Eq, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct GenBridgePoolProofRsp { + /// Ethereum ABI encoded [`RelayProof`]. + pub abi_encoded_proof: Vec, + /// Appendix data of all requested pending transfers. + pub appendices: Option>>, +} + +impl GenBridgePoolProofRsp { + /// Retrieve all [`PendingTransfer`] instances returned from the RPC server. + pub fn pending_transfers(self) -> impl Iterator { + RelayProof::decode(&self.abi_encoded_proof) + .into_iter() + .flat_map(|proof| proof.transfers) + .zip(self.appendices.into_iter().flatten()) + .map(|(event, appendix)| { + let event: TransferToEthereum = event.into(); + PendingTransfer::from_parts(&event, appendix) + }) + } +} router! {ETH_BRIDGE, // Get the current contents of the Ethereum bridge pool @@ -57,12 +105,12 @@ router! {ETH_BRIDGE, // Generate a merkle proof for the inclusion of requested // transfers in the Ethereum bridge pool ( "pool" / "proof" ) - -> RelayProofBytes = (with_options generate_bridge_pool_proof), + -> GenBridgePoolProofRsp = (with_options generate_bridge_pool_proof), // Iterates over all ethereum events and returns the amount of // voting power backing each `TransferToEthereum` event. ( "pool" / "transfer_to_eth_progress" ) - -> HashMap + -> HashMap = transfer_to_ethereum_progress, // Request a proof of a validator set signed off for @@ -104,31 +152,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. @@ -273,8 +325,11 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - if let Ok((transfer_hashes, relayer)) = - <(Vec, Address)>::try_from_slice(request.data.as_slice()) + if let Ok(GenBridgePoolProofReq { + transfers: transfer_hashes, + relayer, + with_appendix, + }) = BorshDeserialize::try_from_slice(request.data.as_slice()) { // get the latest signed merkle root of the Ethereum bridge pool let (signed_root, height) = ctx @@ -318,14 +373,19 @@ where .into(), ))); } - let transfers = values - .iter() - .map(|bytes| { - PendingTransfer::try_from_slice(bytes) - .expect("Deserializing storage shouldn't fail") - .into() - }) - .collect(); + let (transfers, appendices) = values.iter().fold( + (vec![], vec![]), + |(mut transfers, mut appendices), bytes| { + let pending = PendingTransfer::try_from_slice(bytes) + .expect("Deserializing storage shouldn't fail"); + let eth_transfer = (&pending).into(); + if with_appendix { + appendices.push(pending.into_appendix()); + } + transfers.push(eth_transfer); + (transfers, appendices) + }, + ); // get the membership proof match tree.get_sub_tree_existence_proof( &keys, @@ -336,7 +396,7 @@ where .wl_storage .ethbridge_queries() .get_validator_set_args(None); - let data = RelayProof { + let relay_proof = RelayProof { validator_set_args: validator_args.into(), signatures: sort_sigs( &voting_powers, @@ -349,9 +409,13 @@ where batch_nonce: signed_root.data.1.into(), relayer_address: relayer.to_string(), }; - let data = ethers::abi::AbiEncode::encode(data) - .try_to_vec() - .expect("Serializing a relay proof should not fail."); + let rsp = GenBridgePoolProofRsp { + abi_encoded_proof: ethers::abi::AbiEncode::encode( + relay_proof, + ), + appendices: with_appendix.then_some(appendices), + }; + let data = rsp.try_to_vec().into_storage_result()?; Ok(EncodedResponseQuery { data, ..Default::default() @@ -372,7 +436,7 @@ where /// backing each `TransferToEthereum` event. fn transfer_to_ethereum_progress( ctx: RequestCtx<'_, D, H>, -) -> storage_api::Result> +) -> storage_api::Result> where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -427,6 +491,12 @@ where ) .average_voting_power(ctx.wl_storage); for transfer in transfers { + let key = get_key_from_hash(&transfer.keccak256()); + let transfer = ctx + .wl_storage + .read::(&key) + .into_storage_result()? + .expect("The transfer must be present in storage"); pending_events.insert(transfer, voting_power); } } @@ -566,6 +636,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 +648,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 +658,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 +858,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 +900,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 +961,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(), @@ -954,9 +1027,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - (vec![transfer.keccak256()], bertha_address()) - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -979,7 +1056,7 @@ mod test_ethbridge_router { let data = RelayProof { validator_set_args: validator_args.into(), signatures: sort_sigs(&voting_powers, &signed_root.signatures), - transfers: vec![transfer.into()], + transfers: vec![(&transfer).into()], pool_root: signed_root.data.0.0, proof: proof.proof.into_iter().map(|hash| hash.0).collect(), proof_flags: proof.flags, @@ -987,17 +1064,17 @@ mod test_ethbridge_router { relayer_address: bertha_address().to_string(), }; let proof = ethers::abi::AbiEncode::encode(data); - assert_eq!(proof, resp.data); + assert_eq!(proof, resp.data.abi_encoded_proof); } - /// Test if the merkle tree including a transfer - /// has had its root signed, then we cannot generate - /// a proof. + /// Test if the merkle tree including a transfer has not had its + /// root signed, then we cannot generate a proof. #[tokio::test] async fn test_cannot_get_proof() { let mut client = TestClient::new(RPC); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -1069,9 +1146,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - (vec![transfer2.keccak256()], bertha_address()) - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer2.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -1088,6 +1169,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 +1241,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(), @@ -1181,15 +1264,8 @@ mod test_ethbridge_router { ) .expect("Test failed"); - let event_transfer = - namada_core::types::ethereum_events::TransferToEthereum { - asset: transfer.transfer.asset, - receiver: transfer.transfer.recipient, - amount: transfer.transfer.amount, - gas_payer: transfer.gas_fee.payer.clone(), - gas_amount: transfer.gas_fee.amount, - sender: transfer.transfer.sender.clone(), - }; + let event_transfer: namada_core::types::ethereum_events::TransferToEthereum + = (&transfer).into(); let eth_event = EthereumEvent::TransfersToEthereum { nonce: Default::default(), transfers: vec![event_transfer.clone()], @@ -1250,10 +1326,8 @@ mod test_ethbridge_router { .transfer_to_ethereum_progress(&client) .await .unwrap(); - let expected: HashMap< - namada_core::types::ethereum_events::TransferToEthereum, - FractionalVotingPower, - > = [(event_transfer, voting_power)].into_iter().collect(); + let expected: HashMap = + [(transfer, voting_power)].into_iter().collect(); assert_eq!(expected, resp); } @@ -1269,6 +1343,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(), @@ -1329,9 +1404,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - vec![(transfer.keccak256(), bertha_address())] - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -1352,9 +1431,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - vec![transfer.keccak256()] - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -1364,45 +1447,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 +1458,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(&key, supply_amount) + .expect("Test failed"); + let key = whitelist::Key { + asset: ERC20_TOKEN, + suffix: whitelist::KeyType::Cap, + } + .into(); client .wl_storage - .write(&minted_balance_key(&token), amount) + .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 544889b2d1..956b2a1b8c 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 be65562b6d..0f4f1f4a39 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.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethers", diff --git a/wasm/wasm_source/src/tx_bridge_pool.rs b/wasm/wasm_source/src/tx_bridge_pool.rs index bf73e83f7d..c1a65403a7 100644 --- a/wasm/wasm_source/src/tx_bridge_pool.rs +++ b/wasm/wasm_source/src/tx_bridge_pool.rs @@ -1,7 +1,7 @@ //! A tx for adding a transfer request across the Ethereum bridge //! into the bridge pool. use borsh::{BorshDeserialize, BorshSerialize}; -use eth_bridge::storage::{bridge_pool, native_erc20_key, wrapped_erc20s}; +use eth_bridge::storage::{bridge_pool, native_erc20_key}; use eth_bridge_pool::{GasFee, PendingTransfer, TransferToEthereum}; use namada_tx_prelude::*; @@ -45,7 +45,7 @@ fn apply_tx(ctx: &mut Ctx, signed: Tx) -> TxResult { )?; } else { // Otherwise we escrow ERC20 tokens. - let token = wrapped_erc20s::token(&asset); + let token = transfer.token_address(); token::transfer( ctx, sender, diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 77b2082a4e..6405e7100e 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethers",