Skip to content

Commit e9e8950

Browse files
committed
ethereum: warn if fee is higher than 10%
Only for standard transactions, where the fee and amount are of the same unit. In ERC20-txs, the fees are in ETH while the token is of a different unit.
1 parent eea8ff0 commit e9e8950

File tree

5 files changed

+92
-5
lines changed

5 files changed

+92
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
77
## Firmware
88

99
### [Unreleased]
10-
- Bitcoin: warn if the transaction fee is higher than 10% of the coins sent
10+
- Bitcoin, Ethereum: warn if the transaction fee is higher than 10% of the coins sent
1111
- ETH Testnets: add Goerli and remove deprecated Rinkeby and Ropsten
1212

1313
### 9.13.1

src/rust/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rust/bitbox02-rust/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ sha2 = { version = "0.9.2", default-features = false }
3535
sha3 = { version = "0.9.1", default-features = false, optional = true }
3636
zeroize = "1.5.5"
3737
num-bigint = { version = "0.4.3", default-features = false, optional = true }
38+
num-traits = { version = "0.2", default-features = false, optional = true }
3839
bip32-ed25519 = { git = "https://github.com/digitalbitbox/rust-bip32-ed25519", tag = "v0.1.0", optional = true }
3940
bs58 = { version = "0.4.0", default-features = false, features = ["alloc", "check"], optional = true }
4041
bech32 = { version = "0.8.1", default-features = false, optional = true }
@@ -70,6 +71,7 @@ app-ethereum = [
7071
# enable these dependencies
7172
"sha3",
7273
"num-bigint",
74+
"num-traits",
7375
# enable this feature in the deps
7476
"bitbox02/app-ethereum",
7577
]

src/rust/bitbox02-rust/src/hww/api/ethereum/amount.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
use alloc::string::String;
1616
use num_bigint::BigUint;
17+
use num_traits::{ToPrimitive, Zero};
1718

1819
pub struct Amount<'a> {
1920
pub unit: &'a str,
@@ -45,6 +46,15 @@ impl<'a> Amount<'a> {
4546
}
4647
}
4748

49+
/// Computes the percentage of the fee of the amount, up to one decimal point.
50+
/// Returns None if the amount is 0 or either fee or amount cannot be represented by `f64`.
51+
pub fn calculate_percentage(fee: &BigUint, amount: &BigUint) -> Option<f64> {
52+
if amount.is_zero() {
53+
return None;
54+
}
55+
Some(100. * fee.to_f64()? / amount.to_f64()?)
56+
}
57+
4858
#[cfg(test)]
4959
mod tests {
5060
use super::*;
@@ -136,4 +146,26 @@ mod tests {
136146
);
137147
}
138148
}
149+
150+
#[test]
151+
pub fn test_calculate_percentage() {
152+
let p = |f: u64, a: u64| calculate_percentage(&f.into(), &a.into());
153+
assert_eq!(p(1, 0), None);
154+
assert_eq!(p(3, 4), Some(75.));
155+
assert_eq!(p(0, 100), Some(0.));
156+
assert_eq!(p(1, 100), Some(1.));
157+
assert_eq!(p(9, 100), Some(9.));
158+
assert_eq!(p(10, 100), Some(10.));
159+
assert_eq!(p(99, 100), Some(99.));
160+
assert_eq!(p(909, 1000), Some(90.9));
161+
assert_eq!(
162+
calculate_percentage(
163+
// 63713280000000000
164+
&BigUint::from_bytes_be(b"\xe2\x5a\xe3\xfd\xe0\x00\x00"),
165+
// 530564000000000000
166+
&BigUint::from_bytes_be(b"\x07\x5c\xf1\x25\x9e\x9c\x40\x00"),
167+
),
168+
Some(12.008594627603833)
169+
);
170+
}
139171
}

src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use super::amount::Amount;
15+
use super::amount::{calculate_percentage, Amount};
1616
use super::params::Params;
1717
use super::pb;
1818
use super::Error;
@@ -174,9 +174,10 @@ async fn verify_standard_transaction(
174174
let total = Amount {
175175
unit: params.unit,
176176
decimals: WEI_DECIMALS,
177-
value: amount.value.add(&fee.value),
177+
value: (&amount.value).add(&fee.value),
178178
};
179-
transaction::verify_total_fee(&total.format(), &fee.format(), None).await?;
179+
let percentage = calculate_percentage(&fee.value, &amount.value);
180+
transaction::verify_total_fee(&total.format(), &fee.format(), percentage).await?;
180181
Ok(())
181182
}
182183

@@ -384,7 +385,58 @@ mod tests {
384385
);
385386
}
386387

387-
/// Standard ETH transaction on an unusual keypath (Goerly on mainnet keypath)
388+
/// Test a transaction with an unusually high fee.
389+
#[test]
390+
fn test_high_fee_warning() {
391+
const KEYPATH: &[u32] = &[44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0];
392+
393+
static mut UI_COUNTER: u32 = 0;
394+
mock(Data {
395+
ui_transaction_address_create: Some(Box::new(|_amount, _address| true)),
396+
ui_transaction_fee_create: Some(Box::new(|total, fee, longtouch| {
397+
assert_eq!(total, "0.59427728 ETH");
398+
assert_eq!(fee, "0.06371328 ETH");
399+
assert!(!longtouch);
400+
true
401+
})),
402+
ui_confirm_create: Some(Box::new(move |params| {
403+
match unsafe {
404+
UI_COUNTER += 1;
405+
UI_COUNTER
406+
} {
407+
1 => {
408+
assert_eq!(params.title, "High fee");
409+
assert_eq!(params.body, "The fee rate\nis 12.0%.\nProceed?");
410+
assert!(params.longtouch);
411+
true
412+
}
413+
_ => panic!("too many user confirmations"),
414+
}
415+
})),
416+
..Default::default()
417+
});
418+
mock_unlocked();
419+
assert!(block_on(process(&pb::EthSignRequest {
420+
coin: pb::EthCoin::Eth as _,
421+
keypath: KEYPATH.to_vec(),
422+
nonce: b"\x1f\xdc".to_vec(),
423+
// fee=gas_price*gas_limit=63713280000000000
424+
gas_price: b"\x01\x65\xa0\xbc\x00\x00".to_vec(),
425+
gas_limit: b"\xa2\x08".to_vec(),
426+
recipient:
427+
b"\x04\xf2\x64\xcf\x34\x44\x03\x13\xb4\xa0\x19\x2a\x35\x28\x14\xfb\xe9\x27\xb8\x85"
428+
.to_vec(),
429+
// 530564000000000000
430+
value: b"\x07\x5c\xf1\x25\x9e\x9c\x40\x00".to_vec(),
431+
data: b"".to_vec(),
432+
host_nonce_commitment: None,
433+
chain_id: 0,
434+
}))
435+
.is_ok());
436+
assert_eq!(unsafe { UI_COUNTER }, 1);
437+
}
438+
439+
/// Standard ETH transaction on an unusual keypath (Goerli on mainnet keypath)
388440
#[test]
389441
pub fn test_process_warn_unusual_keypath() {
390442
const KEYPATH: &[u32] = &[44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0];

0 commit comments

Comments
 (0)