Solidity by Example – Understanding Modular Contracts

5/5 - (2 votes)
Solidity by Example - Understanding Modular Contracts

This article continues on the Solidity Smart Contract Examples series and implements an example of a modular contract checking that the balances being sent between the addresses comply with the requirements (docs).

  • We’ll first lay out the entire smart contract example without the comments for readability and development purposes.
  • Then we’ll dissect it part by part, analyze it and explain it.
  • Following this path, we’ll get a hands-on experience with smart contracts and good practices in coding, understanding, and debugging smart contracts.

Smart Contract – Modular Contracts

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

library Balances {
    function move(mapping(address => uint256) storage balances, address from, address to, uint amount) internal {
        require(balances[from] >= amount);
        require(balances[to] + amount >= balances[to]);
        balances[from] -= amount;
        balances[to] += amount;
    }
}

contract Token {
    mapping(address => uint256) balances;
    using Balances for *;
    mapping(address => mapping (address => uint256)) allowed;

    event Transfer(address from, address to, uint amount);
    event Approval(address owner, address spender, uint amount);

    constructor (uint256 amount)
    {
        balances[msg.sender] = amount;
    }

    function transfer(address to, uint amount) external returns (bool success) {
        balances.move(msg.sender, to, amount);
        emit Transfer(msg.sender, to, amount);
        return true;

    }

    function transferFrom(address from, address to, uint amount) external returns (bool success) {
        require(allowed[from][msg.sender] >= amount);
        allowed[from][msg.sender] -= amount;
        balances.move(from, to, amount);
        emit Transfer(from, to, amount);
        return true;
    }

    function approve(address spender, uint tokens) external returns (bool success) {
        require(allowed[msg.sender][spender] == 0, "");
        allowed[msg.sender][spender] = tokens;
        emit Approval(msg.sender, spender, tokens);
        return true;
    }

    function balanceOf(address tokenOwner) external view returns (uint balance) {
        return balances[tokenOwner];
    }
}

Understanding the Code

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

πŸ’‘ Note: “Libraries are similar to contracts, but their purpose is that they are deployed only once at a specific address and their code is reused using the DELEGATECALL feature of the EVM.” (docs).

DELEGATECALL ensures that when a contract calls a library function, the function code is executed in the context of the calling contract, meaning that this keyword references the calling contract; the storage from the calling contract is also available.

library Balances {

Declares a Balances library.


    function move(mapping(address => uint256) storage balances, address from, address to, uint amount) internal {

Declares a move function that has four parameters:

  • balances is a reference to a mapping data structure in the storage memory area that maps an address to the balance amount; the parameter will be implicitly passed from the object the function is attached to (object balances.move(...) below).
  • Parameters from and to are origin and destination addresses;
  • amount is an unsigned integer representing the amount to be transferred from the origin address to the destination address.

There are two requirements that need to be satisfied to enable the function execution: the from address has to hold the balance that’s equal or greater than the amount due for transfer; the to address increased by the amount must be at least equal or greater than the original destination balance.

        require(balances[from] >= amount);
        require(balances[to] + amount >= balances[to]);

Decreases the origin balance (from).

        balances[from] -= amount;

Increases the destination balance (to).

        balances[to] += amount;
    }
}

Declares the contract.

contract Token {

Declares the mapping data structure for keeping track of balances.

    mapping(address => uint256) balances;

Note: We’re using the directive using ... for ...; to attach functions from the library Balances as member functions to all types, denoted by the asterisk/wildcard symbol *. A type has to be convertible to the first parameter of all attached functions.

In our case, all types (because of the *) have to be convertible to mapping(address => uint256) storage reference, because that’s the type of the first parameter of the library function move(...).

We’ll go into more depth on the using directive in the future articles, when we’ll cover Solidity types topic, but for now, let’s just remember this is a way to use a library.

    using Balances for *;

Records an allowed transfer between an origin address from, a destination address to, and the amount transfer.

    mapping(address => mapping (address => uint256)) allowed;

Declares the Transfer event.

    event Transfer(address from, address to, uint amount);

Declares the Approval event.

    event Approval(address owner, address spender, uint amount);

Sets the initial funds amount for the originator.

    constructor (uint256 amount)
    {
        balances[msg.sender] = amount;
    }

Defines the transfer(...) function that uses/wraps around the library’s move(...) function to simulate transfer of the funds, i.e. the amount.

    function transfer(address to, uint amount) external returns (bool success) {

Calls the balances.move(...) function, i.e. a member method.

        balances.move(msg.sender, to, amount);

Emits the Transfer event to mark that the transfer passed.

        emit Transfer(msg.sender, to, amount);

Returns true after a successful pass, just as we’d expect from any other well-behaved function.

        return true;
    }

Defines the transferFrom(...) function enabling the caller of the transferFrom(...) function to transfer funds from the origin to the destination in any amount, but not exceeding the allowance (a number of tokens) given from the funds originator.

    function transferFrom(address from, address to, uint amount) external returns (bool success) {

Requires that the allowance is greater or equal than the amount being transferred.

        require(allowed[from][msg.sender] >= amount);

Lowers the caller’s allowance by the amount of funds being transferred.

        allowed[from][msg.sender] -= amount;

Transfers the funds from the origin address to the destination address.

        balances.move(from, to, amount);

Emits the Transfer event, signaling a successful transfer.

        emit Transfer(from, to, amount);

Returns true after a successful funds transfer.

        return true;
    }

The approve(...) function enables the funds originator to give the allowance, i.e., permission to spend an amount of tokens in originator’s name to a spender that will spend some or all it, by transferring tokens to the receiver.

    function approve(address spender, uint tokens) external returns (bool success) {

The spender should have 0 tokens before receiving any allowance.

        require(allowed[msg.sender][spender] == 0, "");

The spender is getting the allowance.

        allowed[msg.sender][spender] = tokens;

Emits the Approval event, signaling a successful approval of allowance to a spender.

        emit Approval(msg.sender, spender, tokens);

Returns true after a successful approval of allowance.

        return true;
    }

Defines the balanceOf(...) function returning the balance of a token owner.

    function balanceOf(address tokenOwner) external view returns (uint balance) {

Returns the token owner’s balance.

        return balances[tokenOwner];
    }
}

Examining the Contract Arguments

This section contains additional information for running the contract. We should expect that our example accounts may change with each refresh/reload of Remix.

Our contract creation argument is the amount of funds available to the funds originator. We’ll assume the originator’s initial funds are 100 Wei, making the contract creation argument very simple:

100

Contract Test Scenario

  1. Account 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 deploys the contract while setting its balance to 100 Wei.
  2. Account 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 approves the allowance of 30 Wei to account 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2.
  3. Account 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 transfers 20 Wei from account 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 to account 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db.
  4. Account 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 tries transfering 20 Wei (with only 10 Wei remaining in allowance) to its balance and fails.
  5. Account 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 transfers the remaining 10 Wei in allowance to its balance and succeeds.
  6. Account 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 transfers its 10 Wei directly from its balance to account 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db.

Conclusion

We continued our smart contract example series with this article that implements a modular contract for transferring tokens/funds between several accounts.

In the beginning, only one account holds the funds and gives the allowance to the second account, which first transfers an amount from the first to the third account’s balances, then to itself (the second account’s balance), and finally makes another transfer from its balance directly to the third account’s balance.

First, we laid out clean source code (without any comments) for readability purposes.

Second, we dissected the code, analyzed it, and explained each possibly non-trivial segment.


Programmer Humor – Blockchain

“Blockchains are like grappling hooks, in that it’s extremely cool when you encounter a problem for which they’re the right solution, but it happens way too rarely in real life.” source xkcd