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
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
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
}
};
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
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));
}
}
Ahora lo compilamos.
npx hardhat compile
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)
);
});
});
});
Ejecutamos los tests y nos aseguramos que todo funcionará bien haciendo las pruebas en el estado actual de mainnet forkeando.
npx hardhat test
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;
});
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
Y lo auto verificamos si no lo hizo automáticamente
npx hardhat verify --network goerli ADDRESSDELCONTRATO PARAM1 PARAM2
Por ejemplo debería quedar algo así
npx hardhat verify --network goerli 0xFA4A4d277A8F8FF6158615c8FaE2Abfe147fb64c 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D 0x730bF3B67090511A64ABA060FbD2F7903536321E
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
}
}
.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
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)
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)