Left-facing arrow
back to blog
SOFTWARE DEVELOPMENT

Contract Composability: The Building Blocks of Ethereum Smart Contract Development

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

-- CODE language-shell -- 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 <code>IERC20 interface<code> definition, when included in a Solidity project, allows any contract in that project to implement <code>IERC20<code> 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:

-- CODE language-shell -- 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 <code>ERC20 contract<code> (which is <code>IERC20<code>) provides implementations for all of the functions defined in the <code>IERC20 interface<code> 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 <code>0xee70B92dCC35fcEfB2d51fC1ad08Ae2611439CBa<code>.

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 <code>IERC20<code> interface is well known, external tools and contracts can just call <code>balanceOf()<code> or <code>approve()<code> or <code>totalBalance()<code> or <code>transfer()<code> 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 <code>0xee70B92dCC35fcEfB2d51fC1ad08Ae2611439CBa<code>?

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

-- CODE language-shell -- 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 <code>InsecurePublicWallet<code>. Lol, this thing. Don't deploy or try to use this please.

Within this contract, we create a variable called <code>myTokenContract<code>, 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 <code>IERC20<code> interface.

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

<code>getInsecurePublicWalletBalance()<code> is a function that checks the balance of our deployed token, for <code>this<code> <code>InsecurePublicWallet<code>. 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.

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

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 <code>0xee70B92dCC35fcEfB2d51fC1ad08Ae2611439CBa<code> contract) which are transferred to <code>InsecurePublicWallet<code> 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
Adam Gall

Build with Us

We are always interested in connecting with people who want to fund, innovate
or work in the decentralized economy.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.