Em julho de 2023, durante minha participação no Concurso de Auditoria C4 da Tapioca DAO, rapidamente percebi que o protocolo estava terrivelmente inseguro, resultando em 60 vulnerabilidades de alta gravidade e 96 de média gravidade. Essa situação assustadora foi principalmente atribuída a uma falha procedural evidente - uma profunda falta de testes no processo de desenvolvimento.
Este artigo tem como objetivo navegar por algumas das minhas descobertas notáveis da auditoria. Mais importante ainda, ele demonstrará como um framework de testes robusto poderia ter prevenido essas vulnerabilidades, protegendo o protocolo. Ao analisar essas questões através da lente de testes minuciosos, buscamos destacar o valor indispensável das práticas de teste no âmbito do desenvolvimento de contratos inteligentes.
Foto minha auditando o protocolo
Precisão Negada
Esta vulnerabilidade está focada no contrato CompoundStrategy
, responsável por gerenciar os investimentos dos usuários no Compound. O contrato processa depósitos convertendo ETH em cETH do Compound e lida com saques calculando a quantidade de cETH a ser resgatado com base no retorno de ETH esperado, usando a taxa de câmbio internamente armazenada.
uint256 pricePerShare = cToken.exchangeRateStored();
uint256 toWithdraw = (((amount - queued) * (10 ** 18)) / pricePerShare);
cToken.redeem(toWithdraw);
No entanto, essa implementação carecia de testes abrangentes, especialmente em relação ao alinhamento da lógica de taxa de câmbio com os mecanismos próprios do Compound.
Dissecando a Discrepância na Taxa de Câmbio
O processo de resgate do Compound começa buscando a taxa de câmbio armazenada, semelhante ao contrato CompoundStrategy
. A diferença está no cálculo do redeemAmount
.
// Lógica de cálculo da taxa de câmbio do Compound
(vars.mathErr, vars.exchangeRateMantissa) = exchangeRateStoredInternal();
(vars.mathErr, vars.redeemAmount) = mulScalarTruncate(Exp({mantissa: vars.exchangeRateMantissa}), redeemTokensIn);
Para verificar se a função mulScalarTruncate
do Compound está alinhada com a lógica de divisão da estratégia, testamos os cálculos usando o Remix, consulte o Gist.
// Lógica de cálculo da taxa de câmbio da Tapioca
function getToWithdraw(uint256 amount, uint256 exchangeRate) pure external returns (uint) {
return amount * (10 ** 18) / exchangeRate;
}
Após obter a taxa de câmbio do Compound e testá-la em relação à lógica de saques da estratégia, descobrimos uma divergência crítica. O cálculo de saque da estratégia subestimou a quantidade de cETH necessária para resgatar o ETH esperado, resultando em uma falta na quantidade esperada de ETH.
A Questão Subjacente e Suas Implicações
O problema central reside na abordagem inconsistente ao calcular a taxa de câmbio. O método de divisão direta do contrato CompoundStrategy
falhou em replicar a operação mais intrincada mulScalarTruncate
do Compound, levando a uma estimativa incorreta da quantidade necessária de cETH para saques.
Consequentemente, o contrato da estratégia frequentemente não conseguia atender à quantidade de ETH necessária para operações, desencadeando um revert devido a fundos insuficientes.
require(
wrappedNative.balanceOf(address(this)) >= amount,
"CompoundStrategy: not enough"
);
Mitigação e Prevenção
Para alinhar-se com os cálculos precisos do Compound, a estratégia deve utilizar a função CEther.redeemUnderlying
do Compound para cálculos diretos e precisos da quantidade de cETH necessária.
No entanto, esse erro poderia ter sido facilmente evitado com uma abordagem de desenvolvimento centrada em testes. Se a estratégia tivesse passado por testes de integração com contratos reais do Compound, simulando cenários reais de depósito e retirada, essa disparidade teria sido identificada desde o início.
Recompensas Bloqueadas
Nesta vulnerabilidade, exploramos o contrato StargateStrategy
, que se comunica com a liquidez do Stargate (lpStaking
). O problema central reside na forma como as recompensas do token Stargate são tratadas. Quando os provedores de liquidez (LPs
) depositam ativos na função lpStaking.deposit
, eles ganham recompensas em tokens Stargate. No entanto, essas recompensas só são contabilizadas durante a execução da função compound
, não durante o depósito. Isso leva a uma falha crítica em que as recompensas obtidas durante a execução do _deposited
ficam irremediavelmente bloqueadas no contrato.
Exame da Falha
A lógica do contrato na função lpStaking.deposit
transfere as recompensas não reclamadas para o remetente. No entanto, há uma discrepância na forma como as recompensas são tratadas em diferentes partes do contrato:
// lpStaking.deposit
if (user.amount > 0) {
uint256 pending = user.amount.mul(pool.accStargatePerShare).div(1e12).sub(user.rewardDebt);
safeStargateTransfer(msg.sender, pending);
}
A função compound
inclui corretamente a lógica para contabilizar tokens de recompensa recém-adquiridos de sua chamada para lpStaking.deposit
:
// compound()
…
if (stgBalanceAfter > stgBalanceBefore) {
// Lógica para lidar com tokens Stargate recém-adquiridos
…
}
No entanto, a função _stake
, chamada por _deposited
, não tem uma lógica semelhante para lidar com as recompensas, o que leva ao problema de bloqueio:
// _stake()
lpStaking.deposit(lpStakingPid, toStake);
// Ausência de lógica para lidar com recompensas aqui
Consequentemente, apesar de as recompensas serem geradas durante a execução do _deposited
, elas não são processadas da mesma forma que aquelas geradas durante o compound
. Isso resulta no bloqueio permanente desses tokens dentro do contrato.
Mitigação e Prevenção
Para resolver esse problema, é recomendado que a função compound
troque todo o saldo de tokens de recompensa, em vez de apenas o saldo recém-adquirido. Isso garantiria que as recompensas acumuladas em qualquer contexto sejam gerenciadas adequadamente.
Mais importante ainda, essa vulnerabilidade destaca uma falha significativa no processo de teste. A falta de testes abrangentes, especialmente testes de integração que simulam cenários do mundo real, permitiu que essa falha passasse despercebida. Se houvesse testes abrangentes cobrindo vários casos de uso, incluindo o cenário em que LPs depositam e depois compõem recompensas, é provável que esse problema tivesse sido identificado.
Ainda Mais Recompensas Bloqueadas
Esta vulnerabilidade examina uma falha crítica no contrato AaveStrategy
, relacionada ao tratamento de tokens stkAave. Esses tokens são acumulados como recompensas de atividades de empréstimo, mas devido a uma falha no design do contrato, eles se tornam irremediavelmente bloqueados dentro do contrato.
Detalhes da Falha de Bloqueio de stkAave
O contrato AaveStrategy
está projetado para ganhar tokens stkAave como recompensas para emprestar tokens nativos embrulhados. Essas recompensas são reivindicadas e processadas dentro da função compound
do contrato:
// compound()
// Reivindicando recompensas stkAave
uint256 unclaimedStkAave = incentivesController.getUserUnclaimedRewards(address(this));
if (unclaimedStkAave > 0) {
…
incentivesController.claimRewards(tokens, type(uint256).max, address(this));
}
Após a reivindicação, a estratégia converte os tokens Aave em tokens nativos embrulhados para reinvestimento. No entanto, o contrato não possui nenhuma funcionalidade para lidar diretamente com os tokens stkAave:
// Troca de tokens Aave por tokens nativos embrulhados
…
swapper.swap(swapData, minAmount, address(this), "");
…
lendingPool.deposit(address(wrappedNative), queued, address(this), 0);
Como resultado, os tokens stkAave, uma vez obtidos, permanecem irreversivelmente bloqueados dentro do contrato.
Mitigação e Prevenção
Para mitigar esse problema, é imperativo implementar um mecanismo para lidar com os tokens stkAave, possivelmente através de uma função emergencyWithdraw
. Isso garantiria que todos os aspectos de gerenciamento de ativos dentro do contrato sejam considerados e operacionais.
Mais importante ainda, esse erro destaca uma lacuna significativa nos procedimentos de teste empregados. Simplesmente testar um estado final do ciclo de vida deste contrato teria revelado a incapacidade de retirar todos os tokens.
Conclusão
As percepções obtidas a partir de nossa análise pintam um quadro claro: as vulnerabilidades do protocolo derivam predominantemente de uma deficiência significativa nas práticas de teste abrangente. Essa lacuna se manifestou em mais de 150 vulnerabilidades de gravidade alta e média, um indicador claro de que a metodologia de desenvolvimento atual requer uma revisão completa. A ausência de testes rigorosos comprometeu não apenas a segurança do protocolo, mas também diminuiu a eficácia do processo de auditoria. Auditores, em vez de se aprofundarem em vulnerabilidades complexas e nuances, foram relegados a identificar falhas básicas que testes robustos deveriam ter detectado.
Esse cenário destaca uma verdade fundamental no campo do desenvolvimento de contratos inteligentes: testes abrangentes não são apenas uma etapa no processo; são a espinha dorsal do desenvolvimento seguro e confiável de contratos. Procedimentos robustos de teste fazem mais do que apenas descobrir erros; eles fornecem insights sobre padrões de comportamento, brechas de segurança e eficiência de gás. Esses insights são inestimáveis para os desenvolvedores, permitindo-lhes refinar e otimizar seu código.
Para os desenvolvedores que buscam elevar a qualidade e a segurança de seus contratos inteligentes, concentrar-se em aprimorar seus processos de teste é imperativo. Isso envolve a adoção das melhores práticas no desenvolvimento orientado por testes, aproveitando os testes unitários e de integração, incorporando testes de fuzz e simulando cenários do mundo real para garantir a resiliência do contrato sob diversas condições. Ao investir tempo e recursos no desenvolvimento de conjuntos de testes abrangentes, os desenvolvedores podem reduzir significativamente as vulnerabilidades, simplificar o processo de auditoria e construir uma maior confiança em seus contratos.
Para uma compreensão mais profunda e orientação prática sobre aprimorar os processos de teste no desenvolvimento de contratos inteligentes, um recurso valioso é "Testing Smart Contracts" (Teste de contratos inteligentes). Este guia oferece uma riqueza de informações, desde princípios básicos até estratégias avançadas de teste, equipando os desenvolvedores com o conhecimento para construir contratos inteligentes mais seguros, eficientes e confiáveis.
Artigo escrito por Kaden. Traduzido por Marcelo Panegali
Latest comments (0)