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:
- Visão geral de staking
- Arquitetura
- Especificações
- Casos de teste
- Contrato de Staking
- Correção de bugs (erros)
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:
- Contrato de token ERC20
- Contrato ERC20Staking
Vamos usar o token ERC20 que criamos no tutorial anterior.
Especificações
Construiremos um contrato ERC20Staking baseado nas seguintes especificações.
- As contas podem depositar tokens no contrato.
- As contas só podem depositar um token específico.
- As contas podem retirar tokens do contrato.
- As contas podem requerer recompensas do contrato.
- As contas podem acumular recompensas do contrato.
- A taxa de 0,01% por hora deve ser aplicada às recompensas.
Além disso, na implantação, queremos fazer o seguinte:
- 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:
- declarar um mapeamento
balanceOf.
- 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:
rewards(address) external.
_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.
Oldest comments (0)