Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(evm): improve safety of ERC20 transfers, accounting for boolean success return values and recipient balance changes that don't match the ERC20 transfer amount. #2090

Merged
merged 11 commits into from
Oct 30, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ reflected on all occurences of "base fee" in the codebase. This fixes [#2059](ht
and the [related comments from @Unique-Divine and @berndartmueller](https://github.com/NibiruChain/nibiru/issues/2059#issuecomment-2408625724).
- [#2084](https://github.com/NibiruChain/nibiru/pull/2084) - feat(evm-forge): foundry support and template for Nibiru EVM develoment
- [#2088](https://github.com/NibiruChain/nibiru/pull/2088) - refactor(evm): remove outdated comment and improper error message text
- [#2090](https://github.com/NibiruChain/nibiru/pull/2090) - fix(evm): transfer for native to erc20 for specific tokens


#### Dapp modules: perp, spot, oracle, etc
Expand Down
42 changes: 20 additions & 22 deletions x/evm/keeper/msg_server.go
Unique-Divine marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -592,10 +592,10 @@

recipientBalanceBefore, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve balance")
return nil, errors.Wrap(err, "failed to retrieve recipient balance")

Check warning on line 595 in x/evm/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

x/evm/keeper/msg_server.go#L595

Added line #L595 was not covered by tests
}
if recipientBalanceBefore == nil {
return nil, fmt.Errorf("failed to retrieve balance, balance is nil")
return nil, fmt.Errorf("failed to retrieve recipient balance, balance is nil")

Check warning on line 598 in x/evm/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

x/evm/keeper/msg_server.go#L598

Added line #L598 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we only intend for ERC20 transfers to properly send an amount that alters the recipient balance, let's put the balanceOf recipient checks before and after to inside of ERC20().Transfer. That'll make using it more safe.

}

// Escrow Coins on module account
Expand All @@ -619,52 +619,50 @@
return nil, errors.Wrap(err, "failed to retrieve balance")
}
if evmModuleBalance == nil {
return nil, fmt.Errorf("failed to retrieve balance, balance is nil")
return nil, fmt.Errorf("failed to retrieve EVM module account balance, balance is nil")

Check warning on line 622 in x/evm/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

x/evm/keeper/msg_server.go#L622

Added line #L622 was not covered by tests
}
if evmModuleBalance.Cmp(coin.Amount.BigInt()) < 0 {
return nil, fmt.Errorf("insufficient balance in EVM module account")
}

// unescrow ERC-20 tokens from EVM module address
res, err := k.ERC20().Transfer(
erc20Addr,
evm.EVM_MODULE_ADDRESS,
recipient,
coin.Amount.BigInt(),
ctx,
)
input, err := k.ERC20().ABI.Pack("transfer", recipient, coin.Amount.BigInt())
if err != nil {
return nil, errors.Wrap(err, "failed to transfer ERC20 tokens")
return nil, errors.Wrap(err, "failed to pack ABI args")

Check warning on line 631 in x/evm/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

x/evm/keeper/msg_server.go#L631

Added line #L631 was not covered by tests
}
if !res {
return nil, fmt.Errorf("failed to transfer ERC20 tokens")
_, err = k.ERC20().CallContractWithInput(ctx, evm.EVM_MODULE_ADDRESS, &erc20Addr, true, input)
if err != nil {
return nil, errors.Wrap(err, "failed to transfer ERC20 tokens")

Check warning on line 635 in x/evm/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

x/evm/keeper/msg_server.go#L635

Added line #L635 was not covered by tests
Unique-Divine marked this conversation as resolved.
Show resolved Hide resolved
}

// Check expected Receiver balance after transfer execution
// For fee-on-transfer tokens, the actual amount received may be less than the amount transferred.
// Retrieve the recipient's balance after the transfer to calculate the actual received amount.
recipientBalanceAfter, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve balance")
return nil, errors.Wrap(err, "failed to retrieve recipient balance after transfer")

Check warning on line 642 in x/evm/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

x/evm/keeper/msg_server.go#L642

Added line #L642 was not covered by tests
}
if recipientBalanceAfter == nil {
return nil, fmt.Errorf("failed to retrieve balance, balance is nil")
return nil, fmt.Errorf("failed to retrieve recipient balance after transfer, balance is nil")

Check warning on line 645 in x/evm/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

x/evm/keeper/msg_server.go#L645

Added line #L645 was not covered by tests
Unique-Divine marked this conversation as resolved.
Show resolved Hide resolved
}

expectedFinalBalance := big.NewInt(0).Add(recipientBalanceBefore, coin.Amount.BigInt())
if r := recipientBalanceAfter.Cmp(expectedFinalBalance); r != 0 {
return nil, fmt.Errorf("expected balance after transfer to be %s, got %s", expectedFinalBalance, recipientBalanceAfter)
// Burn escrowed Coins based on the actual amount received by the recipient
actualReceivedAmount := big.NewInt(0).Sub(recipientBalanceAfter, recipientBalanceBefore)
if actualReceivedAmount.Sign() <= 0 {
return nil, fmt.Errorf("no ERC20 tokens were received by the recipient")

Check warning on line 651 in x/evm/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

x/evm/keeper/msg_server.go#L651

Added line #L651 was not covered by tests
}

// Burn escrowed Coins
err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(coin))
burnCoin := sdk.NewCoin(coin.Denom, sdk.NewIntFromBigInt(actualReceivedAmount))
err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(burnCoin))
Unique-Divine marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, errors.Wrap(err, "failed to burn coins")
}

// Emit event with the actual amount received
_ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{
Sender: sender.String(),
Erc20ContractAddress: funTokenMapping.Erc20Addr.String(),
ToEthAddr: recipient.String(),
BankCoin: coin,
BankCoin: burnCoin,
})

return &evm.MsgConvertCoinToEvmResponse{}, nil
Expand Down
Loading