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): transfer for native to erc20 #2090

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

Conversation

matthiasmatt
Copy link
Contributor

@matthiasmatt matthiasmatt commented Oct 22, 2024

Purpose / Abstract

Description:
The current implementation lacks support for certain non-standard ERC20 tokens, specifically those with "Fee on Transfer" mechanisms and those that have missing return values in their transfer functions.

According to the project documentation, these tokens should be properly handled by the system. The issue arises because fee-on-transfer tokens deduct a fee during transfer operations, meaning the recipient receives less than the amount specified. The fee is usually burned or redistributed according to the token's smart contract logic.

The existing code assumes however, that the actual transferred amount is equal to the intended transfer amount, so assertions or checks enforcing this equality fail when interacting with fee-on-transfer tokens, leading to transaction failures or incorrect accounting of token balances.

Similarly, some ERC20 tokens do not return a boolean value upon executing transfer or transferFrom functions, even though the ERC20 standard specifies that these functions should return true on success.

In the x/evm/keeper/erc20.go:73, the Transfer function, the code attempts to unpack the return value of the transfer function. If a token does not return any value, this unpacking operation fails, causing the transaction to revert and making it impossible to interact with such tokens through the current implementation.

There is an existing issue documented in #2063 acknowledging that some tokens do not adhere strictly to the ERC20 standard. While tokens that lack fundamental ERC20 functions (like no transfer function) are rightly considered non-ERC20 and can be excluded, tokens with minor deviations like fee-on-transfer or missing return values are prevalent and widely used.

Recommendation:
We recommend that to improve compatibility and user experience, the system should be updated to handle these "weird" ERC20 token behaviors gracefully. For fee-on-transfer tokens, the transfer logic should be adjusted to not assume that the transfer amount equals the received amount. After performing a transfer, the recipient's balance before and after the transfer should be retrieved to calculate the actual amount received, ensuring that any accounting or state updates reflect the actual amount received.

For tokens with missing return values, function calls should be modified to use low-level calls that do not expect a return value, avoiding unpacking the return value when calling the transfer function. After the transfer call, it's important to verify that the transaction did not revert by checking for errors or ensuring the transaction receipt indicates success.

Additionally, documentation and user guidance should be updated to inform users

Summary by CodeRabbit

  • New Features

    • Enhanced ERC20 token transfer logic now provides the actual amount received after transfers.
    • Introduced debug_traceCall method for improved call tracing.
    • Added validation for FunToken creation fees.
    • Improved handling of gas consumption within ERC20 contract execution.
  • Bug Fixes

    • Fixed ERC20 metadata utilization during FunToken creation.
    • Corrected gas price representation in RefundGas.
    • Addressed state consistency issues in precompile execution.
  • Documentation

    • Updated the CHANGELOG.md to reflect new features and fixes.
  • Tests

    • Enhanced test cases for ERC20 transfer logic and contract response validation.

@matthiasmatt matthiasmatt requested a review from a team as a code owner October 22, 2024 15:17
Copy link
Contributor

coderabbitai bot commented Oct 22, 2024

Walkthrough

The pull request introduces several updates to the Nibiru project, primarily focusing on enhancing ERC20 contract interactions and improving the handling of token transfers. Key changes include modifications to the CHANGELOG.md, updates to method signatures across various files, and the introduction of new methods for better error handling and state management. Notably, the handling of ERC20 transfers has been refined to check for success values and ensure accurate reporting of token amounts transferred.

Changes

File Path Change Summary
CHANGELOG.md Updated to reflect new entries under "Unreleased," detailing fixes, features, and improvements related to ERC20 metadata, gas handling, and FunToken creation.
x/evm/keeper/msg_server.go Modified ConvertCoinToEvm method to utilize new methods for coin conversion; improved logic and event emissions.
x/evm/keeper/erc20.go Updated Transfer method to return balanceIncrease and enhanced error handling for transfer operations.
x/evm/keeper/erc20_test.go Adjusted TestERC20Calls to include validation of transfer amounts and improved clarity in assertions.
x/evm/precompile/funtoken.go Enhanced bankSend method with additional error handling and validation checks for transfer amounts; updated logic for minting and burning tokens.
x/evm/embeds/artifacts/contracts/IFunToken.sol Updated ABI for bankSend function to include an output parameter sentAmount.
x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json Updated bytecode fields in the JSON artifact.
x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json Updated bytecode fields in the JSON artifact.
x/evm/embeds/contracts/IFunToken.sol Modified bankSend function signature to include a return value for sentAmount.
x/evm/precompile/funtoken_test.go Updated tests for funtoken to improve clarity and validate the response from the contract call.

Assessment against linked issues

Objective Addressed Explanation
Handle the case of ERC20.transfer returning success=false no error on the call (#[2080])

Possibly related PRs

🐇 In the garden, I hop with glee,
New tokens flow, as smooth as can be.
With checks in place, and balances right,
Our FunToken shines, a true delight!
Hooray for changes, let’s dance and play,
In the world of ERC20, we leap all day! 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@matthiasmatt matthiasmatt changed the title feat: fix transfer for native to erc20 fix(evm): transfer for native to erc20 Oct 22, 2024
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 3199ae5 and eb72b31.

📒 Files selected for processing (2)
  • CHANGELOG.md (1 hunks)
  • x/evm/keeper/msg_server.go (2 hunks)
🧰 Additional context used
🔇 Additional comments (1)
CHANGELOG.md (1)

138-138: LGTM! The changelog entry is well-formatted and accurately describes the fix.

The entry properly documents the fix for native to ERC20 token transfers, follows the changelog format guidelines, and aligns with the PR objectives of addressing non-standard ERC20 token handling.

Comment on lines 595 to 598
return nil, errors.Wrap(err, "failed to retrieve EVM module account balance")
}
if recipientBalanceBefore == 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")
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 22, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

Correct error messages to reflect the recipient's balance

The error messages at lines 595 and 598 incorrectly refer to the "EVM module account balance" when they should reference the "recipient's balance" to accurately reflect the operation being performed.

Apply this diff to correct the error messages:

 if err != nil {
-    return nil, errors.Wrap(err, "failed to retrieve EVM module account balance")
+    return nil, errors.Wrap(err, "failed to retrieve recipient balance before transfer")
 }
 if recipientBalanceBefore == nil {
-    return nil, fmt.Errorf("failed to retrieve EVM module account balance, balance is nil")
+    return nil, fmt.Errorf("failed to retrieve recipient balance before transfer, balance is nil")
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return nil, errors.Wrap(err, "failed to retrieve EVM module account balance")
}
if recipientBalanceBefore == 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")
return nil, errors.Wrap(err, "failed to retrieve recipient balance before transfer")
}
if recipientBalanceBefore == nil {
return nil, fmt.Errorf("failed to retrieve recipient balance before transfer, balance is nil")

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with Code Rabbit - it's not a module account balance.

Copy link
Contributor

Choose a reason for hiding this comment

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

@onikonychev, glad we agree! Correcting the error messages to reflect the recipient's balance will improve clarity.

(^‿^)/

Comment on lines 629 to 635
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")
}
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")
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 22, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use low-level call to handle ERC20 tokens that do not return a value

Some ERC20 tokens do not return a value from their transfer function, which can cause the transaction to revert if the return value is expected. To ensure compatibility with these tokens, consider using a low-level call instead of ABI.Pack and CallContractWithInput.

Apply this diff to use a low-level call:

- input, err := k.ERC20().ABI.Pack("transfer", recipient, coin.Amount.BigInt())
- if err != nil {
-     return nil, errors.Wrap(err, "failed to pack ABI args")
- }
- _, err = k.ERC20().CallContractWithInput(ctx, evm.EVM_MODULE_ADDRESS, &erc20Addr, true, input)
+ success, err := k.CallEVM(ctx, evm.EVM_MODULE_ADDRESS, &erc20Addr, "transfer(address,uint256)", recipient, coin.Amount.BigInt())
+ if !success {
+     return nil, fmt.Errorf("ERC20 token transfer failed")
+ }
 if err != nil {
     return nil, errors.Wrap(err, "failed to transfer ERC20 tokens")
 }

This adjustment uses a low-level call that does not rely on the return value of the transfer function, improving compatibility with non-standard tokens.

Committable suggestion was skipped due to low confidence.

Copy link
Contributor

Choose a reason for hiding this comment

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

what was wrong with res, err := k.ERC20().Transfer() ?

Copy link
Contributor

Choose a reason for hiding this comment

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

@onikonychev, the k.ERC20().Transfer() method may not handle ERC20 tokens that do not return a value from their transfer function. Some ERC20 tokens do not return a boolean value upon executing the transfer method, which can cause issues if the implementation expects a return value. By using a low-level call with k.CallEVM, we avoid relying on the return value, ensuring compatibility with such non-standard tokens.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What he said!

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

@matthiasmatt , you're supposed to parse the output as a bool and handle the case it's false. You can still use k.ERC20().Transfer(). It just needs to be updated to handle that false case.

Comment on lines 649 to 651
actualReceivedAmount := big.NewInt(0).Sub(recipientBalanceAfter, recipientBalanceBefore)
if actualReceivedAmount.Sign() == 0 {
return nil, fmt.Errorf("no ERC20 tokens were received by the recipient")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure actualReceivedAmount is positive to handle negative values

The current check only verifies if actualReceivedAmount is zero. If the value is negative, the code may proceed incorrectly. Update the condition to check if actualReceivedAmount is less than or equal to zero to handle both zero and negative values.

Apply this diff to update the condition:

 if actualReceivedAmount.Sign() == 0 {
+    return nil, fmt.Errorf("no ERC20 tokens were received by the recipient")
+}
+if actualReceivedAmount.Sign() < 0 {
     return nil, fmt.Errorf("received amount is negative, possible balance inconsistency")
 }

Committable suggestion was skipped due to low confidence.

Comment on lines 595 to 598
return nil, errors.Wrap(err, "failed to retrieve EVM module account balance")
}
if recipientBalanceBefore == 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")
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with Code Rabbit - it's not a module account balance.

Comment on lines 629 to 635
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")
}
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")
Copy link
Contributor

Choose a reason for hiding this comment

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

what was wrong with res, err := k.ERC20().Transfer() ?

@matthiasmatt matthiasmatt enabled auto-merge (squash) October 23, 2024 11:04
Copy link

codecov bot commented Oct 23, 2024

Codecov Report

Attention: Patch coverage is 65.85366% with 14 lines in your changes missing coverage. Please review.

Project coverage is 64.53%. Comparing base (16393ca) to head (e4b932d).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
x/evm/keeper/erc20.go 66.66% 8 Missing and 3 partials ⚠️
x/evm/keeper/msg_server.go 57.14% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2090      +/-   ##
==========================================
+ Coverage   64.36%   64.53%   +0.17%     
==========================================
  Files         270      271       +1     
  Lines       21192    21250      +58     
==========================================
+ Hits        13640    13714      +74     
+ Misses       6603     6585      -18     
- Partials      949      951       +2     
Files with missing lines Coverage Δ
x/evm/precompile/funtoken.go 47.13% <100.00%> (ø)
x/evm/keeper/msg_server.go 73.89% <57.14%> (+2.63%) ⬆️
x/evm/keeper/erc20.go 75.35% <66.66%> (-1.25%) ⬇️

... and 1 file with indirect coverage changes

Comment on lines 641 to 645
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve balance")
return nil, errors.Wrap(err, "failed to retrieve recipient balance after transfer")
}
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")
Copy link
Member

Choose a reason for hiding this comment

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

Could simplify this with

if err != nil {
  // ...
} else if  recipientBalanceAfter == nil {
  // ...
}

and use the same error message for both

Copy link
Member

@Unique-Divine Unique-Divine Oct 24, 2024

Choose a reason for hiding this comment

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

This PR only editing msg_server.go this much is not addressing the issue. The change needed is to use the success boolean returned by ERC20().Transfer.

Ref: ERC20 Specification

"Callers MUST handle false from returns (bool success). Callers MUST NOT assume that false is never returned!"

The following implementation could work, for example. Then you can freely use the function and depend on its error value.

func (e erc20Calls) Transfer(
	contract, from, to gethcommon.Address, amount *big.Int,
	ctx sdk.Context,
) (success bool, err error) {
	input, err := e.ABI.Pack("transfer", to, amount)
	if err != nil {
		return false, fmt.Errorf("failed to pack ABI args: %w", err)
	}
	resp, err := e.CallContractWithInput(ctx, from, &contract, true, input)
	if err != nil {
		return false, err
	}

	var erc20Bool ERC20Bool
	err = e.ABI.UnpackIntoInterface(&erc20Bool, "transfer", resp.Ret)
	// ➡️ This check guarantees that a boolean is parsed out
	if err != nil {
		return false, err
	}

	// ➡️ This check fixes the issue
	success = erc20Bool.Value
	if !success {
		return false, fmt.Errorf("called executed, but returned success=false")
	}
	return success, nil
}

We should also comment on why we treat success=false as an error.

Edit: On second thought, if you write it like this, it removes the need for the boolean return value entirely. You could rewrite the function to only return an error. @matthiasmatt

Comment on lines 595 to 598
return nil, errors.Wrap(err, "failed to retrieve recipient balance")
}
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")
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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (5)
x/evm/keeper/erc20_test.go (1)

Line range hint 1-63: Add documentation for the received parameter

Consider adding comments explaining:

  1. The purpose of the new received parameter in transfer operations
  2. How it helps handle non-standard ERC20 implementations
  3. Its role in accurate balance accounting

This documentation would help other developers understand the changes and their relationship to the audit findings.

x/evm/evmmodule/genesis_test.go (1)

Line range hint 102-104: Update balance verification for fee-on-transfer tokens.

The balance verification assumes the exact transfer amounts, which may not hold true for fee-on-transfer tokens where the received amount could be less than the sent amount.

Consider storing the actual received amounts during transfers and using those for verification:

+  // Store actual received amounts during transfers
+  var (
+    actualReceivedA *big.Int
+    actualReceivedB *big.Int
+  )
+  _, err, actualReceivedA = deps.EvmKeeper.ERC20().Transfer(...)
+  _, err, actualReceivedB = deps.EvmKeeper.ERC20().Transfer(...)

   // Verify erc20 balances for users A, B and sender
   balance, err := deps.EvmKeeper.ERC20().BalanceOf(erc20Addr, toUserA, deps.Ctx)
   s.Require().NoError(err)
-  s.Require().Equal(amountToSendA, balance)
+  s.Require().Equal(actualReceivedA, balance)

   balance, err = deps.EvmKeeper.ERC20().BalanceOf(erc20Addr, toUserB, deps.Ctx)
   s.Require().NoError(err)
-  s.Require().Equal(amountToSendB, balance)
+  s.Require().Equal(actualReceivedB, balance)

Also applies to: 107-109

x/evm/keeper/erc20.go (3)

83-91: Enhance error messages for better debugging

While the error handling is correct, consider adding more context to error messages to aid in debugging transfer failures.

-		return false, fmt.Errorf("failed to pack ABI args: %w", err), received
+		return false, fmt.Errorf("failed to pack transfer args for contract %s: %w", contract.Hex(), err), received

109-122: LGTM: Comprehensive handling of non-standard ERC20 implementations

The implementation successfully handles both standard ERC20 tokens that return boolean values and non-standard ones that don't. Consider adding debug logging for better observability.

 	if err == nil {
+		k.Logger(ctx).Debug("ERC20 transfer returned success value", "success", erc20Bool.Value)
 		success = erc20Bool.Value
 		return success, nil, received
 	}

+	k.Logger(ctx).Debug("ERC20 transfer did not return value, falling back to balance check")
 	success = true
 	return

76-122: Excellent architectural solution for non-standard ERC20 tokens

The implementation provides a robust solution for handling non-standard ERC20 tokens by:

  1. Using balance tracking instead of relying on transfer return values
  2. Supporting fee-on-transfer tokens through actual amount verification
  3. Maintaining compatibility with both standard and non-standard implementations
  4. Providing clear error handling and validation

This approach aligns well with real-world ERC20 token implementations and provides better reliability for token transfers.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between f356315 and 43ae3e1.

📒 Files selected for processing (5)
  • x/evm/evmmodule/genesis_test.go (1 hunks)
  • x/evm/keeper/erc20.go (1 hunks)
  • x/evm/keeper/erc20_test.go (1 hunks)
  • x/evm/keeper/msg_server.go (1 hunks)
  • x/evm/precompile/funtoken.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • x/evm/keeper/msg_server.go
🔇 Additional comments (5)
x/evm/keeper/erc20_test.go (2)

34-39: LGTM! Good test coverage for failed transfers

The addition of the received amount verification enhances the test by ensuring that failed transfers result in zero tokens received, which aligns well with the PR's objective of accurate balance tracking.


44-48: 🛠️ Refactor suggestion

Add test cases for non-standard ERC20 tokens

While the current test verifies basic transfer functionality, consider adding test cases for:

  1. Fee-on-transfer tokens where received amount is less than transfer amount
  2. ERC20 tokens that don't return values from transfer functions

This would ensure complete coverage of the scenarios mentioned in the PR objectives.

Here's a suggested test structure:

s.T().Log("Transfer - Fee on transfer token")
{
    // Mock a fee-on-transfer token where 10% is deducted
    transferAmount := big.NewInt(1000)
    expectedReceived := big.NewInt(900)  // 10% fee
    _, err, received := deps.EvmKeeper.ERC20().Transfer(
        feeToken.Address, 
        evm.EVM_MODULE_ADDRESS, 
        deps.Sender.EthAddr, 
        transferAmount, 
        deps.Ctx,
    )
    s.Require().NoError(err)
    s.Require().Equal(received, expectedReceived)
}

Let's verify if there are any existing test cases for these scenarios:

x/evm/keeper/erc20.go (3)

76-78: LGTM: Method signature enhancement for fee-on-transfer support

The addition of received *big.Int to the return values enables tracking of actual received amounts, which is crucial for handling fee-on-transfer tokens.


79-82: LGTM: Pre-transfer balance tracking implementation

The implementation correctly captures the recipient's initial balance, which is necessary for calculating the actual received amount after transfer.


93-104: LGTM: Robust implementation for fee-on-transfer tokens

The implementation correctly:

  1. Tracks post-transfer balance
  2. Calculates actual received amount
  3. Validates that tokens were actually received
  4. Handles fee-on-transfer tokens by not assuming received = amount

The comments effectively explain the reasoning behind not checking for exact amounts.

@@ -54,11 +54,11 @@ func (s *Suite) TestExportInitGenesis() {
s.Require().NoError(err)

// Transfer ERC-20 tokens to user A
_, err = deps.EvmKeeper.ERC20().Transfer(erc20Addr, fromUser, toUserA, amountToSendA, deps.Ctx)
_, err, _ = deps.EvmKeeper.ERC20().Transfer(erc20Addr, fromUser, toUserA, amountToSendA, deps.Ctx)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Update transfer result handling for fee-on-transfer tokens.

The test ignores the actual received amount (third return value) from Transfer(). For fee-on-transfer tokens, the received amount could be less than the sent amount, which this test wouldn't catch.

Consider updating the test to handle fee-on-transfer scenarios:

-  _, err, _ = deps.EvmKeeper.ERC20().Transfer(erc20Addr, fromUser, toUserA, amountToSendA, deps.Ctx)
+  _, err, receivedAmount := deps.EvmKeeper.ERC20().Transfer(erc20Addr, fromUser, toUserA, amountToSendA, deps.Ctx)
   s.Require().NoError(err)
+  // For fee-on-transfer tokens, verify actual received amount
+  balance, err := deps.EvmKeeper.ERC20().BalanceOf(erc20Addr, toUserA, deps.Ctx)
+  s.Require().NoError(err)
+  s.Require().Equal(receivedAmount, balance)

Also applies to: 61-61

Comment on lines 57 to 62
_, err, _ = deps.EvmKeeper.ERC20().Transfer(erc20Addr, fromUser, toUserA, amountToSendA, deps.Ctx)
s.Require().NoError(err)

// Transfer ERC-20 tokens to user B
_, err = deps.EvmKeeper.ERC20().Transfer(erc20Addr, fromUser, toUserB, amountToSendB, deps.Ctx)
_, err, _ = deps.EvmKeeper.ERC20().Transfer(erc20Addr, fromUser, toUserB, amountToSendB, deps.Ctx)
s.Require().NoError(err)
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

🛠️ Refactor suggestion

Based on the code review, I can now generate the final response:

Add test cases for non-standard ERC20 tokens

The current tests only use standard OpenZeppelin ERC20 implementations (TestERC20 and ERC20Minter), which follow the standard ERC20 interface. Given that the PR aims to handle non-standard tokens, you should add test cases for:

  1. Fee-on-transfer tokens (tokens that take a fee during transfer)
  2. Tokens that don't return a value from transfer
  3. Tokens with non-standard decimals
  4. Tokens that return false without reverting

Example approach:

  • Create mock contracts that implement these non-standard behaviors
  • Add test cases in x/evm/evmmodule/genesis_test.go using these mock contracts
  • Verify the system handles these edge cases correctly

The reason for this categorization is that the codebase only tests standard ERC20 implementations (OpenZeppelin's ERC20), which doesn't cover the non-standard token scenarios mentioned in the PR objectives. This gap in test coverage could lead to issues when handling non-standard tokens in production.

🔗 Analysis chain

Add test cases for non-standard ERC20 tokens.

The test only covers the happy path with a standard ERC20 token. Given the PR objectives to handle non-standard tokens, additional test cases are needed.

Let's verify if there are any test cases for non-standard tokens:

Consider adding test cases for:

  1. Fee-on-transfer tokens
  2. Tokens that don't return values from transfer
  3. Tokens that return false without reverting

Let me try a different approach to verify the test coverage for non-standard ERC20 tokens.

This will help us:

  1. Locate all test files related to ERC20
  2. See the test function implementations to check for non-standard token scenarios

Let me check the actual ERC20 contract implementation and test setup to understand what kind of tokens are being tested.

This will help us understand:

  1. The type of ERC20 tokens being created in tests
  2. The actual contract implementation
  3. Any test utilities that might be using different token implementations

Let me check the ERC20 contract implementation that's being used in the tests.

This will help us understand:

  1. The ERC20 contract implementation being used in tests
  2. Any existing transfer-related test cases
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for test cases involving non-standard ERC20 tokens
rg -l "TestERC20" | xargs rg -l "fee.*transfer|non.*standard"

Length of output: 61


Script:

#!/bin/bash
# First, let's find all ERC20-related test files
fd ".*test.*" | rg -l "ERC20"

# Then, let's examine the test implementations to understand the coverage
rg -A 10 "func.*Test.*ERC20.*" 

Length of output: 2233


Script:

#!/bin/bash
# Let's examine the ERC20 contract implementation and test setup
rg -l "CreateFunToken" | xargs rg -A 10 "CreateFunToken"

# Also check for any ERC20 contract deployments or mocks
fd ".*\.sol" | xargs cat

# Check test utilities and helpers
rg -A 5 "ERC20" x/evm/evmtest/

Length of output: 66063


Script:

#!/bin/bash
# Check the ERC20 contract implementation
cat x/evm/embeds/contracts/ERC20Minter.sol
cat x/evm/embeds/contracts/TestERC20.sol

# Look for any test files that might test non-standard behavior
rg -A 10 "func.*Test.*Transfer" x/evm/

Length of output: 3136

@@ -159,7 +159,7 @@ func (p precompileFunToken) bankSend(

// Caller transfers ERC20 to the EVM account
transferTo := evm.EVM_MODULE_ADDRESS
_, err = p.evmKeeper.ERC20().Transfer(erc20, caller, transferTo, amount, ctx)
_, err, _ = p.evmKeeper.ERC20().Transfer(erc20, caller, transferTo, amount, ctx)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle the Received Amount from Transfer Method

At line 162:

_, err, _ = p.evmKeeper.ERC20().Transfer(erc20, caller, transferTo, amount, ctx)

The Transfer method now returns an additional value received *big.Int, which represents the actual amount transferred. Ignoring this value may lead to incorrect accounting, especially for tokens with fee-on-transfer mechanisms where the received amount could be less than the requested amount.

Consider capturing and utilizing the received amount to ensure accurate processing:

-receivedAmount, err, _ := p.evmKeeper.ERC20().Transfer(erc20, caller, transferTo, amount, ctx)
+receivedAmount, err, _ := p.evmKeeper.ERC20().Transfer(erc20, caller, transferTo, amount, ctx)
 if err != nil {
     return nil, fmt.Errorf("failed to send from caller to the EVM account: %w", err)
 }
+// Update the amount to reflect the actual received amount
+amount = receivedAmount

By handling receivedAmount, you ensure that subsequent operations use the correct token amount, which is crucial for maintaining consistency and correctness in transactions involving tokens with special transfer behaviors.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (4)
x/evm/keeper/erc20.go (2)

76-78: Update function documentation to reflect new return value.

The method signature has been updated to return the actual received amount, which is great for handling fee-on-transfer tokens. However, the function documentation needs to be updated to explain the new received return value and its significance.

Add this to the function documentation:

 /*
 Transfer implements "ERC20.transfer"
 
     ```solidity
     /// @dev Moves `amount` tokens from the caller's account to `to`.
     /// Returns a boolean value indicating whether the operation succeeded.
     /// Emits a {Transfer} event.
     function transfer(address to, uint256 amount) external returns (bool);
     ```
+
+ @return success - boolean indicating if the transfer was successful
+ @return err - error if any occurred during the transfer
+ @return received - actual amount of tokens received by the recipient, which may be less than
+                   the transfer amount for fee-on-transfer tokens
 */

109-118: Robust handling of non-standard ERC20 implementations but needs test coverage.

The implementation correctly handles both standard and non-standard ERC20 tokens:

  • For standard tokens: Uses the contract's return value
  • For non-standard tokens: Falls back to checking actual token receipt

However, the fallback path (lines 121-122) lacks test coverage. Consider adding test cases for:

  1. Standard ERC20 tokens that return true/false
  2. Non-standard tokens that don't return a value
  3. Tokens that return invalid data

Would you like me to help generate comprehensive test cases for these scenarios?

Also applies to: 121-122

x/evm/keeper/msg_server.go (2)

593-593: Increase test coverage for critical error handling paths

The line added at 593 handles a critical error case but is not covered by tests. Consider adding unit tests to cover this error path to enhance reliability.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 593-593: x/evm/keeper/msg_server.go#L593
Added line #L593 was not covered by tests


578-578: Increase test coverage for nil balance error case

The error handling at line 578 for a nil balance is not covered by tests. Adding unit tests for this scenario will help ensure proper error handling.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 578-578: x/evm/keeper/msg_server.go#L578
Added line #L578 was not covered by tests

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 43ae3e1 and 80d5353.

📒 Files selected for processing (5)
  • CHANGELOG.md (1 hunks)
  • x/evm/keeper/erc20.go (1 hunks)
  • x/evm/keeper/erc20_test.go (1 hunks)
  • x/evm/keeper/msg_server.go (1 hunks)
  • x/evm/precompile/funtoken.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • x/evm/keeper/erc20_test.go
🧰 Additional context used
🪛 GitHub Check: codecov/patch
x/evm/keeper/erc20.go

[warning] 81-82: x/evm/keeper/erc20.go#L81-L82
Added lines #L81 - L82 were not covered by tests


[warning] 86-86: x/evm/keeper/erc20.go#L86
Added line #L86 was not covered by tests


[warning] 95-96: x/evm/keeper/erc20.go#L95-L96
Added lines #L95 - L96 were not covered by tests


[warning] 104-104: x/evm/keeper/erc20.go#L104
Added line #L104 was not covered by tests


[warning] 121-122: x/evm/keeper/erc20.go#L121-L122
Added lines #L121 - L122 were not covered by tests

x/evm/keeper/msg_server.go

[warning] 578-578: x/evm/keeper/msg_server.go#L578
Added line #L578 was not covered by tests


[warning] 593-593: x/evm/keeper/msg_server.go#L593
Added line #L593 was not covered by tests

🔇 Additional comments (3)
x/evm/precompile/funtoken.go (1)

147-147: ⚠️ Potential issue

CRITICAL: Security vulnerability in fee-on-transfer token handling

The current implementation ignores the actual received amount from the Transfer method, which creates a critical vulnerability when handling fee-on-transfer tokens. This could lead to token inflation as the system would mint more bank coins than the actual ERC20 tokens received.

Example scenario:

  1. User transfers 100 tokens of a fee-on-transfer token
  2. Due to the fee mechanism, only 95 tokens are actually received
  3. Current code mints 100 bank coins instead of 95
  4. This creates 5 extra bank coins, breaking the 1:1 peg

Apply this fix to properly handle the received amount and add error handling:

-_, err, _ = p.evmKeeper.ERC20().Transfer(erc20, caller, transferTo, amount, ctx)
+receivedAmount, err, success := p.evmKeeper.ERC20().Transfer(erc20, caller, transferTo, amount, ctx)
 if err != nil {
     return nil, fmt.Errorf("failed to send from caller to the EVM account: %w", err)
 }
+if !success {
+    return nil, fmt.Errorf("transfer failed: ERC20 contract returned false")
+}
+if receivedAmount == nil {
+    return nil, fmt.Errorf("transfer failed: received amount is nil")
+}
+if receivedAmount.Cmp(amount) < 0 {
+    // Update amount to the actual received amount for fee-on-transfer tokens
+    amount = receivedAmount
+}

Run this verification script to identify potential fee-on-transfer tokens in the system:

x/evm/keeper/msg_server.go (2)

578-578: Previous comment still applies: Correct the error message to reflect the recipient's balance

The error message refers to the "EVM module account balance," but it should reference the "recipient's balance" to accurately reflect the operation being performed.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 578-578: x/evm/keeper/msg_server.go#L578
Added line #L578 was not covered by tests


596-597: Previous comment still applies: Ensure actualReceivedAmount is positive

It's important to check that actualReceivedAmount is greater than zero to prevent potential issues with zero or negative amounts during the burn operation.

Comment on lines 79 to 82
recipientBalanceBefore, err := e.BalanceOf(contract, to, ctx)
if err != nil {
return false, errors.Wrap(err, "failed to retrieve recipient balance"), received
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Based on the search results, I can now generate the final response:

Test coverage needed for fee-on-transfer token handling

The balance tracking implementation in erc20.go lacks specific test coverage for fee-on-transfer scenarios. While there is a test for malicious transfer behavior (TestFunTokenFromERC20MaliciousTransfer), it doesn't verify the balance tracking logic for:

  • Balance retrieval error paths
  • Actual token amount received calculation
  • Zero/negative received amount validation

Suggested test cases to add:

  • Test with a fee-on-transfer token that deducts fees from the transfer amount
  • Test error handling when balance checks fail
  • Test validation when received amount is zero or negative
🔗 Analysis chain

Balance tracking implementation looks good but needs test coverage.

The implementation correctly handles fee-on-transfer tokens by:

  1. Tracking balance before and after transfer
  2. Computing actual received amount
  3. Validating that some tokens were received

However, several critical paths lack test coverage:

  • Error handling for balance retrieval (lines 81-82, 95-96)
  • Validation of received amount (line 104)

Let's verify the test coverage:

Also applies to: 93-96, 98-104


Let me check the actual test implementations to verify the coverage.


Let me search for test files in a broader context to find balance-related tests.


Let me check the test implementations in the funtoken test files since they might contain the balance verification tests.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for existing test cases in the codebase
# Look for test files that might contain relevant test cases
fd -e go -e _test.go | xargs rg -l "TestERC20.*Transfer|TestFeeOnTransfer"

Length of output: 168


Script:

#!/bin/bash
# Check the test implementations for transfer-related tests
rg -A 10 "TestERC20.*Transfer|TestFeeOnTransfer" x/evm/keeper/funtoken_from_erc20_test.go x/evm/embeds/embeds_test.go

# Also search for any test cases that verify balances
rg -B 2 -A 5 "BalanceOf.*ctx" x/evm/keeper/funtoken_from_erc20_test.go x/evm/embeds/embeds_test.go

Length of output: 1147


Script:

#!/bin/bash
# Search for test files related to ERC20 keeper
fd -e go test.go keeper | xargs rg -l "erc20"

# Search for balance verification patterns in test files
rg -B 2 -A 5 "balance.*Before|balance.*After|received.*amount" x/evm/keeper/

Length of output: 1676


Script:

#!/bin/bash
# Check funtoken test files for balance verification tests
rg -B 5 -A 10 "Transfer.*\(" x/evm/keeper/funtoken_from_erc20_test.go x/evm/keeper/funtoken_state_test.go

# Also check for any balance verification in the precompile tests
rg -B 5 -A 10 "Transfer.*\(" x/evm/precompile/funtoken_test.go

Length of output: 1409

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 81-82: x/evm/keeper/erc20.go#L81-L82
Added lines #L81 - L82 were not covered by tests

Comment on lines 592 to 593
if err != nil || !success {
return nil, errors.Wrap(err, "failed to transfer ERC-20 tokens")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle nil error correctly when transfer fails

If err is nil but success is false, calling errors.Wrap(err, ...) with a nil error may result in an unintended error message. Ensure that the error handling correctly constructs the error message when err is nil.

Apply this diff to fix the error handling:

 if err != nil || !success {
-    return nil, errors.Wrap(err, "failed to transfer ERC-20 tokens")
+    if err != nil {
+        return nil, errors.Wrap(err, "failed to transfer ERC-20 tokens")
+    } else {
+        return nil, fmt.Errorf("failed to transfer ERC-20 tokens")
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err != nil || !success {
return nil, errors.Wrap(err, "failed to transfer ERC-20 tokens")
if err != nil || !success {
if err != nil {
return nil, errors.Wrap(err, "failed to transfer ERC-20 tokens")
} else {
return nil, fmt.Errorf("failed to transfer ERC-20 tokens")
}
}
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 593-593: x/evm/keeper/msg_server.go#L593
Added line #L593 was not covered by tests

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (3)
x/evm/keeper/erc20.go (3)

79-82: Enhance error context for balance retrieval failures

While the error handling is good, consider adding more context about why the balance check failed, as it could help diagnose issues with non-standard tokens.

-		return false, errors.Wrap(err, "failed to retrieve recipient balance"), received
+		return false, errors.Wrapf(err, "failed to retrieve recipient balance for address %s", to.String()), received

109-122: Add logging for non-standard token behavior

The fallback mechanism for non-standard tokens is good, but consider adding debug logs when encountering such tokens to help with monitoring and debugging.

 	if err == nil {
 		// should be true if the transfer was successful but we do it anyway
 		// to respect the contract's return value
 		success = erc20Bool.Value
 
 		return success, nil, received
 	}
+	// Log non-standard token behavior
+	k.Logger(ctx).Debug(
+		"encountered non-standard ERC20 token",
+		"contract", contract.String(),
+		"error", err.Error(),
+	)
 
 	success = true
 	return

76-122: Add test coverage for fee-on-transfer scenarios

The implementation needs test coverage for:

  1. Fee-on-transfer tokens where received amount < sent amount
  2. Tokens with non-standard return values
  3. Edge cases in balance calculation

Would you like me to help create comprehensive test cases for these scenarios?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 80d5353 and e4b932d.

📒 Files selected for processing (4)
  • CHANGELOG.md (2 hunks)
  • x/evm/keeper/erc20.go (1 hunks)
  • x/evm/keeper/msg_server.go (1 hunks)
  • x/evm/precompile/funtoken.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • CHANGELOG.md
  • x/evm/precompile/funtoken.go
🔇 Additional comments (5)
x/evm/keeper/erc20.go (3)

76-78: LGTM: Method signature enhancement for fee-on-transfer support

The addition of received *big.Int as a return value enables proper tracking of actual received tokens, which is crucial for fee-on-transfer tokens.


86-91: LGTM: Consistent error handling

The error handling maintains a consistent pattern of returning the received amount even in error cases.


93-104: Verify balance calculation safety

While the balance difference calculation is logically correct, consider:

  1. Using SafeSub to prevent integer overflow
  2. The possibility of concurrent transfers affecting the balance

Let's check if there are any existing safety measures:

x/evm/keeper/msg_server.go (2)

592-593: Handle nil error correctly when transfer fails

If err is nil but success is false, using errors.Wrap(err, ...) with a nil error may result in an unintended error message. Update the error handling to correctly construct the error message when err is nil.


578-578: Correct error message to reflect the EVM module account balance

The error message refers to the "EVM module account balance" when retrieving the module's balance. Ensure this accurately reflects the account involved in the operation.

Comment on lines 596 to 597
burnCoin := sdk.NewCoin(coin.Denom, sdk.NewIntFromBigInt(actualReceivedAmount))
err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(burnCoin))
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure actualReceivedAmount is positive before burning coins

Before creating burnCoin and burning coins, verify that actualReceivedAmount is positive to avoid potential errors when dealing with zero or negative amounts.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (4)
x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json (1)

25-31: Excellent addition of sentAmount return value for handling non-standard tokens!

This change is crucial for properly handling "Fee on Transfer" tokens where the actual amount received differs from the amount sent. By returning the sentAmount, the contract can now accurately track token transfers and maintain correct balance accounting.

Consider documenting in the interface comments that:

  1. For standard ERC20 tokens, sentAmount will equal the input amount
  2. For fee-on-transfer tokens, sentAmount may be less than the input amount
  3. This return value should be used for balance tracking instead of the input amount
x/evm/embeds/contracts/IFunToken.sol (1)

8-14: LGTM! Consider adding examples for clarity.

The documentation clearly explains the parameters and the return value, particularly highlighting the potential difference between sent and received amounts for fee-on-transfer tokens.

Consider adding a documentation example to illustrate the fee-on-transfer scenario:

    /// @return sentAmount - amount of tokens received by the recipient. This may
    /// not be equal to `amount` if the corresponding ERC20 contract has a fee or
    /// deduction on transfer.
+   /// @dev Example: If amount=100 and token has 2% transfer fee, sentAmount=98
x/evm/keeper/erc20_test.go (1)

Line range hint 1-56: Consider restructuring tests for better isolation

The current test structure combines multiple scenarios in a single test function. This makes it harder to understand failures and maintain the tests.

Consider these improvements:

  1. Split into separate test functions:
func (s *Suite) TestERC20Transfer_InsufficientFunds() {}
func (s *Suite) TestERC20Transfer_Success() {}
func (s *Suite) TestERC20Transfer_FeeOnTransfer() {}
func (s *Suite) TestERC20Transfer_NoReturnValue() {}
func (s *Suite) TestERC20Transfer_FalseReturn() {}
  1. Add helper functions for token deployment:
func (s *Suite) deployMockToken(opts MockTokenOpts) *evmtest.FunToken {
    // Configure token behavior (fees, return values, etc.)
}

type MockTokenOpts struct {
    HasFeeOnTransfer bool
    FeePercentage   uint8
    ReturnsValue    bool
    AlwaysReturns   bool
}
x/evm/keeper/erc20.go (1)

111-119: Consider improving error message formatting.

The implementation correctly handles fee-on-transfer tokens by tracking actual balance changes. However, the error message could be more readable.

-			"amount of ERC20 tokens received MUST be positive: the balance of recipient %s would've changed by %v for token %s",
-			to.Hex(), balanceIncrease.String(), contract.Hex(),
+			"amount of ERC20 tokens received MUST be positive: recipient=%s received_amount=%v token=%s",
+			to.Hex(), balanceIncrease.String(), contract.Hex(),
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between e4b932d and b2c863d.

📒 Files selected for processing (10)
  • CHANGELOG.md (1 hunks)
  • x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json (1 hunks)
  • x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json (1 hunks)
  • x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json (1 hunks)
  • x/evm/embeds/contracts/IFunToken.sol (1 hunks)
  • x/evm/keeper/erc20.go (1 hunks)
  • x/evm/keeper/erc20_test.go (2 hunks)
  • x/evm/keeper/msg_server.go (3 hunks)
  • x/evm/precompile/funtoken.go (2 hunks)
  • x/evm/precompile/funtoken_test.go (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json
  • x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • CHANGELOG.md
  • x/evm/precompile/funtoken.go
🔇 Additional comments (9)
x/evm/embeds/contracts/IFunToken.sol (1)

15-19: LGTM! Verify implementation in precompile.

The addition of sentAmount return value is a crucial enhancement for handling fee-on-transfer tokens correctly. This change aligns perfectly with the PR objectives and maintains backward compatibility.

Let's verify the precompile implementation handles this new return value:

✅ Verification successful

The initial search revealed the precompile exists but we need to verify:

  1. If the implementation returns the actual sent amount
  2. If there are tests covering this functionality

The new search will help us see the complete implementation and any test coverage.


The previous search showed the beginning of the bankSend implementation and some test references, but we need to see:

  1. The complete implementation to verify if it returns the actual sent amount
  2. The test contract implementation to understand how the return value is handled in tests

We need to see:

  1. The rest of the bankSend implementation, particularly how it handles the return value after validating the address
  2. The location of the test contract to verify how it handles the return value

The previous search was cut off at the address validation part, and we couldn't find the test contract in the expected location.


Implementation correctly handles fee-on-transfer tokens and returns actual sent amount

The precompile implementation properly handles fee-on-transfer tokens by:

  1. Capturing the actual transferred amount via gotAmount from ERC20().Transfer()
  2. Using this gotAmount for subsequent operations (minting/burning)
  3. Correctly returning the actual sent amount via method.Outputs.Pack(gotAmount)
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the precompile implementation matches the interface changes

# Test 1: Check if the precompile code handles the new return value
rg -A 10 "func.*[Bb]ank[Ss]end.*" 

# Test 2: Look for related test cases
rg -A 10 "TestBankSend.*fee.*transfer" 

Length of output: 2063


Script:

#!/bin/bash
# Let's check the implementation details of bankSend in the precompile
rg -B 5 -A 20 "func \(p precompileFunToken\) bankSend" x/evm/precompile/funtoken.go

# Also check if there are any test files related to bankSend
fd ".*test.*\.go$" | xargs rg "bankSend"

Length of output: 1692


Script:

#!/bin/bash
# Let's see the complete implementation of bankSend and its return value handling
rg -B 20 -A 40 "func \(p precompileFunToken\) bankSend" x/evm/precompile/funtoken.go

# Also check the test contract implementation to see how it handles the return value
rg -A 20 "contract.*TestContract" x/evm/keeper/testdata/contracts/

Length of output: 1963


Script:

#!/bin/bash
# Let's see the complete implementation by searching more lines after the last visible part
rg -A 60 "toAddr, err := sdk.AccAddressFromBech32\(to\)" x/evm/precompile/funtoken.go

# Also try to find the test contract in a different location
fd -t f "TestContract.sol"

Length of output: 2022

x/evm/precompile/funtoken_test.go (3)

129-136: LGTM! Improved variable naming.

The variable rename from resp to ethTxResp enhances code readability by better describing the response type from the EVM transaction.


138-143: LGTM! Improved test readability.

The multi-line formatting of balance assertions enhances readability while maintaining the critical checks for token balance accuracy.


148-156: 🛠️ Refactor suggestion

Add test cases for fee-on-transfer tokens.

While the new validation is good, consider adding test cases that verify the handling of fee-on-transfer tokens where the received amount differs from the sent amount. This would better align with the PR objectives of handling non-standard ERC20 tokens.

Example test case structure:

s.T().Log("Test fee-on-transfer token")
{
    // Mock a fee-on-transfer token where recipient receives 90% of sent amount
    amtToSend := int64(1000)
    expectedReceived := int64(900)  // 90% of sent amount
    
    // ... perform transfer ...
    
    // Verify actual received amount
    var sentAmt *big.Int
    err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface(
        &sentAmt,
        string(precompile.FunTokenMethod_BankSend),
        ethTxResp.Ret,
    )
    s.NoError(err)
    s.Require().Equal(expectedReceived, sentAmt.Int64())
}

Let me verify if there are any existing test cases for fee-on-transfer tokens:

x/evm/keeper/erc20.go (3)

77-81: LGTM! Balance tracking implementation is correct.

The implementation properly retrieves and tracks the recipient's balance before the transfer, with appropriate error handling and descriptive error messages.


98-102: LGTM! Improved error handling for non-reverting transfers.

The explicit success check addresses issue #2080, properly handling tokens like ZRX and BAT that return false instead of reverting on failed transfers.


76-77: LGTM! Verify callers of this method.

The signature change from (bool, error) to (*big.Int, error) is a good improvement that allows tracking the actual amount of tokens received, which is crucial for fee-on-transfer tokens.

✅ Verification successful

Let me analyze the callers of the Transfer method to ensure we have a complete picture.


Let me check one more pattern to ensure we haven't missed any direct calls to Transfer.


All callers of Transfer method have been updated correctly

Based on the verification results:

  • The only caller that uses the returned balance value is in precompile/funtoken.go, which correctly captures it as gotAmount
  • All other callers in test files only check the error return value using _, err := pattern, which is appropriate since they don't need the balance change
  • No instances of incorrect usage patterns were found
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find all callers of the Transfer method to ensure they're updated
# to handle the new return value.

# Search for method calls
rg -B 2 -A 2 "Transfer\s*\([^)]*\)" --type go

Length of output: 8260


Script:

#!/bin/bash
# Let's look at the actual implementation of the Transfer method callers
# to see how they handle the return value

# First, let's check the precompile/funtoken.go implementation
ast-grep --pattern 'gotAmount, err := p.evmKeeper.ERC20().Transfer($$$)'

# Then check the test implementations
ast-grep --pattern '_, err := $$.Transfer($$$)'
ast-grep --pattern 'err := $$.Transfer($$$)'

Length of output: 292


Script:

#!/bin/bash
# Search for any Transfer method calls that might store the return value
# in a variable or use it directly
ast-grep --pattern '$$ := $$.Transfer($$$)'
ast-grep --pattern '$$ = $$.Transfer($$$)'

# Also check for direct usage in if statements or function calls
ast-grep --pattern 'if $$.Transfer($$$)'
ast-grep --pattern '$$.Transfer($$$)'

Length of output: 173

x/evm/keeper/msg_server.go (2)

494-496: LGTM! Clear separation of concerns based on token origin.

The code now correctly routes the conversion based on whether the token originated from a coin or ERC20, improving the handling of different token types.


Line range hint 500-544: LGTM! Well-structured implementation with proper error handling.

The function follows a clear two-step process with proper error handling and event emission. The implementation correctly handles the minting of ERC20 tokens for coin-originated tokens.

Comment on lines +37 to +38
amt := big.NewInt(9_420)
_, err := deps.EvmKeeper.ERC20().Transfer(contract, deps.Sender.EthAddr, evm.EVM_MODULE_ADDRESS, amt, deps.Ctx)
Copy link
Contributor

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:

s.T().Log("Transfer - Fee on transfer token")
{
    amt := big.NewInt(1000)
    expectedReceivedAmt := big.NewInt(950) // Assuming 5% fee
    sentAmt, err := deps.EvmKeeper.ERC20().Transfer(
        feeToken, // Mock fee-on-transfer token
        evm.EVM_MODULE_ADDRESS,
        deps.Sender.EthAddr,
        amt,
        deps.Ctx,
    )
    s.Require().NoError(err)
    s.Require().Equal(sentAmt.String(), expectedReceivedAmt.String())
}

Comment on lines +47 to +56
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())
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

  1. Add test cases for tokens that return false instead of reverting:
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)
}
  1. Add test cases for tokens without return values:
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())
}
  1. Make balance assertions more explicit:
-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))

Comment on lines +598 to +609
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,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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,
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")
}
// Emit event with the actual amount received
_ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{
Sender: sender.String(),
Erc20ContractAddress: funTokenMapping.Erc20Addr.String(),
ToEthAddr: recipient.String(),
BankCoin: burnCoin,

Comment on lines +586 to +595
// 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")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Enhance error handling for failed transfers.

The current implementation should handle the case where the transfer returns false (indicating failure) but doesn't revert. For tokens like ZRX and BAT that may return false without reverting, we should:

  1. Check the transfer success status
  2. Return the escrowed coins to the sender if the transfer fails

Apply this diff to improve error handling:

 actualSentAmount, err := k.ERC20().Transfer(
     erc20Addr,
     evm.EVM_MODULE_ADDRESS,
     recipient,
     coin.Amount.BigInt(),
     ctx,
 )
 if err != nil {
     // Return escrowed coins to sender
+    if sendErr := k.bankKeeper.SendCoinsFromModuleToAccount(
+        ctx,
+        evm.ModuleName,
+        sender,
+        sdk.NewCoins(coin),
+    ); sendErr != nil {
+        return nil, errors.Wrap(sendErr, "failed to return escrowed coins after transfer error")
+    }
     return nil, errors.Wrap(err, "failed to transfer ERC-20 tokens")
 }
+
+// Handle case where transfer returns false without reverting
+if actualSentAmount.Sign() == 0 {
+    // Return escrowed coins to sender
+    if sendErr := k.bankKeeper.SendCoinsFromModuleToAccount(
+        ctx,
+        evm.ModuleName,
+        sender,
+        sdk.NewCoins(coin),
+    ); sendErr != nil {
+        return nil, errors.Wrap(sendErr, "failed to return escrowed coins after transfer failure")
+    }
+    return nil, fmt.Errorf("ERC20 transfer returned false")
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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")
// 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 escrowed coins to sender
if sendErr := k.bankKeeper.SendCoinsFromModuleToAccount(
ctx,
evm.ModuleName,
sender,
sdk.NewCoins(coin),
); sendErr != nil {
return nil, errors.Wrap(sendErr, "failed to return escrowed coins after transfer error")
}
return nil, errors.Wrap(err, "failed to transfer ERC-20 tokens")
}
// Handle case where transfer returns false without reverting
if actualSentAmount.Sign() == 0 {
// Return escrowed coins to sender
if sendErr := k.bankKeeper.SendCoinsFromModuleToAccount(
ctx,
evm.ModuleName,
sender,
sdk.NewCoins(coin),
); sendErr != nil {
return nil, errors.Wrap(sendErr, "failed to return escrowed coins after transfer failure")
}
return nil, fmt.Errorf("ERC20 transfer returned false")
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[evm] bug: Handle the case of ERC20.transfer returning success=false no error on the call
3 participants