Attribute Macro stylus_proc::external

source ·
#[external]
Expand description

Just as with storage, Stylus SDK methods are Solidity ABI-equivalent. This means that contracts written in different programming languages are fully interoperable. You can even automatically export your Rust contract as a Solidity interface so that others can add it to their Solidity projects.

This macro makes methods “external” so that other contracts can call them by implementing the Router trait.

#[external]
impl Contract {
    // our owner method is now callable by other contracts
    pub fn owner(&self) -> Result<Address, Vec<u8>> {
        Ok(self.owner.get())
    }
}

impl Contract {
    // our set_owner method is not
    pub fn set_owner(&mut self, new_owner: Address) -> Result<(), Vec<u8>> {
        ...
    }
}

Note that, currently, all external methods must return a Result with the error type Vec<u8>. We intend to change this very soon. In the current model, Vec<u8> becomes the program’s revert data, which we intend to both make optional and richly typed.

§#[payable]

As in Solidity, methods may accept ETH as call value.

#[external]
impl Contract {
    #[payable]
    pub fn credit(&mut self) -> Result<(), Vec<u8> {
        self.erc20.add_balance(msg::sender(), msg::value())
    }
}

In the above, msg::value is the amount of ETH passed to the contract in wei, which may be used to pay for something depending on the contract’s business logic. Note that you have to annotate the method with #[payable], or else calls to it will revert. This is required as a safety measure to prevent users losing funds to methods that didn’t intend to accept ether.

§#[pure] #[view], and #write

For aesthetics, these additional purity attributes exist to clarify that a method is pure, view, or write. They aren’t necessary though, since the #[external] macro can figure purity out for you based on the types of the arguments.

For example, if a method includes an &self, it’s at least view. If you’d prefer it be write, applying #[write] will make it so. Note however that the reverse is not allowed. An &mut self method cannot be made #[view], since it might mutate state.

Please refer to the SDK Feature Overview for more information on defining methods.

§Inheritance, #[inherit], and #[borrow]

Composition in Rust follows that of Solidity. Types that implement Router, the trait that #[external] provides, can be connected via inheritance.

#[external]
#[inherit(Erc20)]
impl Token {
    pub fn mint(&mut self, amount: U256) -> Result<(), Vec<u8>> {
        ...
    }
}

#[external]
impl Erc20 {
    pub fn balance_of() -> Result<U256> {
        ...
    }
}

Because Token inherits Erc20 in the above, if Token has the #[entrypoint], calls to the contract will first check if the requested method exists within Token. If a matching function is not found, it will then try the Erc20. Only after trying everything Token inherits will the call revert.

Note that because methods are checked in that order, if both implement the same method, the one in Token will override the one in Erc20, which won’t be callable. This allows for patterns where the developer imports a crate implementing a standard, like ERC 20, and then adds or overrides just the methods they want to without modifying the imported Erc20 type.

Inheritance can also be chained. #[inherit(Erc20, Erc721)] will inherit both Erc20 and Erc721, checking for methods in that order. Erc20 and Erc721 may also inherit other types themselves. Method resolution finds the first matching method by Depth First Search.

Note that for the above to work, Token must implement Borrow<Erc20> and BorrowMut<Erc20>. You can implement this yourself, but for simplicity, #[solidity_storage] and sol_storage! provide a #[borrow] annotation.

sol_storage! {
    #[entrypoint]
    pub struct Token {
        #[borrow]
        Erc20 erc20;
        ...
    }

    pub struct Erc20 {
        ...
    }
}

In the future we plan to simplify the SDK so that Borrow isn’t needed and so that Router composition is more configurable. The motivation for this becomes clearer in complex cases of multi-level inheritance, which we intend to improve.

§Exporting a Solidity interface

Recall that Stylus contracts are fully interoperable across all languages, including Solidity. The Stylus SDK provides tools for exporting a Solidity interface for your contract so that others can call it. This is usually done with the cargo stylus CLI tool.

The SDK does this automatically via a feature flag called export-abi that causes the #[external] and #[entrypoint] macros to generate a main function that prints the Solidity ABI to the console.

cargo run --features export-abi --target <triple>

Note that because the above actually generates a main function that you need to run, the target can’t be wasm32-unknown-unknown like normal. Instead you’ll need to pass in your target triple, which cargo stylus figures out for you. This main function is also why the following commonly appears in the main.rs file of Stylus contracts.

#![cfg_attr(not(feature = "export-abi"), no_main)]

Here’s an example output. Observe that the method names change from Rust’s snake_case to Solidity’s camelCase. For compatibility reasons, onchain method selectors are always camelCase. We’ll provide the ability to customize selectors very soon. Note too that you can use argument names like “address” without fear. The SDK will prepend an _ when necessary.

interface Erc20 {
    function name() external pure returns (string memory);

    function balanceOf(address _address) external view returns (uint256);
}

interface Weth is Erc20 {
    function mint() external payable;

    function burn(uint256 amount) external;
}