DEV Community

Cover image for Lanzamiento de Tokens: Nivel Dios
Ahmed Castro
Ahmed Castro

Posted on

Lanzamiento de Tokens: Nivel Dios

El ecosistema web3 sigue creciendo así como las herramientas que usamos para crear y lanzar smart contracts. En este video exploramos las herramientas nuevas que he estado usando para lanzar proyectos tokens y cualquier tipo de smart contracts.

Antes de comenzar

Para este tutorial ocuparás NodeJs que recomiendo descargarlo en Linux via NVM, y también necesitarás Metamask u otra wallet compatible con fondos en Goerli que puedes obetener desde un faucet.

1. Creamos un proyecto de Hardhat

mkdir MyToken
cd MyToken
npm install --save hardhat
npx hardhat
Enter fullscreen mode Exit fullscreen mode

2. Instalamos librerías adicionales

Instalamos dotenv para proteger nuestra llave privada, @nomiclabs/hardhat-etherscan para auto verificar en etherscan y @openzeppelin/contracts para hacer uso de los contratos de OpenZeppelin.

npm install --save dotenv @nomiclabs/hardhat-etherscan @openzeppelin/contracts
Enter fullscreen mode Exit fullscreen mode

3. Configuramos Hardhat

Configuramos Hardhat para poder usar la versión ahora mas reciente de Solidity 0.8.16, también para habilitar el forkeo de mainnet y el autoverificador de contratos.

hardhat.config.js

require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-etherscan");
require('dotenv').config()

module.exports = {
  solidity: "0.8.16",
  networks: {
    hardhat: {
      forking: {
        url: process.env.MAINNET_RPC_URL,
      },
    },
    goerli: {
      url: process.env.GOERLI_RPC_URL,
      accounts: [process.env.GOERLI_PRIVATE_KEY],
    }
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY
  }
};
Enter fullscreen mode Exit fullscreen mode

También colocamos nuestras variables ocultas en el archivo .env. Las RPC_URL las conseguimos en infura o alchemy, la llave privada en metamask y la API Key desde etherscan.

.env

MAINNET_RPC_URL=https://mainnet.infura.io/v3/TULLAVE
GOERLI_RPC_URL=https://goerli.infura.io/v3/TULLAVE
GOERLI_PRIVATE_KEY=TULLAVE
ETHERSCAN_API_KEY=TULLAVE
Enter fullscreen mode Exit fullscreen mode

4. Compilamos nuestro token

Borramos el archivo de ejemplo que viene por defecto contracts/Lock.sol y colocamos nuestro contrato de token en la misma carpeta.

contracts/MyToken.sol

// SPDX-License-Identifier: MIT

pragma solidity 0.8.16;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

interface IUniswapV2Factory {
    function createPair(address tokenA, address tokenB) external returns (address pair);
}

interface IUniswapV2Router02 {
    function factory() external pure returns (address);
    function WETH() external pure returns (address);

    function addLiquidityETH(
        address token,
        uint256 amountTokenDesired,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline
    ) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity);

    function swapExactTokensForETHSupportingFeeOnTransferTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external;

    function swapETHForExactTokens(
        uint amountOut,
        address[] calldata path,
        address to,
        uint deadline
    ) external payable returns (uint[] memory amounts);
}

contract MyToken is Context, IERC20, IERC20Metadata {
    // Openzeppelin variables
    mapping(address => uint256) private _balances;

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

    uint256 private _totalSupply;

    string private _name;
    string private _symbol;

    // My variables

    address public vaultAddress;

    address public pair;

    uint public feeDecimal = 2;
    enum FeesIndex{ BUY, SELL, P2P }
    uint[] public feePercentages;

    mapping(address => bool) public is_taxless;

    bool private isInFeeTransfer;

    constructor(address swapRouter, address _vaultAddress)
    {
        IUniswapV2Router02 _uniswapV2Router = IUniswapV2Router02(swapRouter);
        pair = IUniswapV2Factory(_uniswapV2Router.factory()).createPair(address(this), _uniswapV2Router.WETH());

        // Edit here
        _name = "My Token";
        _symbol = "TKN";
        vaultAddress = _vaultAddress;

        feePercentages.push(1000); // Buy  fee is 10.00%
        feePercentages.push(1500); // Sell fee is 15.00%
        feePercentages.push(500);  // Buy  fee is  5.00%
        // End edit

        is_taxless[msg.sender] = true;
        is_taxless[vaultAddress] = true;
        is_taxless[address(this)] = true;
        is_taxless[address(0)] = true;
        _mint(msg.sender, 1_000_000 ether);
    }

    function name() public view virtual override returns (string memory) {
        return _name;
    }

    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    function decimals() public view virtual override returns (uint8) {
        return 18;
    }

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

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

    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

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

    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, _allowances[owner][spender] + addedValue);
        return true;
    }

    function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
        address owner = _msgSender();
        uint256 currentAllowance = _allowances[owner][spender];
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
        unchecked {
            _approve(owner, spender, currentAllowance - subtractedValue);
        }

        return true;
    }

    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        // My implementation
        uint feesCollected;
        if (!is_taxless[from] && !is_taxless[to]) {
            bool sell = to == pair;
            bool p2p = from != pair && to != pair;

            uint fee = calculateFee(p2p ? FeesIndex.P2P : sell ? FeesIndex.SELL : FeesIndex.BUY, amount);

            feesCollected += fee;
        }

        amount -= feesCollected;
        _balances[from] -= feesCollected;
        _balances[vaultAddress] += feesCollected;
        // End my implementation

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
        }
        _balances[to] += amount;

        emit Transfer(from, to, amount);
    }

    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _totalSupply += amount;
        _balances[account] += amount;
        emit Transfer(address(0), account, amount);
    }

    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");

        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        unchecked {
            _balances[account] = accountBalance - amount;
        }
        _totalSupply -= amount;

        emit Transfer(account, address(0), amount);
    }

    function _approve(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    function _spendAllowance(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

    // My Functions

    function calculateFee(FeesIndex fee_index, uint amount) internal view returns(uint) {
        return (amount * feePercentages[uint(fee_index)])  / (10**(feeDecimal + 2));
    }
}
Enter fullscreen mode Exit fullscreen mode

Ahora lo compilamos.

npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

5. Pruebas unitarias forkeando mainnet

Borramos el archivo ejemplo test/MyToken.js y agregamos el nuestro propio. Las pruebas unitarias nos ayudan a aseguramos que todo esté funcionando bien.

test/MyToken.js

const {
  time,
  loadFixture,
} = require("@nomicfoundation/hardhat-network-helpers");
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
const { expect } = require("chai");

describe("Lock", function () {
  // We define a fixture to reuse the same setup in every test.
  // We use loadFixture to run this setup once, snapshot that state,
  // and reset Hardhat Network to that snapshopt in every test.
  async function launchAndFundWalletA() {
    const blockNumBefore = await ethers.provider.getBlockNumber();
    const blockBefore = await ethers.provider.getBlock(blockNumBefore);
    deadline = blockBefore.timestamp + 500;

    const [deployer, vaultWallet, walletA, walletB] = await ethers.getSigners();

    const MyToken = await ethers.getContractFactory("MyToken");
    const uniswapRouter = await ethers.getContractAt("IUniswapV2Router02", "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D");
    const myToken = await MyToken.deploy(uniswapRouter.address, vaultWallet.address);

    await myToken.approve(uniswapRouter.address, ethers.utils.parseEther("500000"))
    await uniswapRouter.addLiquidityETH(
      myToken.address,
      ethers.utils.parseEther("500000"),
      ethers.utils.parseEther("0"),
      ethers.utils.parseEther("0"),
      deployer.address,
      deadline,
      { value: ethers.utils.parseEther("1") })

    await myToken.transfer(walletA.address, ethers.utils.parseEther("1000"))
    return { uniswapRouter, myToken, vaultWallet, walletA, walletB, deadline };
  }

  describe("Fee collection", function () {
    it("Should collect fees on P2P", async function () {
      const { uniswapRouter, myToken, vaultWallet, walletA, walletB } = await loadFixture(launchAndFundWalletA);
      await myToken.connect(walletA).transfer(walletB.address, ethers.utils.parseEther("100"))
      expect(
        ethers.utils.parseEther("95.0")
      ).to.equal(
        await myToken.balanceOf(walletB.address)
      );
      expect(
        ethers.utils.parseEther("5.0")
      ).to.equal(
        await myToken.balanceOf(vaultWallet.address)
      );
    });

    it("Should collect fees on Buy", async function () {
      const { uniswapRouter, myToken, vaultWallet, walletA, walletB, deadline } = await loadFixture(launchAndFundWalletA);

      await myToken.connect(walletA).approve(uniswapRouter.address, ethers.utils.parseEther("100.0"))
      await uniswapRouter.connect(walletA).swapETHForExactTokens(
          ethers.utils.parseEther("100.0"),
          [await uniswapRouter.WETH(), myToken.address],
          walletA.address,
          deadline,
          { value: ethers.utils.parseEther("0.1") })

      expect(
        ethers.utils.parseEther("10.0")
      ).to.equal(
        await myToken.balanceOf(vaultWallet.address)
      );
    });

    it("Should collect fees on Sell", async function () {
      const { uniswapRouter, myToken, vaultWallet, walletA, walletB, deadline } = await loadFixture(launchAndFundWalletA);

      await myToken.connect(walletA).approve(uniswapRouter.address, ethers.utils.parseEther("100.0"))
      await uniswapRouter.connect(walletA).swapExactTokensForETHSupportingFeeOnTransferTokens(
        ethers.utils.parseEther("100.0"),
        ethers.utils.parseEther("0"),
        [myToken.address, await uniswapRouter.WETH()],
        walletA.address,
        deadline)

      expect(
        ethers.utils.parseEther("15.0")
      ).to.equal(
        await myToken.balanceOf(vaultWallet.address)
      );
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Ejecutamos los tests y nos aseguramos que todo funcionará bien haciendo las pruebas en el estado actual de mainnet forkeando.

npx hardhat test
Enter fullscreen mode Exit fullscreen mode

6. Lanzamiento y Auto Verificación

Creamos nuestro archivo de deploy donde configuramos la rutina de lanzamientos y definimos los parametros de los constructores.

scripts/deploy.js

const hre = require("hardhat");

async function main() {
  const MyToken = await ethers.getContractFactory("MyToken");
  uniswapRouterAddress = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
  vaultAddress = "0x18747BE67c5886881075136eb678cEADaf808028"
  const myToken = await MyToken.deploy(uniswapRouterAddress, vaultAddress);
  await myToken.deployed();
  console.log("myToken launched at:", myToken.address);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

Ahora lanzamos. Enste caso lo hacemos en gerli pero en el archivo hardhat.config.js podemos configurar el destino.

npx hardhat run scripts/deploy.js --network goerli
Enter fullscreen mode Exit fullscreen mode

Y lo auto verificamos si no lo hizo automáticamente

npx hardhat verify --network goerli ADDRESSDELCONTRATO PARAM1 PARAM2
Enter fullscreen mode Exit fullscreen mode

Por ejemplo debería quedar algo así

npx hardhat verify --network goerli 0xFA4A4d277A8F8FF6158615c8FaE2Abfe147fb64c 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D 0x730bF3B67090511A64ABA060FbD2F7903536321E
Enter fullscreen mode Exit fullscreen mode

Bono: Test automáticos con Github Actions

Los tests automáticos nos van a permitir detectar errores en cada cambio en el código. Nuestros tests se ejecutarán autómaticamente cada vez que se haga una push o pull request y nos avisará si algún test no pasó.

Primero debemos hacer un par de modificaciones en el archivo de configuración de hardhat para que pueda operar sin necesidad de tener todas las variables.

hardhat.config.js

require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-etherscan");
require('dotenv').config()

module.exports = {
  solidity: "0.8.16",
  networks: {
    hardhat: {
      forking: {
        url: process.env.MAINNET_RPC_URL,
      },
    }
  }
};

if(process.env.GOERLI_RPC_URL && process.env.GOERLI_PRIVATE_KEY)
{
  module.exports["networks"]["goerli"] = {
    url: process.env.GOERLI_RPC_URL,
    accounts: [process.env.GOERLI_PRIVATE_KEY],
  }
}

if(process.env.ETHERSCAN_API_KEY)
{
  module.exports["etherscan"] = {
    apiKey: process.env.ETHERSCAN_API_KEY
  }
}
Enter fullscreen mode Exit fullscreen mode

.github/workflows/unit-tests.yml

name: Node.js CI

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x]

    steps:
      - uses: actions/checkout@v3
      - name: Mainnet Forking Tests
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm install
      - run: export MAINNET_RPC_URL=${{ secrets.MAINNET_RPC_URL }}; npx hardhat test
Enter fullscreen mode Exit fullscreen mode

Para final agrega esta badge a tu Readme para presumir tu workflow.

README.md

![workflow](https://github.com/TUUSUARIO/TUREPO/actions/workflows/unit-tests.yml/badge.svg)
Enter fullscreen mode Exit fullscreen mode

Gracias por ver este video!

Sígannos en dev.to y en Youtube para todo lo relacionado al desarrollo en Blockchain en Español.

Top comments (0)