Skip to content

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

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() {


   }


}

implantação / deve ter um token

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

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_;
   }


}

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)
})

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 balanceStaked que 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_;
   }


}

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)
})

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;
}

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)
})

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%
// ...

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

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]
 )
})

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_);
}

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))
})

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_;
}

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)
})

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;
}

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))
})

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_;
}

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
})

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
})

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
 )
})

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_);
}

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

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"))
})

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);
}

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

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)]
 )
})

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);
}

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))
})

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;
}

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)
})

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;
}

reivindicação / eventos

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

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);
}

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

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]
 )
})

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
}

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))
})

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;
}

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))
})

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;
}

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))
})

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;
}

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)
})

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;
}

acumulação / eventos

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

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);
}

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

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)]
 )
})

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_);
}

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))
})

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_;
}

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)
})

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_;
}

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))
})

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_;
}

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")
})

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_;
}

retirada / eventos

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

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_);
}

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))
})

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_);
}

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

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.