Contract Composability: The Building Blocks of Ethereum Smart Contract Development

Adam Gall
Decent DAO
Published in
5 min readOct 9, 2019

Smart Contracts deployed onto Ethereum networks do not live in a silo.

Anyone can interact with them, depending on the business-layer permissions we program into the contracts. “Anyone” includes other “people” — accounts which are submitting transactions to the blockchain — but more importantly also includes other contracts, themselves.

As you design your smart contracts, it’s important to keep in mind the interface that you’re defining. Since smart contracts have the unique constraint that they can’t be edited after deployment (there are ways to work around this through the concept of “proxy contracts”), as more and more contracts build upon each other, a hard layer of logic will ossify at the base. Let’s make sure we get it right.

In this tutorial we’ll describe how to design your smart contracts so that others can easily interact with them, and how to interact with other smart contracts from your own.

Defining By Example

The most well known “interface” in Ethereum is by far the ERC-20 token. ERC-20 is simply a set of “function signatures” (or “ type signatures”) which define the functions that a contract must implement in order to “be” an ERC-20 token on Ethereum.

Let’s see what that looks like, in Solidity

pragma solidity ^0.5.2;

interface IERC20 {
function transfer(address to, uint256 value) external returns (bool);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function totalSupply() external view returns (uint256);
function balanceOf(address who) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// Used in an actual project: https://github.com/decentorganization/coinflation/blob/develop/contracts/token/ERC20/IERC20.sol

This IERC20 interface definition, when included in a Solidity project, allows any contract in that project to implement IERC20 and immediately be compatible with the hundreds of other contracts and tools in the wild that support ERC20 tokens.

Let’s take a look at what a simple implementation looks like:

pragma solidity ^0.5.2;

import "./IERC20.sol";
import "../../math/SafeMath.sol";

contract ERC20 is IERC20 {
using SafeMath for uint256;

mapping (address => uint256) private _balances;

mapping (address => mapping (address => uint256)) private _allowed;

uint256 private _totalSupply;

function totalSupply() public view returns (uint256) {
return _totalSupply;
}

function balanceOf(address owner) public view returns (uint256) {
return _balances[owner];
}

function allowance(address owner, address spender) public view returns (uint256) {
return _allowed[owner][spender];
}

function transfer(address to, uint256 value) public returns (bool) {
_transfer(msg.sender, to, value);
return true;
}

function approve(address spender, uint256 value) public returns (bool) {
_approve(msg.sender, spender, value);
return true;
}

function transferFrom(address from, address to, uint256 value) public returns (bool) {
_transfer(from, to, value);
_approve(from, msg.sender, _allowed[from][msg.sender].sub(value));
return true;
}

function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
_approve(msg.sender, spender, _allowed[msg.sender][spender].add(addedValue));
return true;
}

function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
_approve(msg.sender, spender, _allowed[msg.sender][spender].sub(subtractedValue));
return true;
}

function _transfer(address from, address to, uint256 value) internal {
require(to != address(0));

_balances[from] = _balances[from].sub(value);
_balances[to] = _balances[to].add(value);
emit Transfer(from, to, value);
}

function _approve(address owner, address spender, uint256 value) internal {
require(spender != address(0));
require(owner != address(0));

_allowed[owner][spender] = value;
emit Approval(owner, spender, value);
}
}
// Used in an actual project: https://github.com/decentorganization/coinflation/blob/develop/contracts/token/ERC20/ERC20.sol

Without getting into the details of this implementation, we can see that this ERC20 contract (which is IERC20) provides implementations for all of the functions defined in the IERC20 interface above.

Great! This is pretty simple so far, but it provides an excellent base on which to build.

Interacting with Existing Contracts

Let’s say we compile this Solidity project, deploy it to the Ropsten testnet, and it receives an address at 0xee70B92dCC35fcEfB2d51fC1ad08Ae2611439CBa.

There are a plethora of on-and-off-chain ERC20 wallets out there which can now natively interact with our token.

Get it? Since the IERC20 interface is well known, external tools and contracts can just call balanceOf() or approve() or totalBalance() or transfer() on any contract that implements the interface.

So how do we write another smart contract that interacts with this ERC20 token which we’ve deployed at 0xee70B92dCC35fcEfB2d51fC1ad08Ae2611439CBa?

Easy! Declare the existing contract’s interface into your new contract, instantiate an instance of it given the deployed address, and start calling functions.

pragma solidity ^0.5.2;

interface IERC20 {
function transfer(address to, uint256 value) external returns (bool);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function totalSupply() external view returns (uint256);
function balanceOf(address who) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}

contract InsecurePublicWallet {
IERC20 myTokenContract;

constructor() {
address myTokenContractAddress = 0xee70B92dCC35fcEfB2d51fC1ad08Ae2611439CBa;
myTokenContract = IERC20(myTokenContractAddress);
}

function getInsecurePublicWalletBalance() {
address insecurePublicWalletAddress = address(this)
return myTokenContract.balanceOf(insecurePublicWalletAddress);
}

function stealTheTokens() {
uint256 thisContractsBalance = getInsecurePublicWalletBalance();
return myTokenContract.transfer(msg.sender, thisContractsBalance);
}
}

Let’s take a deeper look at this terribly simple terrible contract.

First, we define the same ERC20 interface from above. This is just so that we can call these functions from within our new contract here. Need to have those function signatures defined somewhere so the Solidity compiler knows what’s going on.

Next, we create a contract called InsecurePublicWallet. Lol, this thing. Don't deploy or try to use this please.

Within this contract, we create a variable called myTokenContract, which is a reference to the deployed ERC20 contract from above. In the constructor we have the token's address hardcoded, and then "instantiate" a reference to the existing token contract "using" the IERC20 interface.

Now the myTokenContract object has all of the IERC20 functions available to it! We'll implement two contract-level functions in InsecurePublicWallet, each one of those functions will call a different function on the deployed ERC20 token, through the myTokenContract object.

getInsecurePublicWalletBalance() is a function that checks the balance of our deployed token, for this InsecurePublicWallet. Our new contract here can own tokens from the original token contract, and this function simply checks it's own balance and returns the result.

stealTheTokens() is a function that allows anyone to steal any tokens owned by InsecurePublicWallet. It does that by first checking the token balance of InsecurePublicWallet (through getInsecurePublicWalletBalance()), then calling the transfer function on myTokenContract in order to transfer all of the InsecurePublicWallet's tokens to the person executing this function call ( msg.sender).

Conclusion

As an example, this contract is pretty simple and displays the ways in which you can build contracts which use and build upon other contracts. But practically speaking, it’s really silly, for reasons which should be obvious at this point. Any tokens (from the 0xee70B92dCC35fcEfB2d51fC1ad08Ae2611439CBa contract) which are transferred to InsecurePublicWallet can be stolen by anyone.

You should now have a better understanding of how to:

  • create contracts using an interface-first methodology, which allow for your contracts to be easily used by other contracts
  • use the interfaces of existing deployed contracts to use and interact with them from within your own contracts

Questions? Comments? Improvements? Reply below!

Originally published at https://blog.decentlabs.io on October 9, 2019.

--

--

Adam Gall
Decent DAO

master of web and mobile applications, and their integrations with bitcoin and ethereum