Use OpenZeppelin with Mamba

Integrate OpenZeppelin Library with Mamba

OpenZeppelin is a collection of libraries which you can integrate with your smart contracts. However, it's only available for Solidity, not Vyper. Let's get started! Create an empty Mamba project.


(.venv) $ mkdir openzeppelinsample
(.venv) $ cd openzeppelinsample
(.venv) $ mamba init

To install OpenZeppelin, you must have Node installed. Go inside contracts directory and install the library using NPM.


(.venv) $ cd contracts
(.venv) $ npm install @openzeppelin/contracts
(.venv) $ cd ..

Create a Solidity file named OpenZeppelinSample.sol in contracts directory. Add the following code to it:


pragma solidity ^0.8.1;

// SPDX-License-Identifier: GPL-v3.0

import "contracts/node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "contracts/node_modules/@openzeppelin/contracts/finance/PaymentSplitter.sol";


contract OpenZeppelinSample is ERC20, PaymentSplitter {

  constructor(address[] memory payees, uint256[] memory shares_)
              ERC20(unicode"OpenZeppelinSample 😃", "OZS")
              PaymentSplitter(payees, shares_) {
    uint256 dec = 10 ** decimals();
    _mint(msg.sender, 400_000_000 * dec);
    _mint(address(this), 100_000_000 * dec);
  }

  function buyCoin() external payable virtual {
    uint256 token = msg.value * 1000;
    assert (balanceOf(address(this)) >= token);
    emit PaymentReceived(_msgSender(), msg.value);
    _transfer(address(this), _msgSender(), token);
  }

}

Let's discuss the code. Look at the import statements.


import "contracts/node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "contracts/node_modules/@openzeppelin/contracts/finance/PaymentSplitter.sol";

Here, we add the libraries using import statements. The OpenZeppelin library is located inside the node_modules directory which is located inside the contracts library in our Mamba project.

We use OpenZeppelin library using inheritance as you can see in our contract defintion.


contract OpenZeppelinSample is ERC20, PaymentSplitter {

Here, we combine ERC20 token and PaymentSplitter contracts. If you want to learn about PaymentSplitter, you can go here.

Both ERC20 token and PaymentSplitter contracts need to be initialized.


constructor(address[] memory payees, uint256[] memory shares_)
              ERC20(unicode"OpenZeppelinSample 😃", "OZS")
              PaymentSplitter(payees, shares_) {

Our constructor receives two arguments. We pass these arguments to PaymentSplitter constructor. For ERC20 constructor, we send the name and symbol of the smart contract as arguments.

Then in the definition of our constructor, we mint the token.


    uint256 dec = 10 ** decimals();
    _mint(msg.sender, 400_000_000 * dec);
    _mint(address(this), 100_000_000 * dec);

Here, we create 400 millions of token and give it to the creator and 100 millions of token for smart contract. The 100 millions of token is for sale. The _mint method comes from the ERC20 library.

So we need to create a method to sell this token.


  function buyCoin() external payable virtual {
    uint256 token = msg.value * 1000;
    assert (balanceOf(address(this)) >= token);
    emit PaymentReceived(_msgSender(), msg.value);
    _transfer(address(this), _msgSender(), token);
  }

Here, we sell 1000 token for 1 ETH. The _transfer method and the balanceOf method come from the ERC20 library. The PaymentReceived event comes from the PaymentSplitter library.

Let's compile our smart contract.


(.venv) $ mamba compile

Let's create a test in test/test_openzeppelinsample.py. Add the following code to it:


from black_mamba.testlib import contract, eth_tester, TestContract
import pytest
from eth_tester.exceptions import TransactionFailed
import web3


class TestOpenZeppelinSample(TestContract):

    decimals = 10**18

    def test_init(self, eth_tester):
        accounts = eth_tester.get_accounts()
        parameter1 = [accounts[0], accounts[1]]
        parameter2 = [80, 20]
        ozs_contract = contract("OpenZeppelinSample", [parameter1, parameter2])

        assert ozs_contract.functions.name().call() == "OpenZeppelinSample 😃"
        assert ozs_contract.functions.symbol().call() == "OZS"
        assert ozs_contract.functions.totalSupply().call() == 500_000_000 * self.decimals
        assert ozs_contract.functions.balanceOf(accounts[0]).call() == 400_000_000 * self.decimals
        assert ozs_contract.functions.balanceOf(ozs_contract.address).call() == 100_000_000 * self.decimals

        assert ozs_contract.functions.totalShares().call() == 100
        assert ozs_contract.functions.shares(accounts[0]).call() == 80
        assert ozs_contract.functions.shares(accounts[1]).call() == 20

    def test_buyCoin(self, eth_tester):
        accounts = eth_tester.get_accounts()
        parameter1 = [accounts[0], accounts[1]]
        parameter2 = [80, 20]
        ozs_contract = contract("OpenZeppelinSample", [parameter1, parameter2])

        ether = web3.Web3.toWei(2, "ether")
        assert ozs_contract.functions.balanceOf(accounts[3]).call() == 0
        assert ozs_contract.functions.buyCoin().transact({"value": ether, "from": accounts[3]})
        assert ozs_contract.functions.balanceOf(accounts[3]).call() == 2_000 * self.decimals

        random_account = accounts[8]
        assert ozs_contract.functions.released(accounts[0]).call() == 0
        assert ozs_contract.functions.release(accounts[0]).transact({"from": random_account})
        assert ozs_contract.functions.released(accounts[0]).call() == 1.6 * self.decimals

        ether = web3.Web3.toWei(10, "ether")
        assert ozs_contract.functions.buyCoin().transact({"value": ether, "from": accounts[4]})
        assert ozs_contract.functions.release(accounts[0]).transact({"from": random_account})
        assert ozs_contract.functions.released(accounts[0]).call() == (8 + 1.6) * self.decimals

This test is self-explanatory. You can test it this way:


(.venv) $ py.test test/test_openzeppelinsample.py