-
Notifications
You must be signed in to change notification settings - Fork 193
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
Changes from 10 commits
9d59336
a0afa5a
476ffbc
eb72b31
f356315
43ae3e1
80d5353
3ad846b
e4b932d
b2c863d
2235d9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,7 +34,8 @@ func (s *Suite) TestERC20Calls() { | |
|
||
s.T().Log("Transfer - Not enough funds") | ||
{ | ||
_, err := deps.EvmKeeper.ERC20().Transfer(contract, deps.Sender.EthAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(9_420), deps.Ctx) | ||
amt := big.NewInt(9_420) | ||
_, err := deps.EvmKeeper.ERC20().Transfer(contract, deps.Sender.EthAddr, evm.EVM_MODULE_ADDRESS, amt, deps.Ctx) | ||
s.ErrorContains(err, "ERC20: transfer amount exceeds balance") | ||
// balances unchanged | ||
evmtest.AssertERC20BalanceEqual(s.T(), deps, contract, deps.Sender.EthAddr, big.NewInt(0)) | ||
|
@@ -43,10 +44,16 @@ func (s *Suite) TestERC20Calls() { | |
|
||
s.T().Log("Transfer - Success (sanity check)") | ||
{ | ||
_, err := deps.EvmKeeper.ERC20().Transfer(contract, evm.EVM_MODULE_ADDRESS, deps.Sender.EthAddr, big.NewInt(9_420), deps.Ctx) | ||
amt := big.NewInt(9_420) | ||
sentAmt, err := deps.EvmKeeper.ERC20().Transfer( | ||
contract, evm.EVM_MODULE_ADDRESS, deps.Sender.EthAddr, amt, deps.Ctx, | ||
) | ||
s.Require().NoError(err) | ||
evmtest.AssertERC20BalanceEqual(s.T(), deps, contract, deps.Sender.EthAddr, big.NewInt(9_420)) | ||
evmtest.AssertERC20BalanceEqual(s.T(), deps, contract, evm.EVM_MODULE_ADDRESS, big.NewInt(60_000)) | ||
evmtest.AssertERC20BalanceEqual( | ||
s.T(), deps, contract, deps.Sender.EthAddr, big.NewInt(9_420)) | ||
evmtest.AssertERC20BalanceEqual( | ||
s.T(), deps, contract, evm.EVM_MODULE_ADDRESS, big.NewInt(60_000)) | ||
s.Require().Equal(sentAmt.String(), amt.String()) | ||
Comment on lines
+47
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enhance test coverage for non-standard ERC20 tokens While this test verifies basic transfer functionality, it doesn't cover the specific cases mentioned in issue #2080 and the PR objectives.
s.T().Log("Transfer - Handle false return value")
{
amt := big.NewInt(1000)
sentAmt, err := deps.EvmKeeper.ERC20().Transfer(
falseReturningToken, // Mock token that returns false
evm.EVM_MODULE_ADDRESS,
deps.Sender.EthAddr,
amt,
deps.Ctx,
)
s.Require().Error(err) // Should handle false return as error
s.Require().Nil(sentAmt)
}
s.T().Log("Transfer - Handle missing return value")
{
amt := big.NewInt(1000)
sentAmt, err := deps.EvmKeeper.ERC20().Transfer(
noReturnToken, // Mock token without return value
evm.EVM_MODULE_ADDRESS,
deps.Sender.EthAddr,
amt,
deps.Ctx,
)
s.Require().NoError(err)
s.Require().Equal(sentAmt.String(), amt.String())
}
-evmtest.AssertERC20BalanceEqual(
- s.T(), deps, contract, deps.Sender.EthAddr, big.NewInt(9_420))
-evmtest.AssertERC20BalanceEqual(
- s.T(), deps, contract, evm.EVM_MODULE_ADDRESS, big.NewInt(60_000))
+// Initial balance (0) + transferred amount (9_420) = 9_420
+evmtest.AssertERC20BalanceEqual(
+ s.T(), deps, contract, deps.Sender.EthAddr, amt)
+// Initial balance (69_420) - transferred amount (9_420) = 60_000
+evmtest.AssertERC20BalanceEqual(
+ s.T(), deps, contract, evm.EVM_MODULE_ADDRESS,
+ new(big.Int).Sub(big.NewInt(69_420), amt)) |
||
} | ||
|
||
s.T().Log("Burn tokens - Allowed as non-owner") | ||
|
Unique-Divine marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -491,30 +491,31 @@ func (k *Keeper) ConvertCoinToEvm( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fungibleTokenMapping := funTokens[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if fungibleTokenMapping.IsMadeFromCoin { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return k.convertCoinNativeCoin(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return k.convertCoinToEvmBornCoin(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return k.convertCoinNativeERC20(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return k.convertCoinToEvmBornERC20(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Converts a native coin to an ERC20 token. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// EVM module owns the ERC-20 contract and can mint the ERC-20 tokens. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
func (k Keeper) convertCoinNativeCoin( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Converts Bank Coins for FunToken mapping that was born from a coin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// (IsMadeFromCoin=true) into the ERC20 tokens. EVM module owns the ERC-20 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// contract and can mint the ERC-20 tokens. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
func (k Keeper) convertCoinToEvmBornCoin( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ctx sdk.Context, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
sender sdk.AccAddress, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
recipient gethcommon.Address, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
coin sdk.Coin, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
funTokenMapping evm.FunToken, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) (*evm.MsgConvertCoinToEvmResponse, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Step 1: Escrow bank coins with EVM module account | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Step 1: Send Bank Coins to the EVM module | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, evm.ModuleName, sdk.NewCoins(coin)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return nil, errors.Wrap(err, "failed to send coins to module account") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
erc20Addr := funTokenMapping.Erc20Addr.Address | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Step 2: mint ERC-20 tokens for recipient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Step 2: Mint ERC20 tokens to the recipient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
evmResp, err := k.CallContract( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ctx, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
embeds.SmartContract_ERC20Minter.ABI, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -542,10 +543,11 @@ func (k Keeper) convertCoinNativeCoin( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return &evm.MsgConvertCoinToEvmResponse{}, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Converts a coin that was originally an ERC20 token, and that was converted to a bank coin, back to an ERC20 token. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// EVM module does not own the ERC-20 contract and cannot mint the ERC-20 tokens. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// EVM module has escrowed tokens in the first conversion from ERC-20 to bank coin. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
func (k Keeper) convertCoinNativeERC20( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Converts a coin that was originally an ERC20 token, and that was converted to | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// a bank coin, back to an ERC20 token. EVM module does not own the ERC-20 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// contract and cannot mint the ERC-20 tokens. EVM module has escrowed tokens in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// the first conversion from ERC-20 to bank coin. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
func (k Keeper) convertCoinToEvmBornERC20( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ctx sdk.Context, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
sender sdk.AccAddress, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
recipient gethcommon.Address, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -554,14 +556,6 @@ func (k Keeper) convertCoinNativeERC20( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) (*evm.MsgConvertCoinToEvmResponse, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
erc20Addr := funTokenMapping.Erc20Addr.Address | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
recipientBalanceBefore, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return nil, errors.Wrap(err, "failed to retrieve balance") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if recipientBalanceBefore == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return nil, fmt.Errorf("failed to retrieve balance, balance is nil") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Escrow Coins on module account | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if err := k.bankKeeper.SendCoinsFromAccountToModule( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ctx, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -583,52 +577,36 @@ func (k Keeper) convertCoinNativeERC20( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Supply ERC-20 tokens from EVM module address | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
actualSentAmount, err := k.ERC20().Transfer( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
erc20Addr, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
evm.EVM_MODULE_ADDRESS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
recipient, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
coin.Amount.BigInt(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ctx, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return nil, errors.Wrap(err, "failed to transfer ERC20 tokens") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if !res { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return nil, fmt.Errorf("failed to transfer ERC20 tokens") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Check expected Receiver balance after transfer execution | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
recipientBalanceAfter, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return nil, errors.Wrap(err, "failed to retrieve balance") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if recipientBalanceAfter == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return nil, fmt.Errorf("failed to retrieve balance, balance is nil") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return nil, errors.Wrap(err, "failed to transfer ERC-20 tokens") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(coin)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
burnCoin := sdk.NewCoin(coin.Denom, sdk.NewIntFromBigInt(actualSentAmount)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(burnCoin)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+596
to
+607
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verify actualSentAmount before burning coins. The code should verify that actualSentAmount is positive before creating and burning coins to handle potential issues with fee-on-transfer tokens. Apply this diff to add the verification: +if actualSentAmount.Sign() <= 0 {
+ return nil, fmt.Errorf("invalid actual sent amount: %s", actualSentAmount)
+}
+
burnCoin := sdk.NewCoin(coin.Denom, sdk.NewIntFromBigInt(actualSentAmount))
err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(burnCoin))
if err != nil {
return nil, errors.Wrap(err, "failed to burn coins")
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return &evm.MsgConvertCoinToEvmResponse{}, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add test cases for fee-on-transfer tokens
While this test covers the basic insufficient funds scenario, we should add test cases for fee-on-transfer tokens where the received amount differs from the sent amount.
Consider adding these test scenarios: