From 67262e1561119a3083ed1b95547272ee4ac6dc90 Mon Sep 17 00:00:00 2001 From: Cmitchelle7 Date: Sat, 29 Aug 2026 10:31:07 -0700 Subject: [PATCH 1/2] fix: reject self-transfer in transfer() to match transfer_from (#84) --- contracts/eco-token/src/token.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/contracts/eco-token/src/token.rs b/contracts/eco-token/src/token.rs index 8de9c81..cd775d8 100644 --- a/contracts/eco-token/src/token.rs +++ b/contracts/eco-token/src/token.rs @@ -187,6 +187,7 @@ impl TokenContract { /// # Panics /// /// * Panics if `amount <= 0` + /// * Panics if `from == to` /// * Panics if `from` has insufficient balance /// /// # Auth @@ -200,6 +201,10 @@ impl TokenContract { panic!("token: amount must be positive"); } + if from == to { + panic!("token: cannot transfer to self"); + } + let from_balance = storage::read_balance(&e, &from); if from_balance < amount { panic!("token: insufficient balance"); @@ -1525,6 +1530,7 @@ mod test { } #[test] + #[should_panic(expected = "token: cannot transfer to self")] fn test_transfer_to_self() { let e = Env::default(); let admin = Address::generate(&e); @@ -1542,8 +1548,30 @@ mod test { e.mock_all_auths(); client.mint(&user, &1000); client.transfer(&user, &user, &500); + } - assert_eq!(client.balance(&user), 1000); + #[test] + fn test_transfer_valid_addresses_still_works() { + let e = Env::default(); + let admin = Address::generate(&e); + let from = Address::generate(&e); + let to = Address::generate(&e); + let contract_id = e.register(TokenContract, ()); + let client = TokenContractClient::new(&e, &contract_id); + + client.initialize( + &admin, + &String::from_str(&e, "ECO"), + &String::from_str(&e, "ECO"), + &7, + ); + + e.mock_all_auths(); + client.mint(&from, &1000); + client.transfer(&from, &to, &400); + + assert_eq!(client.balance(&from), 600); + assert_eq!(client.balance(&to), 400); } #[test] From 27da1ca9761239aad0f1eca5c6225f98d6389542 Mon Sep 17 00:00:00 2001 From: Cmitchelle7 Date: Sun, 30 Aug 2026 03:22:36 -0700 Subject: [PATCH 2/2] docs: add changelog entry for self-transfer fix in transfer() (#84) --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28d4313..bd56ff4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,14 @@ package version is bumped. - **[#67] Prevent self-transfer allowance drain in `transfer_from` and fix storage re-fetch TOCTOU in `spend_allowance`.** `transfer_from` now panics with `"token: cannot transfer to self"` when `from == to`, preventing spenders from burning an owner's allowance without transferring tokens. `spend_allowance` in `storage.rs` now accepts the `&Allowance` struct directly instead of re-fetching from persistent storage with `.unwrap()`, eliminating a potential TOCTOU window. +- **[#84] Reject self-transfer in `transfer` to match `transfer_from`.** + `transfer_from` already rejected `from == to` (see #67 above), but `transfer` + had no equivalent guard: a self-transfer silently succeeded as a no-op while + still consuming the caller's `require_auth()` signature — the same security- + model inconsistency #67 was fixed to prevent, just on the other entry point. + `transfer` now panics with `"token: cannot transfer to self"` when + `from == to`, matching `transfer_from`'s existing message exactly. + #### `reward-engine` - **[#71] Make `approve_proof` and the approve path of `resolve_dispute`