diff --git a/Cargo.lock b/Cargo.lock index aa03a7179..f8af963e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2981,6 +2981,7 @@ dependencies = [ "educe", "hex-literal", "num-traits", + "paste", "proptest", "rand 0.9.1", "ruint", diff --git a/Cargo.toml b/Cargo.toml index 152602797..ee3099c78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,6 +146,8 @@ syn = { version = "2.0.58", features = ["full"] } proc-macro2 = "1.0.79" quote = "1.0.35" +paste = "1.0.15" + motsu = "=0.10.0" # members diff --git a/contracts/src/access/control/extensions/enumerable.rs b/contracts/src/access/control/extensions/enumerable.rs index e12cad7d3..d52009fa4 100644 --- a/contracts/src/access/control/extensions/enumerable.rs +++ b/contracts/src/access/control/extensions/enumerable.rs @@ -190,6 +190,7 @@ mod tests { } } + #[cfg_attr(coverage_nightly, coverage(off))] #[public] impl IAccessControl for AccessControlEnumerableExample { type Error = control::Error; diff --git a/contracts/src/access/ownable.rs b/contracts/src/access/ownable.rs index a44a7d53c..44698cf99 100644 --- a/contracts/src/access/ownable.rs +++ b/contracts/src/access/ownable.rs @@ -283,6 +283,8 @@ impl IErc165 for Ownable { #[cfg(test)] mod tests { + use core::ops::{Deref, DerefMut}; + use motsu::prelude::*; use stylus_sdk::{alloy_primitives::Address, prelude::*}; @@ -295,7 +297,7 @@ mod tests { fn constructor(contract: Contract, alice: Address) { contract.sender(alice).constructor(alice).unwrap(); - let owner = contract.sender(alice).owner(); + let owner = IOwnable::owner(contract.sender(alice).deref()); assert_eq!(owner, alice); contract.assert_emitted(&OwnershipTransferred { @@ -326,9 +328,7 @@ mod tests { ) { contract.sender(alice).constructor(alice).unwrap(); - contract - .sender(alice) - .transfer_ownership(bob) + IOwnable::transfer_ownership(contract.sender(alice).deref_mut(), bob) .expect("should transfer ownership"); let owner = contract.sender(alice).owner(); assert_eq!(owner, bob); @@ -347,8 +347,19 @@ mod tests { ) { contract.sender(alice).constructor(bob).unwrap(); - let err = contract.sender(alice).transfer_ownership(bob).unwrap_err(); - assert!(matches!(err, Error::UnauthorizedAccount(_))); + let err = IOwnable::transfer_ownership( + contract.sender(alice).deref_mut(), + bob, + ) + .motsu_unwrap_err(); + + assert_eq!( + err, + Error::UnauthorizedAccount(OwnableUnauthorizedAccount { + account: alice + }) + .encode() + ); } #[motsu::test] @@ -358,12 +369,17 @@ mod tests { ) { contract.sender(alice).constructor(alice).unwrap(); - let err = contract - .sender(alice) - .transfer_ownership(Address::ZERO) - .unwrap_err(); + let err = IOwnable::transfer_ownership( + contract.sender(alice).deref_mut(), + Address::ZERO, + ) + .motsu_unwrap_err(); - assert!(matches!(err, Error::InvalidOwner(_))); + assert_eq!( + err, + Error::InvalidOwner(OwnableInvalidOwner { owner: Address::ZERO }) + .encode() + ); } #[motsu::test] @@ -373,10 +389,8 @@ mod tests { ) { contract.sender(alice).constructor(alice).unwrap(); - contract - .sender(alice) - .renounce_ownership() - .expect("should renounce ownership"); + IOwnable::renounce_ownership(contract.sender(alice).deref_mut()) + .motsu_expect("should renounce ownership"); let owner = contract.sender(alice).owner(); assert_eq!(owner, Address::ZERO); @@ -394,8 +408,17 @@ mod tests { ) { contract.sender(alice).constructor(bob).unwrap(); - let err = contract.sender(alice).renounce_ownership().unwrap_err(); - assert!(matches!(err, Error::UnauthorizedAccount(_))); + let err = + IOwnable::renounce_ownership(contract.sender(alice).deref_mut()) + .motsu_unwrap_err(); + + assert_eq!( + err, + Error::UnauthorizedAccount(OwnableUnauthorizedAccount { + account: alice + }) + .encode() + ); } #[motsu::test] diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index a36bb1521..e91882440 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -618,55 +618,61 @@ impl IErc165 for VestingWallet { #[cfg(test)] mod tests { - use motsu::prelude::Contract; + use motsu::prelude::*; use stylus_sdk::{ alloy_primitives::{uint, Address, U256, U64}, block, }; use super::*; - use crate::token::erc20::Erc20; + use crate::token::erc20::{Erc20, IErc20}; - const BALANCE: u64 = 1000; + const BALANCE: U256 = uint!(1000_U256); - const DURATION: u64 = 4 * 365 * 86400; // 4 years + const DURATION: U64 = uint!(126144000_U64); // 4 years - fn start() -> u64 { - block::timestamp() + 3600 // 1 hour - } - - impl VestingWallet { - fn init(&mut self, start: u64, duration: u64) -> (U64, U64) { - let start = U64::from(start); - let duration = U64::from(duration); - self.start.set(start); - self.duration.set(duration); - (start, duration) - } + fn start() -> U64 { + U64::from(block::timestamp() + 3600) // 1 hour } #[motsu::test] fn reads_start(contract: Contract, alice: Address) { - let (start, _) = contract.sender(alice).init(start(), DURATION); + let start = start(); + contract + .sender(alice) + .constructor(alice, start, DURATION) + .motsu_expect("should construct"); assert_eq!(U256::from(start), contract.sender(alice).start()); } #[motsu::test] fn reads_duration(contract: Contract, alice: Address) { - let (_, duration) = contract.sender(alice).init(0, DURATION); - assert_eq!(U256::from(duration), contract.sender(alice).duration()); + contract + .sender(alice) + .constructor(alice, U64::ZERO, DURATION) + .motsu_expect("should construct"); + assert_eq!(U256::from(DURATION), contract.sender(alice).duration()); } #[motsu::test] fn reads_end(contract: Contract, alice: Address) { - let (start, duration) = contract.sender(alice).init(start(), DURATION); + contract + .sender(alice) + .constructor(alice, start(), DURATION) + .motsu_expect("should construct"); - assert_eq!(U256::from(start + duration), contract.sender(alice).end()); + assert_eq!( + U256::from(start()) + U256::from(DURATION), + contract.sender(alice).end() + ); } #[motsu::test] fn reads_max_end(contract: Contract, alice: Address) { - contract.sender(alice).init(u64::MAX, u64::MAX); + contract + .sender(alice) + .constructor(alice, U64::MAX, U64::MAX) + .motsu_expect("should construct"); assert_eq!( U256::from(U64::MAX) + U256::from(U64::MAX), contract.sender(alice).end() @@ -678,7 +684,13 @@ mod tests { contract: Contract, alice: Address, ) { - let (start, duration) = contract.sender(alice).init(start(), DURATION); + let start = start(); + let duration = DURATION; + + contract + .sender(alice) + .constructor(alice, start, duration) + .motsu_expect("should construct"); let one = uint!(1_U256); let two = uint!(2_U256); @@ -710,7 +722,12 @@ mod tests { contract: Contract, alice: Address, ) { - let (start, _) = contract.sender(alice).init(start(), 0); + let start = start(); + + contract + .sender(alice) + .constructor(alice, start, U64::ZERO) + .motsu_expect("should construct"); let two = uint!(2_U256); @@ -731,24 +748,30 @@ mod tests { erc20: Contract, alice: Address, ) { - vesting_wallet.sender(alice).init(start(), DURATION); + vesting_wallet + .sender(alice) + .constructor(alice, start(), DURATION) + .motsu_expect("should construct"); erc20 .sender(alice) ._mint(vesting_wallet.address(), U256::from(BALANCE)) .unwrap(); let start = start(); - for i in 0..64 { - let timestamp = i * DURATION / 60 + start; + for i in 0..64_u64 { + let timestamp: u64 = + i * DURATION.to::() / 60 + start.to::(); let expected_amount = U256::from(std::cmp::min( BALANCE, - BALANCE * (timestamp - start) / DURATION, + BALANCE * (U256::from(timestamp) - U256::from(start)) + / U256::from(DURATION), )); let vested_amount = vesting_wallet .sender(alice) .vested_amount_erc20(erc20.address(), timestamp) .unwrap(); + assert_eq!( expected_amount, vested_amount, "\n---\ni: {i}\nstart: {start}\ntimestamp: {timestamp}\n---\n" @@ -777,4 +800,316 @@ mod tests { .sender(alice) .supports_interface(fake_interface_id.into())); } + + #[motsu::test] + fn released_initially_zero( + vesting_wallet: Contract, + erc20: Contract, + alice: Address, + ) { + // No constructor call, no state changes. + assert_eq!(U256::ZERO, vesting_wallet.sender(alice).released_eth()); + assert_eq!( + U256::ZERO, + vesting_wallet.sender(alice).released_erc20(erc20.address()) + ); + } + + #[motsu::test] + fn releasable_erc20_reverts_on_invalid_token( + contract: Contract, + invalid_token: Contract, + alice: Address, + ) { + contract + .sender(alice) + .constructor(alice, U64::ZERO, DURATION) + .motsu_expect("should construct"); + let err = contract + .sender(alice) + .releasable_erc20(invalid_token.address()) + .motsu_expect_err("should revert"); + assert!(matches!( + err, + Error::InvalidToken(InvalidToken { + token + }) if token == invalid_token.address() + )); + } + + #[storage] + struct InvalidTokenMock; + + unsafe impl TopLevelStorage for InvalidTokenMock {} + + #[public] + impl InvalidTokenMock {} + + #[motsu::test] + fn vested_amount_erc20_reverts_on_invalid_token( + contract: Contract, + invalid_token: Contract, + alice: Address, + ) { + contract + .sender(alice) + .constructor(alice, U64::ZERO, DURATION) + .motsu_expect("should construct"); + let err = contract + .sender(alice) + .vested_amount_erc20(invalid_token.address(), 0) + .motsu_expect_err("should revert"); + assert!(matches!( + err, + Error::InvalidToken(InvalidToken { + token + }) if token == invalid_token.address() + )); + } + + #[motsu::test] + fn release_erc20_transfers_all_and_emits_event( + vesting_wallet: Contract, + erc20: Contract, + alice: Address, + ) { + // Set owner and configure vesting to release all immediately. + vesting_wallet + .sender(alice) + .constructor(alice, U64::from(0), U64::from(0)) + .motsu_expect("should construct"); + + // Mint tokens to the vesting wallet. + erc20 + .sender(alice) + ._mint(vesting_wallet.address(), U256::from(BALANCE)) + .motsu_expect("should mint"); + + // Release ERC20 to owner (alice). + vesting_wallet + .sender(alice) + .release_erc20(erc20.address()) + .motsu_expect("should release"); + + // Owner received full balance. + assert_eq!(U256::from(BALANCE), erc20.sender(alice).balance_of(alice)); + // Contract holds no remaining tokens. + assert_eq!( + U256::ZERO, + erc20.sender(alice).balance_of(vesting_wallet.address()) + ); + // Released mapping increased and event emitted. + assert_eq!( + U256::from(BALANCE), + vesting_wallet.sender(alice).released_erc20(erc20.address()) + ); + vesting_wallet.assert_emitted(&ERC20Released { + token: erc20.address(), + amount: U256::from(BALANCE), + }); + } + + #[motsu::test] + #[should_panic = "scaled allocation exceeds `U256::MAX`"] + fn vesting_schedule_overflow_panics( + contract: Contract, + alice: Address, + ) { + // Configure a non-zero duration so we take the linear branch. + let start = start(); + let duration = U64::from(10); + + contract + .sender(alice) + .constructor(alice, start, duration) + .motsu_expect("should construct"); + + // Choose timestamp strictly between start and end so we hit the + // multiplication path: scaled_allocation = total * elapsed. + let mid = start + U64::from(2); // elapsed >= 2 + + // This should overflow: U256::MAX * elapsed > U256::MAX + contract.sender(alice).vesting_schedule(U256::MAX, mid); + } + + #[motsu::test] + fn receive_accepts_eth_and_increases_balance( + contract: Contract, + alice: Address, + ) { + // Construct and send ETH to the contract via receive. + contract + .sender(alice) + .constructor(alice, U64::ZERO, U64::ZERO) + .motsu_expect("should construct"); + + let value = U256::from(101); + alice.fund(value); + + let before_alice = alice.balance(); + let before_contract = contract.balance(); + + contract + .sender_and_value(alice, value) + .receive() + .motsu_expect("should receive ETH"); + + assert_eq!(before_alice - value, alice.balance()); + assert_eq!(before_contract + value, contract.balance()); + } + + #[motsu::test] + fn owner_works(contract: Contract, alice: Address) { + contract + .sender(alice) + .constructor(alice, U64::ZERO, U64::ZERO) + .motsu_expect("should construct"); + assert_eq!(alice, contract.sender(alice).owner()); + } + + #[motsu::test] + fn transfer_ownership_works( + contract: Contract, + alice: Address, + bob: Address, + ) { + contract + .sender(alice) + .constructor(alice, U64::ZERO, U64::ZERO) + .motsu_expect("should construct"); + + contract + .sender(alice) + .transfer_ownership(bob) + .motsu_expect("owner should transfer"); + assert_eq!(bob, contract.sender(alice).owner()); + } + + #[motsu::test] + fn renounce_ownership_works( + contract: Contract, + alice: Address, + ) { + contract + .sender(alice) + .constructor(alice, U64::ZERO, U64::ZERO) + .motsu_expect("should construct"); + contract + .sender(alice) + .renounce_ownership() + .motsu_expect("owner should renounce"); + assert_eq!(Address::ZERO, contract.sender(alice).owner()); + } + + #[motsu::test] + fn releasable_eth_full_when_zero_duration( + contract: Contract, + alice: Address, + ) { + // Configure immediate vesting, deposit ETH, expect all releasable. + contract + .sender(alice) + .constructor(alice, U64::ZERO, U64::ZERO) + .motsu_expect("should construct"); + + let value = U256::from(BALANCE); + alice.fund(value); + contract + .sender_and_value(alice, value) + .receive() + .motsu_expect("should receive ETH"); + + assert_eq!(value, contract.sender(alice).releasable_eth()); + } + + #[storage] + struct PayableReceiver; + + unsafe impl TopLevelStorage for PayableReceiver {} + + #[public] + impl PayableReceiver { + #[receive] + fn receive(&mut self) -> Result<(), Vec> { + Ok(()) + } + } + + #[motsu::test] + fn release_eth_transfers_and_emits( + contract: Contract, + alice: Address, + receiver: Contract, + ) { + // Immediate vesting: all ETH becomes releasable and is sent to owner. + contract + .sender(alice) + .constructor(alice, U64::ZERO, U64::ZERO) + .motsu_expect("should construct"); + + // Transfer ownership to a payable receiver contract so the low-level + // ETH transfer succeeds in motsu unit tests. + contract + .sender(alice) + .transfer_ownership(receiver.address()) + .motsu_expect("should transfer ownership to receiver"); + + let value = U256::from(BALANCE); + alice.fund(value); + + contract + .sender_and_value(alice, value) + .receive() + .motsu_expect("should receive ETH"); + + let before_receiver = receiver.balance(); + let before_contract = contract.balance(); + + contract.sender(alice).release_eth().motsu_expect("should release ETH"); + + let released = contract.sender(alice).released_eth(); + assert_eq!(released, value); + assert_eq!(before_receiver + released, receiver.balance()); + assert_eq!(before_contract - released, contract.balance()); + + contract.assert_emitted(&EtherReleased { amount: released }); + } + + #[motsu::test] + fn check_vested_amount_eth( + contract: Contract, + alice: Address, + ) { + // Linear vesting for ETH mirrors ERC20 test logic. + let start_ts = start(); + contract + .sender(alice) + .constructor(alice, start_ts, DURATION) + .motsu_expect("should construct"); + + // Deposit ETH into the contract. + let value = U256::from(BALANCE); + alice.fund(value); + contract + .sender_and_value(alice, value) + .receive() + .motsu_expect("should receive ETH"); + + for i in 0..64_u64 { + let timestamp: u64 = + i * DURATION.to::() / 60 + start_ts.to::(); + let expected_amount = U256::from(std::cmp::min( + BALANCE, + BALANCE * (U256::from(timestamp) - U256::from(start_ts)) + / U256::from(DURATION), + )); + + let vested_amount = + contract.sender(alice).vested_amount_eth(timestamp); + assert_eq!( + expected_amount, vested_amount, + "\n---\ni: {i}\nstart: {start_ts}\ntimestamp: {timestamp}\n---\n", + ); + } + } } diff --git a/contracts/src/proxy/beacon/proxy.rs b/contracts/src/proxy/beacon/proxy.rs index eb0390d8f..f6af8fd05 100644 --- a/contracts/src/proxy/beacon/proxy.rs +++ b/contracts/src/proxy/beacon/proxy.rs @@ -92,7 +92,6 @@ unsafe impl IProxy for BeaconProxy { #[cfg(test)] mod tests { - use alloy_sol_macro::sol; use alloy_sol_types::{SolCall, SolError, SolValue}; use motsu::prelude::*; use stylus_sdk::{ @@ -103,8 +102,11 @@ mod tests { use super::*; use crate::{ - proxy::beacon::IBeacon, - token::erc20::{self, Erc20, IErc20}, + proxy::{ + beacon::IBeacon, + tests::{Erc20Example, IERC20}, + }, + token::erc20, }; #[entrypoint] @@ -161,76 +163,6 @@ mod tests { } } - #[storage] - struct Erc20Example { - erc20: Erc20, - } - - #[public] - #[implements(IErc20)] - impl Erc20Example { - fn mint( - &mut self, - to: Address, - value: U256, - ) -> Result<(), erc20::Error> { - self.erc20._mint(to, value) - } - } - - unsafe impl TopLevelStorage for Erc20Example {} - - #[public] - impl IErc20 for Erc20Example { - type Error = erc20::Error; - - fn balance_of(&self, account: Address) -> U256 { - self.erc20.balance_of(account) - } - - fn total_supply(&self) -> U256 { - self.erc20.total_supply() - } - - fn transfer( - &mut self, - to: Address, - value: U256, - ) -> Result { - self.erc20.transfer(to, value) - } - - fn transfer_from( - &mut self, - from: Address, - to: Address, - value: U256, - ) -> Result { - self.erc20.transfer_from(from, to, value) - } - - fn allowance(&self, owner: Address, spender: Address) -> U256 { - self.erc20.allowance(owner, spender) - } - - fn approve( - &mut self, - spender: Address, - value: U256, - ) -> Result { - self.erc20.approve(spender, value) - } - } - - sol! { - interface IERC20 { - function balanceOf(address account) external view returns (uint256); - function totalSupply() external view returns (uint256); - function mint(address to, uint256 value) external; - function transfer(address to, uint256 value) external returns (bool); - } - } - #[motsu::test] fn constructs( proxy: Contract, diff --git a/contracts/src/proxy/beacon/upgradeable.rs b/contracts/src/proxy/beacon/upgradeable.rs index 6def9028e..314c7a23b 100644 --- a/contracts/src/proxy/beacon/upgradeable.rs +++ b/contracts/src/proxy/beacon/upgradeable.rs @@ -223,12 +223,12 @@ mod tests { use stylus_sdk::alloy_primitives::Address; use super::*; - use crate::{proxy::beacon::IBeacon, token::erc20::Erc20}; + use crate::proxy::{beacon::IBeacon, tests::Erc20Example}; #[motsu::test] fn constructor( beacon: Contract, - erc20: Contract, + erc20: Contract, alice: Address, ) { beacon.sender(alice).constructor(erc20.address(), alice).unwrap(); @@ -267,7 +267,7 @@ mod tests { #[motsu::test] fn constructor_with_zero_owner( beacon: Contract, - erc20: Contract, + erc20: Contract, alice: Address, ) { let err = beacon @@ -304,8 +304,8 @@ mod tests { #[motsu::test] fn upgrade_to_valid_implementation( beacon: Contract, - erc20: Contract, - erc20_2: Contract, + erc20: Contract, + erc20_2: Contract, alice: Address, ) { beacon.sender(alice).constructor(erc20.address(), alice).unwrap(); @@ -329,7 +329,7 @@ mod tests { #[motsu::test] fn upgrade_to_invalid_implementation( beacon: Contract, - erc20: Contract, + erc20: Contract, alice: Address, ) { beacon.sender(alice).constructor(erc20.address(), alice).unwrap(); @@ -359,8 +359,8 @@ mod tests { #[motsu::test] fn upgrade_to_unauthorized( beacon: Contract, - erc20: Contract, - erc20_2: Contract, + erc20: Contract, + erc20_2: Contract, alice: Address, bob: Address, ) { @@ -390,7 +390,7 @@ mod tests { #[motsu::test] fn upgrade_to_same_implementation( beacon: Contract, - erc20: Contract, + erc20: Contract, alice: Address, ) { beacon.sender(alice).constructor(erc20.address(), alice).unwrap(); @@ -414,7 +414,7 @@ mod tests { #[motsu::test] fn upgrade_to_zero_address( beacon: Contract, - erc20: Contract, + erc20: Contract, alice: Address, ) { beacon.sender(alice).constructor(erc20.address(), alice).unwrap(); @@ -443,9 +443,9 @@ mod tests { #[motsu::test] fn multiple_upgrades_emit_events( beacon: Contract, - erc20: Contract, - erc20_2: Contract, - erc20_3: Contract, + erc20: Contract, + erc20_2: Contract, + erc20_3: Contract, alice: Address, ) { beacon.sender(alice).constructor(erc20.address(), alice).unwrap(); @@ -476,8 +476,8 @@ mod tests { #[motsu::test] fn transfer_ownership( beacon: Contract, - erc20: Contract, - erc20_2: Contract, + erc20: Contract, + erc20_2: Contract, alice: Address, bob: Address, ) { @@ -515,7 +515,7 @@ mod tests { #[motsu::test] fn transfer_ownership_to_zero_address( beacon: Contract, - erc20: Contract, + erc20: Contract, alice: Address, ) { beacon.sender(alice).constructor(erc20.address(), alice).unwrap(); @@ -543,7 +543,7 @@ mod tests { #[motsu::test] fn transfer_ownership_unauthorized( beacon: Contract, - erc20: Contract, + erc20: Contract, alice: Address, bob: Address, charlie: Address, @@ -572,8 +572,8 @@ mod tests { #[motsu::test] fn renounce_ownership( beacon: Contract, - erc20: Contract, - erc20_2: Contract, + erc20: Contract, + erc20_2: Contract, alice: Address, ) { beacon.sender(alice).constructor(erc20.address(), alice).unwrap(); @@ -604,7 +604,7 @@ mod tests { #[motsu::test] fn renounce_ownership_unauthorized( beacon: Contract, - erc20: Contract, + erc20: Contract, alice: Address, bob: Address, ) { @@ -632,9 +632,9 @@ mod tests { #[motsu::test] fn upgrade_after_ownership_transfer_chain( beacon: Contract, - erc20: Contract, - erc20_2: Contract, - erc20_3: Contract, + erc20: Contract, + erc20_2: Contract, + erc20_3: Contract, alice: Address, bob: Address, charlie: Address, @@ -692,8 +692,8 @@ mod tests { #[motsu::test] fn upgrade_after_renounce_and_transfer( beacon: Contract, - erc20: Contract, - erc20_2: Contract, + erc20: Contract, + erc20_2: Contract, alice: Address, ) { beacon.sender(alice).constructor(erc20.address(), alice).unwrap(); diff --git a/contracts/src/proxy/erc1967/mod.rs b/contracts/src/proxy/erc1967/mod.rs index 005671ea5..74fa14733 100644 --- a/contracts/src/proxy/erc1967/mod.rs +++ b/contracts/src/proxy/erc1967/mod.rs @@ -92,7 +92,6 @@ unsafe impl IProxy for Erc1967Proxy { #[cfg(test)] mod tests { - use alloy_sol_macro::sol; use alloy_sol_types::{SolCall, SolError, SolValue}; use motsu::prelude::*; use stylus_sdk::{ @@ -102,7 +101,10 @@ mod tests { }; use super::*; - use crate::token::erc20::{self, Erc20, IErc20}; + use crate::{ + proxy::tests::{Erc20Example, IERC20}, + token::erc20, + }; #[entrypoint] #[storage] @@ -131,76 +133,6 @@ mod tests { } } - #[storage] - struct Erc20Example { - erc20: Erc20, - } - - #[public] - #[implements(IErc20)] - impl Erc20Example { - fn mint( - &mut self, - to: Address, - value: U256, - ) -> Result<(), erc20::Error> { - self.erc20._mint(to, value) - } - } - - unsafe impl TopLevelStorage for Erc20Example {} - - #[public] - impl IErc20 for Erc20Example { - type Error = erc20::Error; - - fn balance_of(&self, account: Address) -> U256 { - self.erc20.balance_of(account) - } - - fn total_supply(&self) -> U256 { - self.erc20.total_supply() - } - - fn transfer( - &mut self, - to: Address, - value: U256, - ) -> Result { - self.erc20.transfer(to, value) - } - - fn transfer_from( - &mut self, - from: Address, - to: Address, - value: U256, - ) -> Result { - self.erc20.transfer_from(from, to, value) - } - - fn allowance(&self, owner: Address, spender: Address) -> U256 { - self.erc20.allowance(owner, spender) - } - - fn approve( - &mut self, - spender: Address, - value: U256, - ) -> Result { - self.erc20.approve(spender, value) - } - } - - sol! { - interface IERC20 { - function balanceOf(address account) external view returns (uint256); - function totalSupply() external view returns (uint256); - function mint(address to, uint256 value) external; - function transfer(address to, uint256 value) external returns (bool); - } - } - #[motsu::test] fn constructs( proxy: Contract, diff --git a/contracts/src/proxy/erc1967/utils.rs b/contracts/src/proxy/erc1967/utils.rs index 391e70242..ac1a9d460 100644 --- a/contracts/src/proxy/erc1967/utils.rs +++ b/contracts/src/proxy/erc1967/utils.rs @@ -397,11 +397,12 @@ impl Erc1967Utils { #[cfg(test)] mod tests { + use core::ops::Deref; + use alloy_sol_types::SolCall; use motsu::prelude::*; use stylus_sdk::{ alloy_primitives::{Address, U256}, - alloy_sol_types::sol, function_selector, prelude::*, storage::StorageAddress, @@ -483,14 +484,27 @@ mod tests { } } - sol! { - #[derive(Debug)] - #[allow(missing_docs)] - error ImplementationSolidityError(); + use sol_types::*; - #[derive(Debug)] - #[allow(missing_docs)] - event ImplementationEvent(); + #[cfg_attr(coverage_nightly, coverage(off))] + mod sol_types { + use stylus_sdk::alloy_sol_types::sol; + + sol! { + #[derive(Debug)] + #[allow(missing_docs)] + error ImplementationSolidityError(); + + #[derive(Debug)] + #[allow(missing_docs)] + event ImplementationEvent(); + } + + sol! { + interface IImplementation { + function emitOrError(bool should_error) external; + } + } } #[derive(SolidityError, Debug)] @@ -525,12 +539,6 @@ mod tests { } } - sol! { - interface IImplementation { - function emitOrError(bool should_error) external; - } - } - #[motsu::test] fn get_implementation_returns_zero_by_default( contract: Contract, @@ -968,6 +976,7 @@ mod tests { ); } + #[cfg_attr(coverage_nightly, coverage(off))] #[motsu::test] #[ignore = "TODO: motsu doesn't properly reset custom storage slots on transaction revert. See https://github.com/OpenZeppelin/stylus-test-helpers/issues/112"] fn upgrade_beacon_to_and_call_with_beacon_returning_invalid_implementation( @@ -1216,4 +1225,43 @@ mod tests { assert_eq!(admin, bob); assert_eq!(beacon_address, beacon.address()); } + + #[storage] + struct InvalidBeacon; + + unsafe impl TopLevelStorage for InvalidBeacon {} + + #[public] + impl InvalidBeacon { + fn implementation(&self) -> Result> { + Err("Invalid implementation".into()) + } + } + + #[motsu::test] + fn get_beacon_implementation_errors_with_empty_result_and_no_code( + contract: Contract, + invalid_beacon: Contract, + alice: Address, + ) { + // Use an EOA address (alice) as the beacon. A call to an address with + // no code returns success with empty returndata. Combined with + // target.has_code() == false, + // AddressUtils::verify_call_result_from_target must return + // AddressEmptyCode. + let err = Erc1967Utils::get_beacon_implementation( + contract.sender(alice).deref(), + invalid_beacon.address(), + ) + .expect_err( + "expected EmptyCode when beacon call returns empty and has no code", + ); + + assert!(matches!( + err, + Error::FailedCallWithReason(address::FailedCallWithReason { + reason, + }) if reason.to_vec() == Into::>::into("Invalid implementation") + )); + } } diff --git a/contracts/src/proxy/mod.rs b/contracts/src/proxy/mod.rs index 04cd3405c..ee122d0c9 100644 --- a/contracts/src/proxy/mod.rs +++ b/contracts/src/proxy/mod.rs @@ -111,7 +111,6 @@ pub unsafe trait IProxy: TopLevelStorage + Sized { #[cfg(test)] mod tests { - use alloy_sol_macro::sol; use alloy_sol_types::{SolCall, SolError, SolValue}; use motsu::prelude::*; use stylus_sdk::{ @@ -165,7 +164,7 @@ mod tests { } #[storage] - struct Erc20Example { + pub struct Erc20Example { erc20: Erc20, } @@ -183,6 +182,7 @@ mod tests { unsafe impl TopLevelStorage for Erc20Example {} + #[cfg_attr(coverage_nightly, coverage(off))] #[public] impl IErc20 for Erc20Example { type Error = erc20::Error; @@ -225,15 +225,21 @@ mod tests { } } - sol! { - interface IERC20 { - function balanceOf(address account) external view returns (uint256); - function totalSupply() external view returns (uint256); - function mint(address to, uint256 value) external; - function transfer(address to, uint256 value) external returns (bool); + pub(crate) use erc20_interface::IERC20; + + #[cfg_attr(coverage_nightly, coverage(off))] + mod erc20_interface { + use alloy_sol_macro::sol; + + sol! { + interface IERC20 { + function balanceOf(address account) external view returns (uint256); + function totalSupply() external view returns (uint256); + function mint(address to, uint256 value) external; + function transfer(address to, uint256 value) external returns (bool); + } } } - #[motsu::test] fn constructs( proxy: Contract, diff --git a/contracts/src/proxy/utils/uups_upgradeable.rs b/contracts/src/proxy/utils/uups_upgradeable.rs index 29be364b2..d0cce8316 100644 --- a/contracts/src/proxy/utils/uups_upgradeable.rs +++ b/contracts/src/proxy/utils/uups_upgradeable.rs @@ -311,6 +311,17 @@ impl UUPSUpgradeable { /// /// * [`crate::proxy::erc1967::Erc1967Proxy::Upgraded`]: Emitted when the /// implementation is upgraded. + #[cfg_attr(coverage_nightly, coverage(off))] + // TODO: remove the coverage attribute once we motsu supports delegate calls + // and custom storage slot setting. See: + // * https://github.com/OpenZeppelin/stylus-test-helpers/issues/111 + // * https://github.com/OpenZeppelin/stylus-test-helpers/issues/112 + // * https://github.com/OpenZeppelin/stylus-test-helpers/issues/114 + // + // For now, this function is marked as `#[cfg_attr(coverage_nightly, + // coverage(off))]` as it is extensively covered in e2e tests, which cannot + // be included in the coverage report for now. See: + // `examples/uups-proxy/tests/uups-proxy.rs` fn _upgrade_to_and_call_uups( &mut self, new_implementation: Address, diff --git a/contracts/src/token/erc1155/extensions/supply.rs b/contracts/src/token/erc1155/extensions/supply.rs index c7a8a21c3..20f5a72a6 100644 --- a/contracts/src/token/erc1155/extensions/supply.rs +++ b/contracts/src/token/erc1155/extensions/supply.rs @@ -373,17 +373,22 @@ impl Erc1155Supply { #[cfg(test)] mod tests { - use motsu::prelude::Contract; + use motsu::prelude::*; use stylus_sdk::{ alloy_primitives::{fixed_bytes, Address, U256}, prelude::*, }; use super::*; - use crate::token::erc1155::{ - tests::{random_token_ids, random_values}, - ERC1155InvalidReceiver, ERC1155InvalidSender, - }; + use crate::{ + token::erc1155::{ + receiver::IErc1155Receiver, + tests::{random_token_ids, random_values}, + ERC1155InvalidReceiver, ERC1155InvalidSender, + InvalidReceiverWithReason, + }, + utils::introspection::erc165::IErc165, + }; // for interface_id() unsafe impl TopLevelStorage for Erc1155Supply {} @@ -402,7 +407,7 @@ mod tests { values.clone(), &vec![].into(), ) - .expect("should mint"); + .motsu_expect("should mint"); (token_ids, values) } } @@ -462,7 +467,7 @@ mod tests { let err = contract .sender(alice) ._mint(invalid_receiver, token_id, two, &vec![].into()) - .expect_err("should revert with `InvalidReceiver`"); + .motsu_expect_err("should revert with `InvalidReceiver`"); assert!(matches!( err, @@ -486,11 +491,11 @@ mod tests { contract .sender(alice) ._mint(bob, token_id, U256::MAX / two, &vec![].into()) - .expect("should mint to bob"); + .motsu_expect("should mint to bob"); contract .sender(alice) ._mint(dave, token_id, U256::MAX / two, &vec![].into()) - .expect("should mint to dave"); + .motsu_expect("should mint to dave"); // This should panic. _ = contract.sender(alice)._mint(bob, token_id, three, &vec![].into()); } @@ -506,7 +511,7 @@ mod tests { contract .sender(alice) ._mint(bob, token_ids[0], U256::MAX, &vec![].into()) - .expect("should mint"); + .motsu_expect("should mint"); // This should panic. _ = contract.sender(alice)._mint( bob, @@ -526,7 +531,7 @@ mod tests { contract .sender(alice) ._burn(bob, token_ids[0], values[0]) - .expect("should burn"); + .motsu_expect("should burn"); assert_eq!( U256::ZERO, @@ -546,7 +551,7 @@ mod tests { contract .sender(alice) ._burn_batch(bob, token_ids.clone(), values.clone()) - .expect("should burn batch"); + .motsu_expect("should burn batch"); for &token_id in &token_ids { assert_eq!( @@ -574,7 +579,7 @@ mod tests { let err = contract .sender(alice) ._burn(invalid_sender, token_ids[0], values[0]) - .expect_err("should not burn token for invalid sender"); + .motsu_expect_err("should not burn token for invalid sender"); assert!(matches!( err, @@ -595,7 +600,7 @@ mod tests { contract .sender(alice) ._update(Address::ZERO, Address::ZERO, token_ids.clone(), values) - .expect("should supply"); + .motsu_expect("should supply"); assert_eq!( U256::ZERO, contract.sender(alice).total_supply(token_ids[0]) @@ -628,4 +633,552 @@ mod tests { .sender(alice) .supports_interface(fake_interface_id.into())); } + + // ---------------- Additional tests for full coverage ----------------- + + #[motsu::test] + fn balance_of_batch_works( + contract: Contract, + alice: Address, + bob: Address, + ) { + // Mint 3 token ids to bob + let (ids, values) = contract.sender(alice).init(bob, 3); + let accounts = vec![bob, bob, bob]; + let balances = contract + .sender(alice) + .balance_of_batch(accounts, ids.clone()) + .motsu_expect("should get balances"); + assert_eq!(balances, values); + } + + #[motsu::test] + fn balance_of_batch_invalid_length( + contract: Contract, + alice: Address, + bob: Address, + ) { + let (ids, _values) = contract.sender(alice).init(bob, 2); + let accounts = vec![bob]; // mismatch lengths (1 vs 2) + let err = contract + .sender(alice) + .balance_of_batch(accounts, ids) + .motsu_expect_err("should fail on array length mismatch"); + // just ensure it is the ERC1155 error enum variant + assert!(matches!(err, Error::InvalidArrayLength(_))); + } + + #[motsu::test] + fn approval_set_and_query( + contract: Contract, + alice: Address, + bob: Address, + ) { + // Initially not approved + assert!(!contract.sender(alice).is_approved_for_all(alice, bob)); + + // Approve bob + contract + .sender(alice) + .set_approval_for_all(bob, true) + .motsu_expect("should approve"); + assert!(contract.sender(alice).is_approved_for_all(alice, bob)); + + // Revoke + contract + .sender(alice) + .set_approval_for_all(bob, false) + .motsu_expect("should revoke"); + assert!(!contract.sender(alice).is_approved_for_all(alice, bob)); + } + + #[motsu::test] + fn approval_reverts_on_zero_operator( + contract: Contract, + alice: Address, + ) { + let zero = Address::ZERO; + let err = contract + .sender(alice) + .set_approval_for_all(zero, true) + .motsu_expect_err("should fail for zero operator"); + assert!(matches!( + err, + Error::InvalidOperator(crate::token::erc1155::ERC1155InvalidOperator { operator }) if operator == zero + )); + } + + #[motsu::test] + fn safe_transfer_from_by_owner( + contract: Contract, + alice: Address, + bob: Address, + ) { + // Mint to alice + let (ids, values) = contract.sender(alice).init(alice, 1); + let id = ids[0]; + let value = values[0]; + + // Transfer from alice to bob as owner (no approval needed) + contract + .sender(alice) + .safe_transfer_from(alice, bob, id, value, vec![].into()) + .motsu_expect("owner should transfer"); + + assert_eq!(U256::ZERO, contract.sender(alice).balance_of(alice, id)); + assert_eq!(value, contract.sender(alice).balance_of(bob, id)); + } + + #[motsu::test] + fn safe_transfer_from_by_operator( + contract: Contract, + alice: Address, + bob: Address, + dave: Address, + ) { + // Mint to alice + let (ids, values) = contract.sender(alice).init(alice, 1); + let id = ids[0]; + let value = values[0]; + + // Alice approves bob + contract + .sender(alice) + .set_approval_for_all(bob, true) + .motsu_expect("should approve"); + + // Bob transfers from alice to dave + contract + .sender(bob) + .safe_transfer_from(alice, dave, id, value, vec![].into()) + .motsu_expect("operator should transfer"); + + assert_eq!(U256::ZERO, contract.sender(alice).balance_of(alice, id)); + assert_eq!(value, contract.sender(alice).balance_of(dave, id)); + } + + #[motsu::test] + fn safe_transfer_from_missing_approval( + contract: Contract, + alice: Address, + bob: Address, + dave: Address, + ) { + // Mint to alice + let (ids, values) = contract.sender(alice).init(alice, 1); + let id = ids[0]; + let value = values[0]; + + // Bob tries to transfer without approval + let err = contract + .sender(bob) + .safe_transfer_from(alice, dave, id, value, vec![].into()) + .motsu_expect_err("should fail: missing approval"); + + assert!(matches!( + err, + Error::MissingApprovalForAll(crate::token::erc1155::ERC1155MissingApprovalForAll { operator, owner }) if operator == bob && owner == alice + )); + } + + #[motsu::test] + fn safe_transfer_from_invalid_receiver_zero( + contract: Contract, + alice: Address, + ) { + let (ids, values) = contract.sender(alice).init(alice, 1); + let id = ids[0]; + let value = values[0]; + let to = Address::ZERO; + let err = contract + .sender(alice) + .safe_transfer_from(alice, to, id, value, vec![].into()) + .motsu_expect_err("should fail for zero receiver"); + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { receiver }) if receiver == to + )); + } + + #[motsu::test] + fn safe_batch_transfer_from_works( + contract: Contract, + alice: Address, + bob: Address, + ) { + // Mint 3 ids to alice + let (ids, values) = contract.sender(alice).init(alice, 3); + contract + .sender(alice) + .safe_batch_transfer_from( + alice, + bob, + ids.clone(), + values.clone(), + vec![].into(), + ) + .motsu_expect("batch transfer should work"); + + for (&id, &value) in ids.iter().zip(values.iter()) { + assert_eq!( + U256::ZERO, + contract.sender(alice).balance_of(alice, id) + ); + assert_eq!(value, contract.sender(alice).balance_of(bob, id)); + } + } + + #[motsu::test] + fn safe_batch_transfer_from_invalid_array_length( + contract: Contract, + alice: Address, + bob: Address, + ) { + // Mint 2 ids to alice + let (ids, mut values) = contract.sender(alice).init(alice, 2); + // Make lengths mismatched by dropping one value + values.pop(); + let err = contract + .sender(alice) + .safe_batch_transfer_from( + alice, + bob, + ids.clone(), + values, + vec![].into(), + ) + .motsu_expect_err("should fail for array length mismatch"); + assert!(matches!(err, Error::InvalidArrayLength(_))); + + drop(contract); + + let contract = Contract::::from_tag("new_contract"); + let (mut ids, values) = contract.sender(alice).init(alice, 2); + // Also mismatch ids by adding one more id + ids.push(U256::from(999u64)); + + let err2 = contract + .sender(alice) + .safe_batch_transfer_from(alice, bob, ids, values, vec![].into()) + .motsu_expect_err("should still fail for array length mismatch"); + assert!(matches!(err2, Error::InvalidArrayLength(_))); + } + + // ---------------- Receiver mocks for acceptance-check tests + // ---------------- + + #[storage] + struct BadSelectorReceiver; + + unsafe impl TopLevelStorage for BadSelectorReceiver {} + + #[public] + #[implements(IErc1155Receiver, IErc165)] + impl BadSelectorReceiver {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc1155Receiver for BadSelectorReceiver { + #[selector(name = "onERC1155Received")] + fn on_erc1155_received( + &mut self, + _operator: Address, + _from: Address, + _id: U256, + _value: U256, + _data: Bytes, + ) -> Result> { + Ok(B32::ZERO) // wrong selector -> must be rejected + } + + #[selector(name = "onERC1155BatchReceived")] + fn on_erc1155_batch_received( + &mut self, + _operator: Address, + _from: Address, + _ids: Vec, + _values: Vec, + _data: Bytes, + ) -> Result> { + Ok(B32::ZERO) // wrong selector -> must be rejected + } + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc165 for BadSelectorReceiver { + fn supports_interface(&self, interface_id: B32) -> bool { + // declare support so calls are attempted + ::interface_id() == interface_id + || ::interface_id() == interface_id + } + } + + #[storage] + struct RevertingReceiver; + + unsafe impl TopLevelStorage for RevertingReceiver {} + + #[public] + #[implements(IErc1155Receiver, IErc165)] + impl RevertingReceiver {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc1155Receiver for RevertingReceiver { + #[selector(name = "onERC1155Received")] + fn on_erc1155_received( + &mut self, + _operator: Address, + _from: Address, + _id: U256, + _value: U256, + _data: Bytes, + ) -> Result> { + Err("Receiver rejected single".into()) + } + + #[selector(name = "onERC1155BatchReceived")] + fn on_erc1155_batch_received( + &mut self, + _operator: Address, + _from: Address, + _ids: Vec, + _values: Vec, + _data: Bytes, + ) -> Result> { + Err("Receiver rejected batch".into()) + } + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc165 for RevertingReceiver { + fn supports_interface(&self, interface_id: B32) -> bool { + ::interface_id() == interface_id + || ::interface_id() == interface_id + } + } + + #[storage] + struct EmptyReasonReceiver; + + unsafe impl TopLevelStorage for EmptyReasonReceiver {} + + #[public] + #[implements(IErc1155Receiver, IErc165)] + impl EmptyReasonReceiver {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc1155Receiver for EmptyReasonReceiver { + #[selector(name = "onERC1155Received")] + fn on_erc1155_received( + &mut self, + _operator: Address, + _from: Address, + _id: U256, + _value: U256, + _data: Bytes, + ) -> Result> { + Err(Vec::new()) + } + + #[selector(name = "onERC1155BatchReceived")] + fn on_erc1155_batch_received( + &mut self, + _operator: Address, + _from: Address, + _ids: Vec, + _values: Vec, + _data: Bytes, + ) -> Result> { + Err(Vec::new()) + } + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc165 for EmptyReasonReceiver { + fn supports_interface(&self, interface_id: B32) -> bool { + ::interface_id() == interface_id + || ::interface_id() == interface_id + } + } + + // ----------------------- Acceptance-check failures ---------------------- + + #[motsu::test] + fn mint_rejects_when_receiver_returns_wrong_selector( + contract: Contract, + bad: Contract, + alice: Address, + ) { + let id = U256::from(1); + let value = U256::from(5); + + let err = contract + .sender(alice) + ._mint(bad.address(), id, value, &vec![].into()) + .motsu_expect_err( + "receiver returning wrong selector must be rejected", + ); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { receiver }) if receiver == bad.address() + )); + assert_eq!( + U256::ZERO, + contract.sender(alice).balance_of(bad.address(), id) + ); + } + + #[motsu::test] + fn mint_bubbles_revert_reason_from_receiver( + contract: Contract, + reverting: Contract, + alice: Address, + ) { + let id = U256::from(2); + let value = U256::from(7); + + let err = contract + .sender(alice) + ._mint(reverting.address(), id, value, &vec![].into()) + .motsu_expect_err("receiver reverting should bubble reason"); + + assert!(matches!( + err, + Error::InvalidReceiverWithReason(InvalidReceiverWithReason { reason }) if reason == "Receiver rejected single" + )); + assert_eq!( + U256::ZERO, + contract.sender(alice).balance_of(reverting.address(), id) + ); + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[motsu::test] + #[ignore = "TODO: un-ignore when https://github.com/OpenZeppelin/stylus-test-helpers/issues/118 is fixed"] + fn mint_rejects_on_empty_revert_reason( + contract: Contract, + empty: Contract, + alice: Address, + ) { + let id = U256::from(3); + let value = U256::from(9); + + let err = contract + .sender(alice) + ._mint(empty.address(), id, value, &vec![].into()) + .motsu_expect_err("empty revert must map to InvalidReceiver"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { receiver }) if receiver == empty.address() + )); + } + + #[motsu::test] + fn transfer_rejects_when_receiver_returns_wrong_selector( + contract: Contract, + bad: Contract, + alice: Address, + ) { + let (ids, values) = contract.sender(alice).init(alice, 1); + let id = ids[0]; + let value = values[0]; + + let err = contract + .sender(alice) + .safe_transfer_from(alice, bad.address(), id, value, vec![].into()) + .motsu_expect_err("wrong selector should be rejected in transfer"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { receiver }) if receiver == bad.address() + )); + assert_eq!(value, contract.sender(alice).balance_of(alice, id)); + } + + #[motsu::test] + fn transfer_bubbles_revert_reason_from_receiver( + contract: Contract, + reverting: Contract, + alice: Address, + ) { + let (ids, values) = contract.sender(alice).init(alice, 1); + let id = ids[0]; + let value = values[0]; + + let err = contract + .sender(alice) + .do_safe_transfer_from( + alice, + reverting.address(), + vec![id], + vec![value], + &vec![].into(), + ) + .motsu_expect_err("revert reason should bubble in transfer"); + + assert!(matches!( + err, + Error::InvalidReceiverWithReason(InvalidReceiverWithReason { reason }) if reason == "Receiver rejected batch" || reason == "Receiver rejected single" + )); + assert_eq!(value, contract.sender(alice).balance_of(alice, id)); + } + + // ----------------------- Other internal error paths ---------------------- + + #[motsu::test] + fn burn_reverts_on_insufficient_balance( + contract: Contract, + alice: Address, + bob: Address, + ) { + let (ids, values) = contract.sender(alice).init(bob, 1); + let token_id = ids[0]; + let balance = values[0]; + + let err = contract + .sender(alice) + ._burn(bob, token_id, balance + U256::from(1)) + .motsu_expect_err("should not burn more than balance"); + + assert!(matches!( + err, + Error::InsufficientBalance(crate::token::erc1155::ERC1155InsufficientBalance { sender, balance: b, needed, token_id: tid }) + if sender == bob && b == balance && needed == balance + U256::from(1) && tid == token_id + )); + } + + #[motsu::test] + fn do_safe_transfer_from_reverts_when_from_is_zero( + contract: Contract, + alice: Address, + bob: Address, + ) { + let (ids, values) = contract.sender(alice).init(alice, 1); + let id = ids[0]; + let value = values[0]; + let invalid_from = Address::ZERO; + + let err = contract + .sender(alice) + .do_safe_transfer_from( + invalid_from, + bob, + vec![id], + vec![value], + &vec![].into(), + ) + .motsu_expect_err("should error when from is zero"); + + assert!(matches!( + err, + Error::InvalidSender(ERC1155InvalidSender { sender }) if sender == invalid_from + )); + } } diff --git a/contracts/src/token/erc1155/mod.rs b/contracts/src/token/erc1155/mod.rs index 720f31993..af7281b62 100644 --- a/contracts/src/token/erc1155/mod.rs +++ b/contracts/src/token/erc1155/mod.rs @@ -1127,7 +1127,7 @@ enum Transfer { #[cfg(test)] mod tests { use alloy_primitives::{aliases::B32, uint, Address, U256}; - use motsu::prelude::Contract; + use motsu::prelude::*; use super::*; use crate::utils::introspection::erc165::IErc165; @@ -1198,6 +1198,466 @@ mod tests { assert_eq!(U256::ZERO, balance); } + // ---------------- Receiver mocks for acceptance checks ----------------- + + #[storage] + struct BadSelectorReceiver; + + unsafe impl TopLevelStorage for BadSelectorReceiver {} + + #[public] + #[implements(IErc1155Receiver, IErc165)] + impl BadSelectorReceiver {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc165 for BadSelectorReceiver { + fn supports_interface(&self, interface_id: B32) -> bool { + // Declare support for IErc1155Receiver so calls are attempted + ::interface_id() == interface_id + || ::interface_id() == interface_id + } + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc1155Receiver for BadSelectorReceiver { + #[selector(name = "onERC1155Received")] + fn on_erc1155_received( + &mut self, + _operator: Address, + _from: Address, + _id: U256, + _value: U256, + _data: Bytes, + ) -> Result> { + Ok(B32::ZERO) // wrong selector -> should be rejected + } + + #[selector(name = "onERC1155BatchReceived")] + fn on_erc1155_batch_received( + &mut self, + _operator: Address, + _from: Address, + _ids: Vec, + _values: Vec, + _data: Bytes, + ) -> Result> { + Ok(B32::ZERO) // wrong selector -> should be rejected + } + } + + #[storage] + struct RevertingReceiver; + + unsafe impl TopLevelStorage for RevertingReceiver {} + + #[public] + #[implements(IErc1155Receiver, IErc165)] + impl RevertingReceiver {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc165 for RevertingReceiver { + fn supports_interface(&self, interface_id: B32) -> bool { + ::interface_id() == interface_id + || ::interface_id() == interface_id + } + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc1155Receiver for RevertingReceiver { + #[selector(name = "onERC1155Received")] + fn on_erc1155_received( + &mut self, + _operator: Address, + _from: Address, + _id: U256, + _value: U256, + _data: Bytes, + ) -> Result> { + Err("Receiver rejected single".into()) + } + + #[selector(name = "onERC1155BatchReceived")] + fn on_erc1155_batch_received( + &mut self, + _operator: Address, + _from: Address, + _ids: Vec, + _values: Vec, + _data: Bytes, + ) -> Result> { + Err("Receiver rejected batch".into()) + } + } + + // A receiver that returns the correct acceptance selectors for both flows + #[storage] + struct SuccessReceiver; + + unsafe impl TopLevelStorage for SuccessReceiver {} + + #[public] + #[implements(IErc1155Receiver, IErc165)] + impl SuccessReceiver {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc165 for SuccessReceiver { + fn supports_interface(&self, interface_id: B32) -> bool { + ::interface_id() == interface_id + || ::interface_id() == interface_id + } + } + + #[public] + impl IErc1155Receiver for SuccessReceiver { + #[selector(name = "onERC1155Received")] + fn on_erc1155_received( + &mut self, + _operator: Address, + _from: Address, + _id: U256, + _value: U256, + _data: Bytes, + ) -> Result> { + Ok(SINGLE_TRANSFER_FN_SELECTOR) + } + + #[selector(name = "onERC1155BatchReceived")] + fn on_erc1155_batch_received( + &mut self, + _operator: Address, + _from: Address, + _ids: Vec, + _values: Vec, + _data: Bytes, + ) -> Result> { + Ok(BATCH_TRANSFER_FN_SELECTOR) + } + } + + // A receiver that reverts with an empty reason (covers Err(Revert) with + // empty data) + #[storage] + struct EmptyReasonReceiver; + + unsafe impl TopLevelStorage for EmptyReasonReceiver {} + + #[public] + #[implements(IErc1155Receiver, IErc165)] + impl EmptyReasonReceiver {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc1155Receiver for EmptyReasonReceiver { + #[selector(name = "onERC1155Received")] + fn on_erc1155_received( + &mut self, + _operator: Address, + _from: Address, + _id: U256, + _value: U256, + _data: Bytes, + ) -> Result> { + Err(Vec::new()) + } + + #[selector(name = "onERC1155BatchReceived")] + fn on_erc1155_batch_received( + &mut self, + _operator: Address, + _from: Address, + _ids: Vec, + _values: Vec, + _data: Bytes, + ) -> Result> { + Err(Vec::new()) + } + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc165 for EmptyReasonReceiver { + fn supports_interface(&self, interface_id: B32) -> bool { + ::interface_id() == interface_id + || ::interface_id() == interface_id + } + } + + // A receiver that exposes the expected selectors but with no return value, + // producing a successful call with empty return data (ABI decode error -> + // non-Revert call error) + #[storage] + struct MisdeclaredReceiver; + + unsafe impl TopLevelStorage for MisdeclaredReceiver {} + + #[cfg_attr(coverage_nightly, coverage(off))] + impl MisdeclaredReceiver { + // mock interface_id function + fn interface_id(&self) -> B32 { + let single_transfer_fn_selector = B32::new(function_selector!( + "onERC1155Received", + Address, + Address, + U256, + U256, + Bytes + )); + + let batch_transfer_fn_selector = B32::new(function_selector!( + "onERC1155BatchReceived", + Address, + Address, + Vec, + Vec, + Bytes + )); + + single_transfer_fn_selector ^ batch_transfer_fn_selector + } + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc165 for MisdeclaredReceiver { + fn supports_interface(&self, interface_id: B32) -> bool { + // Pretend to support the receiver so the call is attempted + self.interface_id() == interface_id + || ::interface_id() == interface_id + } + } + + // Expose selectors without implementing IErc1155Receiver return types + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl MisdeclaredReceiver { + #[selector(name = "onERC1155Received")] + fn on_erc1155_received( + &mut self, + _operator: Address, + _from: Address, + _id: U256, + _value: U256, + _data: Bytes, + ) { + // return no data + } + + #[selector(name = "onERC1155BatchReceived")] + fn on_erc1155_batch_received( + &mut self, + _operator: Address, + _from: Address, + _ids: Vec, + _values: Vec, + _data: Bytes, + ) { + // return no data + } + } + + // -------------------- _check_on_erc1155_received ----------------------- + + #[motsu::test] + fn check_on_received_rejects_wrong_selector_on_mint( + contract: Contract, + bad_receiver: Contract, + alice: Address, + ) { + let id = U256::from(1); + let value = U256::from(5); + + let err = contract + .sender(alice) + ._mint(bad_receiver.address(), id, value, &vec![].into()) + .motsu_expect_err( + "receiver returning wrong selector must be rejected", + ); + + assert!( + matches!(err, Error::InvalidReceiver(ERC1155InvalidReceiver { receiver }) if receiver == bad_receiver.address()) + ); + + // State unchanged for receiver + assert_eq!( + U256::ZERO, + contract.sender(alice).balance_of(bad_receiver.address(), id) + ); + } + + #[motsu::test] + fn check_on_received_bubbles_revert_reason_on_mint( + contract: Contract, + reverting_receiver: Contract, + alice: Address, + ) { + let id = U256::from(2); + let value = U256::from(7); + + let err = contract + .sender(alice) + ._mint(reverting_receiver.address(), id, value, &vec![].into()) + .motsu_expect_err( + "receiver reverting should return InvalidReceiverWithReason", + ); + + assert!(matches!( + err, + Error::InvalidReceiverWithReason(InvalidReceiverWithReason { reason }) + if reason == "Receiver rejected single" + )); + + assert_eq!( + U256::ZERO, + contract.sender(alice).balance_of(reverting_receiver.address(), id) + ); + } + + // --------------- _update_with_acceptance_check error path -------------- + + #[motsu::test] + fn update_with_acceptance_check_reverts_state_on_rejection( + contract: Contract, + bad_receiver: Contract, + alice: Address, + ) { + let id = U256::from(3); + let value = U256::from(11); + + // Mint to Alice (EOA) + contract + .sender(alice) + ._mint(alice, id, value, &vec![].into()) + .motsu_expect("mint to EOA should succeed"); + + let alice_before = contract.sender(alice).balance_of(alice, id); + assert_eq!(alice_before, value); + + // Attempt transfer to rejecting contract + let err = contract + .sender(alice) + .safe_transfer_from( + alice, + bad_receiver.address(), + id, + value, + vec![].into(), + ) + .motsu_expect_err("transfer to rejecting receiver must fail"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { receiver }) + if receiver == bad_receiver.address() + )); + + // Balances unchanged after failed transfer + let alice_after = contract.sender(alice).balance_of(alice, id); + let bad_after = + contract.sender(alice).balance_of(bad_receiver.address(), id); + assert_eq!(alice_after, value); + assert_eq!(bad_after, U256::ZERO); + } + + // --------------------- Additional coverage tests ---------------------- + + // Success flow for single and batch + #[motsu::test] + fn check_on_received_success_single_and_batch( + contract: Contract, + receiver: Contract, + alice: Address, + ) { + // single + let id = U256::from(10); + let value = U256::from(3); + contract + .sender(alice) + ._mint(receiver.address(), id, value, &vec![].into()) + .motsu_expect("mint to accepting receiver should succeed"); + assert_eq!( + value, + contract.sender(alice).balance_of(receiver.address(), id) + ); + + // batch + let ids = vec![U256::from(21), U256::from(22)]; + let vals = vec![U256::from(5), U256::from(7)]; + contract + .sender(alice) + ._mint_batch( + receiver.address(), + ids.clone(), + vals.clone(), + &vec![].into(), + ) + .motsu_expect("batch mint to accepting receiver should succeed"); + for (tid, val) in ids.into_iter().zip(vals.into_iter()) { + assert_eq!( + val, + contract.sender(alice).balance_of(receiver.address(), tid) + ); + } + } + + // Err(Revert) but empty reason -> InvalidReceiver + #[cfg_attr(coverage_nightly, coverage(off))] + #[motsu::test] + #[ignore = "TODO: un-ignore when https://github.com/OpenZeppelin/stylus-test-helpers/issues/118 is fixed"] + fn check_on_received_empty_reason_revert( + contract: Contract, + empty_reason_receiver: Contract, + alice: Address, + ) { + let id = U256::from(100); + let value = U256::from(1); + let err = contract + .sender(alice) + ._mint(empty_reason_receiver.address(), id, value, &vec![].into()) + .motsu_expect_err("empty revert should map to InvalidReceiver"); + + assert!( + matches!(err, Error::InvalidReceiver(ERC1155InvalidReceiver { receiver }) if receiver == empty_reason_receiver.address()) + ); + assert_eq!( + U256::ZERO, + contract + .sender(alice) + .balance_of(empty_reason_receiver.address(), id) + ); + } + + // Err but not Revert (decode error due to empty success return) -> + // InvalidReceiver + #[motsu::test] + fn check_on_received_non_revert_error( + contract: Contract, + misdeclared_receiver: Contract, + alice: Address, + ) { + let id = U256::from(200); + let value = U256::from(2); + let err = contract + .sender(alice) + ._mint(misdeclared_receiver.address(), id, value, &vec![].into()) + .motsu_expect_err("decode error should map to InvalidReceiver"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { receiver }) if receiver == misdeclared_receiver.address() + )); + assert_eq!( + U256::ZERO, + contract + .sender(alice) + .balance_of(misdeclared_receiver.address(), id) + ); + } + #[motsu::test] fn error_when_array_length_mismatch( contract: Contract, @@ -1214,7 +1674,7 @@ mod tests { let err = contract .sender(alice) .balance_of_batch(accounts, token_ids) - .expect_err("should return `Error::InvalidArrayLength`"); + .motsu_expect_err("should return `Error::InvalidArrayLength`"); assert!(matches!( err, @@ -1238,7 +1698,7 @@ mod tests { let balances = contract .sender(alice) .balance_of_batch(accounts, token_ids) - .expect("should return a vector of `U256::ZERO`"); + .motsu_expect("should return a vector of `U256::ZERO`"); let expected = vec![U256::ZERO; 4]; assert_eq!(expected, balances); @@ -1257,13 +1717,12 @@ mod tests { .setter(bob) .set(false); - contract - .sender(alice) - .set_approval_for_all(bob, true) - .expect("should approve Bob for operations on all Alice's tokens"); + contract.sender(alice).set_approval_for_all(bob, true).motsu_expect( + "should approve Bob for operations on all Alice's tokens", + ); assert!(contract.sender(alice).is_approved_for_all(alice, bob)); - contract.sender(alice).set_approval_for_all(bob, false).expect( + contract.sender(alice).set_approval_for_all(bob, false).motsu_expect( "should disapprove Bob for operations on all Alice's tokens", ); assert!(!contract.sender(alice).is_approved_for_all(alice, bob)); @@ -1279,7 +1738,9 @@ mod tests { let err = contract .sender(alice) .set_approval_for_all(invalid_operator, true) - .expect_err("should not approve for all for invalid operator"); + .motsu_expect_err( + "should not approve for all for invalid operator", + ); assert!(matches!( err, @@ -1297,7 +1758,7 @@ mod tests { contract .sender(alice) ._mint(alice, token_id, value, &vec![0, 1, 2, 3].into()) - .expect("should mint tokens for Alice"); + .motsu_expect("should mint tokens for Alice"); let balance = contract.sender(alice).balance_of(alice, token_id); @@ -1316,7 +1777,7 @@ mod tests { let err = contract .sender(alice) ._mint(invalid_receiver, token_id, value, &vec![0, 1, 2, 3].into()) - .expect_err("should not mint tokens for invalid receiver"); + .motsu_expect_err("should not mint tokens for invalid receiver"); assert!(matches!( err, @@ -1339,7 +1800,7 @@ mod tests { values.clone(), &vec![0, 1, 2, 3].into(), ) - .expect("should batch mint tokens"); + .motsu_expect("should batch mint tokens"); token_ids.iter().zip(values.iter()).for_each(|(&token_id, &value)| { assert_eq!( @@ -1351,7 +1812,7 @@ mod tests { let balances = contract .sender(alice) .balance_of_batch(vec![alice; 4], token_ids.clone()) - .expect("should return balances"); + .motsu_expect("should return balances"); assert_eq!(values, balances); } @@ -1370,7 +1831,7 @@ mod tests { values.clone(), &vec![0, 1, 2, 3].into(), ) - .expect("should batch mint tokens"); + .motsu_expect("should batch mint tokens"); assert_eq!( expected_balance, @@ -1380,7 +1841,7 @@ mod tests { let balances = contract .sender(alice) .balance_of_batch(vec![alice; 4], vec![token_id; 4]) - .expect("should return balances"); + .motsu_expect("should return balances"); assert_eq!(vec![expected_balance; 4], balances); } @@ -1402,7 +1863,9 @@ mod tests { values, &vec![0, 1, 2, 3].into(), ) - .expect_err("should not batch mint tokens for invalid receiver"); + .motsu_expect_err( + "should not batch mint tokens for invalid receiver", + ); assert!(matches!( err, @@ -1423,7 +1886,7 @@ mod tests { let err = contract .sender(alice) ._mint_batch(alice, token_ids, values, &vec![0, 1, 2, 3].into()) - .expect_err( + .motsu_expect_err( "should not batch mint tokens when not equal array lengths", ); @@ -1445,7 +1908,7 @@ mod tests { contract .sender(alice) ._burn(alice, token_id, value) - .expect("should burn tokens"); + .motsu_expect("should burn tokens"); let balances = contract.sender(alice).balance_of(alice, token_id); @@ -1463,7 +1926,7 @@ mod tests { let err = contract .sender(alice) ._burn(invalid_sender, token_ids[0], values[0]) - .expect_err("should not burn token for invalid sender"); + .motsu_expect_err("should not burn token for invalid sender"); assert!(matches!( err, @@ -1483,7 +1946,9 @@ mod tests { let err = contract .sender(alice) ._burn(alice, token_ids[0], values[0] + uint!(1_U256)) - .expect_err("should not burn token when insufficient balance"); + .motsu_expect_err( + "should not burn token when insufficient balance", + ); assert!(matches!( err, @@ -1503,12 +1968,12 @@ mod tests { contract .sender(alice) ._burn_batch(alice, token_ids.clone(), values.clone()) - .expect("should batch burn tokens"); + .motsu_expect("should batch burn tokens"); let balances = contract .sender(alice) .balance_of_batch(vec![alice; 4], token_ids.clone()) - .expect("should return balances"); + .motsu_expect("should return balances"); assert_eq!(vec![U256::ZERO; 4], balances); } @@ -1521,7 +1986,7 @@ mod tests { contract .sender(alice) ._mint(alice, token_id, value, &vec![0, 1, 2, 3].into()) - .expect("should mint token"); + .motsu_expect("should mint token"); contract .sender(alice) @@ -1535,7 +2000,7 @@ mod tests { uint!(20_U256), ], ) - .expect("should batch burn tokens"); + .motsu_expect("should batch burn tokens"); assert_eq!( U256::ZERO, @@ -1554,7 +2019,9 @@ mod tests { let err = contract .sender(alice) ._burn_batch(invalid_sender, token_ids, values) - .expect_err("should not batch burn tokens for invalid sender"); + .motsu_expect_err( + "should not batch burn tokens for invalid sender", + ); assert!(matches!( err, @@ -1578,7 +2045,7 @@ mod tests { token_ids.clone(), values.clone().into_iter().map(|x| x + uint!(1_U256)).collect(), ) - .expect_err( + .motsu_expect_err( "should not batch burn tokens when insufficient balance", ); @@ -1603,7 +2070,7 @@ mod tests { let err = contract .sender(alice) ._burn_batch(alice, token_ids, append(values, 4)) - .expect_err( + .motsu_expect_err( "should not batch burn tokens when not equal array lengths", ); @@ -1629,7 +2096,7 @@ mod tests { contract .sender(bob) .set_approval_for_all(alice, true) - .expect("should approve Bob's tokens to Alice"); + .motsu_expect("should approve Bob's tokens to Alice"); contract .sender(alice) @@ -1640,7 +2107,7 @@ mod tests { amount_one, vec![].into(), ) - .expect("should transfer tokens from Alice to Bob"); + .motsu_expect("should transfer tokens from Alice to Bob"); contract .sender(alice) .safe_transfer_from( @@ -1650,7 +2117,7 @@ mod tests { amount_two, vec![].into(), ) - .expect("should transfer tokens from Alice to Bob"); + .motsu_expect("should transfer tokens from Alice to Bob"); let balance_id_one = contract.sender(alice).balance_of(dave, token_ids[0]); @@ -1678,7 +2145,9 @@ mod tests { values[0], vec![].into(), ) - .expect_err("should not transfer tokens to the `Address::ZERO`"); + .motsu_expect_err( + "should not transfer tokens to the `Address::ZERO`", + ); assert!(matches!( err, @@ -1699,7 +2168,7 @@ mod tests { contract .sender(invalid_sender) .set_approval_for_all(alice, true) - .unwrap(); + .motsu_unwrap(); let err = contract .sender(alice) @@ -1710,7 +2179,9 @@ mod tests { values[0], vec![].into(), ) - .expect_err("should not transfer tokens from the `Address::ZERO`"); + .motsu_expect_err( + "should not transfer tokens from the `Address::ZERO`", + ); assert!(matches!( err, @@ -1737,7 +2208,7 @@ mod tests { values[0], vec![].into(), ) - .expect_err("should not transfer tokens without approval"); + .motsu_expect_err("should not transfer tokens without approval"); assert!(matches!( err, @@ -1759,7 +2230,7 @@ mod tests { contract .sender(bob) .set_approval_for_all(alice, true) - .expect("should approve Bob's tokens to Alice"); + .motsu_expect("should approve Bob's tokens to Alice"); let err = contract .sender(alice) @@ -1770,7 +2241,9 @@ mod tests { values[0] + uint!(1_U256), vec![].into(), ) - .expect_err("should not transfer tokens with insufficient balance"); + .motsu_expect_err( + "should not transfer tokens with insufficient balance", + ); assert!(matches!( err, @@ -1795,7 +2268,7 @@ mod tests { contract .sender(dave) .set_approval_for_all(alice, true) - .expect("should approve Dave's tokens to Alice"); + .motsu_expect("should approve Dave's tokens to Alice"); contract .sender(alice) @@ -1806,7 +2279,7 @@ mod tests { values[0], vec![0, 1, 2, 3].into(), ) - .expect("should transfer tokens from Alice to Bob"); + .motsu_expect("should transfer tokens from Alice to Bob"); let balance = contract.sender(alice).balance_of(charlie, token_ids[0]); @@ -1831,7 +2304,9 @@ mod tests { values, &vec![0, 1, 2, 3].into(), ) - .expect_err("should not transfer tokens to the `Address::ZERO`"); + .motsu_expect_err( + "should not transfer tokens to the `Address::ZERO`", + ); assert!(matches!( err, @@ -1852,7 +2327,7 @@ mod tests { contract .sender(invalid_sender) .set_approval_for_all(alice, true) - .unwrap(); + .motsu_unwrap(); let err = contract .sender(alice) @@ -1863,7 +2338,9 @@ mod tests { values[0], vec![0, 1, 2, 3].into(), ) - .expect_err("should not transfer tokens from the `Address::ZERO`"); + .motsu_expect_err( + "should not transfer tokens from the `Address::ZERO`", + ); assert!(matches!( err, @@ -1890,7 +2367,7 @@ mod tests { values[0], vec![0, 1, 2, 3].into(), ) - .expect_err("should not transfer tokens without approval"); + .motsu_expect_err("should not transfer tokens without approval"); assert!(matches!( err, @@ -1913,7 +2390,7 @@ mod tests { contract .sender(bob) .set_approval_for_all(alice, true) - .expect("should approve Bob's tokens to Alice"); + .motsu_expect("should approve Bob's tokens to Alice"); let err = contract .sender(alice) @@ -1924,7 +2401,9 @@ mod tests { values[0] + uint!(1_U256), vec![0, 1, 2, 3].into(), ) - .expect_err("should not transfer tokens with insufficient balance"); + .motsu_expect_err( + "should not transfer tokens with insufficient balance", + ); assert!(matches!( err, @@ -1951,7 +2430,7 @@ mod tests { contract .sender(dave) .set_approval_for_all(alice, true) - .expect("should approve Dave's tokens to Alice"); + .motsu_expect("should approve Dave's tokens to Alice"); contract .sender(alice) @@ -1962,7 +2441,7 @@ mod tests { vec![amount_one, amount_two], vec![].into(), ) - .expect("should transfer tokens from Alice to Bob"); + .motsu_expect("should transfer tokens from Alice to Bob"); let balance_id_one = contract.sender(alice).balance_of(bob, token_ids[0]); @@ -1990,7 +2469,9 @@ mod tests { values.clone(), vec![].into(), ) - .expect_err("should not transfer tokens to the `Address::ZERO`"); + .motsu_expect_err( + "should not transfer tokens to the `Address::ZERO`", + ); assert!(matches!( err, @@ -2011,7 +2492,7 @@ mod tests { contract .sender(invalid_sender) .set_approval_for_all(alice, true) - .unwrap(); + .motsu_unwrap(); let err = contract .sender(alice) @@ -2022,7 +2503,9 @@ mod tests { values.clone(), vec![].into(), ) - .expect_err("should not transfer tokens from the `Address::ZERO`"); + .motsu_expect_err( + "should not transfer tokens from the `Address::ZERO`", + ); assert!(matches!( err, @@ -2049,7 +2532,7 @@ mod tests { values.clone(), vec![].into(), ) - .expect_err("should not transfer tokens without approval"); + .motsu_expect_err("should not transfer tokens without approval"); assert!(matches!( err, @@ -2072,7 +2555,7 @@ mod tests { contract .sender(charlie) .set_approval_for_all(alice, true) - .expect("should approve Charlie's tokens to Alice"); + .motsu_expect("should approve Charlie's tokens to Alice"); let err = contract .sender(alice) @@ -2083,7 +2566,9 @@ mod tests { vec![values[0] + uint!(1_U256), values[1]], vec![].into(), ) - .expect_err("should not transfer tokens with insufficient balance"); + .motsu_expect_err( + "should not transfer tokens with insufficient balance", + ); assert!(matches!( err, @@ -2108,7 +2593,7 @@ mod tests { contract .sender(dave) .set_approval_for_all(alice, true) - .expect("should approve Dave's tokens to Alice"); + .motsu_expect("should approve Dave's tokens to Alice"); let err = contract .sender(alice) @@ -2119,7 +2604,7 @@ mod tests { append(values, 4), vec![].into(), ) - .expect_err( + .motsu_expect_err( "should not transfer tokens when not equal array lengths", ); @@ -2143,7 +2628,7 @@ mod tests { contract .sender(dave) .set_approval_for_all(alice, true) - .expect("should approve Dave's tokens to Alice"); + .motsu_expect("should approve Dave's tokens to Alice"); contract .sender(alice) @@ -2154,7 +2639,7 @@ mod tests { values.clone(), vec![0, 1, 2, 3].into(), ) - .expect("should transfer tokens from Alice to Bob"); + .motsu_expect("should transfer tokens from Alice to Bob"); let balance_id_one = contract.sender(alice).balance_of(bob, token_ids[0]); @@ -2182,7 +2667,9 @@ mod tests { values.clone(), vec![0, 1, 2, 3].into(), ) - .expect_err("should not transfer tokens to the `Address::ZERO`"); + .motsu_expect_err( + "should not transfer tokens to the `Address::ZERO`", + ); assert!(matches!( err, @@ -2203,7 +2690,7 @@ mod tests { contract .sender(invalid_sender) .set_approval_for_all(alice, true) - .unwrap(); + .motsu_unwrap(); let err = contract .sender(alice) @@ -2214,7 +2701,9 @@ mod tests { values.clone(), vec![0, 1, 2, 3].into(), ) - .expect_err("should not transfer tokens from the `Address::ZERO`"); + .motsu_expect_err( + "should not transfer tokens from the `Address::ZERO`", + ); assert!(matches!( err, @@ -2241,7 +2730,7 @@ mod tests { values.clone(), vec![0, 1, 2, 3].into(), ) - .expect_err("should not transfer tokens without approval"); + .motsu_expect_err("should not transfer tokens without approval"); assert!(matches!( err, @@ -2264,7 +2753,7 @@ mod tests { contract .sender(charlie) .set_approval_for_all(alice, true) - .expect("should approve Charlie's tokens to Alice"); + .motsu_expect("should approve Charlie's tokens to Alice"); let err = contract .sender(alice) @@ -2275,7 +2764,9 @@ mod tests { vec![values[0] + uint!(1_U256), values[1]], vec![0, 1, 2, 3].into(), ) - .expect_err("should not transfer tokens with insufficient balance"); + .motsu_expect_err( + "should not transfer tokens with insufficient balance", + ); assert!(matches!( err, @@ -2300,7 +2791,7 @@ mod tests { contract .sender(dave) .set_approval_for_all(alice, true) - .expect("should approve Dave's tokens to Alice"); + .motsu_expect("should approve Dave's tokens to Alice"); let err = contract .sender(alice) @@ -2311,7 +2802,7 @@ mod tests { append(values, 4), vec![0, 1, 2, 3].into(), ) - .expect_err( + .motsu_expect_err( "should not transfer tokens when not equal array lengths", ); diff --git a/contracts/src/token/erc20/extensions/erc4626.rs b/contracts/src/token/erc20/extensions/erc4626.rs index 15c0da4de..267f86ebc 100644 --- a/contracts/src/token/erc20/extensions/erc4626.rs +++ b/contracts/src/token/erc20/extensions/erc4626.rs @@ -1127,7 +1127,6 @@ impl Erc4626 { // } // } -#[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod tests { use alloy_primitives::{address, aliases::B32, Address, U256, U8}; @@ -1248,6 +1247,7 @@ mod tests { } } + #[cfg_attr(coverage_nightly, coverage(off))] #[public] impl IErc20Metadata for Erc4626TestExample { fn name(&self) -> String { @@ -1263,6 +1263,7 @@ mod tests { } } + #[cfg_attr(coverage_nightly, coverage(off))] #[public] impl IErc165 for Erc4626TestExample { fn supports_interface(&self, interface_id: B32) -> bool { @@ -1334,6 +1335,20 @@ mod tests { } } + #[motsu::test] + fn constructor( + vault: Contract, + token: Contract, + alice: Address, + ) { + vault.sender(alice).constructor(token.address(), U8::ZERO); + assert_eq!(vault.sender(alice).decimals(), uint!(18_U8)); + assert_eq!(vault.sender(alice).asset(), token.address()); + + vault.sender(alice).erc4626.decimals_offset.set(uint!(12_U8)); + assert_eq!(vault.sender(alice).decimals(), uint!(30_U8)); + } + #[motsu::test] fn decimals_returns_default_value_when_underlying_decimals_exceeds_u8_max( vault: Contract, diff --git a/contracts/src/token/erc20/extensions/flash_mint.rs b/contracts/src/token/erc20/extensions/flash_mint.rs index c9d373075..da5c885c4 100644 --- a/contracts/src/token/erc20/extensions/flash_mint.rs +++ b/contracts/src/token/erc20/extensions/flash_mint.rs @@ -369,6 +369,7 @@ impl Erc20FlashMint { #[cfg(test)] mod tests { + use alloy_primitives::B256; use motsu::prelude::*; use stylus_sdk::{ abi::Bytes, @@ -377,6 +378,132 @@ mod tests { }; use super::*; + use crate::token::erc20::interface::Erc20Interface; + + trait IErc3156FlashBorrower { + fn on_flash_loan( + &mut self, + initiator: Address, + token: Address, + amount: U256, + fee: U256, + data: Bytes, + ) -> Result>; + } + + // --- Borrower mocks --- + #[storage] + struct WrongSelectorBorrower; + + unsafe impl TopLevelStorage for WrongSelectorBorrower {} + + // Provide a router for the concrete type as well + #[public] + #[implements(IErc3156FlashBorrower)] + impl WrongSelectorBorrower {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc3156FlashBorrower for WrongSelectorBorrower { + fn on_flash_loan( + &mut self, + _initiator: Address, + _token: Address, + _amount: U256, + _fee: U256, + _data: Bytes, + ) -> Result> { + // Return an incorrect selector to trigger the wrong-selector branch + Ok(B256::ZERO) + } + } + + #[storage] + struct CallbackOkNoApproveBorrower; + + unsafe impl TopLevelStorage for CallbackOkNoApproveBorrower {} + + // Provide a router for the concrete type as well + #[public] + #[implements(IErc3156FlashBorrower)] + impl CallbackOkNoApproveBorrower {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc3156FlashBorrower for CallbackOkNoApproveBorrower { + fn on_flash_loan( + &mut self, + _initiator: Address, + _token: Address, + _amount: U256, + _fee: U256, + _data: Bytes, + ) -> Result> { + // Signal success but do not set allowance, so spend_allowance fails + Ok(super::BORROWER_CALLBACK_VALUE.into()) + } + } + + #[storage] + struct GoodBorrower; + + unsafe impl TopLevelStorage for GoodBorrower {} + + // Provide a router for the concrete type as well + #[public] + #[implements(IErc3156FlashBorrower)] + impl GoodBorrower {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc3156FlashBorrower for GoodBorrower { + fn on_flash_loan( + &mut self, + _initiator: Address, + token: Address, + amount: U256, + fee: U256, + _data: Bytes, + ) -> Result> { + // Approve the token contract itself to pull back amount + fee + let allowance = amount + .checked_add(fee) + .expect("allowance should not exceed `U256::MAX`"); + let token_iface = Erc20Interface::new(token); + let ok = token_iface + .approve(Call::new_in(self), token, allowance) + .map_err(|e| e)?; + if !ok { + return Err(b"approve returned false".to_vec()); + } + Ok(super::BORROWER_CALLBACK_VALUE.into()) + } + } + + #[storage] + struct ErrorBorrower; + + unsafe impl TopLevelStorage for ErrorBorrower {} + + // Provide a router for the concrete type as well + #[public] + #[implements(IErc3156FlashBorrower)] + impl ErrorBorrower {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc3156FlashBorrower for ErrorBorrower { + fn on_flash_loan( + &mut self, + _initiator: Address, + _token: Address, + _amount: U256, + _fee: U256, + _data: Bytes, + ) -> Result> { + Err("Borrower error".into()) + } + } #[storage] struct Erc20FlashMintTestExample { @@ -384,8 +511,10 @@ mod tests { erc20: Erc20, } + unsafe impl TopLevelStorage for Erc20FlashMintTestExample {} + #[public] - #[implements(IErc3156FlashLender)] + #[implements(IErc3156FlashLender, IErc20)] impl Erc20FlashMintTestExample {} #[public] @@ -421,7 +550,48 @@ mod tests { } } - unsafe impl TopLevelStorage for Erc20FlashMintTestExample {} + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc20 for Erc20FlashMintTestExample { + type Error = Error; + + fn total_supply(&self) -> U256 { + self.erc20.total_supply() + } + + fn balance_of(&self, account: Address) -> U256 { + self.erc20.balance_of(account) + } + + fn transfer( + &mut self, + to: Address, + value: U256, + ) -> Result { + Ok(self.erc20.transfer(to, value)?) + } + + fn allowance(&self, owner: Address, spender: Address) -> U256 { + self.erc20.allowance(owner, spender) + } + + fn approve( + &mut self, + spender: Address, + value: U256, + ) -> Result { + Ok(self.erc20.approve(spender, value)?) + } + + fn transfer_from( + &mut self, + from: Address, + to: Address, + value: U256, + ) -> Result { + Ok(self.erc20.transfer_from(from, to, value)?) + } + } #[motsu::test] fn max_flash_loan_token_match( @@ -500,6 +670,31 @@ mod tests { )); } + #[motsu::test] + fn flash_loan_reverts_when_receiver_is_invalid( + contract: Contract, + alice: Address, + ) { + // A very hacky way of forcing the receiver to have code, but fail the + // flash loan call during `_mint`. This is a workaround for our + // current implementation which makes it impossible to meaningfully + // override the `_mint` function to force it to fail. + let invalid_receiver_with_code: Contract = + Contract::::new_at(Address::ZERO); + + let err = contract + .sender(alice) + .flash_loan( + invalid_receiver_with_code.address(), + contract.address(), + U256::MAX, + vec![0, 1].into(), + ) + .motsu_expect_err("should return Error::InvalidReceiver"); + + assert!(matches!(err, Error::InvalidReceiver(_))); + } + #[motsu::test] fn flash_loan_reverts_when_exceeded_max_loan( contract: Contract, @@ -571,6 +766,231 @@ mod tests { )); } + #[motsu::test] + fn flash_loan_reverts_when_callback_returns_wrong_selector( + contract: Contract, + borrower: Contract, + alice: Address, + ) { + // Ensure fee could be anything; default zero is fine. + let err = contract + .sender(alice) + .flash_loan( + borrower.address(), + contract.address(), + uint!(123_U256), + vec![].into(), + ) + .motsu_expect_err("should revert due to wrong selector"); + + assert!(matches!( + err, + Error::ERC3156InvalidReceiver(ERC3156InvalidReceiver { receiver }) + if receiver == borrower.address() + )); + } + + #[motsu::test] + fn flash_loan_reverts_when_insufficient_allowance_after_callback( + contract: Contract, + borrower: Contract, + alice: Address, + ) { + // Set a non-zero fee to require allowance for value + fee + contract + .sender(alice) + .erc20_flash_mint + .flash_fee_value + .set(uint!(5_U256)); + + let err = contract + .sender(alice) + .flash_loan( + borrower.address(), + contract.address(), + uint!(100_U256), + vec![0xAA].into(), + ) + .motsu_expect_err("should revert due to insufficient allowance"); + + assert!(matches!(err, Error::InsufficientAllowance(_))); + } + + #[motsu::test] + fn flash_loan_reverts_borrower_reverts( + contract: Contract, + borrower: Contract, + alice: Address, + ) { + // Set a non-zero fee to require allowance for value + fee + contract + .sender(alice) + .erc20_flash_mint + .flash_fee_value + .set(uint!(5_U256)); + + let err = contract + .sender(alice) + .flash_loan( + borrower.address(), + contract.address(), + uint!(100_U256), + vec![0xAA].into(), + ) + .motsu_expect_err("should revert due to insufficient allowance"); + + assert!(matches!(err, + Error::ERC3156InvalidReceiver(ERC3156InvalidReceiver { receiver }) if receiver == borrower.address())); + } + + #[motsu::test] + fn flash_loan_succeeds_when_fee_or_receiver_zero( + contract: Contract, + borrower: Contract, + alice: Address, + bob: Address, + ) { + let amount = uint!(100_U256); + + // Case 1: fee = 0, receiver != 0 -> burns amount + contract.sender(alice).erc20_flash_mint.flash_fee_value.set(U256::ZERO); + contract + .sender(alice) + .erc20_flash_mint + .flash_fee_receiver_address + .set(bob); + + let ok = contract + .sender(alice) + .flash_loan( + borrower.address(), + contract.address(), + amount, + vec![].into(), + ) + .motsu_expect("flash loan should succeed when fee is zero"); + assert!(ok); + let receiver_balance = + contract.sender(alice).erc20.balance_of(borrower.address()); + let fee_receiver_balance = contract.sender(alice).erc20.balance_of(bob); + let total_supply = contract.sender(alice).erc20.total_supply(); + assert_eq!(receiver_balance, U256::ZERO); + assert_eq!(fee_receiver_balance, U256::ZERO); + assert_eq!(total_supply, U256::ZERO); + + // Case 2: fee != 0, receiver = 0 -> burns amount + fee + let fee = uint!(5_U256); + contract.sender(alice).erc20_flash_mint.flash_fee_value.set(fee); + contract + .sender(alice) + .erc20_flash_mint + .flash_fee_receiver_address + .set(Address::ZERO); + + // Pre-fund borrower with 'fee' tokens to allow burning amount + fee + contract + .sender(alice) + .erc20 + ._mint(borrower.address(), fee) + .motsu_expect("prefund fee to borrower"); + + let ok = contract + .sender(alice) + .flash_loan( + borrower.address(), + contract.address(), + amount, + vec![0x01].into(), + ) + .motsu_expect( + "flash loan should succeed when fee receiver is zero", + ); + assert!(ok); + let receiver_balance = + contract.sender(alice).erc20.balance_of(borrower.address()); + let fee_receiver_balance = + contract.sender(alice).erc20.balance_of(Address::ZERO); + let total_supply = contract.sender(alice).erc20.total_supply(); + assert_eq!(receiver_balance, U256::ZERO); + assert_eq!(fee_receiver_balance, U256::ZERO); + assert_eq!(total_supply, U256::ZERO); + + // Case 3: fee = 0, receiver = 0 -> burns amount + contract.sender(alice).erc20_flash_mint.flash_fee_value.set(U256::ZERO); + contract + .sender(alice) + .erc20_flash_mint + .flash_fee_receiver_address + .set(Address::ZERO); + + let ok = contract + .sender(alice) + .flash_loan( + borrower.address(), + contract.address(), + amount, + vec![0x02].into(), + ) + .motsu_expect( + "flash loan should succeed when both fee and receiver are zero", + ); + assert!(ok); + let receiver_balance = + contract.sender(alice).erc20.balance_of(borrower.address()); + let total_supply = contract.sender(alice).erc20.total_supply(); + assert_eq!(receiver_balance, U256::ZERO); + assert_eq!(total_supply, U256::ZERO); + } + + #[motsu::test] + fn flash_loan_succeeds_with_fee_and_fee_receiver( + contract: Contract, + borrower: Contract, + alice: Address, + bob: Address, + ) { + let amount = uint!(100_U256); + let fee = uint!(7_U256); + + // Set non-zero fee and non-zero fee receiver + contract.sender(alice).erc20_flash_mint.flash_fee_value.set(fee); + contract + .sender(alice) + .erc20_flash_mint + .flash_fee_receiver_address + .set(bob); + + // Prefund borrower with 'fee' tokens so they can repay value + fee + contract + .sender(alice) + .erc20 + ._mint(borrower.address(), fee) + .motsu_expect("prefund fee to borrower"); + + let ok = contract + .sender(alice) + .flash_loan( + borrower.address(), + contract.address(), + amount, + vec![0xAB].into(), + ) + .motsu_expect( + "flash loan should succeed with fee and fee receiver", + ); + assert!(ok); + + let receiver_balance = + contract.sender(alice).erc20.balance_of(borrower.address()); + let fee_receiver_balance = contract.sender(alice).erc20.balance_of(bob); + let total_supply = contract.sender(alice).erc20.total_supply(); + + assert_eq!(receiver_balance, U256::ZERO); + assert_eq!(fee_receiver_balance, fee); + // After burning `amount`, only `fee` remains in circulation at `bob`. + assert_eq!(total_supply, fee); + } + #[motsu::test] fn interface_id() { let actual = diff --git a/contracts/src/token/erc20/extensions/metadata.rs b/contracts/src/token/erc20/extensions/metadata.rs index 0b523c468..dfd1d1cdb 100644 --- a/contracts/src/token/erc20/extensions/metadata.rs +++ b/contracts/src/token/erc20/extensions/metadata.rs @@ -107,6 +107,16 @@ mod tests { unsafe impl TopLevelStorage for Erc20Metadata {} + #[motsu::test] + fn constructor(contract: Contract, alice: Address) { + let name: String = "Erc20Metadata".to_string(); + let symbol: String = "OZ".to_string(); + contract.sender(alice).constructor(name.clone(), symbol.clone()); + + assert_eq!(contract.sender(alice).name(), name); + assert_eq!(contract.sender(alice).symbol(), symbol); + } + #[motsu::test] fn interface_id() { let actual = ::interface_id(); diff --git a/contracts/src/token/erc20/extensions/wrapper.rs b/contracts/src/token/erc20/extensions/wrapper.rs index 786776483..f8f183fcb 100644 --- a/contracts/src/token/erc20/extensions/wrapper.rs +++ b/contracts/src/token/erc20/extensions/wrapper.rs @@ -390,6 +390,7 @@ mod tests { #[implements(IErc20Metadata, IErc165)] impl DummyErc20Metadata {} + #[cfg_attr(coverage_nightly, coverage(off))] #[public] impl IErc20Metadata for DummyErc20Metadata { fn name(&self) -> String { @@ -405,6 +406,7 @@ mod tests { } } + #[cfg_attr(coverage_nightly, coverage(off))] #[public] impl IErc165 for DummyErc20Metadata { fn supports_interface(&self, _interface_id: B32) -> bool { @@ -519,6 +521,7 @@ mod tests { )); } + #[cfg_attr(coverage_nightly, coverage(off))] #[motsu::test] #[ignore = "TODO: unignore once motsu fixes https://github.com/OpenZeppelin/stylus-test-helpers/issues/115."] fn deposit_for_reverts_when_invalid_asset( @@ -881,23 +884,28 @@ mod tests { } #[storage] - struct NonErc20; + struct InvalidUnderlyingToken; - #[public] - impl NonErc20 {} + unsafe impl TopLevelStorage for InvalidUnderlyingToken {} - unsafe impl TopLevelStorage for NonErc20 {} + #[public] + impl InvalidUnderlyingToken { + fn balance_of(&self, _account: Address) -> Result> { + Err("InvalidUnderlying".into()) + } + } - // TODO: Should be a test for the `Error::InvalidUnderlying` error, - // but impossible with current motsu limitations. + // TODO: update when Erc20Wrapper returns Vec on all errors: https://github.com/OpenZeppelin/rust-contracts-stylus/issues/800 #[motsu::test] - #[ignore] fn recover_reverts_when_invalid_underlying( contract: Contract, + invalid_underlying: Contract, alice: Address, ) { - let invalid_underlying = alice; - contract.sender(alice).wrapper.underlying.set(invalid_underlying); + contract + .sender(alice) + .constructor(invalid_underlying.address()) + .motsu_unwrap(); let err = contract .sender(alice) @@ -905,7 +913,7 @@ mod tests { .motsu_expect_err("should return Error::InvalidUnderlying"); assert!(matches!( - err, Error::InvalidUnderlying(ERC20InvalidUnderlying { token }) if token == invalid_underlying + err, Error::InvalidUnderlying(ERC20InvalidUnderlying { token }) if token == contract.address() )); } diff --git a/contracts/src/token/erc20/mod.rs b/contracts/src/token/erc20/mod.rs index 18568d81f..eb203eaff 100644 --- a/contracts/src/token/erc20/mod.rs +++ b/contracts/src/token/erc20/mod.rs @@ -749,6 +749,15 @@ mod tests { assert_eq!(initial_supply, contract.sender(alice).total_supply()); } + #[motsu::test] + fn burn_reverts_when_account_is_zero( + contract: Contract, + alice: Address, + ) { + let result = contract.sender(alice)._burn(Address::ZERO, uint!(1_U256)); + assert!(matches!(result, Err(Error::InvalidSender(_)))); + } + #[motsu::test] fn transfer(contract: Contract, alice: Address, bob: Address) { let one = uint!(1_U256); @@ -825,6 +834,19 @@ mod tests { assert_eq!(initial_supply, contract.sender(alice).total_supply()); } + #[motsu::test] + fn _transfer_reverts_when_from_is_zero( + contract: Contract, + alice: Address, + ) { + let result = contract.sender(alice)._transfer( + Address::ZERO, + alice, + uint!(1_U256), + ); + assert!(matches!(result, Err(Error::InvalidSender(_)))); + } + #[motsu::test] fn transfer_from(contract: Contract, alice: Address, bob: Address) { // Alice approves Bob. @@ -836,7 +858,10 @@ mod tests { contract.sender(alice)._mint(alice, two).motsu_unwrap(); assert_eq!(two, contract.sender(alice).balance_of(alice)); - contract.sender(bob).transfer_from(alice, bob, one).motsu_unwrap(); + let success = + contract.sender(bob).transfer_from(alice, bob, one).motsu_unwrap(); + + assert!(success); assert_eq!(one, contract.sender(alice).balance_of(alice)); assert_eq!(one, contract.sender(alice).balance_of(bob)); @@ -845,6 +870,31 @@ mod tests { contract.assert_emitted(&Transfer { from: alice, to: bob, value: one }); } + #[motsu::test] + fn transfer_from_succeeds_with_infinite_allowance( + contract: Contract, + alice: Address, + bob: Address, + ) { + // Alice approves Bob. + contract.sender(alice).approve(bob, U256::MAX).motsu_unwrap(); + + // Mint some tokens for Alice. + let one = uint!(1_U256); + contract.sender(alice)._mint(alice, one).motsu_unwrap(); + + let success = + contract.sender(bob).transfer_from(alice, bob, one).motsu_unwrap(); + + assert!(success); + + assert_eq!(U256::ZERO, contract.sender(alice).balance_of(alice)); + assert_eq!(one, contract.sender(alice).balance_of(bob)); + assert_eq!(U256::MAX, contract.sender(alice).allowance(alice, bob)); + + contract.assert_emitted(&Transfer { from: alice, to: bob, value: one }); + } + #[motsu::test] fn error_when_transfer_with_insufficient_balance( contract: Contract, diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 619a9bec0..b32a480dd 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -1376,6 +1376,7 @@ mod tests { unsafe impl TopLevelStorage for PanickingAllowanceToken {} + #[cfg_attr(coverage_nightly, coverage(off))] #[public] impl PanickingAllowanceToken { // External signature matches IERC20.allowance(owner, spender) -> @@ -1386,6 +1387,7 @@ mod tests { } } + #[cfg_attr(coverage_nightly, coverage(off))] #[motsu::test] #[ignore = "See: https://github.com/OpenZeppelin/stylus-test-helpers/issues/116"] fn safe_increase_allowance_reverts_on_allowance_call_panic( diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 48e76645d..ad4428cc4 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -842,22 +842,57 @@ impl IErc165 for Erc721Consecutive { #[cfg(test)] mod tests { use alloy_primitives::{uint, Address, U256}; - use motsu::prelude::Contract; + use motsu::prelude::*; use super::*; + use crate::token::erc721::IErc721Receiver; const FIRST_CONSECUTIVE_TOKEN_ID: U96 = uint!(0_U96); - const MAX_BATCH_SIZE: U96 = uint!(5000_U96); const TOKEN_ID: U256 = uint!(1_U256); const NON_CONSECUTIVE_TOKEN_ID: U256 = uint!(10001_U256); + // ---------------- Receiver mocks for acceptance-check tests + // ---------------- + + #[storage] + struct BadSelectorReceiver721; + + unsafe impl TopLevelStorage for BadSelectorReceiver721 {} + + #[public] + #[implements(IErc721Receiver, IErc165)] + impl BadSelectorReceiver721 {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc721Receiver for BadSelectorReceiver721 { + #[selector(name = "onERC721Received")] + fn on_erc721_received( + &mut self, + _operator: Address, + _from: Address, + _token_id: U256, + _data: Bytes, + ) -> Result> { + Ok(B32::ZERO) // wrong selector -> must be rejected + } + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc165 for BadSelectorReceiver721 { + fn supports_interface(&self, interface_id: B32) -> bool { + ::interface_id() == interface_id + || ::interface_id() == interface_id + } + } + impl Erc721Consecutive { fn init(&mut self, receivers: Vec
, batches: Vec) { - self.first_consecutive_id.set(FIRST_CONSECUTIVE_TOKEN_ID); - self.max_batch_size.set(MAX_BATCH_SIZE); + self.constructor(); for (to, batch_size) in receivers.into_iter().zip(batches) { self._mint_consecutive(to, batch_size) - .expect("should mint consecutively"); + .motsu_expect("should mint consecutively"); } } } @@ -867,7 +902,7 @@ mod tests { let initial_balance = contract .sender(alice) .balance_of(alice) - .expect("should return the balance of Alice"); + .motsu_expect("should return the balance of Alice"); let init_tokens_count = uint!(10_U96); contract.sender(alice).init(vec![alice], vec![init_tokens_count]); @@ -875,7 +910,7 @@ mod tests { let balance1 = contract .sender(alice) .balance_of(alice) - .expect("should return the balance of Alice"); + .motsu_expect("should return the balance of Alice"); assert_eq!(balance1, initial_balance + U256::from(init_tokens_count)); // Check non-consecutive mint. @@ -883,17 +918,17 @@ mod tests { contract .sender(alice) ._mint(alice, non_consecutive_token_id) - .expect("should mint a token for Alice"); + .motsu_expect("should mint a token for Alice"); let owner = contract .sender(alice) .owner_of(non_consecutive_token_id) - .expect("should return the owner of the token"); + .motsu_expect("should return the owner of the token"); assert_eq!(owner, alice); let balance2 = contract .sender(alice) .balance_of(alice) - .expect("should return the balance of Alice"); + .motsu_expect("should return the balance of Alice"); assert_eq!(balance2, balance1 + uint!(1_U256)); } @@ -906,11 +941,11 @@ mod tests { contract .sender(alice) ._mint(alice, TOKEN_ID) - .expect("should mint the token a first time"); + .motsu_expect("should mint the token a first time"); let err = contract .sender(alice) ._mint(alice, TOKEN_ID) - .expect_err("should not mint a token with `TOKEN_ID` twice"); + .motsu_expect_err("should not mint a token with `TOKEN_ID` twice"); assert!(matches!( err, @@ -928,7 +963,7 @@ mod tests { let err = contract .sender(alice) ._mint(invalid_receiver, TOKEN_ID) - .expect_err("should not mint a token for invalid receiver"); + .motsu_expect_err("should not mint a token for invalid receiver"); assert!(matches!( err, @@ -946,7 +981,7 @@ mod tests { let err = contract .sender(alice) ._mint_consecutive(Address::ZERO, uint!(11_U96)) - .expect_err("should not mint consecutive"); + .motsu_expect_err("should not mint consecutive"); assert!(matches!( err, Error::InvalidReceiver(ERC721InvalidReceiver { @@ -965,7 +1000,7 @@ mod tests { let err = contract .sender(alice) ._mint_consecutive(alice, batch_size) - .expect_err("should not mint consecutive"); + .motsu_expect_err("should not mint consecutive"); assert!(matches!( err, Error::ExceededMaxBatchMint(ERC721ExceededMaxBatchMint { @@ -991,46 +1026,46 @@ mod tests { contract .sender(alice) .transfer_from(alice, bob, U256::from(FIRST_CONSECUTIVE_TOKEN_ID)) - .expect("should transfer a token from Alice to Bob"); + .motsu_expect("should transfer a token from Alice to Bob"); let owner = contract .sender(alice) .owner_of(U256::from(FIRST_CONSECUTIVE_TOKEN_ID)) - .expect("token should be owned"); + .motsu_expect("token should be owned"); assert_eq!(owner, bob); // Check that balances changed. let alice_balance = contract .sender(alice) .balance_of(alice) - .expect("should return the balance of Alice"); + .motsu_expect("should return the balance of Alice"); assert_eq!(alice_balance, uint!(1000_U256) - uint!(1_U256)); let bob_balance = contract .sender(alice) .balance_of(bob) - .expect("should return the balance of Bob"); + .motsu_expect("should return the balance of Bob"); assert_eq!(bob_balance, uint!(1000_U256) + uint!(1_U256)); // Check non-consecutive mint. contract .sender(alice) ._mint(alice, NON_CONSECUTIVE_TOKEN_ID) - .expect("should mint a token to Alice"); + .motsu_expect("should mint a token to Alice"); let alice_balance = contract .sender(alice) .balance_of(alice) - .expect("should return the balance of Alice"); + .motsu_expect("should return the balance of Alice"); assert_eq!(alice_balance, uint!(1000_U256)); // Check transfer of the token that wasn't minted consecutive. contract .sender(alice) .transfer_from(alice, bob, NON_CONSECUTIVE_TOKEN_ID) - .expect("should transfer a token from Alice to Bob"); + .motsu_expect("should transfer a token from Alice to Bob"); let alice_balance = contract .sender(alice) .balance_of(alice) - .expect("should return the balance of Alice"); + .motsu_expect("should return the balance of Alice"); assert_eq!(alice_balance, uint!(1000_U256) - uint!(1_U256)); } @@ -1043,18 +1078,18 @@ mod tests { contract .sender(alice) ._burn(U256::from(FIRST_CONSECUTIVE_TOKEN_ID)) - .expect("should burn token"); + .motsu_expect("should burn token"); let alice_balance = contract .sender(alice) .balance_of(alice) - .expect("should return the balance of Alice"); + .motsu_expect("should return the balance of Alice"); assert_eq!(alice_balance, uint!(1000_U256) - uint!(1_U256)); let err = contract .sender(alice) .owner_of(U256::from(FIRST_CONSECUTIVE_TOKEN_ID)) - .expect_err("token should not exist"); + .motsu_expect_err("token should not exist"); assert!(matches!( err, @@ -1067,27 +1102,27 @@ mod tests { contract .sender(alice) ._mint(alice, non_consecutive_token_id) - .expect("should mint a token to Alice"); + .motsu_expect("should mint a token to Alice"); let owner = contract .sender(alice) .owner_of(non_consecutive_token_id) - .expect("should return owner of the token"); + .motsu_expect("should return owner of the token"); assert_eq!(owner, alice); let alice_balance = contract .sender(alice) .balance_of(alice) - .expect("should return the balance of Alice"); + .motsu_expect("should return the balance of Alice"); assert_eq!(alice_balance, uint!(1000_U256)); contract .sender(alice) ._burn(non_consecutive_token_id) - .expect("should burn token"); + .motsu_expect("should burn token"); let err = contract .sender(alice) .owner_of(U256::from(non_consecutive_token_id)) - .expect_err("token should not exist"); + .motsu_expect_err("token should not exist"); assert!(matches!( err, @@ -1100,7 +1135,7 @@ mod tests { let err = contract .sender(alice) ._burn(non_existent_token) - .expect_err("should return Error::NonexistentToken"); + .motsu_expect_err("should return Error::NonexistentToken"); assert!(matches!( err, @@ -1119,17 +1154,17 @@ mod tests { contract .sender(alice) ._mint(alice, TOKEN_ID) - .expect("should mint a token to Alice"); + .motsu_expect("should mint a token to Alice"); contract .sender(alice) .safe_transfer_from(alice, bob, TOKEN_ID) - .expect("should transfer a token from Alice to Bob"); + .motsu_expect("should transfer a token from Alice to Bob"); let owner = contract .sender(alice) .owner_of(TOKEN_ID) - .expect("should return the owner of the token"); + .motsu_expect("should return the owner of the token"); assert_eq!(owner, bob); } @@ -1143,19 +1178,19 @@ mod tests { contract .sender(alice) ._mint(bob, TOKEN_ID) - .expect("should mint token to Bob"); + .motsu_expect("should mint token to Bob"); contract .sender(bob) .approve(alice, TOKEN_ID) - .expect("should approve Bob's token to Alice"); + .motsu_expect("should approve Bob's token to Alice"); contract .sender(alice) .safe_transfer_from(bob, alice, TOKEN_ID) - .expect("should transfer Bob's token to Alice"); + .motsu_expect("should transfer Bob's token to Alice"); let owner = contract .sender(alice) .owner_of(TOKEN_ID) - .expect("should return the owner of the token"); + .motsu_expect("should return the owner of the token"); assert_eq!(owner, alice); } @@ -1169,12 +1204,12 @@ mod tests { contract .sender(alice) ._mint(alice, TOKEN_ID) - .expect("should mint a token to Alice"); + .motsu_expect("should mint a token to Alice"); let err = contract .sender(alice) .safe_transfer_from(dave, bob, TOKEN_ID) - .expect_err("should not transfer from incorrect owner"); + .motsu_expect_err("should not transfer from incorrect owner"); assert!(matches!( err, @@ -1187,7 +1222,31 @@ mod tests { } #[motsu::test] - fn error_when_internal_safe_transfer_nonexistent_token( + fn _safe_transfer_succeeds( + contract: Contract, + alice: Address, + bob: Address, + ) { + contract + .sender(alice) + ._mint(alice, TOKEN_ID) + .expect("should mint a token to Alice"); + + contract + .sender(alice) + ._safe_transfer(alice, bob, TOKEN_ID, &vec![0, 1, 2, 3].into()) + .expect("should transfer a token from Alice to Bob"); + + let owner = contract + .sender(alice) + .owner_of(TOKEN_ID) + .expect("should return the owner of the token"); + + assert_eq!(owner, bob); + } + + #[motsu::test] + fn _safe_transfer_reverts_on_nonexistent_token( contract: Contract, alice: Address, bob: Address, @@ -1195,7 +1254,7 @@ mod tests { let err = contract .sender(alice) ._safe_transfer(alice, bob, TOKEN_ID, &vec![0, 1, 2, 3].into()) - .expect_err("should not transfer a non-existent token"); + .motsu_expect_err("should not transfer a non-existent token"); assert!(matches!( err, @@ -1215,12 +1274,14 @@ mod tests { contract .sender(alice) ._mint(alice, TOKEN_ID) - .expect("should mint a token to Alice"); + .motsu_expect("should mint a token to Alice"); let err = contract .sender(alice) .safe_transfer_from(alice, invalid_receiver, TOKEN_ID) - .expect_err("should not transfer the token to invalid receiver"); + .motsu_expect_err( + "should not transfer the token to invalid receiver", + ); assert!(matches!( err, @@ -1232,7 +1293,7 @@ mod tests { let owner = contract .sender(alice) .owner_of(TOKEN_ID) - .expect("should return the owner of the token"); + .motsu_expect("should return the owner of the token"); assert_eq!(alice, owner); } @@ -1245,7 +1306,7 @@ mod tests { contract .sender(alice) ._mint(alice, TOKEN_ID) - .expect("should mint a token to Alice"); + .motsu_expect("should mint a token to Alice"); contract .sender(alice) @@ -1255,16 +1316,64 @@ mod tests { TOKEN_ID, vec![0, 1, 2, 3].into(), ) - .expect("should transfer a token from Alice to Bob"); + .motsu_expect("should transfer a token from Alice to Bob"); let owner = contract .sender(alice) .owner_of(TOKEN_ID) - .expect("should return the owner of the token"); + .motsu_expect("should return the owner of the token"); assert_eq!(owner, bob); } + #[motsu::test] + fn safe_transfer_from_reverts_when_receiver_returns_wrong_selector( + contract: Contract, + bad: Contract, + alice: Address, + ) { + let token_id = uint!(45_U256); + // Mint to alice + contract.sender(alice)._mint(alice, token_id).motsu_unwrap(); + + let err = contract + .sender(alice) + .safe_transfer_from(alice, bad.address(), token_id) + .motsu_expect_err("wrong selector should be rejected in transfer"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC721InvalidReceiver { receiver }) if receiver == bad.address() + )); + // State unchanged + let owner = contract.sender(alice).owner_of(token_id).motsu_unwrap(); + assert_eq!(owner, alice); + } + + #[motsu::test] + fn _safe_transfer_reverts_when_receiver_returns_wrong_selector( + contract: Contract, + bad: Contract, + alice: Address, + ) { + let token_id = uint!(45_U256); + // Mint to alice + contract.sender(alice)._mint(alice, token_id).motsu_unwrap(); + + let err = contract + .sender(alice) + ._safe_transfer(alice, bad.address(), token_id, &vec![].into()) + .motsu_expect_err("wrong selector should be rejected in transfer"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC721InvalidReceiver { receiver }) if receiver == bad.address() + )); + // State unchanged + let owner = contract.sender(alice).owner_of(token_id).motsu_unwrap(); + assert_eq!(owner, alice); + } + #[motsu::test] fn error_when_internal_safe_transfer_to_invalid_receiver( contract: Contract, @@ -1275,7 +1384,7 @@ mod tests { contract .sender(alice) ._mint(alice, TOKEN_ID) - .expect("should mint a token to Alice"); + .motsu_expect("should mint a token to Alice"); let err = contract .sender(alice) @@ -1285,7 +1394,9 @@ mod tests { TOKEN_ID, &vec![0, 1, 2, 3].into(), ) - .expect_err("should not transfer the token to invalid receiver"); + .motsu_expect_err( + "should not transfer the token to invalid receiver", + ); assert!(matches!( err, @@ -1297,7 +1408,7 @@ mod tests { let owner = contract .sender(alice) .owner_of(TOKEN_ID) - .expect("should return the owner of the token"); + .motsu_expect("should return the owner of the token"); assert_eq!(alice, owner); } @@ -1311,12 +1422,14 @@ mod tests { contract .sender(alice) ._mint(alice, TOKEN_ID) - .expect("should mint a token to Alice"); + .motsu_expect("should mint a token to Alice"); let err = contract .sender(alice) ._safe_transfer(dave, bob, TOKEN_ID, &vec![0, 1, 2, 3].into()) - .expect_err("should not transfer the token from incorrect owner"); + .motsu_expect_err( + "should not transfer the token from incorrect owner", + ); assert!(matches!( err, Error::IncorrectOwner(ERC721IncorrectOwner { @@ -1328,31 +1441,58 @@ mod tests { } #[motsu::test] - fn safe_mints(contract: Contract, alice: Address) { + fn safe_mint_succeeds( + contract: Contract, + alice: Address, + ) { let initial_balance = contract .sender(alice) .balance_of(alice) - .expect("should return the balance of Alice"); + .motsu_expect("should return the balance of Alice"); contract .sender(alice) ._safe_mint(alice, TOKEN_ID, &vec![0, 1, 2, 3].into()) - .expect("should mint a token for Alice"); + .motsu_expect("should mint a token for Alice"); let owner = contract .sender(alice) .owner_of(TOKEN_ID) - .expect("should return the owner of the token"); + .motsu_expect("should return the owner of the token"); assert_eq!(owner, alice); let balance = contract .sender(alice) .balance_of(alice) - .expect("should return the balance of Alice"); + .motsu_expect("should return the balance of Alice"); assert_eq!(initial_balance + uint!(1_U256), balance); } + #[motsu::test] + fn safe_mint_rejects_when_receiver_returns_wrong_selector( + contract: Contract, + bad: Contract, + alice: Address, + ) { + let token_id = uint!(42_U256); + let err = contract + .sender(alice) + ._safe_mint(bad.address(), token_id, &vec![].into()) + .motsu_expect_err( + "receiver returning wrong selector must be rejected", + ); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC721InvalidReceiver { receiver }) if receiver == bad.address() + )); + // Ensure token not minted + let balance = + contract.sender(alice).balance_of(bad.address()).motsu_unwrap(); + assert_eq!(U256::ZERO, balance); + } + #[motsu::test] fn error_when_approve_for_nonexistent_token( contract: Contract, @@ -1362,7 +1502,7 @@ mod tests { let err = contract .sender(alice) .approve(bob, TOKEN_ID) - .expect_err("should not approve for a non-existent token"); + .motsu_expect_err("should not approve for a non-existent token"); assert!(matches!( err, @@ -1382,12 +1522,12 @@ mod tests { contract .sender(alice) ._mint(bob, TOKEN_ID) - .expect("should mint a token"); + .motsu_expect("should mint a token"); let err = contract .sender(alice) .approve(dave, TOKEN_ID) - .expect_err("should not approve when invalid approver"); + .motsu_expect_err("should not approve when invalid approver"); assert!(matches!( err, @@ -1403,27 +1543,57 @@ mod tests { alice: Address, bob: Address, ) { - contract - .sender(alice) - .set_approval_for_all(bob, true) - .expect("should approve Bob for operations on all Alice's tokens"); + contract.sender(alice).set_approval_for_all(bob, true).motsu_expect( + "should approve Bob for operations on all Alice's tokens", + ); assert!(contract.sender(alice).is_approved_for_all(alice, bob)); - contract.sender(alice).set_approval_for_all(bob, false).expect( + contract.sender(alice).set_approval_for_all(bob, false).motsu_expect( "should disapprove Bob for operations on all Alice's tokens", ); assert!(!contract.sender(alice).is_approved_for_all(alice, bob)); } #[motsu::test] - fn error_when_get_approved_of_nonexistent_token( + fn get_approved_token_with_approval( contract: Contract, alice: Address, + bob: Address, ) { - let err = contract + contract + .sender(alice) + ._mint(alice, TOKEN_ID) + .motsu_expect("should mint a token"); + contract + .sender(alice) + .approve(bob, TOKEN_ID) + .motsu_expect("should approve Bob for operations on token"); + + let approved = contract.sender(alice).get_approved(TOKEN_ID); + assert!(matches!(approved, Ok(addr) if addr == bob)); + } + + #[motsu::test] + fn _mint_consecutive_succeeds_for_zero_batch_size( + contract: Contract, + alice: Address, + ) { + let next = contract .sender(alice) - .get_approved(TOKEN_ID) - .expect_err("should not return approved for a non-existent token"); + ._mint_consecutive(alice, U96::ZERO) + .motsu_expect("should mint consecutive tokens"); + assert_eq!(next, U96::ZERO); + } + + #[motsu::test] + fn error_when_get_approved_of_nonexistent_token( + contract: Contract, + alice: Address, + ) { + let err = + contract.sender(alice).get_approved(TOKEN_ID).motsu_expect_err( + "should not return approved for a non-existent token", + ); assert!(matches!( err, diff --git a/contracts/src/token/erc721/extensions/enumerable.rs b/contracts/src/token/erc721/extensions/enumerable.rs index fffad2444..5d8f094b3 100644 --- a/contracts/src/token/erc721/extensions/enumerable.rs +++ b/contracts/src/token/erc721/extensions/enumerable.rs @@ -390,9 +390,8 @@ mod tests { #[public] impl IErc165 for Erc721EnumerableTestExample { fn supports_interface(&self, interface_id: B32) -> bool { - ::interface_id() - == interface_id - || ::interface_id() == interface_id + self.enumerable.supports_interface(interface_id) + || self.erc721.supports_interface(interface_id) } } @@ -647,6 +646,94 @@ mod tests { )); } + #[motsu::test] + fn remove_token_from_owner_enumeration_swaps_when_not_last( + contract: Contract, + alice: Address, + bob: Address, + ) { + // Mint three tokens to Alice and add them to owner enumeration in order + let t0 = U256::from(10); + let t1 = U256::from(11); + let t2 = U256::from(12); + + for &tid in &[t0, t1, t2] { + contract + .sender(alice) + .erc721 + ._mint(alice, tid) + .expect("should mint a token to Alice"); + contract + .sender(alice) + .enumerable + ._add_token_to_owner_enumeration( + alice, + tid, + &contract.sender(alice).erc721, + ) + .expect("should add token to owner enumeration"); + } + + // Sanity: order is [t0, t1, t2] + let id0 = contract + .sender(alice) + .token_of_owner_by_index(alice, U256::from(0)) + .expect("index 0 should exist"); + let id1 = contract + .sender(alice) + .token_of_owner_by_index(alice, U256::from(1)) + .expect("index 1 should exist"); + let id2 = contract + .sender(alice) + .token_of_owner_by_index(alice, U256::from(2)) + .expect("index 2 should exist"); + assert_eq!(id0, t0); + assert_eq!(id1, t1); + assert_eq!(id2, t2); + + // Transfer the middle token out (t1) to decrement Alice's balance, + // then remove it from Alice's enumeration. This should trigger the + // swap-and-pop branch moving t2 into index 1 and clearing index 2. + contract + .sender(alice) + .erc721 + .transfer_from(alice, bob, t1) + .expect("should transfer middle token to Bob"); + + contract + .sender(alice) + .enumerable + ._remove_token_from_owner_enumeration( + alice, + t1, + &contract.sender(alice).erc721, + ) + .expect("should remove token from Alice enumeration"); + + // Now Alice should own [t0, t2] in indices [0, 1], and index 2 should + // be empty + let id0_after = contract + .sender(alice) + .token_of_owner_by_index(alice, U256::from(0)) + .expect("index 0 should still exist"); + let id1_after = contract + .sender(alice) + .token_of_owner_by_index(alice, U256::from(1)) + .expect("index 1 should still exist"); + assert_eq!(id0_after, t0); + assert_eq!(id1_after, t2, "last token should be swapped into index 1"); + + let err = contract + .sender(alice) + .token_of_owner_by_index(alice, U256::from(2)) + .expect_err("index 2 should be cleared after pop"); + assert!(matches!( + err, + Error::OutOfBoundsIndex(ERC721OutOfBoundsIndex { owner, index }) + if owner == alice && index == U256::from(2) + )); + } + #[motsu::test] fn reverts_when_token_of_owner_does_not_own_any_token( contract: Contract, diff --git a/contracts/src/token/erc721/extensions/metadata.rs b/contracts/src/token/erc721/extensions/metadata.rs index 7df27da14..1ee666f2a 100644 --- a/contracts/src/token/erc721/extensions/metadata.rs +++ b/contracts/src/token/erc721/extensions/metadata.rs @@ -118,8 +118,8 @@ impl Erc721Metadata { #[cfg(test)] mod tests { - use alloy_primitives::{aliases::B32, Address}; - use motsu::prelude::Contract; + use alloy_primitives::{aliases::B32, uint, Address}; + use motsu::prelude::*; use super::*; use crate::{ @@ -133,6 +133,8 @@ mod tests { metadata: Erc721Metadata, } + unsafe impl TopLevelStorage for Erc721MetadataExample {} + #[public] #[implements(IErc721Metadata, IErc165)] impl Erc721MetadataExample { @@ -168,7 +170,67 @@ mod tests { } } - unsafe impl TopLevelStorage for Erc721MetadataExample {} + #[motsu::test] + fn constructor(contract: Contract, alice: Address) { + let name: String = "Erc721MetadataExample".to_string(); + let symbol: String = "OZ".to_string(); + contract.sender(alice).constructor(name.clone(), symbol.clone()); + + assert_eq!(contract.sender(alice).name(), name); + assert_eq!(contract.sender(alice).symbol(), symbol); + } + + #[motsu::test] + fn token_uri_returns_empty_string_if_base_uri_is_empty( + contract: Contract, + alice: Address, + ) { + let name: String = "Erc721MetadataExample".to_string(); + let symbol: String = "OZ".to_string(); + contract.sender(alice).constructor(name.clone(), symbol.clone()); + + let token_id = uint!(1_U256); + contract.sender(alice).erc721._mint(alice, token_id).motsu_unwrap(); + + let token_uri = + contract.sender(alice).token_uri(token_id).motsu_unwrap(); + assert!(token_uri.is_empty()); + } + + #[motsu::test] + fn token_uri_returns_base_uri_concatenated_with_token_id( + contract: Contract, + alice: Address, + ) { + let base_uri = "https://example.com/"; + contract.sender(alice).metadata.base_uri.set_str(base_uri); + + let token_id = uint!(1_U256); + contract.sender(alice).erc721._mint(alice, token_id).motsu_unwrap(); + + let token_uri = + contract.sender(alice).token_uri(token_id).motsu_unwrap(); + assert_eq!(token_uri, format!("{}{}", base_uri, token_id)); + } + + #[motsu::test] + fn token_uri_reverts_on_missing_token_id( + contract: Contract, + alice: Address, + ) { + let token_id = uint!(1_U256); + let err = contract + .sender(alice) + .token_uri(token_id) + .motsu_expect_err("should revert on missing token id"); + + assert!(matches!( + err, + erc721::Error::NonexistentToken(erc721::ERC721NonexistentToken { + token_id: t_id + }) if token_id == t_id + )); + } #[motsu::test] fn interface_id() { @@ -192,14 +254,4 @@ mod tests { let fake_interface_id: B32 = 0x12345678_u32.into(); assert!(!contract.sender(alice).supports_interface(fake_interface_id)); } - - #[motsu::test] - fn constructor(contract: Contract, alice: Address) { - let name: String = "Erc721MetadataExample".to_string(); - let symbol: String = "OZ".to_string(); - contract.sender(alice).constructor(name.clone(), symbol.clone()); - - assert_eq!(contract.sender(alice).name(), name); - assert_eq!(contract.sender(alice).symbol(), symbol); - } } diff --git a/contracts/src/token/erc721/extensions/uri_storage.rs b/contracts/src/token/erc721/extensions/uri_storage.rs index bd005621e..052c9ad60 100644 --- a/contracts/src/token/erc721/extensions/uri_storage.rs +++ b/contracts/src/token/erc721/extensions/uri_storage.rs @@ -117,6 +117,8 @@ mod tests { pub uri_storage: Erc721UriStorage, } + unsafe impl TopLevelStorage for Erc721MetadataExample {} + #[public] #[implements(IErc721Metadata, IErc165)] impl Erc721MetadataExample { @@ -157,50 +159,109 @@ mod tests { } } - unsafe impl TopLevelStorage for Erc721MetadataExample {} - #[motsu::test] - fn interface_id() { - let actual = ::interface_id(); - let expected: B32 = 0x5b5e139f.into(); - assert_eq!(actual, expected); + fn constructor(contract: Contract, alice: Address) { + let name: String = "Erc721MetadataExample".to_string(); + let symbol: String = "OZ".to_string(); + contract.sender(alice).constructor(name.clone(), symbol.clone()); + + assert_eq!(contract.sender(alice).name(), name); + assert_eq!(contract.sender(alice).symbol(), symbol); } #[motsu::test] - fn supports_interface( + fn token_uri_returns_token_uri_if_base_uri_is_empty( contract: Contract, alice: Address, ) { - assert!(contract.sender(alice).supports_interface( - ::interface_id() - )); - assert!(contract.sender(alice).supports_interface( - ::interface_id() - )); + contract + .sender(alice) + .erc721 + ._mint(alice, TOKEN_ID) + .motsu_expect("should mint a token for Alice"); - let fake_interface_id: B32 = 0x12345678_u32.into(); - assert!(!contract.sender(alice).supports_interface(fake_interface_id)); + let token_uri = String::from("https://docs.openzeppelin.com/contracts/5.x/api/token/erc721#Erc721URIStorage"); + contract.sender(alice).set_token_uri(TOKEN_ID, token_uri.clone()); + + assert_eq!( + token_uri, + contract + .sender(alice) + .token_uri(TOKEN_ID) + .motsu_expect("should return token URI") + ); } + #[motsu::test] - fn token_uri_works( + fn token_uri_returns_base_uri_concatenated_with_token_id( contract: Contract, alice: Address, ) { + let base_uri = "https://example.com/"; + contract.sender(alice).metadata.base_uri.set_str(base_uri); + contract .sender(alice) .erc721 ._mint(alice, TOKEN_ID) - .expect("should mint a token for Alice"); + .motsu_expect("should mint a token for Alice"); let token_uri = String::from("https://docs.openzeppelin.com/contracts/5.x/api/token/erc721#Erc721URIStorage"); contract.sender(alice).set_token_uri(TOKEN_ID, token_uri.clone()); + let concatenated_token_uri = contract + .sender(alice) + .token_uri(TOKEN_ID) + .motsu_expect("should return token URI"); + assert_eq!( - token_uri, - contract - .sender(alice) - .token_uri(TOKEN_ID) - .expect("should return token URI") + concatenated_token_uri, + format!("{}{}", base_uri, token_uri) ); } + + #[motsu::test] + fn token_uri_calls_parent_function_if_token_uri_is_not_set( + contract: Contract, + alice: Address, + ) { + let base_uri = "https://example.com/"; + contract.sender(alice).metadata.base_uri.set_str(base_uri); + + contract + .sender(alice) + .erc721 + ._mint(alice, TOKEN_ID) + .motsu_expect("should mint a token for Alice"); + + let token_uri = contract + .sender(alice) + .token_uri(TOKEN_ID) + .motsu_expect("should return token URI"); + + assert_eq!(token_uri, format!("{}{}", base_uri, TOKEN_ID)); + } + + #[motsu::test] + fn interface_id() { + let actual = ::interface_id(); + let expected: B32 = 0x5b5e139f.into(); + assert_eq!(actual, expected); + } + + #[motsu::test] + fn supports_interface( + contract: Contract, + alice: Address, + ) { + assert!(contract.sender(alice).supports_interface( + ::interface_id() + )); + assert!(contract.sender(alice).supports_interface( + ::interface_id() + )); + + let fake_interface_id: B32 = 0x12345678_u32.into(); + assert!(!contract.sender(alice).supports_interface(fake_interface_id)); + } } diff --git a/contracts/src/token/erc721/extensions/wrapper.rs b/contracts/src/token/erc721/extensions/wrapper.rs index b0255fb7d..b48a08c7d 100644 --- a/contracts/src/token/erc721/extensions/wrapper.rs +++ b/contracts/src/token/erc721/extensions/wrapper.rs @@ -402,7 +402,7 @@ mod tests { use super::*; use crate::{ - token::erc721::{self, IErc721}, + token::erc721::{self, tests::EmptyReasonReceiver721, IErc721}, utils::introspection::erc165::IErc165, }; @@ -416,6 +416,8 @@ mod tests { erc721: Erc721, } + unsafe impl TopLevelStorage for Erc721WrapperTestExample {} + #[public] #[implements(IErc721, IErc721Wrapper, IErc165)] impl Erc721WrapperTestExample { @@ -433,6 +435,7 @@ mod tests { } } + #[cfg_attr(coverage_nightly, coverage(off))] #[public] impl IErc721 for Erc721WrapperTestExample { type Error = erc721::Error; @@ -551,6 +554,7 @@ mod tests { } } + #[cfg_attr(coverage_nightly, coverage(off))] #[public] impl IErc165 for Erc721WrapperTestExample { fn supports_interface(&self, interface_id: B32) -> bool { @@ -558,8 +562,6 @@ mod tests { } } - unsafe impl TopLevelStorage for Erc721WrapperTestExample {} - #[motsu::test] fn underlying_works( contract: Contract, @@ -573,18 +575,17 @@ mod tests { assert_eq!(contract.sender(alice).underlying(), erc721_address); } - // TODO: motsu should revert on calling a function that doesn't exist at - // specified address. + #[cfg_attr(coverage_nightly, coverage(off))] #[motsu::test] - #[ignore] - fn deposit_for_reverts_when_unsupported_token( + #[ignore = "TODO: un-ignore when motsu supports returning empty revert reasons, see: https://github.com/OpenZeppelin/stylus-test-helpers/issues/118"] + fn deposit_for_reverts_when_underlying_reverts_without_reason( contract: Contract, + invalid_underlying: Contract, alice: Address, ) { let token_ids = random_token_ids(1); - let invalid_token = alice; - contract.sender(alice).constructor(invalid_token); + contract.sender(alice).constructor(invalid_underlying.address()); let err = contract .sender(alice) @@ -594,7 +595,7 @@ mod tests { assert!(matches!( err, Error::UnsupportedToken(ERC721UnsupportedToken { token } - ) if token == invalid_token + ) if token == invalid_underlying.address() )); } @@ -890,6 +891,45 @@ mod tests { )); } + #[cfg_attr(coverage_nightly, coverage(off))] + #[motsu::test] + #[ignore = "TODO: un-ignore when motsu supports returning empty revert reasons, see: https://github.com/OpenZeppelin/stylus-test-helpers/issues/118"] + fn withdraw_to_reverts_when_underlying_reverts_without_reason( + contract: Contract, + invalid_underlying: Contract, + alice: Address, + ) { + let token_ids = random_token_ids(1); + + contract.sender(alice).constructor(invalid_underlying.address()); + + for &token_id in &token_ids { + contract + .sender(alice) + .erc721 + ._mint(alice, token_id) + .motsu_expect("should mint {token_id} for {alice}"); + + contract + .sender(alice) + .approve(contract.address(), token_id) + .motsu_expect( + "should approve {token_id} for {contract.address()}", + ); + } + + let err = contract + .sender(alice) + .withdraw_to(alice, token_ids.clone()) + .motsu_expect_err("should return empty reason"); + + assert!(matches!( + err, + Error::Erc721FailedOperation(Erc721FailedOperation { token } + ) if token == invalid_underlying.address() + )); + } + #[motsu::test] fn withdraw_to_works( contract: Contract, @@ -1059,18 +1099,28 @@ mod tests { ); } - // TODO: motsu should revert on calling a function that doesn't exist at - // specified address. + #[storage] + struct InvalidToken; + + unsafe impl TopLevelStorage for InvalidToken {} + + #[public] + impl InvalidToken { + fn owner_of(&self, _token_id: U256) -> Result> { + Err("InvalidToken".into()) + } + } + + // TODO: update when Erc721Wrapper returns Vec on all errors: https://github.com/OpenZeppelin/rust-contracts-stylus/issues/801 #[motsu::test] - #[ignore] fn recover_reverts_when_invalid_token( contract: Contract, + invalid_token: Contract, alice: Address, ) { let token_id = random_token_ids(1)[0]; - let invalid_token_address = alice; - contract.sender(alice).constructor(invalid_token_address); + contract.sender(alice).constructor(invalid_token.address()); let err = contract .sender(alice) @@ -1080,7 +1130,7 @@ mod tests { assert!(matches!( err, Error::Erc721FailedOperation(Erc721FailedOperation { token }) - if token == invalid_token_address + if token == invalid_token.address() )); } diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs index 2f76949c2..9cc973d81 100644 --- a/contracts/src/token/erc721/mod.rs +++ b/contracts/src/token/erc721/mod.rs @@ -1066,7 +1066,7 @@ mod tests { ERC721IncorrectOwner, ERC721InsufficientApproval, ERC721InvalidApprover, ERC721InvalidOperator, ERC721InvalidOwner, ERC721InvalidReceiver, ERC721InvalidSender, ERC721NonexistentToken, - Erc721, Error, IErc721, + Erc721, Error, IErc721, IErc721Receiver, }; use crate::utils::introspection::erc165::IErc165; @@ -1140,6 +1140,226 @@ mod tests { assert_eq!(initial_balance + uint!(1_U256), balance); } + // ---------------- Receiver mocks for acceptance-check tests + // ---------------- + + #[storage] + struct BadSelectorReceiver721; + + unsafe impl TopLevelStorage for BadSelectorReceiver721 {} + + #[public] + #[implements(IErc721Receiver, IErc165)] + impl BadSelectorReceiver721 {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc721Receiver for BadSelectorReceiver721 { + #[selector(name = "onERC721Received")] + fn on_erc721_received( + &mut self, + _operator: Address, + _from: Address, + _token_id: U256, + _data: Bytes, + ) -> Result> { + Ok(B32::ZERO) // wrong selector -> must be rejected + } + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc165 for BadSelectorReceiver721 { + fn supports_interface(&self, interface_id: B32) -> bool { + ::interface_id() == interface_id + || ::interface_id() == interface_id + } + } + + #[storage] + struct RevertingReceiver721; + + unsafe impl TopLevelStorage for RevertingReceiver721 {} + + #[public] + #[implements(IErc721Receiver, IErc165)] + impl RevertingReceiver721 {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc721Receiver for RevertingReceiver721 { + #[selector(name = "onERC721Received")] + fn on_erc721_received( + &mut self, + _operator: Address, + _from: Address, + _token_id: U256, + _data: Bytes, + ) -> Result> { + Err("Receiver rejected".into()) + } + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc165 for RevertingReceiver721 { + fn supports_interface(&self, interface_id: B32) -> bool { + ::interface_id() == interface_id + || ::interface_id() == interface_id + } + } + + #[storage] + pub struct EmptyReasonReceiver721; + + unsafe impl TopLevelStorage for EmptyReasonReceiver721 {} + + #[public] + #[implements(IErc721Receiver, IErc165)] + impl EmptyReasonReceiver721 {} + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc721Receiver for EmptyReasonReceiver721 { + #[selector(name = "onERC721Received")] + fn on_erc721_received( + &mut self, + _operator: Address, + _from: Address, + _token_id: U256, + _data: Bytes, + ) -> Result> { + Err(Vec::new()) + } + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[public] + impl IErc165 for EmptyReasonReceiver721 { + fn supports_interface(&self, interface_id: B32) -> bool { + ::interface_id() == interface_id + || ::interface_id() == interface_id + } + } + + // ----------------------- Acceptance-check failures ---------------------- + + #[motsu::test] + fn safe_mint_rejects_when_receiver_returns_wrong_selector( + contract: Contract, + bad: Contract, + alice: Address, + ) { + let token_id = uint!(42_U256); + let err = contract + .sender(alice) + ._safe_mint(bad.address(), token_id, &vec![].into()) + .motsu_expect_err( + "receiver returning wrong selector must be rejected", + ); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC721InvalidReceiver { receiver }) if receiver == bad.address() + )); + // Ensure token not minted + let balance = + contract.sender(alice).balance_of(bad.address()).motsu_unwrap(); + assert_eq!(U256::ZERO, balance); + } + + #[motsu::test] + fn safe_mint_bubbles_revert_reason_from_receiver( + contract: Contract, + reverting: Contract, + alice: Address, + ) { + let token_id = uint!(43_U256); + let err = contract + .sender(alice) + ._safe_mint(reverting.address(), token_id, &vec![].into()) + .motsu_expect_err("receiver reverting should bubble reason"); + + assert!(matches!( + err, + Error::InvalidReceiverWithReason(super::InvalidReceiverWithReason { reason }) if reason == "Receiver rejected" + )); + // Ensure token not minted + let balance = contract + .sender(alice) + .balance_of(reverting.address()) + .motsu_unwrap(); + assert_eq!(U256::ZERO, balance); + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[motsu::test] + #[ignore = "TODO: un-ignore when https://github.com/OpenZeppelin/stylus-test-helpers/issues/118 is fixed"] + fn safe_mint_rejects_on_empty_revert_reason( + contract: Contract, + empty: Contract, + alice: Address, + ) { + let token_id = uint!(44_U256); + let err = contract + .sender(alice) + ._safe_mint(empty.address(), token_id, &vec![].into()) + .motsu_expect_err("empty revert must map to InvalidReceiver"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC721InvalidReceiver { receiver }) if receiver == empty.address() + )); + } + + #[motsu::test] + fn safe_transfer_rejects_when_receiver_returns_wrong_selector( + contract: Contract, + bad: Contract, + alice: Address, + ) { + let token_id = uint!(45_U256); + // Mint to alice + contract.sender(alice)._mint(alice, token_id).motsu_unwrap(); + + let err = contract + .sender(alice) + .safe_transfer_from(alice, bad.address(), token_id) + .motsu_expect_err("wrong selector should be rejected in transfer"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC721InvalidReceiver { receiver }) if receiver == bad.address() + )); + // State unchanged + let owner = contract.sender(alice).owner_of(token_id).motsu_unwrap(); + assert_eq!(owner, alice); + } + + #[motsu::test] + fn safe_transfer_bubbles_revert_reason_from_receiver( + contract: Contract, + reverting: Contract, + alice: Address, + ) { + let token_id = uint!(46_U256); + // Mint to alice + contract.sender(alice)._mint(alice, token_id).motsu_unwrap(); + + let err = contract + .sender(alice) + .safe_transfer_from(alice, reverting.address(), token_id) + .motsu_expect_err("revert reason should bubble in transfer"); + + assert!(matches!( + err, + Error::InvalidReceiverWithReason(super::InvalidReceiverWithReason { reason }) if reason == "Receiver rejected" + )); + // State unchanged + let owner = contract.sender(alice).owner_of(token_id).motsu_unwrap(); + assert_eq!(owner, alice); + } + #[motsu::test] fn error_when_minting_token_id_twice( contract: Contract, @@ -1937,7 +2157,7 @@ mod tests { } #[motsu::test] - fn get_approved_nonexistent_token( + fn _get_approved_returns_zero_address_for_nonexistent_token( contract: Contract, alice: Address, ) { @@ -1954,7 +2174,8 @@ mod tests { .sender(alice) ._mint(alice, TOKEN_ID) .expect("should mint a token"); - let approved = contract.sender(alice)._get_approved(TOKEN_ID); + let approved = + contract.sender(alice).get_approved(TOKEN_ID).motsu_unwrap(); assert_eq!(Address::ZERO, approved); } @@ -1973,7 +2194,8 @@ mod tests { .approve(bob, TOKEN_ID) .expect("should approve Bob for operations on token"); - let approved = contract.sender(alice)._get_approved(TOKEN_ID); + let approved = + contract.sender(alice).get_approved(TOKEN_ID).motsu_unwrap(); assert_eq!(bob, approved); } @@ -1992,7 +2214,8 @@ mod tests { .set_approval_for_all(bob, true) .expect("should approve Bob for operations on all Alice's tokens"); - let approved = contract.sender(alice)._get_approved(TOKEN_ID); + let approved = + contract.sender(alice).get_approved(TOKEN_ID).motsu_unwrap(); assert_eq!(Address::ZERO, approved); } diff --git a/contracts/src/utils/address.rs b/contracts/src/utils/address.rs index 54a14d520..f0a1ba59a 100644 --- a/contracts/src/utils/address.rs +++ b/contracts/src/utils/address.rs @@ -140,3 +140,73 @@ impl AddressUtils { } } } + +#[cfg(test)] +mod tests { + use motsu::prelude::*; + + use super::*; + + #[test] + fn revert_returns_failed_call() { + let error = stylus_sdk::call::Error::Revert(vec![]); + let result = AddressUtils::revert(error); + assert!(matches!(result, Error::FailedCall(FailedCall {}))); + } + + #[test] + fn revert_returns_failed_call_with_reason() { + let error = stylus_sdk::call::Error::Revert(vec![1, 2, 3]); + let result = AddressUtils::revert(error); + assert!(matches!( + result, + Error::FailedCallWithReason(FailedCallWithReason { reason: _ }) + )); + } + + #[storage] + struct TargetMock; + + unsafe impl TopLevelStorage for TargetMock {} + + #[public] + impl TargetMock {} + + #[motsu::test] + fn verify_call_result_from_target_returns_empty_data_when_target_has_code( + target: Contract, + ) { + let empty_data: Vec = vec![]; + let result = AddressUtils::verify_call_result_from_target( + target.address(), + Ok(empty_data.clone()), + ) + .motsu_expect("should be able to verify call result"); + + assert_eq!(result, empty_data); + } + + #[cfg_attr(coverage_nightly, coverage(off))] + #[test] + #[ignore = "TODO: un-ignore when this is fixed: https://github.com/OpenZeppelin/stylus-test-helpers/issues/115"] + fn verify_call_result_from_target_returns_data_when_target_has_no_code() { + let data: Vec = vec![1, 2, 3]; + + let result = AddressUtils::verify_call_result_from_target( + Address::ZERO, + Ok(data.clone()), + ) + .motsu_expect("should be able to verify call result"); + + assert_eq!(result, data); + } + + #[test] + fn verify_call_result_from_target_returns_address_empty_code() { + let result = AddressUtils::verify_call_result_from_target( + Address::ZERO, + Ok(vec![]), + ); + assert!(matches!(result, Err(Error::EmptyCode(_)))); + } +} diff --git a/contracts/src/utils/cryptography/ecdsa.rs b/contracts/src/utils/cryptography/ecdsa.rs index e7ca4cacf..b1daa80d1 100644 --- a/contracts/src/utils/cryptography/ecdsa.rs +++ b/contracts/src/utils/cryptography/ecdsa.rs @@ -205,7 +205,10 @@ fn check_if_malleable(s: &B256) -> Result<(), Error> { #[cfg(test)] mod tests { + use core::ops::Deref; + use alloy_primitives::{b256, B256}; + use motsu::prelude::*; use super::*; @@ -248,4 +251,43 @@ mod tests { let result = check_if_malleable(&invalid_s); assert!(result.is_ok()); } + + #[storage] + struct ContractStorage; + + unsafe impl TopLevelStorage for ContractStorage {} + + #[public] + impl ContractStorage {} + + #[motsu::test] + fn _recover_fails_when_v_is_0_or_1( + contract: Contract, + alice: Address, + ) { + let err = _recover(contract.sender(alice).deref(), MSG_HASH, 0, R, S) + .expect_err("should return ECDSAInvalidSignature"); + + assert!(matches!( + err, + Error::InvalidSignature(ECDSAInvalidSignature {}) + )); + } + + #[motsu::test] + fn _recover_fails_when_recovered_address_is_zero( + contract: Contract, + alice: Address, + ) { + let invalid_v = 30; + + let err = + _recover(contract.sender(alice).deref(), MSG_HASH, invalid_v, R, S) + .expect_err("should return ECDSAInvalidSignature"); + + assert!(matches!( + err, + Error::InvalidSignature(ECDSAInvalidSignature {}) + )); + } } diff --git a/contracts/src/utils/math/alloy.rs b/contracts/src/utils/math/alloy.rs index e538253f3..3600393c4 100644 --- a/contracts/src/utils/math/alloy.rs +++ b/contracts/src/utils/math/alloy.rs @@ -199,12 +199,18 @@ impl Math for U256 { #[cfg(test)] mod tests { use alloy_primitives::{ - private::proptest::{prop_assume, proptest}, + private::proptest::{prop_assert, prop_assume, proptest}, uint, U256, U512, }; use crate::utils::math::alloy::{Math, Rounding}; + #[test] + fn check_sqrt_edge_cases() { + assert_eq!(U256::ZERO.sqrt(), U256::ZERO); + assert_eq!(U256::from(1).sqrt(), U256::from(1)); + } + #[test] fn check_sqrt() { proptest!(|(value: U256)| { @@ -248,22 +254,45 @@ mod tests { } #[test] - #[should_panic = "division by U256::ZERO in `Math::mul_div`"] fn check_mul_div_panics_when_denominator_is_zero() { proptest!(|(x: U256, y: U256)| { - // This should panic. - _ = x.mul_div(y, U256::ZERO, Rounding::Floor); + let result = std::panic::catch_unwind(|| { + _ = x.mul_div(y, U256::ZERO, Rounding::Floor); + }); + + prop_assert!(result.is_err()); + + // Extract and check the panic message + let err = result.unwrap_err(); + let panic_msg = err.downcast_ref::<&str>() + .map(|s| *s) + .or_else(|| err.downcast_ref::().map(|s| s.as_str())) + .unwrap_or(""); + + prop_assert!(panic_msg.contains("division by U256::ZERO in `Math::mul_div`")); }) } #[test] - #[should_panic = "should fit into `U256` in `Math::mul_div`"] fn check_mul_div_panics_when_result_overflows() { proptest!(|(x: U256, y: U256)| { prop_assume!(x != U256::ZERO, "Guaranteed `x` for overflow."); prop_assume!(y > U256::MAX / x, "Guaranteed `y` for overflow."); - // This should panic. - _ = x.mul_div(y, U256::from(1), Rounding::Floor); + + let result = std::panic::catch_unwind(|| { + _ = x.mul_div(y, U256::from(1), Rounding::Floor); + }); + + prop_assert!(result.is_err()); + + // Extract and check the panic message + let err = result.unwrap_err(); + let panic_msg = err.downcast_ref::<&str>() + .map(|s| *s) + .or_else(|| err.downcast_ref::().map(|s| s.as_str())) + .unwrap_or(""); + + prop_assert!(panic_msg.contains("should fit into `U256` in `Math::mul_div`")); }) } } diff --git a/contracts/src/utils/structs/checkpoints/mod.rs b/contracts/src/utils/structs/checkpoints/mod.rs index c1f9537f8..9d53b709d 100644 --- a/contracts/src/utils/structs/checkpoints/mod.rs +++ b/contracts/src/utils/structs/checkpoints/mod.rs @@ -430,6 +430,15 @@ mod tests { ); } + #[motsu::test] + #[should_panic = "should get checkpoint at index `1`"] + fn at_panics_on_exceeding_length( + checkpoint: Contract>, + alice: Address, + ) { + checkpoint.sender(alice).at(uint!(1_U32)); + } + #[motsu::test] fn push_same_value(checkpoint: Contract>, alice: Address) { let first_key = uint!(1_U96); diff --git a/contracts/src/utils/structs/enumerable_set/mod.rs b/contracts/src/utils/structs/enumerable_set/mod.rs index e343b030a..c13b2fe9c 100644 --- a/contracts/src/utils/structs/enumerable_set/mod.rs +++ b/contracts/src/utils/structs/enumerable_set/mod.rs @@ -456,6 +456,12 @@ mod tests { let set1: BTreeSet<_> = values1.into_iter().collect(); let set2: BTreeSet<_> = values2.into_iter().collect(); prop_assert_eq!(set1, set2); + + contract1.sender(alice).clear(); + contract2.sender(bob).clear(); + + prop_assert_eq!(contract1.sender(alice).length(), U256::ZERO); + prop_assert_eq!(contract2.sender(bob).length(), U256::ZERO); }); } diff --git a/lib/crypto/Cargo.toml b/lib/crypto/Cargo.toml index 408620f77..1bf5ce8de 100644 --- a/lib/crypto/Cargo.toml +++ b/lib/crypto/Cargo.toml @@ -21,6 +21,7 @@ ruint = { workspace = true, optional = true } alloy-primitives = { workspace = true, features = ["arbitrary"] } rand.workspace = true proptest.workspace = true +paste.workspace = true [features] ruint = ["dep:ruint"] diff --git a/lib/crypto/src/arithmetic/uint.rs b/lib/crypto/src/arithmetic/uint.rs index fc7387c44..4961d9641 100644 --- a/lib/crypto/src/arithmetic/uint.rs +++ b/lib/crypto/src/arithmetic/uint.rs @@ -1119,14 +1119,42 @@ impl WideUint { mod test { use proptest::prelude::*; + use super::*; use crate::{ - arithmetic::{ - uint::{from_str_hex, from_str_radix, Uint, WideUint, U256}, - BigInteger, Limb, - }, + arithmetic::{BigInteger, Limb}, bits::BitIteratorBE, }; + macro_rules! test_uxxx_default { + ($($type:ident),* $(,)?) => { + $( + paste::paste! { + #[test] + fn []() { + let uint = $type::default(); + assert_eq!(uint, $type::ZERO); + } + } + )* + }; + } + + // Usage: Generate tests for all UXXX types + test_uxxx_default! { + U64, + U128, + U192, + U256, + U384, + U448, + U512, + U576, + U640, + U704, + U768, + U832, + } + #[test] fn convert_from_str_radix() { let uint_from_base10: Uint<4> = from_str_radix( @@ -1378,27 +1406,26 @@ mod test { test_ruint_conversion!(ruint_u256, U256, 256); } - mod primitive_conversion_test { + mod primitive_conversion { use super::*; - macro_rules! test_uint_conversion { - ($test_name:ident, $type:ty) => { - #[test] - fn $test_name() { - proptest!(|(expected_primitive_num: $type)| { - let num: U256 = expected_primitive_num.into(); - let primitive_num: $type = num.into(); - assert_eq!(expected_primitive_num, primitive_num); - }); - } + macro_rules! test_conversion { + ($($type:ty),*) => { + $( + paste::paste! { + #[test] + fn $type() { + proptest!(|(expected_primitive_num: $type)| { + let num: U256 = expected_primitive_num.into(); + let primitive_num: $type = num.into(); + assert_eq!(expected_primitive_num, primitive_num); + }); + } + } + )* }; } - test_uint_conversion!(uint_u8, u8); - test_uint_conversion!(uint_u16, u16); - test_uint_conversion!(uint_u32, u32); - test_uint_conversion!(uint_u64, u64); - test_uint_conversion!(uint_u128, u128); - test_uint_conversion!(uint_usize, usize); + test_conversion!(u8, u16, u32, u64, u128, usize); } } diff --git a/lib/crypto/src/eddsa.rs b/lib/crypto/src/eddsa.rs index 9586893b8..e72f55eeb 100644 --- a/lib/crypto/src/eddsa.rs +++ b/lib/crypto/src/eddsa.rs @@ -401,6 +401,15 @@ mod test { use super::*; + #[test] + fn signing_key_creation_is_idempotent() { + proptest!(|(secret_key: SecretKey)| { + let signing_key_1 = SigningKey::from_bytes(&secret_key); + let signing_key_2 = SigningKey::from_bytes(&secret_key); + prop_assert!(signing_key_1.verifying_key() == signing_key_2.verifying_key()); + }) + } + #[test] fn sign_and_verify_known_message() { let secret_key: SecretKey = [1u8; SECRET_KEY_LENGTH]; diff --git a/lib/crypto/src/hash.rs b/lib/crypto/src/hash.rs index 5af4eb2a7..07334f84a 100644 --- a/lib/crypto/src/hash.rs +++ b/lib/crypto/src/hash.rs @@ -226,4 +226,18 @@ mod tests { assert_eq!(hash1, hash2); }) } + + #[test] + fn hash_one_deterministic() { + proptest!(|(a: Vec)| { + let builder = KeccakBuilder; + let hash1 = builder.hash_one(a.clone()); + let hash2 = builder.hash_one(a.clone()); + prop_assert_eq!(hash1, hash2); + + let a = [a, vec![1]].concat(); + let hash3 = builder.hash_one(a); + prop_assert_ne!(hash1, hash3); + }) + } } diff --git a/lib/crypto/src/merkle.rs b/lib/crypto/src/merkle.rs index 642fbd376..1dd0fa442 100644 --- a/lib/crypto/src/merkle.rs +++ b/lib/crypto/src/merkle.rs @@ -505,6 +505,18 @@ mod tests { }) } + #[test] + fn verify_multi_proof_with_builder_error_on_empty_leaves() { + let leaves = &[]; + let proof_flags = &[true]; + let root = [0u8; 32]; + let proof = &[root, root]; + + let result = + Verifier::verify_multi_proof(proof, proof_flags, root, leaves); + assert!(matches!(result, Err(MultiProofError::NoLeaves))); + } + #[test] fn zero_length_proof_with_matching_leaf_and_root() { let root = [0u8; 32]; @@ -812,4 +824,24 @@ mod tests { // invalid if root != leaf assert!(!Verifier::verify(&proof, root, leaf)); } + + #[test] + fn multiprooferror_display_messages() { + assert_eq!( + format!("{}", MultiProofError::InvalidProofLength), + "invalid multi-proof length" + ); + assert_eq!( + format!("{}", MultiProofError::InvalidRootChild), + "invalid root child generated" + ); + assert_eq!( + format!("{}", MultiProofError::InvalidTotalHashes), + "leaves.len() + proof.len() != total_hashes + 1" + ); + assert_eq!( + format!("{}", MultiProofError::NoLeaves), + "no leaves were provided for a non-trivial tree" + ); + } }