WEB3DEV

Cover image for Tutorial de Solidity: Criação de um contrato Staking ERC20
Fatima Lima
Fatima Lima

Posted on

Tutorial de Solidity: Criação de um contrato Staking ERC20

Este tutorial segue nosso Solidity Tutorial: Create an ERC20 Token. É altamente recomendado que você passe por ele antes de acessar este tutorial, para garantir que você esteja na mesma página.

Neste artigo, abordaremos o seguinte:

  1. Visão geral de staking
  2. Arquitetura
  3. Especificações
  4. Casos de teste
  5. Contrato de Staking
  6. Correção de bugs (erros)

Image description

Foto por Edson Saldaña em Unsplash

Visão Geral de staking

Staking é uma forma de ganhar recompensas enquanto se detém certas criptomoedas.

Em geral, envolve depositar tokens em um contrato de staking e ganhar recompensas com base no número de tokens depositados e na taxa de distribuição das recompensas.

Arquitetura

Neste tutorial, vamos implantar dois contratos:

  1. Contrato de token ERC20
  2. Contrato ERC20Staking

Vamos usar o token ERC20 que criamos no tutorial anterior.

Especificações

Construiremos um contrato ERC20Staking baseado nas seguintes especificações.

  1. As contas podem depositar tokens no contrato.
  2. As contas só podem depositar um token específico.
  3. As contas podem retirar tokens do contrato.
  4. As contas podem requerer recompensas do contrato.
  5. As contas podem acumular recompensas do contrato.
  6. A taxa de 0,01% por hora deve ser aplicada às recompensas.

Além disso, na implantação, queremos fazer o seguinte:

  1. Enviar 80% do suprimento total para o contrato ERC20Staking

Esse será o número de tokens no pool de recompensas.

Casos de Testes

Com base nas especificações acima, podemos desenvolver uma série de testes automatizados para verificar se nosso código funciona como previsto. O arquivo pode ser encontrado no GitHub.

Agora, vamos começar a construir!

Contrato de staking ERC20

Vamos primeiro criar um contrato contracts/ERC20Staking.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Descomentar essa linha para usar o console.log
// importar "hardhat/console.sol";

contract ERC20Staking {

   constructor() {


   }


}
Enter fullscreen mode Exit fullscreen mode

implantação / deve ter um token

it("should have a token", async function () {
 expect(await staking.token()).to.eq(token.address)
})
Enter fullscreen mode Exit fullscreen mode

A primeira questão que resolveremos é a associação de um token à conta. Para fazer isso, vamos definir um token no contrato. Usaremos o OpenZeppelin SafeERC20 para fazer isso.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Descomentar essa linha para usar o console.log
// importar "hardhat/console.sol";

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

contract ERC20Staking {
   using SafeERC20 for IERC20;

   IERC20 public immutable token;

   constructor(IERC20 token_) {
       token = token_;
   }


}
Enter fullscreen mode Exit fullscreen mode

O constructor agora atribui o token ao nosso contrato. Tornamos isso imutável para garantir que não possa ser alterado no futuro.

implantação / deve ter 0 tokens staked (apostados)

it("should have 0 staked", async function () {
 expect(await staking.totalStaked()).to.eq(0)
})
Enter fullscreen mode Exit fullscreen mode

Enfrentamos um primeiro problema de projeto interessante. Os dois tokens apostados e os tokens de recompensa ficarão em um contrato. Isto significa que o contrato deve ser capaz de diferenciá-los. Vamos declarar uma variável balanceStakedque será incrementada quando os tokens forem depositados e decrescida quando os tokens forem retirados.

No momento da implantação, nenhuma conta terá tokens depositados no contrato de staking, portanto, faz sentido que haja 0 tokens apostados.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Descomentar essa linha para usar console.log
// importar "hardhat/console.sol";

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

contract ERC20Staking {
   using SafeERC20 for IERC20;

   IERC20 public immutable token;
   uint public immutable rewardsPerHour = 1000; // 0.01%

   uint public totalStaked = 0;

   constructor(IERC20 token_) {
       token = token_;
   }


}
Enter fullscreen mode Exit fullscreen mode

Quando implantamos o contrato, o balanceStaked será inicialmente 0. Note que essa variável NÃO é imutável, pois precisaremos incrementá-la/decrementá-la.

implantação / deve ter 80.000.000 tokens de recompensa

it("should have 80,000,000 rewards", async function () {
 expect(await staking.totalRewards()).to.eq(initialRewards)
})
Enter fullscreen mode Exit fullscreen mode

Precisamos rastrear o número de tokens de recompensa disponíveis. O cálculo para isto é simples: total de tokens no contrato, menos o total de tokens apostados.

Precisaremos definir uma função totalRewards() que retornará os resultados. Adicione as 2 funções seguintes ao seu contrato.

function totalRewards() external view returns (uint) {
 return _totalRewards();
}

function _totalRewards() internal view returns (uint) {
 return token.balanceOf(address(this)) - totalStaked;
}
Enter fullscreen mode Exit fullscreen mode

A fim de garantir o retorno do mesmo valor, quer o total de recompensas seja necessário interna ou externamente no contrato, colocamos o cálculo em uma função interna. A função externa então simplesmente chama a função interna, eliminando a possibilidade de uma discrepância.

implantação / deve ter 0,01% de recompensas por hora

it("should have 0.01% rewards per hour", async function () {
 expect(await staking.rewardsPerHour()).to.eq(1000)
})
Enter fullscreen mode Exit fullscreen mode

Declararemos uma variável como representação dos 0,01% das recompensas que as contas receberão por hora.

// ...
uint public immutable rewardsPerHour = 1000; // 0.01%
// ...
Enter fullscreen mode Exit fullscreen mode

Por que 1000?

1 Ether = 1 × 10^18 wei

1 Ether / 1000 wei = 1 × 10^15 wei

Se formatarmos isso de forma legível para o ser humano, obtemos 0.001

0.001 × 100 = 0.01%

Staking
   deployment
     ✔ should have a token
     ✔ should have 0 staked
     ✔ should have 80,000,000 rewards
     ✔ should have 0.01% rewards per hour
Enter fullscreen mode Exit fullscreen mode

Todos os testes de implantação devem ser aprovados.

depósito / deve transferir o montante

it("should transfer amount", async function () {
 await expect(staking.deposit(amount)).to.changeTokenBalances(token,
   [signer, staking],
   [amount.mul(-1), amount]
 )
})
Enter fullscreen mode Exit fullscreen mode

Queremos verificar se o saldo dos tokens foi alterado para refletir o depósito. Criamos uma função deposit(amount) que transfere o montante da conta para o contrato de staking.

function deposit(uint amount_) external {
 token.safeTransferFrom(msg.sender, address(this), amount_);
}
Enter fullscreen mode Exit fullscreen mode

depósito / deve incrementar o montante ao saldo

it("should increment balance by amount", async function () {
 const balance = await staking.balanceOf(signer.address)
 await staking.deposit(amount)
 expect(await staking.balanceOf(signer.address)).to.eq(balance.add(amount))
})
Enter fullscreen mode Exit fullscreen mode

Nossa função deposit transfere o montante, mas não temos como verificar o saldo da conta dentro do contrato.

Precisamos:

  1. declarar um mapeamento balanceOf.
  2. atualizar o saldo quando houver depósito de uma conta.
// ...
mapping(address => uint) public balanceOf;
// ...
function deposit(uint amount_) external {
 token.safeTransferFrom(msg.sender, address(this), amount_);
 balanceOf[msg.sender] += amount_;
}
Enter fullscreen mode Exit fullscreen mode

depósito / deve ter lastUpdated igual ao último timestamp (carimbo de tempo) de bloco

it("should have lastUpdated equal to the latest block timestamp", async function () {
 await staking.deposit(amount)
 const latest = await time.latest()
 expect(await staking.lastUpdated(signer.address)).to.eq(latest)
})
Enter fullscreen mode Exit fullscreen mode

Queremos manter um rastreamento de quando uma conta interage com o contrato. Vamos utilizar um mapeamento lastUpdated para armazenar o timestamp em que uma determinada ação foi realizada. Em seguida, atualizaremos esse valor na função deposit.

// ...
mapping(address => uint) public lastUpdated;
// ...
function deposit(uint amount_) external {
 token.safeTransferFrom(msg.sender, address(this), amount_);
 balanceOf[msg.sender] += amount_;
 lastUpdated[msg.sender] = block.timestamp;
}
Enter fullscreen mode Exit fullscreen mode

depósito / deve incrementar o total apostado pelo montante

it("should increment the total staked by amount", async function () {
 const totalStaked = await staking.totalStaked()
 await staking.deposit(amount)
 expect(await staking.totalStaked()).to.eq(totalStaked.add(amount))
})
Enter fullscreen mode Exit fullscreen mode

Anteriormente, declaramos totalStaked para rastrear o número de tokens depositados vs. tokens de recompensa. Precisamos modificar nossa função deposit para conta por isto.

function deposit(uint amount_) external {
 token.safeTransferFrom(msg.sender, address(this), amount_);
 balanceOf[msg.sender] += amount_;
 lastUpdated[msg.sender] = block.timestamp;
 totalStaked += amount_;
}
Enter fullscreen mode Exit fullscreen mode

depósito / validações

Como estamos usando o OpenZeppelin SafeERC20, podemos pular estes testes,mas eu os escrevi para garantir a abrangência dos conceitos.

it("should revert if staking address not approved", async function () {
 await expect(staking.connect(account0).deposit(amount)).to.be.reverted
})
Enter fullscreen mode Exit fullscreen mode

Se uma conta quiser depositar tokens no contrato, precisará primeiro chamar a função approve(spender, amount)no token.

it("should revert if address has insufficient balance", async function () {
 const totalSupply = await token.totalSupply()
 await token.approve(staking.address, totalSupply)
 await expect(staking.deposit(totalSupply)).to.be.reverted
})
Enter fullscreen mode Exit fullscreen mode

As contas não podem depositar mais que seus saldos.

depósitos / eventos

it("should emit Deposit event", async function () {
 await expect(staking.deposit(amount)).to.emit(staking, "Deposit").withArgs(
   signer.address, amount
 )
})
Enter fullscreen mode Exit fullscreen mode

Queremos emitir um evento, Deposit(address, amount) sempre que uma conta fizer um depósito. Os eventos de queima significam que isto é registrado e pode ser escutado off-chain.

Precisamos declarar um evento Deposit e emiti-lo na função deposit.

// ...
event Deposit(address address_, uint amount_);
// ...
function deposit(uint amount_) external {
 token.safeTransferFrom(msg.sender, address(this), amount_);
 balanceOf[msg.sender] += amount_;
 lastUpdated[msg.sender] = block.timestamp;
 totalStaked += amount_;
 emit Deposit(msg.sender, amount_);
}
Enter fullscreen mode Exit fullscreen mode

Agora, todos os testes de depósito passam.

Staking
   deployment
     ✔ should have a token
     ✔ should have 0 staked
     ✔ should have 80,000 rewards
     ✔ should have 0.01% rewards per hour
   deposit
     ✔ should transfer amount (41ms)
     ✔ should increment balance by the amount
     ✔ should have lastUpdated equal to the latest block timestamp
     ✔ should increment the total staked by amount
     validations
       ✔ should revert if staking address not approved
       ✔ should revert if address has insufficient balance (38ms)
     events
       ✔ should emit Deposit event
Enter fullscreen mode Exit fullscreen mode

recompensas

it("should have 10 rewards after one hour", async function () {
 await time.increase(60*60)
 expect(await staking.rewards(signer.address)).to.eq(ethers.utils.parseEther("10"))
})
it("should have 1/36 rewards after one second", async function () {
 await time.increase(1)
 expect(await staking.rewards(signer.address)).to.eq(amount.div(1000).div(3600))
})
it("should have 0.1 reward after 36 seconds", async function () {
 await time.increase(36)
 expect(await staking.rewards(signer.address)).to.eq(ethers.utils.parseEther("0.1"))
})
Enter fullscreen mode Exit fullscreen mode

Quando você faz staking de tokens, você espera recompensas. Precisamos definir uma função para calcular o número de recompensas que uma conta acumulou. Temos 3 testes para isso, todos eles chamam as mesmas funções, com durações diferentes.

Precisamos acrescentar duas funções:

  1. rewards(address) external.
  2. _rewards(address) internal.

A external simplesmente chamará a internal, que mais tarde pode ser chamada em outro lugar em nosso código.

function rewards(address address_) external view returns (uint) {
 return _rewards(address_);
}

function _rewards(address address_) internal view returns (uint) {
 return (block.timestamp - lastUpdated[address_]) * balanceOf[address_] / (rewardsPerHour * 1 hours);
}
Enter fullscreen mode Exit fullscreen mode

O cálculo pega a duração desde a última atualização, multiplicando-a pelo saldo da conta e a divide por nossa taxa multiplicada por 1 hora (3600), para ajustar para a duração.

Agora, todos os testes de recompensas passam.

Staking
   deployment
     ✔ should have a token
     ✔ should have 0 staked
     ✔ should have 80,000 rewards
     ✔ should have 0.01% rewards per hour
   deposit
     ✔ should transfer amount (38ms)
     ✔ should increment balance by the amount
     ✔ should have lastUpdated equal to the latest block timestamp
     ✔ should increment the total staked by amount
     validations
       ✔ should revert if staking address not approved
       ✔ should revert if address has insufficient balance
     events
       ✔ should emit Deposit event
   rewards
     ✔ should have 10 rewards after one hour
     ✔ should have 1/36 rewards after one second
     ✔ should have 0.1 reward after 36 seconds
Enter fullscreen mode Exit fullscreen mode

reivindicação / deve mudar os saldos dos tokens

it("should change token balances", async function () {
 await expect(staking.claim()).to.changeTokenBalances(token,
   [signer, staking],
   [reward, reward.mul(-1)]
 )
})
Enter fullscreen mode Exit fullscreen mode

Quando uma conta solicita recompensas, os tokens devem ser enviados para a conta a partir do pool de recompensas (no contrato de staking). O contrato também precisa saber quantos tokens devem ser enviados.

function claim() external {
 uint amount = _rewards(msg.sender);
 token.safeTransfer(msg.sender, amount);
}
Enter fullscreen mode Exit fullscreen mode

reivindicação / deve aumentar o valor reivindicado

it("should increment claimed", async function () {
 const claimed = await staking.claimed(signer.address)
 await staking.claim()
 expect(await staking.claimed(signer.address)).to.eq(claimed.add(reward))
})
Enter fullscreen mode Exit fullscreen mode

O contrato deve rastrear quantas recompensas foram reivindicadas por uma conta. Assim, introduzimos um mapeamento claimed que irá incrementar cada vez que forem reivindicadas recompensas.

// ...
mapping(address => uint) public claimed;
// ...
function claim() external {
 uint amount = _rewards(msg.sender);
 token.safeTransfer(msg.sender, amount);
 claimed[msg.sender] += amount;
}
Enter fullscreen mode Exit fullscreen mode

reivindicação / deve atualizar lastUpdated

it("should update lastUpdated claimed", async function () {
 await staking.claim()
 const timestamp = await time.latest()
 expect(await staking.lastUpdated(signer.address)).to.eq(timestamp)
})
Enter fullscreen mode Exit fullscreen mode

Como discutido anteriormente, o cálculo da recompensa utiliza o tempo decorrido desde a última atualização. Portanto, após cada reivindicação, é importante atualizar o lastUpdate para o último timestamp de bloco.

function claim() external {
 uint amount = _rewards(msg.sender);
 token.safeTransfer(msg.sender, amount);
 claimed[msg.sender] += amount;
 lastUpdated[msg.sender] = block.timestamp;
}
Enter fullscreen mode Exit fullscreen mode

reivindicação / eventos

it("should emit Claim event", async function () {
 await expect(staking.claim()).to.emit(staking, "Claim").withArgs(
   signer.address, reward
 )
})
Enter fullscreen mode Exit fullscreen mode

Quando uma conta faz uma reivindicação, nós queremos emitir o evento Claim.

// ...
event Claim(address address_, uint amount_);
// ...
function claim() external {
 uint amount = _rewards(msg.sender);
 token.safeTransfer(msg.sender, amount);
 claimed[msg.sender] += amount;
 lastUpdated[msg.sender] = block.timestamp;
 emit Claim(msg.sender, amount);
}
Enter fullscreen mode Exit fullscreen mode

Agora, todos os testes de reivindicação passam.

Staking
   deployment
     ✔ should have a token
     ✔ should have 0 staked
     ✔ should have 80,000 rewards
     ✔ should have 0.01% rewards per hour
   deposit
     ✔ should transfer amount (40ms)
     ✔ should increment balance by the amount
     ✔ should have lastUpdated equal to the latest block timestamp
     ✔ should increment the total staked by amount
     validations
       ✔ should revert if staking address not approved
       ✔ should revert if address has insufficient balance
     events
       ✔ should emit Deposit event
   rewards
     ✔ should have 10 rewards after one hour
     ✔ should have 1/36 rewards after one second
     ✔ should have 0.1 reward after 36 seconds
   claim
     ✔ should change token balances
     ✔ should increment claimed
     ✔ should update lastUpdated claimed
     events
       ✔ should emit Claim even
Enter fullscreen mode Exit fullscreen mode

acumulação / não deve alterar os saldos dos tokens

it("should not change token balances", async function () {
 await expect(staking.compound()).to.changeTokenBalances(token,
   [signer, staking],
   [0, 0]
 )
})
Enter fullscreen mode Exit fullscreen mode

A acumulação é ligeiramente diferente da reivindicação. Quando fazemos a acumulação, o contrato não transfere os tokens. Ao invés disso, ele ajusta os saldos dentro do contrato.

function compound() external {
 uint amount = _rewards(msg.sender);
 // Nenhuma função de transferência chamada
}
Enter fullscreen mode Exit fullscreen mode

acumulação / deve incrementar as recompensas reivindicadas

it("should increment claimed", async function () {
 const claimed = await staking.claimed(signer.address)
 await staking.compound()
 expect(await staking.claimed(signer.address)).to.eq(claimed.add(reward))
})
Enter fullscreen mode Exit fullscreen mode

Quando fazemos a acumulação, estamos reivindicando as recompensas e as colocamos de volta em jogo, portanto, é importante adicioná-las ao montante reivindicado.

function compound() external {
 uint amount = _rewards(msg.sender);
 // Nenhuma função de transferência chamada
 claimed[msg.sender] += amount;
}
Enter fullscreen mode Exit fullscreen mode

acumulação / deve incrementar o saldo da conta

it("should increment account balance", async function () {
 const balanceOf = await staking.balanceOf(signer.address)
 await staking.compound()
 expect(await staking.balanceOf(signer.address)).to.eq(balanceOf.add(reward))
})
Enter fullscreen mode Exit fullscreen mode

Ao acumular, estamos transferindo o saldo das recompensas para o saldo da conta de staking.

function compound() external {
 uint amount = _rewards(msg.sender);
 // Nenhuma função de transferência chamada
 claimed[msg.sender] += amount;
 balanceOf[msg.sender] += amount;
}
Enter fullscreen mode Exit fullscreen mode

acumulação / deve incrementar a participação total

it("should increment total staked", async function () {
 const balance = await staking.totalStaked()
 await staking.compound()
 expect(await staking.totalStaked()).to.eq(balance.add(reward))
})
Enter fullscreen mode Exit fullscreen mode

Uma vez que estamos enviando as recompensas para o saldo das contas de staking, o montante total apostado também está aumentando.

function compound() external {
 uint amount = _rewards(msg.sender);
 // Nenhuma função de transferência chamada
 claimed[msg.sender] += amount;
 balanceOf[msg.sender] += amount;
 totalStaked += amount;
}
Enter fullscreen mode Exit fullscreen mode

acumulação / deve atualizar a lastUpdated

it("should update lastUpdated", async function () {
 await staking.compound()
 const timestamp = await time.latest()
 expect(await staking.lastUpdated(signer.address)).to.eq(timestamp)
})
Enter fullscreen mode Exit fullscreen mode

Assim como acontece com a função claim, precisamos atualizar a lastUpdated, pois ela é usada para calcular o valor das recompensas disponíveis.

function compound() external {
 uint amount = _rewards(msg.sender);
 // Nenhuma função de transferência chamada
 claimed[msg.sender] += amount;
 balanceOf[msg.sender] += amount;
 totalStaked += amount;
 lastUpdated[msg.sender] = block.timestamp;
}
Enter fullscreen mode Exit fullscreen mode

acumulação / eventos

it("should emit Compound event", async function () {
 await expect(staking.compound()).to.emit(staking, "Compound").withArgs(
   signer.address, reward
 )
})
Enter fullscreen mode Exit fullscreen mode

Quando fazemos a acumulação, o contrato deve emitir um evento Compound.

// ...
event Compound(address address_, uint amount_);
// ...
function compound() external {
 uint amount = _rewards(msg.sender);
 // Nenhuma função de transferência chamada
 claimed[msg.sender] += amount;
 balanceOf[msg.sender] += amount;
 totalStaked += amount;
 lastUpdated[msg.sender] = block.timestamp;
 emit Compound(msg.sender, amount);
}
Enter fullscreen mode Exit fullscreen mode

Agora, todos os testes de acumulação passam.

Staking
   deployment
     ✔ should have a token
     ✔ should have 0 staked
     ✔ should have 80,000 rewards
     ✔ should have 0.01% rewards per hour
   deposit
     ✔ should transfer amount (42ms)
     ✔ should increment balance by the amount
     ✔ should have lastUpdated equal to the latest block timestamp
     ✔ should increment the total staked by amount
     validations
       ✔ should revert if staking address not approved
       ✔ should revert if address has insufficient balance
     events
       ✔ should emit Deposit event
   rewards
     ✔ should have 10 rewards after one hour
     ✔ should have 1/36 rewards after one second
     ✔ should have 0.1 reward after 36 seconds
   claim
     ✔ should change token balances
     ✔ should increment claimed
     ✔ should update lastUpdated claimed
     events
       ✔ should emit Claim event
   compound
     ✔ should not change token balances
     ✔ should increment claimed
     ✔ should increment account balance
     ✔ should increment total staked
     ✔ should decrement the rewards balance
     ✔ should update lastUpdated
     Events
       ✔ should emit Compound event
Enter fullscreen mode Exit fullscreen mode

retirada / deve mudar os saldos dos tokens

it("should change token balances", async function () {
 amount = amount.div(2)
 await expect(staking.withdraw(amount)).to.changeTokenBalances(token,
   [signer, staking],
   [amount, amount.mul(-1)]
 )
})
Enter fullscreen mode Exit fullscreen mode

Quando uma conta faz a retirada de tokens, o número de tokens deve ser transferido do contrato de staking para a conta.

function withdraw(uint amount_) external {
 token.safeTransfer(msg.sender, amount_);
}
Enter fullscreen mode Exit fullscreen mode

retirada / deve diminuir o saldo da conta

it("should decrement account balance", async function () {
 const balanceOf = await staking.balanceOf(signer.address)
 await staking.withdraw(amount)
 expect(await staking.balanceOf(signer.address)).to.eq(balanceOf.sub(amount).add(reward))
})
Enter fullscreen mode Exit fullscreen mode

Como a conta está retirando tokens, precisamos diminuir o número de tokens do saldo da conta no contrato.

function withdraw(uint amount_) external {
 token.safeTransfer(msg.sender, amount_);
 balanceOf[msg.sender] -= amount_;
}
Enter fullscreen mode Exit fullscreen mode

retirada / deve acumular

it("should compound", async function () {
 await staking.withdraw(amount)
 const timestamp = await time.latest()
 expect(await staking.balanceOf(signer.address)).to.eq(reward)
 expect(await staking.claimed(signer.address)).to.eq(reward)
 expect(await staking.lastUpdated(signer.address)).to.eq(timestamp)
})
Enter fullscreen mode Exit fullscreen mode

Este aqui é um pouco complicado. Quando uma conta realiza uma retirada, sua taxa de recompensa futura será menor do que a existente. A fim de garantir que eles recebam a quantia certa de recompensas pelo tempo até o saque, nós as acumulamos.

Há um pequeno inconveniente, que é que as contas nunca poderão realizar retirada de 100%, pois uma quantia insignificante estará sempre acumulada.

function compound() external {
 _compound();
}

function _compound() internal {
 uint amount = _rewards(msg.sender);
 // Nenhuma função de transferência chamada
 claimed[msg.sender] += amount;
 balanceOf[msg.sender] += amount;
 totalStaked += amount;
 lastUpdated[msg.sender] = block.timestamp;
 emit Compound(msg.sender, amount);
}

function withdraw(uint amount_) external {
 _compound();
 token.safeTransfer(msg.sender, amount_);
 balanceOf[msg.sender] -= amount_;
}
Enter fullscreen mode Exit fullscreen mode

Como você pode ver, modificamos o código anterior para nos adaptarmos a esta situação. Criamos um _compound() internal que é chamado em ambas as funções compound() e withdraw().

retirada / deve reduzir o token apostado

it("should decrement token staked", async function () {
 const balance = await staking.totalStaked()
 await staking.withdraw(amount)
 expect(await staking.totalStaked()).to.eq(balance.sub(amount).add(reward))
})
Enter fullscreen mode Exit fullscreen mode

Como a conta está retirando tokens apostados, o montante total apostado será decrescido.

function withdraw(uint amount_) external {
 _compound();
 token.safeTransfer(msg.sender, amount_);
 balanceOf[msg.sender] -= amount_;
 totalStaked -= amount_;
}
Enter fullscreen mode Exit fullscreen mode

retirada / deve reverter se o montante for maior do que o saldo da conta

it("should revert if amount greater than account balance", async function () {
 await expect(staking.withdraw(amount.add(1))).to.be.revertedWith("Insufficient funds")
})
Enter fullscreen mode Exit fullscreen mode

Este é um exemplo de um teste de validação. A quantidade máxima de tokens que pode ser retirada por uma conta é igual ao saldo dessa conta no contrato. Se uma conta tentar sacar mais do que seu saldo, a ação deve ser revertida.

function withdraw(uint amount_) external {
 require(balanceOf[msg.sender] >= amount_, "Insufficient funds");
 _compound();
 token.safeTransfer(msg.sender, amount_);
 balanceOf[msg.sender] -= amount_;
 totalStaked -= amount_;
}
Enter fullscreen mode Exit fullscreen mode

retirada / eventos

it("should emit Withdraw event", async function () {
 await expect(staking.withdraw(amount)).to.emit(staking, "Withdraw").withArgs(
   signer.address, amount
 )
})
Enter fullscreen mode Exit fullscreen mode

Queremos emitir um evento Withdraw quando uma conta realizar retirada de tokens.

function withdraw(uint amount_) external {
 require(balanceOf[msg.sender] >= amount_, "Insufficient funds");
 _compound();
 token.safeTransfer(msg.sender, amount_);
 balanceOf[msg.sender] -= amount_;
 totalStaked -= amount_;
 emit Withdraw(msg.sender, amount_);
}
Enter fullscreen mode Exit fullscreen mode

Correção de Bugs

Em nossos testes automatizados, não contabilizamos uma situação em que uma conta deposita um número de tokens várias vezes.

Em nosso código existente, se o usuário não reivindicasse ou acumulasse tokens antes de fazer outro depósito, suas recompensas já acumuladas desapareceriam.

Portanto, acrescentamos um caso de teste complementar:

it("should compound before deposit", async function () {
 amount = amount.div(2)
 await staking.deposit(amount)
 await time.increase(60*60-1)
 const rewards = amount.div(1000)
 await staking.deposit(amount)
 expect(await staking.balanceOf(signer.address)).to.eq(amount.mul(2).add(rewards))
})
Enter fullscreen mode Exit fullscreen mode

O que isto faz é verificar se nós acumulamos as recompensas antes de depositar mais tokens. Em nosso código, é suficiente acrescentar a função_compound() no início da função deposit.

function deposit(uint amount_) external {
 _compound();
 token.safeTransferFrom(msg.sender, address(this), amount_);
 balanceOf[msg.sender] += amount_;
 lastUpdated[msg.sender] = block.timestamp;
 totalStaked += amount_;
 emit Deposit(msg.sender, amount_);
}
Enter fullscreen mode Exit fullscreen mode

Agora, todos os nossos testes passam! Parabéns

Staking
   deployment
     ✔ should have a token
     ✔ should have 0 staked
     ✔ should have 80,000 rewards
     ✔ should have 0.01% rewards per hour
   deposit
     ✔ should transfer amount (41ms)
     ✔ should increment balance by the amount
     ✔ should have lastUpdated equal to the latest block timestamp
     ✔ should increment the total staked by amount
     ✔ should compound before deposit (45ms)
     validations
       ✔ should revert if staking address not approved
       ✔ should revert if address has insufficient balance (40ms)
     events
       ✔ should emit Deposit event
   rewards
     ✔ should have 10 rewards after one hour
     ✔ should have 1/36 rewards after one second
     ✔ should have 0.1 reward after 36 seconds
   claim
     ✔ should change token balances
     ✔ should increment claimed
     ✔ should update lastUpdated claimed
     events
       ✔ should emit Claim event
   compound
     ✔ should not change token balances
     ✔ should increment claimed
     ✔ should increment account balance
     ✔ should increment total staked
     ✔ should decrement the rewards balance
     ✔ should update lastUpdated
     Events
       ✔ should emit Compound event
   withdraw
     ✔ should change token balances
     ✔ should decrement account balance
     ✔ should compound
     ✔ should decrement token staked
     Validations
       ✔ should revert if the amount is greater than the account balance
     Events
       ✔ should emit Withdraw event
Enter fullscreen mode Exit fullscreen mode

O código fonte completo para este tutorial está disponível no GitHub.

Nosso próximo tutorial será sobre a criação de um aplicativo web descentralizado (dapp / web3 app) para este contrato.

Comentários e sugestões de melhorias são bem-vindos!

Esse artigo foi escrito por Cyrille e traduzido por Fátima Lima. O original pode ser lido aqui.

Top comments (0)