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

Mat/prevent invalid funtoken #2150

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions x/evm/keeper/funtoken_from_erc20.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,10 @@ type (
// This function performs the following steps:
// 1. Checks if the ERC20 token is already registered as a FunToken.
// 2. Retrieves the metadata of the existing ERC20 token.
// 3. Verifies that the corresponding bank coin denom is not already registered.
// 4. Sets the bank coin denom metadata in the state.
// 5. Creates and inserts the new FunToken mapping.
// 3. Verifies that the ERC20 token include expected functions.
// 4. Verifies that the corresponding bank coin denom is not already registered.
// 5. Sets the bank coin denom metadata in the state.
// 6. Creates and inserts the new FunToken mapping.
//
// Parameters:
// - ctx: The SDK context for the transaction.
Expand Down Expand Up @@ -121,7 +122,12 @@ func (k *Keeper) createFunTokenFromERC20(

bankDenom := fmt.Sprintf("erc20/%s", erc20.String())

// 3 | Coin already registered with FunToken?
// 3 | Verify that the ERC20 token include expected functions
if err := k.checkErc20ImplementsAllRequired(ctx, erc20); err != nil {
return funtoken, err
}

// 4 | Coin already registered with FunToken?
_, isFound := k.Bank.GetDenomMetaData(ctx, bankDenom)
if isFound {
return funtoken, fmt.Errorf("bank coin denom already registered with denom \"%s\"", bankDenom)
Expand All @@ -130,7 +136,7 @@ func (k *Keeper) createFunTokenFromERC20(
return funtoken, fmt.Errorf("funtoken mapping already created for bank denom \"%s\"", bankDenom)
}

// 4 | Set bank coin denom metadata in state
// 5 | Set bank coin denom metadata in state
bankMetadata := erc20Info.ToBankMetadata(bankDenom, erc20)

err = bankMetadata.Validate()
Expand All @@ -139,7 +145,7 @@ func (k *Keeper) createFunTokenFromERC20(
}
k.Bank.SetDenomMetaData(ctx, bankMetadata)

// 5 | Officially create the funtoken mapping
// 6 | Officially create the funtoken mapping
funtoken = &evm.FunToken{
Erc20Addr: eth.EIP55Addr{
Address: erc20,
Expand All @@ -153,6 +159,14 @@ func (k *Keeper) createFunTokenFromERC20(
)
}

func (k *Keeper) checkErc20ImplementsAllRequired(
ctx sdk.Context, erc20 gethcommon.Address,
) error {
// Check if the ERC20 token implements the required functions
// This is a placeholder for actual implementation
return nil
}

// ToBankMetadata produces the "bank.Metadata" corresponding to a FunToken
// mapping created from an ERC20 token.
//
Expand Down
1 change: 1 addition & 0 deletions x/evm/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context,
}

sender := vm.AccountRef(msg.From())

contractCreation := msg.To() == nil

intrinsicGas, err := core.IntrinsicGas(
Expand Down
111 changes: 111 additions & 0 deletions x/evm/keeper/validate_contract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package keeper

import (
"context"
"encoding/json"
"fmt"
"math/big"
"strings"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"

"github.com/NibiruChain/nibiru/v2/x/evm"
)

// HasMethodInContract does a staticcall with the given `method`'s selector + dummy args.
// If the call reverts with something like "function selector not recognized", returns false.
//
// In your real code, this likely needs to invoke `k.evmKeeper.CallEVM` or similar.
func (k Keeper) HasMethodInContract(
goCtx context.Context,
contractAddr common.Address,
method abi.Method,
) (bool, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

// 1. Build input (4-byte selector + encoded args).
// We choose dummy arguments based on the method signature.
// For example, if method = "balanceOf(address)", we pass a zero address or some known address.
// For method = "transfer(address,uint256)", pass a dummy address and zero uint256, etc.
//
// To illustrate, let's say we pass "0x000000000000000000000000000000000000dEaD" for addresses,
// and 0 for all numeric arguments. This is *just* for signature detection.
dummyArgs := make([]interface{}, len(method.Inputs))
for i, inputDef := range method.Inputs {
switch inputDef.Type.T {
case abi.AddressTy:
dummyArgs[i] = common.HexToAddress("0x000000000000000000000000000000000000dEaD")
case abi.UintTy, abi.IntTy:
dummyArgs[i] = big.NewInt(0)
case abi.BoolTy:
dummyArgs[i] = false
case abi.StringTy:
dummyArgs[i] = ""
default:
// For any types you don't specifically handle, either supply some default
// or handle them according to what your use case needs.
dummyArgs[i] = nil
}
}

input, err := method.Inputs.Pack(dummyArgs...)
if err != nil {
return false, fmt.Errorf("packing dummy args: %w", err)
}

// Prepend the 4-byte method selector
sig := method.ID
callData := append(sig, input...)

// 2. Make a call message
callMsg := evm.JsonTxArgs{
From: &contractAddr,
To: &contractAddr,
Input: (*hexutil.Bytes)(&callData),
}

jsonTxArgs, err := json.Marshal(&callMsg)
if err != nil {
return false, fmt.Errorf("marshaling call message: %w", err)
}

ethCallRequest := evm.EthCallRequest{
Args: jsonTxArgs,
GasCap: 2100000,
ProposerAddress: sdk.ConsAddress(ctx.BlockHeader().ProposerAddress),
ChainId: k.EthChainID(ctx).Int64(),
}

_, err = k.EstimateGasForEvmCallType(goCtx, &ethCallRequest, evm.CallTypeRPC)

if err == nil {
return true, nil
}

if strings.Contains(err.Error(), "caller is not the owner") {
return true, nil
}

return false, nil
}

// checkAllMethods ensure the contract at `contractAddr` has all the methods in `abiMethods`.
func (k Keeper) CheckAllethods(
ctx context.Context,
contractAddr common.Address,
abiMethods []abi.Method,
) error {
for name, method := range abiMethods {
hasMethod, err := k.HasMethodInContract(ctx, contractAddr, method)
if err != nil {
return err
}
if !hasMethod {
return fmt.Errorf("Method %q not found in contract at %s", name, contractAddr)
}
}
return nil
}
104 changes: 104 additions & 0 deletions x/evm/keeper/validate_contract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package keeper_test

import (
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/stretchr/testify/require"

"github.com/NibiruChain/nibiru/v2/x/evm/embeds"
"github.com/NibiruChain/nibiru/v2/x/evm/evmtest"
)

// TestHasMethodInContract_RealKeeper deploys a real ERC20 contract and tests
// the presence/absence of a couple of methods using the actual keeper logic.
func TestHasMethodInContract_RealKeeper(t *testing.T) {
// 1) Build standard test dependencies
deps := evmtest.NewTestDeps()
ctx := sdk.WrapSDKContext(deps.Ctx)
k := deps.App.EvmKeeper

// 2) Deploy the standard ERC20 (Minter) contract
deployResp, err := evmtest.DeployContract(
&deps,
embeds.SmartContract_ERC20Minter,
"ExampleToken",
"EXM",
uint8(18),
)
require.NoError(t, err, "error deploying ERC20 test contract")

// 3) The embedded ERC20 ABI includes balanceOf, transfer, decimals, etc.
erc20Abi := embeds.SmartContract_ERC20Minter.ABI

// For demonstration, let's see if the contract implements "balanceOf"
methodBalanceOf, ok := erc20Abi.Methods["balanceOf"]
require.True(t, ok, `"balanceOf" not found in the ERC20 ABI?`)

// // Now let's see if the keeper says "balanceOf" is recognized
hasMethod, err := k.HasMethodInContract(ctx, deployResp.ContractAddr, methodBalanceOf)
require.NoError(t, err)
require.True(t, hasMethod, "expected contract to have 'balanceOf'")

// 4) Next, let's test a fake method that doesn't exist
fakeMethod := methodBalanceOf
fakeMethod.Name = "someFakeMethod"
fakeMethod.ID = []byte{0xef}

hasMethod, err = k.HasMethodInContract(ctx, deployResp.ContractAddr, fakeMethod)
require.NoError(t, err, "non-existent method calls shouldn't produce a real EVM error")
require.False(t, hasMethod, "expected the contract to NOT have 'someFakeMethod'")
}

// TestCheckAllMethods_RealKeeper uses your keeper’s checkAllethods (assuming
// you renamed it from “checkAllMethods” to a public name).
func TestCheckAllMethods_RealKeeper(t *testing.T) {
// Build test dependencies and context
deps := evmtest.NewTestDeps()
ctx := sdk.WrapSDKContext(deps.Ctx)
k := deps.App.EvmKeeper

// Deploy a standard ERC20 contract
deployResp, err := evmtest.DeployContract(
&deps,
embeds.SmartContract_ERC20Minter,
"DemoToken",
"DMO",
uint8(6),
)
require.NoError(t, err)

// Example: We want to check that it has "balanceOf" and "transfer", but *not* "fakeMethod"
erc20Abi := embeds.SmartContract_ERC20Minter.ABI

// Gather the actual method objects from the ABI
balanceOfMethod, hasBalanceOf := erc20Abi.Methods["balanceOf"]
require.True(t, hasBalanceOf)
transferMethod, hasTransfer := erc20Abi.Methods["transfer"]
require.True(t, hasTransfer)

// Let's also define a known-fake method
fakeMethod := abi.Method{
Name: "fakeMethod",
ID: []byte{0xfa, 0x75, 0x55, 0x0f}, // random
}

// Scenario 1: "balanceOf" + "transfer" => no error
allMethods := []abi.Method{balanceOfMethod, transferMethod}
err = k.CheckAllethods(ctx, deployResp.ContractAddr, allMethods)
require.NoError(t, err, "both balanceOf and transfer exist in standard ERC20")

// Scenario 2: "balanceOf" + "fakeMethod" => we expect an error on second
calls := []abi.Method{balanceOfMethod, fakeMethod}
err = k.CheckAllethods(ctx, deployResp.ContractAddr, calls)
require.Error(t, err, "contract does not have 'fakeMethod'")
require.Contains(t, err.Error(), "not found in contract")

// Scenario 3: check all abi methods
for name, method := range erc20Abi.Methods {
hasMethod, err := k.HasMethodInContract(ctx, deployResp.ContractAddr, method)
require.NoError(t, err)
require.True(t, hasMethod, "expected contract to have %q", name)
}
}
1 change: 0 additions & 1 deletion x/evm/precompile/funtoken_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,6 @@ func (s *FuntokenSuite) TestSendToEvm() {
s.T().Log("1) Create a new FunToken from coin 'ulibi'")
bankDenom := "ulibi"
funtoken := evmtest.CreateFunTokenForBankCoin(&deps, bankDenom, &s.Suite)
fmt.Println(funtoken)
erc20Addr := funtoken.Erc20Addr.Address

s.T().Log("2) Fund the sender with some ulibi on the bank side")
Expand Down
Loading