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.
The receive
function is specifically designed to handle plain Ether transfers - transactions that send Ether to your contract without any calldata.
Key characteristics:
The fallback
function is a catch-all handler for transactions that don't match any existing function signature.
Key characteristics:
receive
function is definedpayable
or non-payableWhen a transaction is sent to your contract, Stylus follows this decision tree:
receive()
(if defined), otherwise call fallback()
fallback()
(if defined), otherwise revertHere'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.
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}
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
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}