Buidler, Waffle & Ethers

Lately, at Balancer we’ve moved from the Truffle development environment to using Buidler, Waffle, and Ethers. The main benefit is being able to use console.log in Solidity during debugging — it’s amazing how much of a difference this makes and for this alone the change over is worth it. Here are some notes I made during the switch over

Ethers

The ethers.js library aims to be a complete and compact library for interacting with the Ethereum Blockchain and its ecosystem. Documentation is here: https://docs.ethers.io and this Web3.js vs Ethers.js guide was useful.

The following gist demonstrates some basic usage of Ethers that creates an instance of a deployed contract and then running some calls against it:

				
					import { assert, expect } from 'chai';
import { ethers, ethereum } from "@nomiclabs/buidler";
import { Signer, utils } from "ethers";
const verbose = process.env.VERBOSE;
const Decimal = require('decimal.js');
const { calcRelativeDiff } = require('./lib/calc_comparisons');
const errorDelta = 10 ** -8;

describe('ExchangeProxy Smart Swaps', function(){
    const toWei = utils.parseEther;
    const fromWei = utils.formatEther;
    const MAX = ethers.constants.MaxUint256;
    const errorDelta = 10 ** -8;

    let registry: any;
    let factory: any;
    let REGISTRY: any;
    let WETH: string;
    let MKR: string;
    let ETH: string = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
    let weth: any;
    let mkr: any;
    let proxy: any;
    let _POOLS: any[] =[];
    let _pools: any[] =[];
    let PROXY: string;
    let admin: string;

    before(async () => {
        const BRegistry = await ethers.getContractFactory('BRegistry');
        const BFactory = await ethers.getContractFactory('BFactory');
        const BPool = await ethers.getContractFactory('BPool');
        const TToken = await ethers.getContractFactory('TToken');
        const ExchangeProxy = await ethers.getContractFactory("ExchangeProxy");
        const Weth9 = await ethers.getContractFactory('WETH9');
        const [adminSigner] = await ethers.getSigners();
        admin = await adminSigner.getAddress();
        factory = await BFactory.deploy();
        await factory.deployed();

        registry = await BRegistry.deploy(factory.address);
        await registry.deployed();

        mkr = await TToken.deploy('Maker', 'MKR', 18);
        await mkr.deployed();
        MKR = mkr.address;

        let weth9 = await Weth9.deploy();
        await weth9.deployed();
        WETH = weth9.address;

        proxy = await ExchangeProxy.deploy(WETH);
        await proxy.deployed();
        PROXY = proxy.address;
        await proxy.setRegistry(registry.address);
        await weth9.approve(PROXY, MAX);
        await mkr.approve(PROXY, MAX);

        // Admin balances
        await weth9.deposit({ value: toWei('10000000000') });

        await mkr.mint(admin,  toWei('1000000000000000000000'));

        // Copy pools printed by https://github.com/balancer-labs/python-SOR/blob/master/Onchain_SOR_test_comparison.py
        // For the following inputs:
        // num_pools = 5 # Number of pools available for this pair
        // max_n_pools = 4
        // swap_type = "swapExactOut"
        // input_amount = 100000 # Number of tokens in the trader wants to sell
        // output_token_eth_price = 0 # One output token buys 0.01 eth
        // seed = 1
        let poolsData = [
            {   'Bmkr': 1033191.1981189704,
                'Bweth': 21709.92411864851,
                'Wmkr': 8.261291241849618,
                'Wweth': 1.7387087581503824,
                'fee': 0.015},
            {   'Bmkr': 911870.2026231368,
                'Bweth': 30347.518852549234,
                'Wmkr': 7.509918308978633,
                'Wweth': 2.4900816910213672,
                'fee': 0.025},
            {   'Bmkr': 1199954.250073062,
                'Bweth': 72017.58337846321,
                'Wmkr': 6.235514183655618,
                'Wweth': 3.764485816344382,
                'fee': 0.01},
            {   'Bmkr': 1079066.970947264,
                'Bweth': 77902.62602094973,
                'Wmkr': 5.8258602061546405,
                'Wweth': 4.1741397938453595,
                'fee': 0.01},
            {   'Bmkr': 1141297.6436731548,
                'Bweth': 128034.7686206643,
                'Wmkr': 4.689466127973144,
                'Wweth': 5.310533872026856,
                'fee': 0.005}
        ]

        for (var i = 0; i < poolsData.length; i++) {
            let poolAddr = await factory.callStatic.newBPool();
            _POOLS.push(poolAddr);
            await factory.newBPool();
            let poolContract = await ethers.getContractAt("BPool", poolAddr);
            _pools.push(poolContract);

            await weth9.approve(_POOLS[i], MAX);
            await mkr.approve(_POOLS[i], MAX);

            await _pools[i].bind(WETH, toWei(poolsData[i]['Bweth'].toString()), toWei(poolsData[i]['Wweth'].toString()));
            await _pools[i].bind(MKR, toWei(poolsData[i]['Bmkr'].toString()), toWei(poolsData[i]['Wmkr'].toString()));
            await _pools[i].setSwapFee(toWei(poolsData[i]['fee'].toString()));

            await _pools[i].finalize();
            /*
            console.log("Pool "+i.toString()+": "+_POOLS[i]+", Liquidity WETH-MKR: "+
                await registry.getNormalizedLiquidity.call(MKR, WETH, _POOLS[i]))
            */
        }

        // Proposing registry. NOTICE _POOLS[0] has been left out since it would make up less than 10% of total liquidity
        await registry.addPools([_POOLS[1], _POOLS[2], _POOLS[3], _POOLS[4]], MKR, WETH);
        await registry.sortPools([MKR, WETH], 10);
    });

    it('joinswapExternAmountIn, MKR In', async () => {
        const [, newUserSigner] = await ethers.getSigners();
        const newUserAddr = await newUserSigner.getAddress();
        const amountIn = toWei('1000');

        await mkr.connect(newUserSigner).approve(PROXY, MAX);
        await mkr.mint(newUserAddr, amountIn);

        const startingMkrBalance = await mkr.balanceOf(newUserAddr);
        const startingBptBalance = await _pools[1].balanceOf(newUserAddr);

        expect(startingBptBalance).to.equal(0);
        expect(startingMkrBalance).to.equal(amountIn);

        const poolAmountOut = await proxy.connect(newUserSigner).callStatic.joinswapExternAmountIn(
            _POOLS[1],
            MKR,
            amountIn,
            toWei('0')
        );

        expect(poolAmountOut.toString()).to.equal('81833525388142100');

        await proxy.connect(newUserSigner).joinswapExternAmountIn(
            _POOLS[1],
            MKR,
            amountIn,
            toWei('0'),
            {
              gasPrice: 0
            }
        );

        const endingMkrBalance = await mkr.balanceOf(newUserAddr);
        const endingBptBalance = await _pools[1].balanceOf(newUserAddr);

        expect(endingMkrBalance).to.equal(0);
        expect(endingBptBalance).to.equal(poolAmountOut);
    });

    it('exitswapExternAmountOut, MKR Out', async () => {
        const [, newUserSigner] = await ethers.getSigners();
        const newUserAddr = await newUserSigner.getAddress();
        const amountOut = toWei('100');

        await _pools[1].connect(newUserSigner).approve(PROXY, MAX);

        const startingMkrBalance = await mkr.balanceOf(newUserAddr);
        const startingBptBalance = await _pools[1].balanceOf(newUserAddr);

        expect(startingMkrBalance).to.equal(0);

        const poolAmountIn = await proxy.connect(newUserSigner).callStatic.exitswapExternAmountOut(
            _POOLS[1],
            MKR,
            amountOut,
            startingBptBalance
        );

        await proxy.connect(newUserSigner).exitswapExternAmountOut(
            _POOLS[1],
            MKR,
            amountOut,
            startingBptBalance,
            {
              gasPrice: 0
            }
        );

        const endingMkrBalance = await mkr.balanceOf(newUserAddr);
        const endingBptBalance = await _pools[1].balanceOf(newUserAddr);

        expect(endingMkrBalance).to.equal(amountOut);
        expect(endingBptBalance).to.equal(startingBptBalance.sub(poolAmountIn));
    });

    it('joinswapExternAmountIn, ETH In', async () => {
        const [, newUserSigner] = await ethers.getSigners();
        const newUserAddr = await newUserSigner.getAddress();
        const amountIn = toWei('1000');

        const startingEthBalance = await newUserSigner.getBalance();
        const startingBptBalance = await _pools[1].balanceOf(newUserAddr);

        const poolAmountOut = await proxy.connect(newUserSigner).callStatic.joinswapExternAmountIn(
            _POOLS[1],
            ETH,
            amountIn,
            toWei('0'),
            {
              value: amountIn
            }
        );

        let tx = await proxy.connect(newUserSigner).joinswapExternAmountIn(
            _POOLS[1],
            ETH,
            amountIn,
            toWei('0'),
            {
              gasPrice: 0,
              value: amountIn
            }
        );

        const endingEthBalance = await newUserSigner.getBalance();
        const endingBptBalance = await _pools[1].balanceOf(newUserAddr);

        expect(endingEthBalance).to.equal(startingEthBalance.sub(amountIn));
        expect(poolAmountOut).to.equal(endingBptBalance.sub(startingBptBalance));
    });

    it('exitswapExternAmountOut, ETH Out', async () => {
        const [, newUserSigner] = await ethers.getSigners();
        const newUserAddr = await newUserSigner.getAddress();
        const amountOut = toWei('100');

        const startingEthBalance = await newUserSigner.getBalance();
        const startingBptBalance = await _pools[1].balanceOf(newUserAddr);

        const poolAmountIn = await proxy.connect(newUserSigner).callStatic.exitswapExternAmountOut(
            _POOLS[1],
            ETH,
            amountOut,
            startingBptBalance,
            {
              gasPrice: 0
            }
        );

        await proxy.connect(newUserSigner).exitswapExternAmountOut(
            _POOLS[1],
            ETH,
            amountOut,
            startingBptBalance,
            {
              gasPrice: 0
            }
        );
        // 999999998999.999291088
        // 999999999099.999291088

        const endingEthBalance = await newUserSigner.getBalance();
        const endingBptBalance = await _pools[1].balanceOf(newUserAddr);
        console.log(startingEthBalance.toString());
        console.log(endingEthBalance.toString());

        expect(endingEthBalance).to.equal(startingEthBalance.add(amountOut));
        expect(endingBptBalance).to.equal(startingBptBalance.sub(poolAmountIn));
    });

});
				
			

Static Calls

let poolAddr = await factory.callStatic.newBPool(); – The contract callStatic pretends that a call is not state-changing and returns the result. This does not actually change any state and is free

Connecting Different Accounts

await _pools[1].connect(newUserSigner).approve(PROXY, MAX); – Using contract connect(signer) calls the contract via the signer specified.

Gas Costs

				
					await proxy.connect(newUserSigner).exitswapExternAmountOut(
            _POOLS[1],
            MKR,
            amountOut,
            startingBptBalance,
            {
              gasPrice: 0
            }
        );
				
			

Setting the gasPrice to 0 like above allows me to run the transaction without spending any Eth on it. This was useful when checking Eth balance changes without having to worry about gas costs.

Custom accounts & balances

				
					const config: BuidlerConfig = {
  solc: {
    version: "0.5.12",
    optimizer: {
      enabled: true,
      runs: 200,
    },
  },
  networks: {
    buidlerevm: {
      blockGasLimit: 20000000,
      accounts: [
        { privateKey: '0xPrefixedPrivateKey1', balance: '1000000000000000000000000000000' },
        { privateKey: '0xPrefixedPrivateKey2', balance: '1000000000000000000000000000000' }
      ]
    },
  },
}
				
			

I needed the test accounts to have more than the 1000Eth balance set by default. In buidler.config.ts you can add accounts with custom balances like above.

Deploying

Deploying is done using scripts. First I updated my buidler.config.ts with the account/key for Kovan that will be used to deploy (i.e. must have Eth):

				
					const config: BuidlerConfig = {
  solc: {
    version: "0.5.12",
    optimizer: {
      enabled: true,
      runs: 200,
    },
  },
  networks: {
    buidlerevm: {
      blockGasLimit: 20000000,
    }
    kovan: {
      url: `https://kovan.infura.io/v3/${process.env.INFURA}`,
      accounts: [`${process.env.KEY}`]
    }
  },
};
				
			

Then I wrote a deploy-script.js:

				
					async function main() {
  // We get the contract to deploy
  const ExchangeProxy = await ethers.getContractFactory("ExchangeProxy");
  const WETH = '0xd0A1E359811322d97991E03f863a0C30C2cF029C';
  const exchangeProxy = await ExchangeProxy.deploy(WETH);  await exchangeProxy.deployed();  console.log("Proxy deployed to:", exchangeProxy.address);
}main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });
				
			

Then run this using: npx buidler run --network kovan deploy-script.js

Console Logging

One of the holy grails of Solidity development and so easy to setup in this case! There are also Solidity stack traces and error messages but unfortunately there was a bug that caused this not to work for our contracts.

To get this going all you need to do is add: import "@nomiclabs/buidler/console.sol"; at the top of your contract then use console.log. More details on what kind of outputs, etc it supports are here. Lifesaver!

Hope some of this was helpful and you enjoy using it as much as me.

Share Now

Facebook
Twitter
LinkedIn
Pinterest
Telegram
WhatsApp
Share on facebook
Share on twitter
Share on linkedin
Share on pinterest
Share on telegram
Share on whatsapp

Table of Contents

Subscribe To Our Newsletter

Can’t find what you’re looking for? Type below and hit enter!