WEB3DEV

Cover image for Tutorial Solidity: Contrato de Vesting Linear
Paulo Gio
Paulo Gio

Posted on • Atualizado em

Tutorial Solidity: Contrato de Vesting Linear

Olá! Neste episódio, iremos construir um contrato de Vesting (aquisição progressiva de direitos) Linear em Solidity.

Vesting Linear

O vesting linear é uma maneira de liberar ou distribuir gradualmente tokens ao longo de um período de tempo, em vez de entregá-los de uma só vez.

O benefício do vesting linear é que ela incentiva o destinatário a permanecer e continuar contribuindo para o projeto ao longo do período de vesting.

Também pode ajudar a gerenciar o risco do projeto, garantindo que o destinatário não receba todos os benefícios imediatamente e possa deixar o projeto sem contribuir para o sucesso a longo prazo.

Também pode reduzir a pressão de venda em um token. Em vez de tornar os tokens disponíveis aos destinatários em um momento específico, os tokens são liberados gradualmente e os destinatários podem reivindicar seus tokens liberados em diferentes intervalos.

Especificações

  • O implantador deve fornecer ao contrato as seguintes entradas:
    • recipients (destinatários)
    • amounts (quantidades)
    • startTime (horário de início)
    • duration (duração)
  • O destinatário não pode reivindicar nenhum token antes que o horário de início seja atingido.
  • Os destinatários podem reivindicar tokens com a seguinte equação: liberado = quantidade * (horário de início + duração - horário atual) / duração

Código

Vamos construir o contrato seguindo uma abordagem de desenvolvimento orientada a testes. Isso significa que escreveremos casos de teste e construiremos nosso contrato passo a passo.

Estamos usando o Hardhat. Se você ainda não o utilizou, sugiro que siga o tutorial deles para configurar seu ambiente. A partir de agora, tudo está acontecendo no diretório do projeto.

Vamos começar criando nosso arquivo de teste, test/LinearVesting.ts.

describe("LinearVesting", function () {

   async function deploy() {
       // código de implantação irá aqui
   }

   // casos de teste vão aqui

})
Enter fullscreen mode Exit fullscreen mode

Implantação / deve ter um token

describe("deployment", function () {
 it('deve ter um token', async function () {
   const { contract, token } = await loadFixture(deploy)
   expect(await contract.token()).to.eq(token.address)
 })
})
Enter fullscreen mode Exit fullscreen mode

Este teste verifica se o implantador associou o contrato a um token.

Para fazer este teste passar, precisaremos fazer algumas coisas:

  1. Adicione a biblioteca de contratos OpenZeppelin ao nosso projeto
yarn add --dev @openzeppelin/contracts
Enter fullscreen mode Exit fullscreen mode
  1. Crie um token: contracts/Token.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

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

contract Token is ERC20 {
   constructor(string memory name_, string memory symbol_, uint totalsuppy_) ERC20(name_, symbol_) {
       _mint(msg.sender, totalsuppy_);
   }
}
Enter fullscreen mode Exit fullscreen mode

Este token receberá como entrada um nome (name), um símbolo (symbol) e um totalSupply. O totalSupply será criado e enviado ao implantador.

  1. Crie nosso contrato de vesting linear:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

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

contract LinearVesting {
   using SafeERC20 for IERC20;

   IERC20 public token;

   constructor(IERC20 token_) {
       token = token_;
   }
}
Enter fullscreen mode Exit fullscreen mode
  1. Atualize nosso arquivo de teste
// adicione as seguintes importações
import { expect } from "chai";
import { ethers } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";

// atualize a função deploy()
async function deploy() {
 // implantação do token
 const Token = await ethers.getContractFactory("Token")
 const token = await Token.deploy("OnMyChain", "OMC", ethers.utils.parseEther("10000"))
 await token.deployed()

 // implantação do contrato
 const Contract = await ethers.getContractFactory("LinearVesting")
 const contract = await Contract.deploy(token.address)
 await contract.deployed()

 return { contract, token }
}
Enter fullscreen mode Exit fullscreen mode

Agora podemos executar nosso primeiro teste usando o npx hardhat test

Implantação / deve ter alocações

it("deve ter alocações", async function () {
 const { contract, recipients, allocations } = await loadFixture(deploy)
 for (let index = 0; index < recipients.length; index++) {
   const recipient = recipients[index];
   const allocation = allocations[index];
   expect(await contract.allocation(recipient)).to.eq(allocation)
 }
})
Enter fullscreen mode Exit fullscreen mode

Aqui estamos verificando se cada destinatário tem sua alocação esperada. Precisamos atualizar o código de implantação (deploy) e o código do contrato.

Em nossa função de implantação de teste do arquivo de teste:

async function deploy() {
 // implantação do token
 const Token = await ethers.getContractFactory("Token")
 const token = await Token.deploy("OnMyChain", "OMC", ethers.utils.parseEther("10000"))
 await token.deployed()

 // gerar array de recipientes e array de alocações
 // destinatários[0] terão alocações[0]
 const recipients = (await ethers.getSigners()).map(s => s.address)
 const allocations = recipients.map((r, idx) => ethers.utils.parseEther((idx * 10).toString()))

 // implantação do contrato
 const Contract = await ethers.getContractFactory("LinearVesting")
 const contract = await Contract.deploy(token.address, recipients, allocations) // adicione os argumentos
 await contract.deployed()

 return { contract, token, recipients, allocations }
}
Enter fullscreen mode Exit fullscreen mode

Atualize o contrato:

// no contrato, declare um mapeamento chamado allocation
mapping(address => uint) allocation;

// na função contratante, adicione destinatários e alocações como entrada
constructor(IERC20 token_, address[] memory recipients_, uint[] memory allocations_) {
 token = token_;
 for (uint i = 0; i < recipients_.length; i++) {
   allocation[recipients_[i]] = allocations_[i];
 }
}
Enter fullscreen mode Exit fullscreen mode

Aqui, nosso construtor está pegando o array de destinatários e alocações. Para cada destinatário, está adicionando a alocação correspondente.

Implantação / deve ter um horário de início

it("deve ter um horário de início", async function () {
 const { contract, startTime } = await loadFixture(deploy)
 expect(await contract.startTime()).to.eq(startTime)
})
Enter fullscreen mode Exit fullscreen mode

Atualize a função de implantação:

async function deploy() {
       // implantação do token
       const Token = await ethers.getContractFactory("Token")
       const token = await Token.deploy("OnMyChain", "OMC", ethers.utils.parseEther("10000"))
       await token.deployed()

       // gerar array de recipientes e array de alocações
       // destinatários[0] terão alocações[0]
       const recipients = (await ethers.getSigners()).map(s => s.address)
       const allocations = recipients.map((r, idx) => ethers.utils.parseEther((idx * 10).toString()))

       const startTime = (await time.latest()) + 60 // começa 60 segundos após a implantação

       // implantação do contrato
       const Contract = await ethers.getContractFactory("LinearVesting")
       const contract = await Contract.deploy(token.address, recipients, allocations, startTime) // adicione os argumentos
       await contract.deployed()

       return { contract, token, recipients, allocations, startTime }
   }
Enter fullscreen mode Exit fullscreen mode

Atualize o contrato:

// ...
contract LinearVesting {
 // ...
 uint public startTime;

 constructor(IERC20 token_, address[] memory recipients_, uint[] memory allocations_, uint startTime_) {
   // ...
   startTime = startTime_;
 }
}
Enter fullscreen mode Exit fullscreen mode

Implantação / deve ter uma duração

it("deve ter uma duração", async function () {
           const { contract, duration } = await loadFixture(deploy)
           expect(await contract.duration()).to.eq(duration)
       })
Enter fullscreen mode Exit fullscreen mode

Atualize a função de implantação:

async function deploy() {
       // implantação do token
       const Token = await ethers.getContractFactory("Token")
       const token = await Token.deploy("OnMyChain", "OMC", ethers.utils.parseEther("10000"))
       await token.deployed()

       // gerar array de recipientes e array de alocações
       // destinatários[0] terão alocações[0]
       const recipients = (await ethers.getSigners()).map(s => s.address)
       const allocations = recipients.map((r, idx) => ethers.utils.parseEther((idx * 10).toString()))

       const startTime = (await time.latest()) + 60 // começa 60 segundos após a implantação
       const duration = 60 * 60 // 1 hora de duração

       // implantação do contrato
       const Contract = await ethers.getContractFactory("LinearVesting")
       const contract = await Contract.deploy(token.address, recipients, allocations, startTime, duration) // adicione os argumentos
       await contract.deployed()

       return { contract, token, recipients, allocations, startTime, duration }
   }
Enter fullscreen mode Exit fullscreen mode

Atualize o contrato:

// ...
contract LinearVesting {
 // ...
 uint public duration;

 constructor(IERC20 token_, address[] memory recipients_, uint[] memory allocations_, uint startTime_, uint duration_) {
   // ...
   duration = duration_;
 }
}
Enter fullscreen mode Exit fullscreen mode

Ok, cobrimos todos os casos de teste básicos para nossa implantação. Se você quiser ver como podemos adicionar validações adicionais e eventos a isso, certifique-se de verificar a seção de bônus no final.

Lógica

Agora vamos mergulhar na lógica do código. Existem algumas variáveis que queremos acompanhar.

  1. claimed — quanto o destinatário já reivindicou
  2. released — quanto foi liberado
  3. available — quanto está disponível para reivindicar (liberado — reivindicado)
  4. outstanding — quanto não foi liberado (alocação — liberado)

Para cada uma dessas variáveis, queremos testar seu valor em três estados diferentes:

  1. Antes do horário de início
  2. Durante o período de vesting (início…início + duração)
  3. Após o período de vesting (início + duração)

Por fim, precisamos definir uma função que o destinatário possa chamar para reivindicar seus tokens disponíveis.

Função claim / deve reverter antes da hora de início

describe("claim", function () {
       it("deve reverter antes da hora de início", async function () {
           const { contract, recipients } = await loadFixture(deploy)
           for await (const recipient of recipients) {
               await expect(contract.connect(recipient).claim()).to.be.revertedWith("LinearVesting: ainda não começou")
           }
       })
   })
Enter fullscreen mode Exit fullscreen mode

Eu refatorei um pouco o código na implantação para fazer com que os destinatários sejam passados como signatários em vez de strings.

// ...
const recipients = await ethers.getSigners()
// ..
// adicionei recipients.map
const contract = await Contract.deploy(token.address, recipients.map(s => s.address), allocations, startTime, duration)
Enter fullscreen mode Exit fullscreen mode

Atualizei o código do contrato:

function claim() external {
 require(block.timestamp > startTime, "LinearVesting: ainda nao comecou");
}
Enter fullscreen mode Exit fullscreen mode

Este código reverte se o block.timestamp for menor ou igual ao startTime.

Função claim / deve transferir tokens disponíveis

it("deve transferir tokens disponíveis", async function () {
 const { contract, token, recipients, allocations, startTime, duration } = await loadFixture(deploy)
 await time.increaseTo(startTime)
 await time.increase((duration / 2) - 1) // aumentar para 50% dos tokens disponíveis

 const allocation = allocations[0];
 const amount = allocation.div(2) // 50%
 await expect(contract.connect(recipients[0]).claim()).to.changeTokenBalances(token,
   [contract, recipients[0]],
   [amount.mul(-1), amount]
 )
})
Enter fullscreen mode Exit fullscreen mode

Vamos começar atualizando a função deploy. Precisamos transferir a soma do valor alocado do implantador para o contrato:

// ...

// Transferir tokens para o contrato
const amount = allocations.reduce((acc, cur) => acc.add(cur), ethers.utils.parseEther("0"))
await token.transfer(contract.address, amount)
Enter fullscreen mode Exit fullscreen mode

Agora vamos atualizar nosso código do contrato. Precisaremos declarar algumas das variáveis mencionadas anteriormente:

contract LinearVesting {
   // ...
   mapping(address => uint) public claimed;

   function claim() external {
       require(block.timestamp >= startTime, "LinearVesting: ainda nao comecou");
       uint amount = _available(msg.sender);
       token.safeTransfer(msg.sender, amount);
   }

   function _available(address address_) internal view returns (uint) {
       return _released(address_) - claimed[address_];
   }

   function _released(address address_) internal view returns (uint) {
       return (allocation[address_] * (block.timestamp - startTime)) / duration;
   }
}
Enter fullscreen mode Exit fullscreen mode

Os tokens são transferidos.

Função claim / deve atualizar claimed

it("deve atualizar claimed", async function () {
           const { contract, token, recipients, allocations, startTime, duration } = await loadFixture(deploy)
           await time.increaseTo(startTime)
           await time.increase((duration / 2) - 1) //aumentar para 50% dos tokens disponíveis

           const allocation = allocations[0];
           const amount = allocation.div(2) // 50%
           expect(await contract.claimed(recipients[0].address)).to.eq(0)
           await contract.connect(recipients[0]).claim()
           expect(await contract.claimed(recipients[0].address)).to.eq(amount)
       })
Enter fullscreen mode Exit fullscreen mode

Código do contrato:

function claim() external {
 // ...
 claimed[msg.sender] += amount;
}
Enter fullscreen mode Exit fullscreen mode

Isso é tudo para as variáveis do contrato, as outras variáveis serão as funções que deixaremos como externas.

Helpers / Antes

describe("helpers", function () {
 describe("Antes do horário de início", function () {

   let contract: Contract
   let recipient: SignerWithAddress
   let allocation: BigNumber

   beforeEach(async function() {
     const fixture = await loadFixture(deploy)
     contract = fixture.contract
     recipient = fixture.recipients[0]
     allocation = fixture.allocations[0]
     await time.increaseTo(fixture.startTime)
   })

   it("deveria ter 0 liberado", async function () {
     expect(await contract.released(recipient.address)).to.eq(0)
   })
   it("deveria ter 0 disponível", async function () {
     expect(await contract.available(recipient.address)).to.eq(0)
   })
   it("deveria ter todos pendentes", async function () {
     expect(await contract.outstanding(recipient.address)).to.eq(allocation)
   })
 })
})
Enter fullscreen mode Exit fullscreen mode

Código:

//...

function available(address address_) external view returns (uint) {
 return _available(address_);
}

function released(address address_) external view returns (uint) {
 return _released(address_);
}

function outstanding(address address_) external view returns (uint) {
 return allocation[address_] - _released(address_);
}
Enter fullscreen mode Exit fullscreen mode

Helpers / Durante

describe("during", function () {
 let contract: Contract;
 let recipient: SignerWithAddress;
 let allocation: BigNumber;

 beforeEach(async function () {
   const fixture = await loadFixture(deploy);
   contract = fixture.contract;
   recipient = fixture.recipients[0];
   allocation = fixture.allocations[0];
   await time.increaseTo(fixture.startTime);
   await time.increase(fixture.duration / 2 - 1);
   await contract.connect(recipient).claim();
 });

 it("deveria ter 50% liberado", async function () {
   expect(await contract.released(recipient.address)).to.eq(allocation.div(2));
 });
 it("deveria ter 0% disponível", async function () {
   expect(await contract.available(recipient.address)).to.eq(0);
 });
 it("deveria ter 50% pendente", async function () {
   expect(await contract.outstanding(recipient.address)).to.eq(
     allocation.div(2)
   );
 });
});
Enter fullscreen mode Exit fullscreen mode

Helpers / Depois

describe("after", function () {
 let contract: Contract;
 let recipient: SignerWithAddress;
 let allocation: BigNumber;

 beforeEach(async function () {
   const fixture = await loadFixture(deploy);
   contract = fixture.contract;
   recipient = fixture.recipients[0];
   allocation = fixture.allocations[0];
   await time.increaseTo(fixture.startTime);
   await time.increase(fixture.duration);
   await contract.connect(recipient).claim();
 });

 it("deveria ter 100% liberado", async function () {
   expect(await contract.released(recipient.address)).to.eq(allocation);
 });
 it("deveria ter 0% disponível", async function () {
   expect(await contract.available(recipient.address)).to.eq(0);
 });
 it("deveria ter 0% pendente", async function () {
   expect(await contract.outstanding(recipient.address)).to.eq(0);
 });
});
Enter fullscreen mode Exit fullscreen mode

Atualize o código do contrato:

function _released(address address_) internal view returns (uint) {
 if (block.timestamp < startTime) {
   return 0;
 } else {
   if (block.timestamp > startTime + duration) {
     return allocation[address_];
   } else {
     return (allocation[address_] * (block.timestamp - startTime)) / duration;
   }
 }
}
Enter fullscreen mode Exit fullscreen mode

Pronto! Temos um contrato básico de vesting linear.

Bônus

Aqui estão algumas melhorias adicionais que você pode implementar:

  • Verificar se os destinatários e alocações têm os mesmos comprimentos.
  • Verificar se não há destinatários duplicados.
  • Emitir um evento após a criação de uma alocação.
  • Verificar se o horário de início é maior ou igual ao último horário registrado no bloco.
  • Adicionar uma função para o horário de término.
  • Emitir um evento após a aquisição dos tokens.
  • Como tornar este um contrato de vesting mais genérico?
    • Vários tokens
    • Diferentes programações de vesting por endereço

Estou ansioso para ver o que você vai criar! Entre em contato.

O código pode ser encontrado neste repositório do GitHub.

Artigo publicado por Cyrille. Traduzido por Paulinho Giovannini.

Oldest comments (0)