-
Notifications
You must be signed in to change notification settings - Fork 201
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
base: main
Are you sure you want to change the base?
Conversation
WalkthroughThe 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 Changes
Assessment against linked issues
Possibly related PRs
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? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
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.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 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.
x/evm/keeper/msg_server.go
Outdated
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") |
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.
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.
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") |
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.
I agree with Code Rabbit - it's not a module account balance.
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.
@onikonychev, glad we agree! Correcting the error messages to reflect the recipient's balance will improve clarity.
(^‿^)/
x/evm/keeper/msg_server.go
Outdated
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") |
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.
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.
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.
what was wrong with res, err := k.ERC20().Transfer()
?
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.
@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.
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.
What he said!
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.
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!
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.
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.
@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.
x/evm/keeper/msg_server.go
Outdated
actualReceivedAmount := big.NewInt(0).Sub(recipientBalanceAfter, recipientBalanceBefore) | ||
if actualReceivedAmount.Sign() == 0 { | ||
return nil, fmt.Errorf("no ERC20 tokens were received by the recipient") |
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.
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.
x/evm/keeper/msg_server.go
Outdated
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") |
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.
I agree with Code Rabbit - it's not a module account balance.
x/evm/keeper/msg_server.go
Outdated
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") |
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.
what was wrong with res, err := k.ERC20().Transfer()
?
Codecov ReportAttention: Patch coverage is
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
|
x/evm/keeper/msg_server.go
Outdated
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") |
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.
Could simplify this with
if err != nil {
// ...
} else if recipientBalanceAfter == nil {
// ...
}
and use the same error message for both
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.
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
fromreturns (bool success)
. Callers MUST NOT assume thatfalse
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
x/evm/keeper/msg_server.go
Outdated
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") |
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.
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.
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.
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 thereceived
parameterConsider adding comments explaining:
- The purpose of the new
received
parameter in transfer operations- How it helps handle non-standard ERC20 implementations
- 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 debuggingWhile 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 implementationsThe 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 tokensThe implementation provides a robust solution for handling non-standard ERC20 tokens by:
- Using balance tracking instead of relying on transfer return values
- Supporting fee-on-transfer tokens through actual amount verification
- Maintaining compatibility with both standard and non-standard implementations
- 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
📒 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 transfersThe 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 suggestionAdd test cases for non-standard ERC20 tokens
While the current test verifies basic transfer functionality, consider adding test cases for:
- Fee-on-transfer tokens where
received
amount is less than transfer amount- 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 supportThe 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 implementationThe 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 tokensThe implementation correctly:
- Tracks post-transfer balance
- Calculates actual received amount
- Validates that tokens were actually received
- Handles fee-on-transfer tokens by not assuming received = amount
The comments effectively explain the reasoning behind not checking for exact amounts.
x/evm/evmmodule/genesis_test.go
Outdated
@@ -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) |
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.
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
x/evm/evmmodule/genesis_test.go
Outdated
_, 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) |
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.
💡 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:
- Fee-on-transfer tokens (tokens that take a fee during transfer)
- Tokens that don't return a value from transfer
- Tokens with non-standard decimals
- 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:
- Fee-on-transfer tokens
- Tokens that don't return values from transfer
- 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:
- Locate all test files related to ERC20
- 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:
- The type of ERC20 tokens being created in tests
- The actual contract implementation
- 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:
- The ERC20 contract implementation being used in tests
- 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
x/evm/precompile/funtoken.go
Outdated
@@ -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) |
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.
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.
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.
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:
- Standard ERC20 tokens that return true/false
- Non-standard tokens that don't return a value
- 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 pathsThe 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 caseThe error handling at line
578
for anil
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
📒 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 testsx/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 issueCRITICAL: 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:
- User transfers 100 tokens of a fee-on-transfer token
- Due to the fee mechanism, only 95 tokens are actually received
- Current code mints 100 bank coins instead of 95
- 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 balanceThe 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: EnsureactualReceivedAmount
is positiveIt's important to check that
actualReceivedAmount
is greater than zero to prevent potential issues with zero or negative amounts during the burn operation.
x/evm/keeper/erc20.go
Outdated
recipientBalanceBefore, err := e.BalanceOf(contract, to, ctx) | ||
if err != nil { | ||
return false, errors.Wrap(err, "failed to retrieve recipient balance"), received | ||
} |
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.
💡 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:
- Tracking balance before and after transfer
- Computing actual received amount
- 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
x/evm/keeper/msg_server.go
Outdated
if err != nil || !success { | ||
return nil, errors.Wrap(err, "failed to transfer ERC-20 tokens") |
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.
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.
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
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.
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:
- Fee-on-transfer tokens where received amount < sent amount
- Tokens with non-standard return values
- 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
📒 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:
- Using
SafeSub
to prevent integer overflow - 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.
x/evm/keeper/msg_server.go
Outdated
burnCoin := sdk.NewCoin(coin.Denom, sdk.NewIntFromBigInt(actualReceivedAmount)) | ||
err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(burnCoin)) |
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.
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.
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.
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:
- For standard ERC20 tokens,
sentAmount
will equal the inputamount
- For fee-on-transfer tokens,
sentAmount
may be less than the inputamount
- 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:
- 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() {}
- 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
📒 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:
- If the implementation returns the actual sent amount
- 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:
- The complete implementation to verify if it returns the actual sent amount
- The test contract implementation to understand how the return value is handled in tests
We need to see:
- The rest of the bankSend implementation, particularly how it handles the return value after validating the address
- 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:
- Capturing the actual transferred amount via
gotAmount
fromERC20().Transfer()
- Using this
gotAmount
for subsequent operations (minting/burning) - 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 asgotAmount
- 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.
amt := big.NewInt(9_420) | ||
_, err := deps.EvmKeeper.ERC20().Transfer(contract, deps.Sender.EthAddr, evm.EVM_MODULE_ADDRESS, amt, deps.Ctx) |
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:
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())
}
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()) |
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.
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.
- 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)
}
- 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())
}
- 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))
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, |
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.
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.
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, |
// 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") |
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.
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:
- Check the transfer success status
- 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.
// 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") | |
} |
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
debug_traceCall
method for improved call tracing.Bug Fixes
Documentation
CHANGELOG.md
to reflect new features and fixes.Tests