Hoje as pessoas confiam nas auditorias e acham que se o contrato foi auditado é totalmente seguro e não tem bugs. Infelizmente, não é. Hoje vou contar como encontrei 2 bugs nos contratos de Staking Multichain e recebi uma recompensa por eles como um hacker whitehat.
A Multichain fez um fork de um Contrato de garantia de voto (vote escrow) da Solidly Exchange. Portanto, esses bugs estão presentes em ambas as exchanges. Eles foram previamente auditados por: Blocksec e Peckshield.
Cálculos incorretos de pontos históricos.
O bug está relacionado com o cálculo do poder de votação dos NFTs veMulti (at*
) em um horário específico e quebra a quantidade de recompensa pendente e, em alguns casos, até leva à perda de todas as recompensas não reivindicadas para um NFT específico, é por isso que o identifico como um bug crítico.
Como a Multichain tem variados stakings em diferentes cadeias e o mercado está em baixa, é muito provável que as pessoas não interajam com um dos contratos por mais de uma semana.
Descrição do bug:
Se o contrato tiver 0 pontos de verificação por semana, a função interna do ponto de verificação gravará vários pontos de verificação em point_history
com o mesmo número de bloco e carimbo de data/hora diferente, e a extrapolação do tempo será interrompida. A linha do problema é L826.
Prova de conceito:
Na função de ponto de verificação há um loop for. Antes do loop for salvamos o ponto de verificação mais recente.
// initial_last_point é usado para extrapolação para calcular o número do bloco
// (aproximadamente, para métodos *At) e salvá-los
// como não podemos descobrir isso exatamente de dentro do contrato
Point memory initial_last_point = last_point;
O problema é que isso não cria realmente uma nova variável com o último ponto, mas sim um ponteiro para o último ponto. Se initial_last_point
for alterado, significa que last_point
também foi alterado.
Prova:
Contrato:
contract Test {
struct Struct {
uint256 a;
uint256 b;
}
function testPointer1() public pure returns(Struct memory point){
Struct memory x = Struct({a: 1, b: 2});
Struct memory y = x;
y.b = 3;
return x;
}
function testPointer2() public pure returns(Struct memory point){
Struct memory x = Struct({a: 1, b: 2});
Struct memory y = Struct({a: x.a, b: x.b});
y.b = 3;
return x;
}
}
Teste:
// No primeiro caso, y é apenas um ponteiro para x
// se você alterar a variável y - a variável x vai ser alterada também
const data1 = await testData.testPointer1();
await expect(data1.b).to.be.equal(3);
// no segundo caso y não é um ponteiro para x, é uma nova variável independente
// se você alterar a variável y - a variável x não vai ser alterada
const data2 = await testData.testPointer2();
await expect(data2.b).to.be.equal(2);
Portanto, se não houver pontos em uma semana, o loop for gravará incorretamente o ponto de verificação em points_history
com um número de bloco -> vários registros de data e hora.
Como isso afetará a distribuição de recompensas:
Pessoas com bloqueios expirados não podem reivindicar suas recompensas, se houver alguma. Elas vão desaparecer. Se uma pessoa com bloqueio expirado reivindicar suas recompensas e retirar seus fundos, todos os outros usuários com NFTs ativos perderão uma quantidade significativa de recompensas pendentes (até 50%). Portanto, a distribuição da recompensa funcionará incorretamente e seu poder de votação estará errado.
Etapas de mitigação:
Reimplantar o contrato com:
// Copia todos os dados do último ponto
Point memory initial_last_point = Point({
bias: last_point.bias,
slope: last_point.slope,
ts: last_point.ts, blk:
last_point.blk
});
O contrato corrigido com logs de console pode ser encontrado aqui.
A outra solução é criar um back-end que chamará a função de ponto de verificação todos os dias, para que não haja chance de cair em um loop for e quebrar recompensas. Multichain usou essa variante e agora eles têm back-end que chama a função de ponto de verificação.
Se você executar o teste localizado no meu repositório github “As recompensas após o saque desaparecem para bloqueios expirados”, você pode ver que ele grava vários pontos de verificação com o mesmo número de bloco e registro de data/hora diferente:
2. O fornecimento de tokens não diminui com a função merge.
O usuário pode fundir dois NFTs em um só, o problema está nessa função.
_burn queima o nft, mas não diminui a quantidade de suprimento, então é possível aumentar o suprimento para uma quantidade de fusão para sempre. A quantidade em oferta não é usada para distribuição de recompensas, então identifico esse bug como médio.
contract Merge {
function merge(uint _from, uint _to) external {
require(attachments[_from] == 0 && !voted[_from], "attached");
require(_from != _to);
require(_isApprovedOrOwner(msg.sender, _from));
require(_isApprovedOrOwner(msg.sender, _to));
LockedBalance memory _locked0 = locked[_from];
LockedBalance memory _locked1 = locked[_to];
uint value0 = uint(int256(_locked0.amount));
uint end = _locked0.end >= _locked1.end ? _locked0.end : _locked1.end;
locked[_from] = LockedBalance(0, 0);
_checkpoint(_from, _locked0, LockedBalance(0, 0));
_burn(_from);
_deposit_for(_to, value0, end, _locked1, DepositType.MERGE_TYPE);
}
}
Prova de conceito:
Aqui está o teste que registra a quantidade em oferta antes e depois da fusão.
it.only('Merge nft bug', async () => {
await veNFT.create_lock(Web3.utils.toWei('1000', 'ether'), 604800 * 2);
await veNFT.create_lock(Web3.utils.toWei('1000', 'ether'), 604800 * 2);
await veNFT.create_lock(Web3.utils.toWei('1000', 'ether'), 604800 * 2);
await veNFT.create_lock(Web3.utils.toWei('1000', 'ether'), 604800 * 2);
console.log('before merge: ', (await veNFT.supply()).toString());
await veNFT.merge(2, 3);
console.log('right after merge: ', (await veNFT.supply()).toString());
console.log(
'amount of tokens on the contract: ',
(await token.balanceOf(veNFT.address)).toString()
);
});
Como você pode ver, a quantidade em oferta aumentou e não é igual à quantidade de tokens.
Em conclusão, quero dizer que não importam quantas auditorias de segurança você tenha, você deve garantir seu código com testes de unidade adequadamente. Especialmente se este contrato bloquear uma grande quantidade de tokens.
Este artigo foi escrito por Vladislav Yaroshuk e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui.
Latest comments (0)