Functions

A function can be declared inside a contract, in which case it has access to the contracts contract storage variables, other contract functions etc. Functions can be also be declared outside a contract.

// get_initial_bound is called from the constructor
function get_initial_bound() returns (uint256 value) {
    value = 102;
}

contract foo {
    uint256 bound = get_initial_bound();

    /** set bound for get with bound */
    function set_bound(uint256 _bound) public {
        bound = _bound;
    }

    // Clamp a value within a bound.
    // The bound can be set with set_bound().
    function get_with_bound(uint256 value) public view returns (uint256) {
        if (value < bound) {
            return value;
        } else {
            return bound;
        }
    }
}

Function can have any number of arguments. Function arguments may have names; if they do not have names then they cannot be used in the function body, but they will be present in the public interface.

The return values may have names as demonstrated in the get_initial_bound() function. When at all of the return values have a name, then the return statement is no longer required at the end of a function body. In stead of returning the values which are provided in the return statement, the values of the return variables at the end of the function is returned. It is still possible to explicitly return some values with a return statement.

Any DocComment before a function will be include in the ABI. Currently only Polkadot supports documentation in the ABI.

Function visibility

Solidity functions have a visibility specifier that restricts the scope in which they can be called. Functions can be declared public, private, internal or external with the following definitions:

  • public functions can be called inside and outside a contract (e.g. by an RPC). They are present in the contract’s ABI or IDL.

  • private functions can only be called inside the contract they are declared.

  • internal functions can only be called internally within the contract or by any contract inherited contract.

  • external functions can exclusively be called by other contracts or directly by an RPC. They are also present in the contract’s ABI or IDL.

Both public and external functions can be called using the syntax this.func(). In this case, the arguments are ABI encoded for the call, as it is treated like an external call. This is the only way to call an external function from inside the same contract it is defined. This method, however, should be avoided for public functions, as it will be more costly to call them than simply using func().

If a function is defined outside a contract, it cannot have a visibility specifier (e.g. public).

Arguments passing and return values

Function arguments can be passed either by position or by name. When they are called by name, arguments can be in any order. However, functions with anonymous arguments (arguments without name) cannot be called this way.

contract foo {
    function bar(uint32 x, bool y) public returns (uint32) {
        if (y) {
            return 2;
        }

        return 3;
    }

    function test() public {
        uint32 a = bar(102, false);
        a = bar({y: true, x: 302});
    }
}

If the function has a single return value, this can be assigned to a variable. If the function has multiple return values, these can be assigned using the Destructuring Statement assignment statement:

contract foo {
    function bar1(uint32 x, bool y) public returns (address, bytes32) {
        return (address(3), hex"01020304");
    }

    function bar2(uint32 x, bool y) public returns (bool) {
        return !y;
    }

    function test() public {
        (address f1, bytes32 f2) = bar1(102, false);
        bool f3 = bar2({x: 255, y: true});
    }
}

It is also possible to call functions on other contracts, which is also known as calling external functions. The called function must be declared public. Calling external functions requires ABI encoding the arguments, and ABI decoding the return values. This much more costly than an internal function call.

contract foo {
    function bar1(uint32 x, bool y) public returns (address, bytes32) {
        return (address(3), hex"01020304");
    }

    function bar2(uint32 x, bool y) public returns (bool) {
        return !y;
    }
}

contract bar {
    function test(foo f) public {
        (address f1, bytes32 f2) = f.bar1(102, false);
        bool f3 = f.bar2({x: 255, y: true});
    }
}

The syntax for calling a contract is the same as that of the external call, except that it must be done on a contract type variable. Errors in external calls can be handled with Try Catch Statement only on Polkadot.

Internal calls and externals calls

An internal function call is executed by the current contract. This is much more efficient than an external call, which requires the address of the contract to call, whose arguments must be abi encoded (also known as serialization). Then, the runtime must set up the VM for the called contract (the callee), decode the arguments, and encode return values. Lastly, the first contract (the caller) must decode return values.

A method call done on a contract type will always be an external call. Note that this returns the current contract, so this.foo() will do an external call, which is much more expensive than foo().

Passing accounts with external calls on Solana

The Solana runtime allows you the specify the accounts to be passed for an external call. This is specified in an array of the struct AccountMeta, see the section on Builtin AccountMeta.

import {AccountMeta} from 'solana';

contract SplToken {
    address constant tokenProgramId = address"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
    address constant SYSVAR_RENT_PUBKEY = address"SysvarRent111111111111111111111111111111111";

    struct InitializeMintInstruction {
        uint8 instruction;
        uint8 decimals;
        address mintAuthority;
        uint8 freezeAuthorityOption;
        address freezeAuthority;
    }

    function create_mint_with_freezeauthority(uint8 decimals, address mintAuthority, address freezeAuthority) public {
        InitializeMintInstruction instr = InitializeMintInstruction({
            instruction: 0,
            decimals: decimals,
            mintAuthority: mintAuthority,
            freezeAuthorityOption: 1,
            freezeAuthority: freezeAuthority
        });

        AccountMeta[2] metas = [
            AccountMeta({pubkey: instr.mintAuthority, is_writable: true, is_signer: false}),
            AccountMeta({pubkey: SYSVAR_RENT_PUBKEY, is_writable: false, is_signer: false})
        ];

        tokenProgramId.call{accounts: metas}(instr);
    }
}

If {accounts} is not specified, all accounts passed to the current transaction are forwarded to the call.

Passing seeds with external calls on Solana

The Solana runtime allows you to specify the seeds to be passed for an external call. This is used for program derived addresses: the seeds are hashed with the calling program id to create program derived addresses. They will automatically have the signer bit set, which allows a contract to sign without using any private keys.

import 'solana';

contract c {
    address constant token = address"mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68";

    function test(address addr, address addr2, bytes seed) public {
        bytes instr = new bytes(1);

        instr[0] = 1;

        AccountMeta[2] metas = [
            AccountMeta({pubkey: addr, is_writable: true, is_signer: true}),
            AccountMeta({pubkey: addr2, is_writable: true, is_signer: true})
        ];

        token.call{accounts: metas, seeds: [ [ "test", seed ], [ "foo", "bar "] ]}(instr);
    }
}

Now if the program derived address for the running program id and the seeds match the address addr and addr2, then then the called program will run with signer and writable bits set for addr and addr2. If they do not match, the Solana runtime will detect that the is_signer is set without the correct signature being provided.

The seeds can provided in any other, which will be used to sign for multiple accounts. In the example above, the seed "test" is concatenated with the value of seed, and that produces one account signature. In adition, "foo" is concatenated with "bar" to produce "foobar" and then used to sign for another account.

The seeds: call parameter is a slice of bytes slices; this means the literal can contain any number of elements, including 0 elements. The values can be bytes or anything that can be cast to bytes.

Passing value and gas with external calls

For external calls, value can be sent along with the call. The callee must be payable. Likewise, a gas limit can be set.

contract foo {
    function bar() public {
        other o = new other();

        o.feh{value: 102, gas: 5000}(102);
    }
}

contract other {
    function feh(uint32 x) public payable {
        // ...
    }
}

Note

The gas cannot be set on Solana for external calls.

State mutability

Some functions only read contract storage (also known as state), and others may write contract storage. Functions that do not write state can be executed off-chain. Off-chain execution is faster, does not require write access, and does not need any balance.

Functions that do not write state come in two flavours: view and pure. pure functions may not read state, and view functions that do read state.

Functions that do write state come in two flavours: payable and non-payable, the default. Functions that are not intended to receive any value, should not be marked payable. The compiler will check that every call does not included any value, and there are runtime checks as well, which cause the function to be reverted if value is sent.

A constructor can be marked payable, in which case value can be passed with the constructor.

Note

If value is sent to a non-payable function on Polkadot, the call will be reverted.

Overriding function selector

When a function is called, the function selector and the arguments are serialized (also known as abi encoded) and passed to the program. The function selector is what the runtime program uses to determine what function was called. On Polkadot, the function selector is generated using a deterministic hash value of the function name and the arguments types. On Solana, the selector is known as discriminator.

The selector value can be overridden with the annotation @selector([0xde, 0xad, 0xbe, 0xa1]).

contract foo {
    // The selector attribute can be an array of values (bytes)
    @selector([1, 2, 3, 4])
    function get_foo() pure public returns (int) {
        return 102;
    }

    @selector([0x05, 0x06, 0x07, 0x08])
    function get_bar() pure public returns (int) {
        return 105;
    }
}

The given example only works for Polkadot, whose selectors are four bytes wide. On Solana, they are eight bytes wide.

Only public and external functions have a selector, and can have their selector overridden. On Polkadot, constructors have selectors too, so they can also have their selector overridden. If a function overrides another one in a base contract, then the selector of both must match.

Warning

On Solana, changing the selector may result in a mismatch between the contract metadata and the actual contract code, because the metadata does not explicitly store the selector.

Use this feature carefully, as it may either break a contract or cause undefined behavior.

Function overloading

Multiple functions with the same name can be declared, as long as the arguments are different in at least one of two ways:

  • The number of arguments must be different

  • The type of at least one of the arguments is different

A function cannot be overloaded by changing the return types or number of returned values. Here is an example of an overloaded function:

contract shape {
    int64 bar;

    function max(int64 val1, int64 val2, int64 val3) public pure returns (int64) {
        int64 val = max(val1, val2);

        return max(val, val3);
    }

    function max(int64 val1, int64 val2) public pure returns (int64) {
        if (val1 >= val2) {
            return val2;
        } else {
            return val1;
        }
    }

    function foo(int64 x, int64 y) public {
        bar = max(bar, x, y);
    }
}

In the function foo, abs() is called with an int64 so the second implementation of the function abs() is called.

Both Polkadot and Solana runtime require unique function names, so overloaded function names will be mangled in the ABI or the IDL. The function name will be concatenated with all of its argument types, separated by underscores, using the following rules:

  • Struct types are represented by their field types (preceded by an extra underscore).

  • Enum types are represented as their underlying uint8 type.

  • Array types are recognizable by having Array appended.

  • Fixed size arrays will additionally have their length appended as well.

The following example illustrates some overloaded functions and their mangled name:

enum E {
    v1,
    v2
}
struct S {
    int256 i;
    bool b;
    address a;
}

contract C {
    // foo_
    function foo() public pure {}

    // foo_uint256_addressArray2Array
    function foo(uint256 i, address[2][] memory a) public pure {}

    // foo_uint8Array2__int256_bool_address
    function foo(E[2] memory e, S memory s) public pure {}
}

Function Modifiers

Function modifiers are used to check pre-conditions or post-conditions for a function call. First a new modifier must be declared which looks much like a function, but uses the modifier keyword rather than function.

contract example {
    address owner;

    modifier only_owner() {
        require(msg.sender == owner);
        _;
        // insert post conditions here
    }

    function foo() public only_owner {
        // ...
    }
}

The function foo can only be run by the owner of the contract, else the require() in its modifier will fail. The special symbol _; will be replaced by body of the function. In fact, if you specify _; twice, the function will execute twice, which might not be a good idea.

On Solana, msg.sender does not exist, so the usual way to implement a similar test is using an authority accounts rather than an owner account.

import 'solana';

contract AuthorityExample {
    address authority;
    uint64 counter;

    constructor(address initial_authority) {
        authority = initial_authority;
    }

    @signer(authorityAccount)
    function set_new_authority(address new_authority) external {
        assert(tx.accounts.authorityAccount.key == authority && tx.accounts.authorityAccount.is_signer);
        authority = new_authority;
    }

    @signer(authorityAccount)
    function inc() external {
        assert(tx.accounts.authorityAccount.key == authority && tx.accounts.authorityAccount.is_signer);
        counter += 1;
    }

    function get() public view returns (uint64) {
        return counter;
    }
}

A modifier cannot have visibility (e.g. public) or mutability (e.g. view) specified, since a modifier is never externally callable. Modifiers can only be used by attaching them to functions.

A modifier can have arguments, just like regular functions. Here if the price is less than 50, foo() itself will never be executed, and execution will return to the caller with nothing done since _; is not reached in the modifier and as result foo() is never executed.

contract example {
    modifier check_price(int64 price) {
        if (price >= 50) {
            _;
        }
    }

    function foo(int64 price) public check_price(price) {
        // ...
    }
}

Multiple modifiers can be applied to single function. The modifiers are executed in the order of the modifiers specified on the function declaration. Execution will continue to the next modifier when the _; is reached. In this example, the only_owner modifier is run first, and if that reaches _;, then check_price is executed. The body of function foo() is only reached once check_price() reaches _;.

contract example {
    address owner;

    // a modifier with no arguments does not need "()" in its declaration
    modifier only_owner() {
        require(msg.sender == owner);
        _;
    }

    modifier check_price(int64 price) {
        if (price >= 50) {
            _;
        }
    }

    function foo(int64 price) public only_owner check_price(price) {
        // ...
    }
}

Modifiers can be inherited or declared virtual in a base contract and then overridden, exactly like functions can be.

abstract contract base {
    address owner;

    modifier only_owner() {
        require(msg.sender == owner);
        _;
    }

    modifier check_price(int64 price) virtual {
        if (price >= 10) {
            _;
        }
    }
}

contract example is base {
    modifier check_price(int64 price) override {
        if (price >= 50) {
            _;
        }
    }

    function foo(int64 price) public only_owner check_price(price) {
        // ...
    }
}

Calling an external function using call()

If you call a function on a contract, then the function selector and any arguments are ABI encoded for you, and any return values are decoded. Sometimes it is useful to call a function without abi encoding the arguments.

You can call a contract directly by using the call() method on the address type. This takes a single argument, which should be the ABI encoded arguments. The return values are a boolean which indicates success if true, and the ABI encoded return value in bytes.

contract A {
    function test(B v) public {
        // the following four lines are equivalent to "uint32 res = v.foo(3,5);"

        // Note that the signature is only hashed and not parsed. So, ensure that the
        // arguments are of the correct type.
        bytes data = abi.encodeWithSignature(
            "foo(uint32,uint32)",
            uint32(3),
            uint32(5)
        );

        (bool success, bytes rawresult) = address(v).call(data);

        assert(success == true);

        uint32 res = abi.decode(rawresult, (uint32));

        assert(res == 8);
    }
}

contract B {
    function foo(uint32 a, uint32 b) pure public returns (uint32) {
        return a + b;
    }
}

Any value or gas limit can be specified for the external call. Note that no check is done to see if the called function is payable, since the compiler does not know what function you are calling.

function test(address foo, bytes rawcalldata) public {
    (bool success, bytes rawresult) = foo.call{value: 102, gas: 1000}(rawcalldata);
}

External calls with the call() method on Solana must have the accounts call argument, regardless of the callee function visibility, because the compiler has no information about the caller function to generate the AccountMeta array automatically.

function test(address foo, bytes rawcalldata) public {
    (bool success, bytes rawresult) = foo.call{accounts: []}(rawcalldata);
}

Calling an external function using delegatecall

External functions can also be called using delegatecall. The difference to a regular call is that delegatecall executes the callee code in the context of the caller:

  • The callee will read from and write to the caller storage.

  • value can’t be specified for delegatecall; instead it will always stay the same in the callee.

  • msg.sender does not change; it stays the same as in the callee.

Refer to the contracts pallet and Ethereum Solidity documentations for more information.

delegatecall is commonly used to implement re-usable libraries and upgradeable contracts.

function delegate(
    address callee,
    bytes input
) public returns(bytes result) {
    (bool ok, result) = callee.delegatecall(input);
    require(ok);
}

Note

delegatecall is not available on Solana.

Note

On Polkadot, specifying gas won’t have any effect on delegatecall.

fallback() and receive() function

When a function is called externally, either via an transaction or when one contract call a function on another contract, the correct function is dispatched based on the function selector in the raw encoded ABI call data. If there is no match, the call reverts, unless there is a fallback() or receive() function defined.

If the call comes with value, then receive() is executed, otherwise fallback() is executed. This made clear in the declarations; receive() must be declared payable, and fallback() must not be declared payable. If a call is made with value and no receive() function is defined, then the call reverts, likewise if call is made without value and no fallback() is defined, then the call also reverts.

Both functions must be declared external.

contract test {
    int32 bar;

    function foo(int32 x) public {
        bar = x;
    }

    fallback() external {
        // execute if function selector does not match "foo(uint32)" and no value sent
    }

    receive() external payable {
        // execute if function selector does not match "foo(uint32)" and value sent
    }
}

Note

On Solana, there is no mechanism to have some code executed if an account gets credited. So, receive() functions are not supported.