WEB3DEV

Cover image for EIP-4626 Ataque profundo de inflação/ Sandwich e como resolver
Dimitris Carvalho Calixto
Dimitris Carvalho Calixto

Posted on

EIP-4626 Ataque profundo de inflação/ Sandwich e como resolver

TL;DR EIP-4626 cofres tokenizados são suscetíveis a ataques de inflação se a taxa de câmbio dos ativos depositados e das ações ERC20 cunhadas não for tratada graciosamente. Neste artigo, nós dissecamos o ataque e discutimos as soluções

Esboço


0. Introdução

1. Dissecando o ataque de inflação

2. Soluções

3. Sumário

Enter fullscreen mode Exit fullscreen mode

0.Introdução

A emissão de um token ERC20 para representar as ações subjacentes dos fundos depositados pelos usuários em um cofre tem sido uma prática comum no espaço DeFi, e EIP-4626 é a proposta de padronização de cofres deste gênero.

Entretanto, há uma discussão em torno do risco da proposta: clique . Em resumo, um invasor com fundos suficientes pode fazer um ataque sandwich aos depositantes por meio de um :

  1. o invasor deposita no cofre para cunhar ações
  2. o invasor também transfere os tokens ERC20 diretamente para o cofre
  3. os depósitos dos depositantes espiões

desde que o total de ativos do invasor ultrapasse o valor do depósito dos depositantes, os depositantes receberão 0 ações para a taxa de câmbio entre os ativos depositados e a representação ERC20 do cofre é enganada.

Como os depositantes recebem 0 ações, o invasor pode retirar todos os ativos do cofre, pois o invasor é o único com ações desde o primeiro passo.

Na minha opnião, é brilhante inventar um ataque original, prestando atenção às nuances do desenho inteligente do contrato e um grande grito ao autor do post!

Assim, este artigo não pretende tirar o crédito do autor, mas sim servir como outra fonte de compreensão do ataque

Vamos mergulhar no ataque com a seleção do editor de melodic death metal: Eluveitie - Inis Mona

1. Dissecação do Ataque de Inflação

Os passos do ataque já estão demonstrados acima, mas vamos fazer a abordagem matemática aqui para dissecar o ataque.

Em primeiro lugar, dê uma olhada no cálculo entre os bens depositados e a representação ERC20 do cofre:


// modified from https://ethereum-magicians.org/t/address-eip-4626-inflation-attacks-with-virtual-shares-and-assets/12677

function _convertToShares(uint256 assets) returns (uint256 shares) {

    uint256 totalSupply = totalSupply();

    return supply == 0

        ? assets.mulDiv(10**decimals(), 10**_asset.decimals())
        : assets.mulDiv(totalSupply, totalAssets());

}

Enter fullscreen mode Exit fullscreen mode
  • quando é o primeiro depósito, o totalSupplyl() de ERC20 do cofre é 0 e nós simplesmente escalamos para cima ou para baixo o número de ativos até casas decimais de ERC20 do cofre com mulDiv()
  • caso contrário, dividindo primeiro o valor dos ativos de entrada astotalAssets(), conhecemos a porção de ativos em comparação com o total de ativos existentes no cofre; depois, multiplicando ativos/totalAssets() pelo totalSupply, obtemos uma quantidade justa de ações com base na porção de ativos de entrada na função
  • usar mulDiv() é garantir que não haja perda de precisão multiplicando primeiro e depois dividindo

Em seguida, precisaremos assumir alguns números de modo que o ataque possa funcionar: digamos que os bens depositados no cofre são WETH, cujas casas decimais são 18. O ERC20 como as ações do cofre também tem 18 decimais, por exemplo, um ETH-Vault cujas casas decimais são 18.

Primeiro depósito

Agora, há um primeiro depositante que quer depositar 1 (1 * 1e18 wei) WETH e o tx é espiado pelo invasor. Aqui está a interrupção:


                   |   totalSupply()   |   totalAssets()

---------------------------------------------------------

original state     |         0         |         0

---------------------------------------------------------

(depois)  Passo 1  |         1         |         1

---------------------------------------------------------

(depois)  Passo 2  |         1         |      1e18 + 1

---------------------------------------------------------

(depois)  Passo 3  |         1         |    2 * 1e18 + 1

Enter fullscreen mode Exit fullscreen mode
  1. O invasor faz o front-runner do depósito e deposita 1 wei WETH e recebe 1 ação: já que o totalSupply é 0, ações = 1 * 10***18 / 10***18 = 1
  2. O invasor também transfere 1 * 1e18 wei WETH, fazendo com que o saldo de totalAssets() do WETH no cofre se torne 1e18 + 1 wei
  3. os depósitos 1e18 wei WETH do depositante espião. Entretanto, o depositante recebe 0 ações: 1e18 * 1 (totalSupply) / (1e18 + 1) = 1e18 / (1e18 + 1) = 0. Como o depositante recebe 0 ações, totalSupply() permanece em 1
  4. o invasor ainda tem 1 ação já cunhada e assim a retirada dessa 1 ação retira tudo no cofre, incluindo o 1e18 wei WETH do depositante

É claro que a razão pela qual 1e1 / (1e18 + 1) é arredondado para 0 é que a divisão é arredondada, mas também vale a pena notar que as divisões em Solidity são sempre arredondadas para zero, não arredondadas para baixo.

Depósito Aleatório

O exemplo anterior demonstra o cenário de um invasor sandwich que ataca o primeiro depositante. O ataque, entretanto, pode ocorrer sempre que os recursos do invasor são abundantes:


                |   totalSupply()   |   totalAssets()

--------------------------------------------------------------

 estado original  |     1e17        |       1e17

--------------------------------------------------------------

(depois)  Passo 1  |    1e17 + 1    |       1e17 + 1

--------------------------------------------------------------

(depois)  Passo 2  |    1e17 + 1    |   1e36 + 1e17 + 1

--------------------------------------------------------------

(depois)  Passo 3  |    1e17 + 1    | 1e36 + 1e18 + 1e17 + 1

Enter fullscreen mode Exit fullscreen mode
  1. originalmente, havia .1 WETH (1e17 wei) no cofre e 1e17 wei ações de ERC20 foram cunhadas
  2. o invasor vê um depositante querendo depositar 1 (1 * 1e18 wei) WETH para o cofre
  3. assim, o invasor novamente cunha 1 wei ERC20 com 1 wei WETH; no entanto, desta vez o invasor tem que enviar um 1e36 wei WETH extra para o cofre
  4. embora depositando 1 (1 * 1e18 wei) WETH, o depositante ainda acaba com 0 ações neste cenário:ações = 1e18 * (1e17 + 1) / (1e36 + 1e17 + 1) = (1e35 + 1e18) / (1e36 + 1e17 + 1) = 0

Realisticamente, é improvável que um invasor queira arriscar fundos de 10¹⁸ mais vezes do que o lucro potencial.

Com este exemplo, sabemos que mesmo que um cofre seja sempre suscetível a um ataque, a probabilidade de ele diminuir devido ao acúmulo de fundos no cofre.

Taxa de Câmbio

Como já caminhamos pelos passos de diferentes cenários de ataque, vamos introduzir o último termo no ataque: taxa de câmbio.

A taxa de câmbio entre ações e ativos depositados descreve quantas unidades de ações se pode obter depositando determinadas quantidades de ativos.


                |   totalSupply()   |   totalAssets()

---------------------------------------------------------

(depois)  Passo 1  |         1         |         1

---------------------------------------------------------

(depois)  Passo 2  |         1         |      1e18 + 1

---------------------------------------------------------

(depois)  Passo 3  |         1         |    2 * 1e18 + 1

Enter fullscreen mode Exit fullscreen mode

Em nosso primeiro cenário, as taxas de câmbio em cada etapa são

  • Passo 1: 1 ação por ativo (WETH)
  • Passo 2: 1 ação por 1e18 + 1 WETH, o que significa que a taxa de câmbio inflaciona com a transferência extra do invasor
  • Passo 3: como a taxa de câmbio é 1 ação por 1e18 + 1 WETH no Passo 2, o depositante que deposita 1e18 wei WETH é ainda menos de 1 ação e, portanto, o depositante recebe 0 ações. E a taxa de câmbio inflaciona novamente para 1 ação por 2 * 1e18 + 1 WETH

Com estas explicações, podemos ver porque o ataque é chamado de Ataque de Inflação: inflar a taxa de câmbio entre ações e ativos.]

Um resumo rápido

Antes de entrar na próxima seção, vamos resumir brevemente a causa raiz do ataque: a taxa de câmbio é manipulada por transferências extras que não passam pelo deposit() regular do cofre.

Se olharmos novamente para a função _convertToShares(), podemos traduzir a manipulação da taxa de câmbio como o dividendo, que é o depósito do depositante, é feito menor do que o divisor manipulado pelo invasor.


// modificado de https://ethereum-magicians.org/t/address-eip-4626-inflation-attacks-with-virtual-shares-and-assets/12677

function _convertToShares(uint256 assets) returns (uint256 shares) {

    uint256 totalSupply = totalSupply();

    return supply == 0

        ? assets.mulDiv(10**decimals(), 10**_asset.decimals())
        : assets.mulDiv(totalSupply, totalAssets());

}

Enter fullscreen mode Exit fullscreen mode

Portanto, é possível simplesmente tornar o dividendo maior do que o divisor?

2. Soluções

No post original, duas soluções parecem elegíveis e práticas: adicionar casas decimais extras e manter a contabilidade interna.

Manter um balanço interno é bastante simples: as transferências diretas do ERC20 para o cofre não serão contadas como parte do totalAssets() e podem parar radicalmente o ataque.

Esta abordagem, entretanto, levanta várias questões do autor, tais como "O que fazer com os fundos extras"? "Os cofres devem ser capazes de receber doações", etc.

Assim, vou me aprofundar apenas sobre a abordagem de adição de decimais extras.

Decimais extras

Esta solução já é sugerida na questão da última seção:

é possível simplesmente tornar o dividendo maior do que o divisor?

E a resposta é Sim! Ao** dar casas decimais extras às ações ERC20 para torná-las maiores do que as dos ativos depositados**, podemos intencionalmente tornar o dividendo maior do que o divisor.

A razão pela qual as casas decimais não foram levantadas antes é que assumimos que as casas decimais das ações fossem 18, o mesmo que WETH. Se observarmos a fórmula assets.mulDiv(10***decimals(), 10**_asset.decimals()), podemos ver que quando os decimals() e _asset.decimals() são os mesmos, eles se cancelam um ao outro.

No entanto, se dizemos que as casas decimais das ações são agora 36 (o que está somando 18 casas decimais extras às 18 originais):

assets.mulDiv(10**36, 10**18) = assets.mul(10**18)

as ações de retorno são então aumentadas em 10**18 e vamos ver como isso pode afetar o ataque no primeiro depósito.


                       |   totalSupply()    |   totalAssets()   

---------------------------------------------------------

(depois)  Passo 1  |    1e18        |       1

---------------------------------------------------------

(depois)  Passo 2  |    1e18        |   1e18 + 1

---------------------------------------------------------

(depois)  Passo 3  |    ~2e18       |   2 * 1e18 + 1

Enter fullscreen mode Exit fullscreen mode
  1. O invasor deposita 1 wei WETH e recebe 1e18 ações: 1 * 10**36 / 10**18 = 10**18 = 1e18. Podemos ver que a taxa de câmbio desta vez não é mais 1 ação por WETH, mas 1e18 ações por WETH
  2. O invasor transfere 1e18 wei WETH e totalAssets() ainda se torna (1e18 + 1) wei. A taxa de câmbio é agora 1 ação por (1e18 + 1) / 1e18 WETH
  3. O depositário espião deposita 1e18 wei WETH. Desta vez, o depositante recebe ações **1e18 wei: `1e18 * 1e18 / (1e18 + 1) = **1e36 / (1e18 + 1) = 999.999.999.999.999.999.999 wei`; vamos arredondar para 1e18 para facilitar a demonstração
  4. Como o depositante não recebe mais 0, mas ações 1e18 em vez disso, o invasor que resgatar ações 1e18 não resultará na retirada de todos os ativos no cofre

A razão pela qual o depositante recebe ~1e18 ações desta vez no Passo 3 é porque, com as casas decimais extras, a taxa de câmbio se torna mais precisa: ao invés da quantidade original de 1 ação por 1e18 + 1 WETH que arredonda para baixo qualquer quantidade inferior a **1e18 + 1**, 1 ação por (1e18 + 1) / 1e18 WETH significa que apenas a quantidade WETH inferior a **(1e18 + 1) / 1e18 ~ 1** será arredondada para 0.

Podemos ver que simplesmente aumentando as casas decimais do token das ações, o ataque é invalidado, de forma efetiva - o ataque ainda pode acontecer enquanto o custo aumenta.

Neste caso, o atacante só pode executar o ataque transferindo 1e36 wei WETH para o cofre de modo que as ações do depósito 1e18 wei WETH do depositante tornam-se 0 novamente: ações = 1e18 * 1e18 / (1e37 + 1e18) = 1e36 / (1e36 + 1e18) = 0


                       |   totalSupply()    |   totalAssets()

---------------------------------------------------------

(depois)  Passo 1  |    1e18        |       1

---------------------------------------------------------

(depois)  Passo 2  |    1e18        |   1e36 + 1e18

---------------------------------------------------------

(depois)  Passo 3  |    1e18        |  1e36 + 2 * 1e18

Enter fullscreen mode Exit fullscreen mode

Quanto ao cenário de depósito aleatório, agora é necessário um invasor **1e54** para realizar o ataque: ações = 1e18 * (1e35 + 1e18) / (1e54 + 1e17 + 1) = (1e53 + 1e36) / (1e54 + 1e17 + 1) = 0.

E a conclusão ainda é: possível, mas improvável.


                      |   totalSupply()     |   totalAssets()

--------------------------------------------------------------

 estado original     |      1e35        |       1e17

--------------------------------------------------------------

(depois)  Passo 1  |    1e35 + 1e18 |       1e17 + 1

--------------------------------------------------------------

(depois)  Passo 2  |    1e35 + 1e18 |   1e54 + 1e17 + 1

--------------------------------------------------------------

(depois)  Passo 3  |    1e35 + 1e18 | 1e54 + 1e18 + 1e17 + 1

Enter fullscreen mode Exit fullscreen mode

Voilá! É tudo o que temos que fazer para esta solução. E quanto aos seus contras?

Contras

Primeiro, os decimais extras adicionados para a prevenção do ataque podem ser menos intuitivos quando os usuários inspecionam os estados contratuais no Etherscan.

Em segundo lugar, haverá algum trabalho extra para os engenheiros do front-end para lidar com os decimais extras.

Mas estes dois são os únicos em que posso pensar, e também não são nada sério em comparação com o benefício.

Comentário Adicional

Alguns podem se perguntar se é possível reverter de alguma forma direta as transferências de tokens ERC20 para o cofre.

A resposta é NÃO porque o ERC20 não tem uma função tokensReceived() como ERC-777: a função thetokensReceived() implementada pelo chamador de uma transferência será sempre evocada e assim podemos inserir nossa lógica desejada (reversão) em tokensReceived() se o bem depositado no cofre for um token ERC-777.

Além disso, se estamos falando de Éter ao invés de WETH como os bens depositados no cofre, então podemos utilizar a função de receive() especializada para Ether e optar por sempre reverter as transferências nele.

3. Sumário

A causa principal do ataque é que a taxa de câmbio é manipulada para ser mais cara do que o valor do depósito do depositante pode pagar.

Embora o ataque aconteça mais facilmente no primeiro depósito, ainda é teoricamente possível, mas gradualmente se torna impraticável à medida que o cofre acumula fundos suficientes para lutar contra ele.

Ao aumentar as casas decimais da representação do ERC20 das ações do cofre, o custo do ataque aumenta de acordo, e assim podemos prevenir o ataque efetivamente com o mínimo de efeitos colaterais!

Por último, deixe qualquer comentário abaixo se você quiser discutir ou encontrar algum erro! Até o próximo 😊

Artigo escrito por Shao . A versão original pode ser encontrada aqui. Traduzido e adaptado por Dimitris Calixto.

Oldest comments (0)