Contract Interactions

Our Contracts Are No Longer Sandboxed!

While we are beginning to write more sophisticated smart contracts, the one thing that holds back our contracts is the inability to interact with other smart contracts. Almost every popular contract on EVM-compatible chains contain logic which allows it to call the functions of other smart contracts.

To understand how contract interactions might look like, consider the following examples:

  • Uniswap: Assume you place a trade which swaps USDC for WETH on app.uniswap.org. Your trade is first sent to the Uniswap V3 SwapRouter.sol which then calls the relevant USDC-WETH pool contract necessary to conduct the desired swap.

  • MultiSig Wallet: All multi-signature wallets (multisigs) on Ethereum and similar chains are smart contracts. Therefore, contract-to-contract interactions are an inherent feature of multisigs.

Primer: Address Type Methods

Before discussing the logistics on contract interactions, we will first discuss the types that allow us to call other contracts in the first place - the address type and contract types.

The address type, as previously discussed in Primitive Values & Types, represents the addresses of accounts. What is unique about the address type is that it comes with built-in methods that allows us to interact with values of the address type.

To motivate our understanding, consider the following variable:

address vitalik = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;

As the name might suggest, this is the address of Vitalik Buterin's Ethereum account. To get an idea of the functions that the address type provide, we can, as an example, get the balance of Vitalik's account.

vitalik.balance;

At the time of writing this, vitalik.balance returns 4119625876629850684131, which is the amount of Wei in his account.

Contract Types and Calling Other Contracts

The address type provides many functions that we will soon see, but with our current understanding of Solidity, it is useful mainly when dealing with EOAs since there is no nice way to call functions of contracts under the address type. Thankfully, Solidity provides us with contract types which allow us to treat contracts like their own types. To understand what we mean by contract types, consider the following code:

contract A {

    uint num = 5;

    function getNum() public view returns(uint) {
        return num;
    }

}

Furthermore, let's assume that address that contract A is deployed at is as follows:

address contractAccount = 0x2D4d2d096375FCc1503DA59c5B7FB4f69EAdf1e3;

Our goal is to be able to call the getNum function of the contract located at contractAccount. Ideally, we want to be able to call getNum directly; using contract types, we first convert contractAccount to type A:

A convertedContract = A(contractAccount);

Having now defined convertedContract, we can now call getNum as follows:

convertedAccount.getNum();

Since contract types are types, we can use them wherever regular types are also used. An example of this can be found below:

contract A {

    function getFive() public pure returns(uint){
        return 5;
    }

}

contract B {

    function callGetFive(A contractToCall) public pure {
        contractToCall.getFive();
    }

}

Sending Ether

While we are now able to call the functions of other smart contracts, there is still one question remaining: how can we send ether to other contracts?

It is trivially known that users can send ether to each other. Since contracts are also accounts, we can send ether to and from contracts as well. In this last section, we will cover how to write code that allows our contracts to send ether to other Ethereum accounts and how to specify our functions to be able to receive ether.

The call Function

Assume we want to send x ether to an account (EOA or contract) with address y. Assuming that the contract we are sending from has enough ether, the syntax below demonstrates how we can send ether via smart contracts:

y.call{value: x}("");

The call function is available to all values of type address. Although we will see later in the course that call is a powerful function, for our current purposes, we will treat call as a function that allows us to send ether to any account.

There are two types of parameters that are used in the call function:

  • Special Parameters: consists of the value field and the gas field. value is the amount of wei we are sending to the account, while gas specifies the maximum amount of gas that can be used when calling the account.

  • Data Parameter: consists of a hexadecimal value to be sent when calling the account

Note that special parameters go inside curly brackets and use colon-notation, while the data parameter goes in rounded brackets and uses equality-notation.

The call function, furthermore, returns two values; these values are as follows:

(bool success, bytes memory data) = y.call{value: x}("");

where success indicates where the call function was successful (i.e. whether the transfer of ether was successful) and data is the data returned from the call function.

Checking for Success

Although a successful call function does not imply that the logic that we intended to occur occurred, it is always a good idea to check that the success return value of a call function is equal to true.

The Payable Keyword

Now that we know how to program our smart contracts to send ether to other accounts (EOAs or contracts), its natural to ask whether if we can include ether as part of function calls. To motivate our curiousity, consider the following example:

contract Club {

    mapping(address => bool) isMember;

    uint membershipFee = 1;

    function becomeMember() public returns(bool) {
        require(msg.value == 1, "Did not pay exact fee!");
        isMember[msg.sender] = true;
        return true;
    }

}

The contract above represents a club where anyone can obtain membership if they pay the require 1 wei membership fee. Examining our contract line-by-line, we have the following:

  • Line 8: we are checking if the user has sent the required 1 wei with the function call

  • Line 9: we are setting the user's membership status to true

  • Line 10: we are return true (indicating to the caller that the membership was successfully obtrained)

To send ether alongside a contract call, we again can attach special parameters like we did for the call function; the syntax for attaching special parameters in the case of the becomeMember function is as follows:

becomeMember{value: 1}();

Tying this all together, the following code example consists of the Club contract and a User contract which applies for club membership:

contract Club {

    mapping(address => bool) isMember;

    uint membershipFee = 1;

    function becomeMember() public returns(bool) {
        require(msg.value == 1, "Did not pay exact fee!");
        isMember[msg.sender] = true;
        return true;
    }

}

contract User {

    function applyForMembership(Club club) public {
        club.becomeMember{value: 1}();
    }

}

If you try to compile this code, however, you will arrive at an angry compiler telling you that you cannot send value to a "nonpayable" function. What we are missing is to specify that becomeMember can receive ether. To do this, we need to attach the payable keyword to the function header of becomeMember. The following code includes the payable keyword:

contract Club {

    mapping(address => bool) isMember;

    uint membershipFee = 1;

    function becomeMember() public payable returns(bool) {
        require(msg.value == 1, "Did not pay exact fee!");
        isMember[msg.sender] = true;
        return true;
    }

}

contract User {

    function applyForMembership(Club club) public {
        club.becomeMember{value: 1}();
    }

}

Note on payable and Visibility

Since sending ether inherently changes the state of the blockchain, any function that receives ether cannot be marked as pure or view. Furthermore, any functions related to the sending of ether (i.e. functions that send ether) also cannot be marked as pure or view.

Preview: Responding to Ether Received

In the case that we send ether alongside a function call, the logic of the function involved will be triggered. However, what if we send ether to a contract without calling a specific function (i.e. sending ether via the call function)? Intuitively, one would guess that nothing happens, just like when we send ether to an EOA. However, as it turns out, contracts can be programmed to respond when they receive ether. Via default functions, developers can dictate what a contract does when it receives ether.

Last updated