Smart Contract Quick start

Developing smart contracts for Ethereum involves a bevy of off-chain tools used for producing and testing bytecode that runs on the Ethereum Virtual Machine (EVM). Some tools also include workflows for deploying this bytecode to the Ethereum network and testnets. There are many options for these tools. This guide walks you through writing and testing a simple smart contract that interacts with the Materia Protocol using one specific set of tools (truffle + npm + mocha).

Requirements

To follow this guide, you must have the following installed:

Bootstrapping a project

You can start from scratch, but it’s easier to use a tool like truffle to bootstrap an empty project. Create an empty directory and run npx truffle init inside that directory to unbox the default Truffle box.

mkdir demo
cd demo
npx truffle init

Setting up npm

In order to reference the Materia contracts, you should use the npm artifacts we deploy containing the core and periphery smart contracts and interfaces. To add npm dependencies, we first initialize the npm package. We can run npm init in the same directory to create a package.json file. You can accept all the defaults and change it later.

npm init

Adding dependencies

Now that we have an npm package, we can add our dependencies. Let’s add both the @materia-dex/materia-contracts-core and @materia-dex/materia-contracts-proxy packages.

npm i --save @materia-dex/materia-contracts-core
npm i --save @materia-dex/materia-contracts-proxy

These packages include both the smart contract source code and the build artifacts.

Writing our contract

We can now get started writing our example contract. For writing Solidity, we recommend IntelliJ or VSCode with a solidity plugin, but you can use any text editor. Let’s write a contract that returns the value of some amount of liquidity shares for a given token pair. First create a couple of files:

mkdir contracts/interfaces
touch contracts/interfaces/ILiquidityValueCalculator.sol
touch contracts/LiquidityValueCalculator.sol

This will be the interface of the contract we implement. Put it in contracts/interfaces/ILiquidityValueCalculator.sol.

pragma solidity ^0.6.6;

interface ILiquidityValueCalculator {
function computeLiquidityShareValue(uint liquidity, address tokenA, address tokenB) external returns (uint tokenAAmount, uint tokenBAmount);
}

Now let’s start with the constructor. You need to know where the MateriaFactory is deployed in order to compute the address of the pair and look up the total supply of liquidity shares, plus the amounts for the reserves. We can store this as an address passed to the constructor.

The factory address is constant on mainnet and all testnets, so it may be tempting to make this value a constant in your contract, but since we need to unit test the contract it should be an argument. You can use solidity immutables to save on gas when accessing this variable.

pragma solidity ^0.6.6;

import './interfaces/ILiquidityValueCalculator.sol';

contract LiquidityValueCalculator is ILiquidityValueCalculator {
address public factory;
constructor(address factory_) public {
factory = factory_;
}
}

Now we need to be able to look up the total supply of liquidity for a pair, and its token balances. Let’s put this in a separate function. To implement it, we must:

  1. Look up the pair address
  2. Get the reserves of the pair
  3. Get the total supply of the pair liquidity
  4. Sort the reserves in the order of tokenA, tokenB

The MateriaLibrary has some helpful methods for this.

pragma solidity ^0.6.6;

import './interfaces/ILiquidityValueCalculator.sol';
import '@materia/materia-contracts-proxy/contracts/libraries/MateriaLibrary.sol';
import '@materia/materia-contracts-core/contracts/interfaces/IMateriaPair.sol';

contract LiquidityValueCalculator is ILiquidityValueCalculator {
function pairInfo(address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB, uint totalSupply) {
IMateriaPair pair = IMateriaPair(MateriaLibrary.pairFor(factory, tokenA, tokenB));
totalSupply = pair.totalSupply();
(uint reserves0, uint reserves1,) = pair.getReserves();
(reserveA, reserveB) = tokenA == pair.token0() ? (reserves0, reserves1) : (reserves1, reserves0);
}
}

Finally we just need to compute the share value. We will leave that as an exercise to the reader.

pragma solidity ^0.6.6;

import './interfaces/ILiquidityValueCalculator.sol';
import '@materia/materia-contracts-proxy/contracts/libraries/MateriaLibrary.sol';
import '@materia/materia-contracts-core/contracts/interfaces/IMateriaPair.sol';

contract LiquidityValueCalculator is ILiquidityValueCalculator {
address public factory;
constructor(address factory_) public {
factory = factory_;
}

function pairInfo(address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB, uint totalSupply) {
IMateriaPair pair = IMateriaPair(MateriaLibrary.pairFor(factory, tokenA, tokenB));
totalSupply = pair.totalSupply();
(uint reserves0, uint reserves1,) = pair.getReserves();
(reserveA, reserveB) = tokenA == pair.token0() ? (reserves0, reserves1) : (reserves1, reserves0);
}
function computeLiquidityShareValue(uint liquidity, address tokenA, address tokenB) external override returns (uint tokenAAmount, uint tokenBAmount) {
revert('TODO');
}
}