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
})
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)
})
})
Este teste verifica se o implantador associou o contrato a um token.
Para fazer este teste passar, precisaremos fazer algumas coisas:
- Adicione a biblioteca de contratos OpenZeppelin ao nosso projeto
yarn add --dev @openzeppelin/contracts
- 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_);
}
}
Este token receberá como entrada um nome (name
), um símbolo (symbol
) e um totalSupply
. O totalSupply
será criado e enviado ao implantador.
- 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_;
}
}
- 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 }
}
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)
}
})
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 }
}
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];
}
}
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)
})
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 }
}
Atualize o contrato:
// ...
contract LinearVesting {
// ...
uint public startTime;
constructor(IERC20 token_, address[] memory recipients_, uint[] memory allocations_, uint startTime_) {
// ...
startTime = startTime_;
}
}
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)
})
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 }
}
Atualize o contrato:
// ...
contract LinearVesting {
// ...
uint public duration;
constructor(IERC20 token_, address[] memory recipients_, uint[] memory allocations_, uint startTime_, uint duration_) {
// ...
duration = duration_;
}
}
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.
- claimed — quanto o destinatário já reivindicou
- released — quanto foi liberado
- available — quanto está disponível para reivindicar (liberado — reivindicado)
- outstanding — quanto não foi liberado (alocação — liberado)
Para cada uma dessas variáveis, queremos testar seu valor em três estados diferentes:
- Antes do horário de início
- Durante o período de vesting (início…início + duração)
- 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")
}
})
})
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)
Atualizei o código do contrato:
function claim() external {
require(block.timestamp > startTime, "LinearVesting: ainda nao comecou");
}
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]
)
})
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)
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;
}
}
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)
})
Código do contrato:
function claim() external {
// ...
claimed[msg.sender] += amount;
}
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)
})
})
})
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_);
}
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)
);
});
});
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);
});
});
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;
}
}
}
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)