Arbitrum Stylus logo

Stylus by Example

Fallback and Receive Functions in Stylus

In Stylus smart contracts, fallback and receive functions are special functions that handle incoming transactions when no other function matches the call signature. These functions are crucial for creating flexible contracts that can handle various types of interactions, including direct Ether transfers and unknown function calls.

Understanding the Difference

Receive Function

The receive function is specifically designed to handle plain Ether transfers - transactions that send Ether to your contract without any calldata.

Key characteristics:

  • Called when a transaction has no calldata (empty data field)
  • Takes no inputs and returns no outputs
  • Optional - if not defined, plain Ether transfers will trigger the fallback function
  • Has higher priority than fallback when both are defined

Fallback Function

The fallback function is a catch-all handler for transactions that don't match any existing function signature.

Key characteristics:

  • Called when no function signature matches the transaction's calldata
  • Also called for plain Ether transfers if no receive function is defined
  • Can be payable or non-payable
  • Can access the calldata that was sent with the transaction
  • If not defined, unmatched calls will revert

Function Call Priority

When a transaction is sent to your contract, Stylus follows this decision tree:

  1. Has calldata?
    • No → Call receive() (if defined), otherwise call fallback()
    • Yes → Try to match a function signature
  2. Function signature matches?
    • Yes → Call the matching function
    • No → Call fallback() (if defined), otherwise revert

Complete Working Example

Here's a comprehensive example that demonstrates both functions with full testing. The tests demonstrate how to verify that the correct events are emitted. In the receive function, we track the total Ether received, the number of times the receive function is called, and emits events on the receive function. The receive function updates the sender's balance and logs the event with the method name "receive".

The fallback function tracks the total Ether received, the number of times it is called on fallback method, and emits events for both Ether transfers and unknown function calls. It also updates the sender's balance and logs the calldata if available. In the testing part we test if each part of receive and fallback functions are working correctly, including the event emissions and balance updates.

src/lib.rs

1#![cfg_attr(not(feature = "export-abi"), no_std)]
2extern crate alloc;
3
4use stylus_sdk::alloy_sol_types::sol;
5use stylus_sdk::{
6    stylus_core::log,
7    alloy_primitives::{U256, Address, FixedBytes},
8    ArbResult,
9    prelude::*,
10};
11use alloc::vec;
12use crate::alloc::string::ToString;
13use crate::vec::Vec;
14// Define persistent storage
15sol_storage! {
16    #[entrypoint]
17    pub struct PaymentTracker {
18        uint256 total_received;
19        uint256 fallback_calls;
20        uint256 receive_calls;
21        mapping(address => uint256) balances;
22    }
23}
24
25// Define events for better tracking
26sol! {
27    event EtherReceived(address indexed sender, uint256 amount, string method);
28    event FallbackTriggered(address indexed sender, uint256 amount, bytes data);
29    event UnknownFunctionCalled(address indexed sender, bytes4 selector);
30}
31
32#[public]
33impl PaymentTracker {
34    // Regular function to check balance
35    pub fn get_balance(&mut self, account: Address) {
36        self.balances.setter(account).get();
37    }
38    
39    // Regular function to get statistics
40    pub fn get_stats(&self) -> (U256, U256, U256) {
41        (
42            self.total_received.get(),
43            self.receive_calls.get(),
44            self.fallback_calls.get()
45        )
46    }
47
48    /// Receive function - handles plain Ether transfers
49    /// This is called when someone sends Ether without any data
50    /// Example: contract.send(1 ether) or contract.transfer(1 ether)
51    #[receive]
52    #[payable]
53    pub fn receive(&mut self) -> Result<(), Vec<u8>> {
54        let sender = self.vm().msg_sender();
55        let amount = self.vm().msg_value();
56        
57        // Update tracking variables
58        self.total_received.set(self.total_received.get() + amount);
59        self.receive_calls.set(self.receive_calls.get() + U256::from(1));
60        
61        // Update sender's balance using setter method
62        let current_balance = self.balances.get(sender);
63        self.balances.setter(sender).set(current_balance + amount);
64        
65        // Log the event
66        log(
67            self.vm(),
68            EtherReceived {
69                sender,
70                amount,
71                method: "receive".to_string(),
72            },
73        );
74        
75        Ok(())
76    }
77
78    /// Fallback function - handles unmatched function calls
79    /// This is called when:
80    /// 1. A function call doesn't match any existing function signature
81    /// 2. Plain Ether transfer when no receive function exists
82    #[fallback]
83    #[payable]
84    pub fn fallback(&mut self, calldata: &[u8]) -> ArbResult {
85        let sender = self.vm().msg_sender();
86        let amount = self.vm().msg_value();
87        
88        // Update tracking
89        self.fallback_calls.set(self.fallback_calls.get() + U256::from(1));
90        
91        if amount > U256::ZERO {
92            // If Ether was sent, track it
93            self.total_received.set(self.total_received.get() + amount);
94            let current_balance = self.balances.get(sender);
95            self.balances.setter(sender).set(current_balance + amount);
96            
97            stylus_sdk::stylus_core::log(
98                self.vm(),
99                EtherReceived {
100                    sender,
101                    amount,
102                    method: "fallback".to_string(),
103                },
104            );
105        }
106        
107        // Log the fallback trigger with calldata - convert to bytes properly
108        log(
109            self.vm(),
110            FallbackTriggered {
111                sender,
112                amount,
113                data: calldata.to_vec().into(),
114            },
115        );
116        
117        // If calldata has at least 4 bytes, extract the function selector
118        if calldata.len() >= 4 {
119            let selector = [calldata[0], calldata[1], calldata[2], calldata[3]];
120            log(
121                self.vm(),
122                UnknownFunctionCalled {
123                    sender,
124                    selector: FixedBytes(selector),
125                },
126            );
127        }
128        
129        // Return empty bytes (successful execution)
130        Ok(vec![])
131    }
132}
133
134#[cfg(test)]
135mod test {
136    use super::*;
137    use stylus_sdk::testing::*;
138    use stylus_sdk::alloy_primitives::{B256, keccak256};
139
140    #[test]
141    fn test_receive_function() {
142        let vm = TestVM::default();
143        let mut contract = PaymentTracker::from(&vm);
144        
145        // Test that contract is created successfully and initial values are correct
146        let (total, receive_calls, fallback_calls) = contract.get_stats();
147        assert_eq!(total, U256::from(0));
148        assert_eq!(receive_calls, U256::from(0));
149        assert_eq!(fallback_calls, U256::from(0));
150        // Override the msg value for future contract method invocations.
151        vm.set_value(U256::from(2));
152        let _ = contract.receive();
153        // Check that the receive function updates stats correctly
154        let (total, receive_calls, fallback_calls) = contract.get_stats();
155        assert_eq!(total, U256::from(2));
156        assert_eq!(receive_calls, U256::from(1));
157        assert_eq!(fallback_calls, U256::from(0));
158        // Check that the balance is updated
159        let balance = contract.balances.get(vm.msg_sender());
160        assert_eq!(balance, U256::from(2));
161    }
162
163    #[test]    
164    fn test_fallback_function() {
165        let vm = TestVM::default();
166        let mut contract = PaymentTracker::from(&vm);
167        
168        // Test that contract is created successfully and initial values are correct
169        let (total, receive_calls, fallback_calls) = contract.get_stats();
170        assert_eq!(total, U256::from(0));
171        assert_eq!(receive_calls, U256::from(0));
172        assert_eq!(fallback_calls, U256::from(0));
173        // Call the fallback function with some data
174        let calldata = vec![0x01, 0x02, 0x03, 0x04];
175        let _ = contract.fallback(&calldata);
176        // Check that the fallback function updates stats correctly
177        let (total, receive_calls, fallback_calls) = contract.get_stats();
178        assert_eq!(total, U256::from(0));
179        assert_eq!(receive_calls, U256::from(0));
180        assert_eq!(fallback_calls, U256::from(1));
181        // Check that the balance is updated
182        let balance = contract.balances.get(vm.msg_sender());
183        assert_eq!(balance, U256::from(0));
184
185        // Check that the fallback triggered event was logged
186        let logs = vm.get_emitted_logs();
187        assert_eq!(logs.len(), 2);
188
189        // Check that the first log is the FallbackTriggered event
190        let event_signature = B256::from(keccak256(
191            "FallbackTriggered(address,uint256,bytes)".as_bytes()
192        ));
193        assert_eq!(logs[0].0[0], event_signature);
194        // Check that the second log is the UnknownFunctionCalled event
195        let unknown_signature = B256::from(keccak256(
196            "UnknownFunctionCalled(address,bytes4)".as_bytes()
197        ));
198        assert_eq!(logs[1].0[0], unknown_signature);
199       
200    }
201
202    #[test]    
203    fn test_fallback_function_with_value() {
204        let vm = TestVM::default();
205        let mut contract = PaymentTracker::from(&vm);
206        
207        // Test that contract is created successfully and initial values are correct
208        let (total, receive_calls, fallback_calls) = contract.get_stats();
209        assert_eq!(total, U256::from(0));
210        assert_eq!(receive_calls, U256::from(0));
211        assert_eq!(fallback_calls, U256::from(0));
212
213        vm.set_value(U256::from(2));
214        let calldata = vec![0x01, 0x02, 0x03, 0x04];
215        // Call the fallback function with calldata and value
216        let _ = contract.fallback(&calldata);
217        // Check that the fallback function updates stats correctly
218        let (total, receive_calls, fallback_calls) = contract.get_stats();
219        assert_eq!(total, U256::from(2));
220        assert_eq!(receive_calls, U256::from(0));
221        assert_eq!(fallback_calls, U256::from(1));
222        // Check that the balance is updated
223        let balance = contract.balances.get(vm.msg_sender());
224        assert_eq!(balance, U256::from(2));
225        // Check that the fallback triggered event was logged
226        let logs = vm.get_emitted_logs();
227        assert_eq!(logs.len(), 3);
228        // Check that the first log is the FallbackTriggered event
229        let event_signature = B256::from(keccak256(
230            "EtherReceived(address,uint256,string)".as_bytes()
231        ));
232        assert_eq!(logs[0].0[0], event_signature);
233        // Check that the second log is the EtherReceived event
234        let ether_received_signature = B256::from(keccak256(
235            "FallbackTriggered(address,uint256,bytes)".as_bytes()
236        ));
237        assert_eq!(logs[1].0[0], ether_received_signature);
238        // Check that the third log is the UnknownFunctionCalled event
239        let unknown_signature = B256::from(keccak256(
240            "UnknownFunctionCalled(address,bytes4)".as_bytes()
241        ));
242        assert_eq!(logs[2].0[0], unknown_signature);
243    }
244}
1#![cfg_attr(not(feature = "export-abi"), no_std)]
2extern crate alloc;
3
4use stylus_sdk::alloy_sol_types::sol;
5use stylus_sdk::{
6    stylus_core::log,
7    alloy_primitives::{U256, Address, FixedBytes},
8    ArbResult,
9    prelude::*,
10};
11use alloc::vec;
12use crate::alloc::string::ToString;
13use crate::vec::Vec;
14// Define persistent storage
15sol_storage! {
16    #[entrypoint]
17    pub struct PaymentTracker {
18        uint256 total_received;
19        uint256 fallback_calls;
20        uint256 receive_calls;
21        mapping(address => uint256) balances;
22    }
23}
24
25// Define events for better tracking
26sol! {
27    event EtherReceived(address indexed sender, uint256 amount, string method);
28    event FallbackTriggered(address indexed sender, uint256 amount, bytes data);
29    event UnknownFunctionCalled(address indexed sender, bytes4 selector);
30}
31
32#[public]
33impl PaymentTracker {
34    // Regular function to check balance
35    pub fn get_balance(&mut self, account: Address) {
36        self.balances.setter(account).get();
37    }
38    
39    // Regular function to get statistics
40    pub fn get_stats(&self) -> (U256, U256, U256) {
41        (
42            self.total_received.get(),
43            self.receive_calls.get(),
44            self.fallback_calls.get()
45        )
46    }
47
48    /// Receive function - handles plain Ether transfers
49    /// This is called when someone sends Ether without any data
50    /// Example: contract.send(1 ether) or contract.transfer(1 ether)
51    #[receive]
52    #[payable]
53    pub fn receive(&mut self) -> Result<(), Vec<u8>> {
54        let sender = self.vm().msg_sender();
55        let amount = self.vm().msg_value();
56        
57        // Update tracking variables
58        self.total_received.set(self.total_received.get() + amount);
59        self.receive_calls.set(self.receive_calls.get() + U256::from(1));
60        
61        // Update sender's balance using setter method
62        let current_balance = self.balances.get(sender);
63        self.balances.setter(sender).set(current_balance + amount);
64        
65        // Log the event
66        log(
67            self.vm(),
68            EtherReceived {
69                sender,
70                amount,
71                method: "receive".to_string(),
72            },
73        );
74        
75        Ok(())
76    }
77
78    /// Fallback function - handles unmatched function calls
79    /// This is called when:
80    /// 1. A function call doesn't match any existing function signature
81    /// 2. Plain Ether transfer when no receive function exists
82    #[fallback]
83    #[payable]
84    pub fn fallback(&mut self, calldata: &[u8]) -> ArbResult {
85        let sender = self.vm().msg_sender();
86        let amount = self.vm().msg_value();
87        
88        // Update tracking
89        self.fallback_calls.set(self.fallback_calls.get() + U256::from(1));
90        
91        if amount > U256::ZERO {
92            // If Ether was sent, track it
93            self.total_received.set(self.total_received.get() + amount);
94            let current_balance = self.balances.get(sender);
95            self.balances.setter(sender).set(current_balance + amount);
96            
97            stylus_sdk::stylus_core::log(
98                self.vm(),
99                EtherReceived {
100                    sender,
101                    amount,
102                    method: "fallback".to_string(),
103                },
104            );
105        }
106        
107        // Log the fallback trigger with calldata - convert to bytes properly
108        log(
109            self.vm(),
110            FallbackTriggered {
111                sender,
112                amount,
113                data: calldata.to_vec().into(),
114            },
115        );
116        
117        // If calldata has at least 4 bytes, extract the function selector
118        if calldata.len() >= 4 {
119            let selector = [calldata[0], calldata[1], calldata[2], calldata[3]];
120            log(
121                self.vm(),
122                UnknownFunctionCalled {
123                    sender,
124                    selector: FixedBytes(selector),
125                },
126            );
127        }
128        
129        // Return empty bytes (successful execution)
130        Ok(vec![])
131    }
132}
133
134#[cfg(test)]
135mod test {
136    use super::*;
137    use stylus_sdk::testing::*;
138    use stylus_sdk::alloy_primitives::{B256, keccak256};
139
140    #[test]
141    fn test_receive_function() {
142        let vm = TestVM::default();
143        let mut contract = PaymentTracker::from(&vm);
144        
145        // Test that contract is created successfully and initial values are correct
146        let (total, receive_calls, fallback_calls) = contract.get_stats();
147        assert_eq!(total, U256::from(0));
148        assert_eq!(receive_calls, U256::from(0));
149        assert_eq!(fallback_calls, U256::from(0));
150        // Override the msg value for future contract method invocations.
151        vm.set_value(U256::from(2));
152        let _ = contract.receive();
153        // Check that the receive function updates stats correctly
154        let (total, receive_calls, fallback_calls) = contract.get_stats();
155        assert_eq!(total, U256::from(2));
156        assert_eq!(receive_calls, U256::from(1));
157        assert_eq!(fallback_calls, U256::from(0));
158        // Check that the balance is updated
159        let balance = contract.balances.get(vm.msg_sender());
160        assert_eq!(balance, U256::from(2));
161    }
162
163    #[test]    
164    fn test_fallback_function() {
165        let vm = TestVM::default();
166        let mut contract = PaymentTracker::from(&vm);
167        
168        // Test that contract is created successfully and initial values are correct
169        let (total, receive_calls, fallback_calls) = contract.get_stats();
170        assert_eq!(total, U256::from(0));
171        assert_eq!(receive_calls, U256::from(0));
172        assert_eq!(fallback_calls, U256::from(0));
173        // Call the fallback function with some data
174        let calldata = vec![0x01, 0x02, 0x03, 0x04];
175        let _ = contract.fallback(&calldata);
176        // Check that the fallback function updates stats correctly
177        let (total, receive_calls, fallback_calls) = contract.get_stats();
178        assert_eq!(total, U256::from(0));
179        assert_eq!(receive_calls, U256::from(0));
180        assert_eq!(fallback_calls, U256::from(1));
181        // Check that the balance is updated
182        let balance = contract.balances.get(vm.msg_sender());
183        assert_eq!(balance, U256::from(0));
184
185        // Check that the fallback triggered event was logged
186        let logs = vm.get_emitted_logs();
187        assert_eq!(logs.len(), 2);
188
189        // Check that the first log is the FallbackTriggered event
190        let event_signature = B256::from(keccak256(
191            "FallbackTriggered(address,uint256,bytes)".as_bytes()
192        ));
193        assert_eq!(logs[0].0[0], event_signature);
194        // Check that the second log is the UnknownFunctionCalled event
195        let unknown_signature = B256::from(keccak256(
196            "UnknownFunctionCalled(address,bytes4)".as_bytes()
197        ));
198        assert_eq!(logs[1].0[0], unknown_signature);
199       
200    }
201
202    #[test]    
203    fn test_fallback_function_with_value() {
204        let vm = TestVM::default();
205        let mut contract = PaymentTracker::from(&vm);
206        
207        // Test that contract is created successfully and initial values are correct
208        let (total, receive_calls, fallback_calls) = contract.get_stats();
209        assert_eq!(total, U256::from(0));
210        assert_eq!(receive_calls, U256::from(0));
211        assert_eq!(fallback_calls, U256::from(0));
212
213        vm.set_value(U256::from(2));
214        let calldata = vec![0x01, 0x02, 0x03, 0x04];
215        // Call the fallback function with calldata and value
216        let _ = contract.fallback(&calldata);
217        // Check that the fallback function updates stats correctly
218        let (total, receive_calls, fallback_calls) = contract.get_stats();
219        assert_eq!(total, U256::from(2));
220        assert_eq!(receive_calls, U256::from(0));
221        assert_eq!(fallback_calls, U256::from(1));
222        // Check that the balance is updated
223        let balance = contract.balances.get(vm.msg_sender());
224        assert_eq!(balance, U256::from(2));
225        // Check that the fallback triggered event was logged
226        let logs = vm.get_emitted_logs();
227        assert_eq!(logs.len(), 3);
228        // Check that the first log is the FallbackTriggered event
229        let event_signature = B256::from(keccak256(
230            "EtherReceived(address,uint256,string)".as_bytes()
231        ));
232        assert_eq!(logs[0].0[0], event_signature);
233        // Check that the second log is the EtherReceived event
234        let ether_received_signature = B256::from(keccak256(
235            "FallbackTriggered(address,uint256,bytes)".as_bytes()
236        ));
237        assert_eq!(logs[1].0[0], ether_received_signature);
238        // Check that the third log is the UnknownFunctionCalled event
239        let unknown_signature = B256::from(keccak256(
240            "UnknownFunctionCalled(address,bytes4)".as_bytes()
241        ));
242        assert_eq!(logs[2].0[0], unknown_signature);
243    }
244}

Cargo.toml

1[package]
2name = "stylus_fallback_test"
3version = "0.1.11"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6homepage = "https://github.com/OffchainLabs/stylus-hello-world"
7repository = "https://github.com/OffchainLabs/stylus-hello-world"
8keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
9description = "Stylus hello world example"
10
11[dependencies]
12alloy-primitives = "=0.8.20"
13alloy-sol-types = "=0.8.20"
14mini-alloc = "0.9.0"
15stylus-sdk = "0.9.0"
16hex = "0.4.3"
17dotenv = "0.15.0"
18
19[dev-dependencies]
20alloy-primitives = { version = "=0.8.20", features = ["sha3-keccak"] }
21tokio = { version = "1.12.0", features = ["full"] }
22ethers = "2.0"
23eyre = "0.6.8"
24stylus-sdk = { version = "0.9.0", features = ["stylus-test"] }
25
26[features]
27export-abi = ["stylus-sdk/export-abi"]
28debug = ["stylus-sdk/debug"]
29
30[[bin]]
31name = "stylus-hello-world"
32path = "src/main.rs"
33
34[lib]
35crate-type = ["lib", "cdylib"]
36
37[profile.release]
38codegen-units = 1
39strip = true
40lto = true
41panic = "abort"
42
43# If you need to reduce the binary size, it is advisable to try other
44# optimization levels, such as "s" and "z"
45opt-level = 3
1[package]
2name = "stylus_fallback_test"
3version = "0.1.11"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6homepage = "https://github.com/OffchainLabs/stylus-hello-world"
7repository = "https://github.com/OffchainLabs/stylus-hello-world"
8keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
9description = "Stylus hello world example"
10
11[dependencies]
12alloy-primitives = "=0.8.20"
13alloy-sol-types = "=0.8.20"
14mini-alloc = "0.9.0"
15stylus-sdk = "0.9.0"
16hex = "0.4.3"
17dotenv = "0.15.0"
18
19[dev-dependencies]
20alloy-primitives = { version = "=0.8.20", features = ["sha3-keccak"] }
21tokio = { version = "1.12.0", features = ["full"] }
22ethers = "2.0"
23eyre = "0.6.8"
24stylus-sdk = { version = "0.9.0", features = ["stylus-test"] }
25
26[features]
27export-abi = ["stylus-sdk/export-abi"]
28debug = ["stylus-sdk/debug"]
29
30[[bin]]
31name = "stylus-hello-world"
32path = "src/main.rs"
33
34[lib]
35crate-type = ["lib", "cdylib"]
36
37[profile.release]
38codegen-units = 1
39strip = true
40lto = true
41panic = "abort"
42
43# If you need to reduce the binary size, it is advisable to try other
44# optimization levels, such as "s" and "z"
45opt-level = 3

src/main.rs

1#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
2
3#[cfg(not(any(test, feature = "export-abi")))]
4#[no_mangle]
5pub extern "C" fn main() {}
6
7#[cfg(feature = "export-abi")]
8fn main() {
9    stylus_fallback_test::print_from_args();
10}
1#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
2
3#[cfg(not(any(test, feature = "export-abi")))]
4#[no_mangle]
5pub extern "C" fn main() {}
6
7#[cfg(feature = "export-abi")]
8fn main() {
9    stylus_fallback_test::print_from_args();
10}