Arbitrum Stylus logo

Stylus by Example

Sending Ether

We have three main ways to send Ether in Rust Stylus: using the transfer_eth method, using low level call method, and sending value while calling an external contract.

It's important to note that the transfer_eth method in Rust Stylus invokes the recipient contract, which may subsequently call other contracts. All the gas is supplied to the recipient, which it may burn. Conversely, the transfer method in Solidity is capped at 2300 gas. In Rust Stylus, you can cap the gas by using the low-level call method with a specified gas. An example of this is provided in the code on bottom of the page.

These two methods are exactly equivalent under the hood:

1transfer_eth(recipient, value)?;
2
3call(Call::new_in(self).value(value), recipient, &[])?;
1transfer_eth(recipient, value)?;
2
3call(Call::new_in(self).value(value), recipient, &[])?;

Where to Send Ether

  1. Externally Owned Account (EOA) Addresses: Directly send Ether to an EOA address.

  2. Solidity Smart Contracts with Receive Function (No Calldata): Send Ether to a Solidity smart contract that has a receive function without providing any calldata.

  3. Solidity Smart Contracts with Fallback Function (With Calldata): Send Ether to a Solidity smart contract that has a fallback function by providing the necessary calldata.

  4. Smart Contracts with Payable Methods (both Solidity and Stylus): Send Ether to smart contracts that have defined payable methods. Payable methods are identified by the payable modifier in Solidity, and the #[payable] macro in Rust.

Below you can find examples for each of these methods and how to define them in a Rust Stylus smart contract using the Stylus SDK:

src/lib.rs

1// Only run this as a WASM if the export-abi feature is not set.
2#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
3extern crate alloc;
4
5use alloy_primitives::Address;
6use stylus_sdk::{
7    abi::Bytes,
8    call::{call, transfer_eth, Call},
9    msg::{self},
10    prelude::*,
11};
12
13sol_interface! {
14    interface ITarget {
15        function receiveEther() external payable;
16    }
17}
18
19#[storage]
20#[entrypoint]
21pub struct SendEther {}
22
23#[public]
24impl SendEther {
25    // Transfer Ether using the transfer_eth method
26    // This can be used to send Ether to an EOA or a Solidity smart contract that has a receive() function implemented
27    #[payable]
28    pub fn send_via_transfer(to: Address) -> Result<(), Vec<u8>> {
29        transfer_eth(to, msg::value())?;
30        Ok(())
31    }
32
33    // Transfer Ether using a low-level call
34    // This can be used to send Ether to an EOA or a Solidity smart contract that has a receive() function implemented
35    #[payable]
36    pub fn send_via_call(&mut self, to: Address) -> Result<(), Vec<u8>> {
37        call(Call::new_in(self).value(msg::value()), to, &[])?;
38        Ok(())
39    }
40
41    // Transfer Ether using a low-level call with a specified gas limit
42    // This can be used to send Ether to an EOA or a Solidity smart contract that has a receive() function implemented
43    #[payable]
44    pub fn send_via_call_gas_limit(&mut self, to: Address, gas_amount: u64) -> Result<(), Vec<u8>> {
45        call(
46            Call::new_in(self).value(msg::value()).gas(gas_amount),
47            to,
48            &[],
49        )?;
50        Ok(())
51    }
52
53    // Transfer Ether using a low-level call with calldata
54    // This can be used to call a Solidity smart contract's fallback function and send Ether along with calldata
55    #[payable]
56    pub fn send_via_call_with_call_data(
57        &mut self,
58        to: Address,
59        data: Bytes,
60    ) -> Result<(), Vec<u8>> {
61        call(Call::new_in(self).value(msg::value()), to, data.as_slice())?;
62        Ok(())
63    }
64
65    // Transfer Ether to another smart contract via a payable method on the target contract
66    // The target contract can be either a Solidity smart contract or a Stylus contract that has a receiveEther function, which is a payable function
67    #[payable]
68    pub fn send_to_stylus_contract(&mut self, to: Address) -> Result<(), Vec<u8>> {
69        let target = ITarget::new(to);
70        let config = Call::new_in(self).value(msg::value());
71        target.receive_ether(config)?;
72        Ok(())
73    }
74}
1// Only run this as a WASM if the export-abi feature is not set.
2#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
3extern crate alloc;
4
5use alloy_primitives::Address;
6use stylus_sdk::{
7    abi::Bytes,
8    call::{call, transfer_eth, Call},
9    msg::{self},
10    prelude::*,
11};
12
13sol_interface! {
14    interface ITarget {
15        function receiveEther() external payable;
16    }
17}
18
19#[storage]
20#[entrypoint]
21pub struct SendEther {}
22
23#[public]
24impl SendEther {
25    // Transfer Ether using the transfer_eth method
26    // This can be used to send Ether to an EOA or a Solidity smart contract that has a receive() function implemented
27    #[payable]
28    pub fn send_via_transfer(to: Address) -> Result<(), Vec<u8>> {
29        transfer_eth(to, msg::value())?;
30        Ok(())
31    }
32
33    // Transfer Ether using a low-level call
34    // This can be used to send Ether to an EOA or a Solidity smart contract that has a receive() function implemented
35    #[payable]
36    pub fn send_via_call(&mut self, to: Address) -> Result<(), Vec<u8>> {
37        call(Call::new_in(self).value(msg::value()), to, &[])?;
38        Ok(())
39    }
40
41    // Transfer Ether using a low-level call with a specified gas limit
42    // This can be used to send Ether to an EOA or a Solidity smart contract that has a receive() function implemented
43    #[payable]
44    pub fn send_via_call_gas_limit(&mut self, to: Address, gas_amount: u64) -> Result<(), Vec<u8>> {
45        call(
46            Call::new_in(self).value(msg::value()).gas(gas_amount),
47            to,
48            &[],
49        )?;
50        Ok(())
51    }
52
53    // Transfer Ether using a low-level call with calldata
54    // This can be used to call a Solidity smart contract's fallback function and send Ether along with calldata
55    #[payable]
56    pub fn send_via_call_with_call_data(
57        &mut self,
58        to: Address,
59        data: Bytes,
60    ) -> Result<(), Vec<u8>> {
61        call(Call::new_in(self).value(msg::value()), to, data.as_slice())?;
62        Ok(())
63    }
64
65    // Transfer Ether to another smart contract via a payable method on the target contract
66    // The target contract can be either a Solidity smart contract or a Stylus contract that has a receiveEther function, which is a payable function
67    #[payable]
68    pub fn send_to_stylus_contract(&mut self, to: Address) -> Result<(), Vec<u8>> {
69        let target = ITarget::new(to);
70        let config = Call::new_in(self).value(msg::value());
71        target.receive_ether(config)?;
72        Ok(())
73    }
74}

Cargo.toml

1[package]
2name = "stylus_sending_ether_example"
3version = "0.1.7"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
7
8[dependencies]
9alloy-primitives = "=0.7.6"
10alloy-sol-types = "=0.7.6"
11mini-alloc = "0.4.2"
12stylus-sdk = "0.6.0"
13hex = "0.4.3"
14
15[dev-dependencies]
16tokio = { version = "1.12.0", features = ["full"] }
17ethers = "2.0"
18eyre = "0.6.8"
19
20[features]
21export-abi = ["stylus-sdk/export-abi"]
22
23[lib]
24crate-type = ["lib", "cdylib"]
25
26[profile.release]
27codegen-units = 1
28strip = true
29lto = true
30panic = "abort"
31opt-level = "s"
1[package]
2name = "stylus_sending_ether_example"
3version = "0.1.7"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
7
8[dependencies]
9alloy-primitives = "=0.7.6"
10alloy-sol-types = "=0.7.6"
11mini-alloc = "0.4.2"
12stylus-sdk = "0.6.0"
13hex = "0.4.3"
14
15[dev-dependencies]
16tokio = { version = "1.12.0", features = ["full"] }
17ethers = "2.0"
18eyre = "0.6.8"
19
20[features]
21export-abi = ["stylus-sdk/export-abi"]
22
23[lib]
24crate-type = ["lib", "cdylib"]
25
26[profile.release]
27codegen-units = 1
28strip = true
29lto = true
30panic = "abort"
31opt-level = "s"