diff --git a/precompiles/common/precompile.go b/precompiles/common/precompile.go new file mode 100644 index 000000000..bee6cfc03 --- /dev/null +++ b/precompiles/common/precompile.go @@ -0,0 +1,169 @@ +package common + +import ( + "fmt" + + "github.com/NibiruChain/nibiru/x/evm/statedb" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/core/vm" +) + +// Precompile is a common struct for all precompiles that holds the common data each +// precompile needs to run which includes the ABI, Gas config, approval expiration and the authz keeper. +type Precompile struct { + abi.ABI + //AuthzKeeper authzkeeper.Keeper + KvGasConfig storetypes.GasConfig + TransientKVGasConfig storetypes.GasConfig +} + +// RequiredGas calculates the base minimum required gas for a transaction or a query. +// It uses the method ID to determine if the input is a transaction or a query and +// uses the Cosmos SDK gas config flat cost and the flat per byte cost * len(argBz) to calculate the gas. +func (p Precompile) RequiredGas(input []byte, isTransaction bool) uint64 { + argsBz := input[4:] + + if isTransaction { + return p.KvGasConfig.WriteCostFlat + (p.KvGasConfig.WriteCostPerByte * uint64(len(argsBz))) + } + + return p.KvGasConfig.ReadCostFlat + (p.KvGasConfig.ReadCostPerByte * uint64(len(argsBz))) +} + +// RunSetup runs the initial setup required to run a transaction or a query. +// It returns the sdk Context, EVM stateDB, ABI method, initial gas and calling arguments. +func (p Precompile) RunSetup( + evm *vm.EVM, + contract *vm.Contract, + readOnly bool, + isTransaction func(name string) bool, +) (ctx sdk.Context, stateDB *statedb.StateDB, method *abi.Method, gasConfig storetypes.Gas, args []interface{}, err error) { + stateDB, ok := evm.StateDB.(*statedb.StateDB) + if !ok { + return sdk.Context{}, nil, nil, uint64(0), nil, fmt.Errorf("not run in EVM") + } + ctx = stateDB.GetContext() + + // NOTE: This is a special case where the calling transaction does not specify a function name. + // In this case we default to a `fallback` or `receive` function on the contract. + + // Simplify the calldata checks + isEmptyCallData := len(contract.Input) == 0 + isShortCallData := len(contract.Input) > 0 && len(contract.Input) < 4 + isStandardCallData := len(contract.Input) >= 4 + + switch { + // Case 1: Calldata is empty + case isEmptyCallData: + method, err = p.emptyCallData(contract) + + // Case 2: calldata is non-empty but less than 4 bytes needed for a method + case isShortCallData: + method, err = p.methodIDCallData() + + // Case 3: calldata is non-empty and contains the minimum 4 bytes needed for a method + case isStandardCallData: + method, err = p.standardCallData(contract) + } + + if err != nil { + return sdk.Context{}, nil, nil, uint64(0), nil, err + } + + // return error if trying to write to state during a read-only call + if readOnly && isTransaction(method.Name) { + return sdk.Context{}, nil, nil, uint64(0), nil, vm.ErrWriteProtection + } + + // if the method type is `function` continue looking for arguments + if method.Type == abi.Function { + argsBz := contract.Input[4:] + args, err = method.Inputs.Unpack(argsBz) + if err != nil { + return sdk.Context{}, nil, nil, uint64(0), nil, err + } + } + + initialGas := ctx.GasMeter().GasConsumed() + + defer HandleGasError(ctx, contract, initialGas, &err)() + + // set the default SDK gas configuration to track gas usage + // we are changing the gas meter type, so it panics gracefully when out of gas + ctx = ctx.WithGasMeter(storetypes.NewGasMeter(contract.Gas)). + WithKVGasConfig(p.KvGasConfig). + WithTransientKVGasConfig(p.TransientKVGasConfig) + // we need to consume the gas that was already used by the EVM + ctx.GasMeter().ConsumeGas(initialGas, "creating a new gas meter") + + return ctx, stateDB, method, initialGas, args, nil +} + +// HandleGasError handles the out of gas panic by resetting the gas meter and returning an error. +// This is used in order to avoid panics and to allow for the EVM to continue cleanup if the tx or query run out of gas. +func HandleGasError(ctx sdk.Context, contract *vm.Contract, initialGas storetypes.Gas, err *error) func() { + return func() { + if r := recover(); r != nil { + switch r.(type) { + case sdk.ErrorOutOfGas: + // update contract gas + usedGas := ctx.GasMeter().GasConsumed() - initialGas + _ = contract.UseGas(usedGas) + + *err = vm.ErrOutOfGas + // FIXME: add InfiniteGasMeter with previous Gas limit. + ctx = ctx.WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) + default: + panic(r) + } + } + } +} + +// emptyCallData is a helper function that returns the method to be called when the calldata is empty. +func (p Precompile) emptyCallData(contract *vm.Contract) (method *abi.Method, err error) { + switch { + // Case 1.1: Send call or transfer tx - 'receive' is called if present and value is transferred + case contract.Value().Sign() > 0 && p.HasReceive(): + return &p.Receive, nil + // Case 1.2: Either 'receive' is not present, or no value is transferred - call 'fallback' if present + case p.HasFallback(): + return &p.Fallback, nil + // Case 1.3: Neither 'receive' nor 'fallback' are present - return error + default: + return nil, vm.ErrExecutionReverted + } +} + +// methodIDCallData is a helper function that returns the method to be called when the calldata is less than 4 bytes. +func (p Precompile) methodIDCallData() (method *abi.Method, err error) { + // Case 2.2: calldata contains less than 4 bytes needed for a method and 'fallback' is not present - return error + if !p.HasFallback() { + return nil, vm.ErrExecutionReverted + } + // Case 2.1: calldata contains less than 4 bytes needed for a method - 'fallback' is called if present + return &p.Fallback, nil +} + +// standardCallData is a helper function that returns the method to be called when the calldata is 4 bytes or more. +func (p Precompile) standardCallData(contract *vm.Contract) (method *abi.Method, err error) { + methodID := contract.Input[:4] + // NOTE: this function iterates over the method map and returns + // the method with the given ID + method, err = p.MethodById(methodID) + + // Case 3.1 calldata contains a non-existing method ID, and `fallback` is not present - return error + if err != nil && !p.HasFallback() { + return nil, err + } + + // Case 3.2: calldata contains a non-existing method ID - 'fallback' is called if present + if err != nil && p.HasFallback() { + return &p.Fallback, nil + } + + return method, nil +} diff --git a/precompiles/erc20/erc20.go b/precompiles/erc20/erc20.go new file mode 100644 index 000000000..8155ecffa --- /dev/null +++ b/precompiles/erc20/erc20.go @@ -0,0 +1,200 @@ +package erc20 + +import ( + "bytes" + "embed" + "fmt" + + precommon "github.com/NibiruChain/nibiru/precompiles/common" + "github.com/NibiruChain/nibiru/x/evm" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" +) + +const ( + // abiPath defines the path to the ERC-20 precompile ABI JSON file. + abiPath = "erc20_abi.json" + + GasTransfer = 3_000_000 + GasApprove = 30_956 + GasIncreaseAllowance = 34_605 + GasDecreaseAllowance = 34_519 + GasName = 3_421 + GasSymbol = 3_464 + GasDecimals = 427 + GasTotalSupply = 2_477 + GasBalanceOf = 2_851 + GasAllowance = 3_246 +) + +//go:embed erc20_abi.json +var f embed.FS + +var _ vm.PrecompiledContract = &Precompile{} + +// Precompile defines the precompiled contract for ERC-20. +type Precompile struct { + precommon.Precompile + funToken evm.FunToken + bankKeeper bankkeeper.Keeper +} + +// NewPrecompile creates a new ERC-20 Precompile +func NewPrecompile( + funToken evm.FunToken, + bankKeeper bankkeeper.Keeper, + // authzKeeper authzkeeper.Keeper, +) (*Precompile, error) { + abiBz, err := f.ReadFile("erc20_abi.json") + if err != nil { + return nil, fmt.Errorf("error loading ABI %s", err) + } + + newAbi, err := abi.JSON(bytes.NewReader(abiBz)) + if err != nil { + return nil, err + } + + return &Precompile{ + Precompile: precommon.Precompile{ + ABI: newAbi, + //AuthzKeeper: authzKeeper, + KvGasConfig: storetypes.GasConfig{}, + TransientKVGasConfig: storetypes.GasConfig{}, + }, + funToken: funToken, + bankKeeper: bankKeeper, + }, nil +} + +// Address defines the address of the ERC-20 precompile contract. +func (p Precompile) Address() common.Address { + return common.HexToAddress(p.funToken.Erc20Addr.String()) +} + +// RequiredGas calculates the contract gas used for the +func (p Precompile) RequiredGas(input []byte) uint64 { + // Validate input length + if len(input) < 4 { + return 0 + } + + methodID := input[:4] + method, err := p.MethodById(methodID) + if err != nil { + return 0 + } + + switch method.Name { + // ERC-20 transactions + case TransferMethod: + return GasTransfer + case TransferFromMethod: + return GasTransfer + //case auth.ApproveMethod: + // return GasApprove + //case auth.IncreaseAllowanceMethod: + // return GasIncreaseAllowance + //case auth.DecreaseAllowanceMethod: + // return GasDecreaseAllowance + // ERC-20 queries + case NameMethod: + return GasName + case SymbolMethod: + return GasSymbol + case DecimalsMethod: + return GasDecimals + case TotalSupplyMethod: + return GasTotalSupply + case BalanceOfMethod: + return GasBalanceOf + //case auth.AllowanceMethod: + // return GasAllowance + default: + return 0 + } +} + +// Run executes the precompiled contract ERC-20 methods defined in the ABI. +func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz []byte, err error) { + ctx, stateDB, method, initialGas, args, err := p.RunSetup(evm, contract, readOnly, p.IsTransaction) + if err != nil { + return nil, err + } + + // This handles any out of gas errors that may occur during the execution of a precompile tx or query. + // It avoids panics and returns the out of gas error so the EVM can continue gracefully. + defer precommon.HandleGasError(ctx, contract, initialGas, &err)() + + bz, err = p.HandleMethod(ctx, contract, stateDB, method, args) + if err != nil { + return nil, err + } + + cost := ctx.GasMeter().GasConsumed() - initialGas + + if !contract.UseGas(cost) { + return nil, vm.ErrOutOfGas + } + + return bz, nil +} + +// IsTransaction checks if the given method name corresponds to a transaction or query. +func (Precompile) IsTransaction(methodName string) bool { + switch methodName { + case TransferMethod, + TransferFromMethod: + //auth.ApproveMethod, + //auth.IncreaseAllowanceMethod, + //auth.DecreaseAllowanceMethod: + return true + default: + return false + } +} + +// HandleMethod handles the execution of each of the ERC-20 methods. +func (p Precompile) HandleMethod( + ctx sdk.Context, + contract *vm.Contract, + stateDB vm.StateDB, + method *abi.Method, + args []interface{}, +) (bz []byte, err error) { + switch method.Name { + // ERC-20 transactions + case TransferMethod: + bz, err = p.Transfer(ctx, contract, stateDB, method, args) + case TransferFromMethod: + bz, err = p.TransferFrom(ctx, contract, stateDB, method, args) + //case auth.ApproveMethod: + // bz, err = p.Approve(ctx, contract, stateDB, method, args) + //case auth.IncreaseAllowanceMethod: + // bz, err = p.IncreaseAllowance(ctx, contract, stateDB, method, args) + //case auth.DecreaseAllowanceMethod: + // bz, err = p.DecreaseAllowance(ctx, contract, stateDB, method, args) + // ERC-20 queries + case NameMethod: + bz, err = p.Name(ctx, contract, stateDB, method, args) + case SymbolMethod: + bz, err = p.Symbol(ctx, contract, stateDB, method, args) + case DecimalsMethod: + bz, err = p.Decimals(ctx, contract, stateDB, method, args) + case TotalSupplyMethod: + bz, err = p.TotalSupply(ctx, contract, stateDB, method, args) + case BalanceOfMethod: + bz, err = p.BalanceOf(ctx, contract, stateDB, method, args) + //case auth.AllowanceMethod: + // bz, err = p.Allowance(ctx, contract, stateDB, method, args) + //default: + // return nil, fmt.Errorf(precommon.ErrUnknownMethod, method.Name) + } + + return bz, err +} diff --git a/precompiles/erc20/erc20_abi.json b/precompiles/erc20/erc20_abi.json new file mode 100644 index 000000000..048513c1d --- /dev/null +++ b/precompiles/erc20/erc20_abi.json @@ -0,0 +1,310 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/precompiles/erc20/query.go b/precompiles/erc20/query.go new file mode 100644 index 000000000..ed7a281c9 --- /dev/null +++ b/precompiles/erc20/query.go @@ -0,0 +1,134 @@ +package erc20 + +import ( + "fmt" + "math/big" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" +) + +const ( + // NameMethod defines the ABI method name for the ERC-20 Name + // query. + NameMethod = "name" + // SymbolMethod defines the ABI method name for the ERC-20 Symbol + // query. + SymbolMethod = "symbol" + // DecimalsMethod defines the ABI method name for the ERC-20 Decimals + // query. + DecimalsMethod = "decimals" + // TotalSupplyMethod defines the ABI method name for the ERC-20 TotalSupply + // query. + TotalSupplyMethod = "totalSupply" + // BalanceOfMethod defines the ABI method name for the ERC-20 BalanceOf + // query. + BalanceOfMethod = "balanceOf" +) + +// Name returns the name of the token. If the token metadata is registered in the +// bank module, it returns its name. Otherwise, it returns the base denomination of +// the token capitalized (e.g. uatom -> Atom). +func (p Precompile) Name( + ctx sdk.Context, + _ *vm.Contract, + _ vm.StateDB, + method *abi.Method, + _ []interface{}, +) ([]byte, error) { + panic("not implemented") +} + +// Symbol returns the symbol of the token. If the token metadata is registered in the +// bank module, it returns its symbol. Otherwise, it returns the base denomination of +// the token in uppercase (e.g. uatom -> ATOM). +func (p Precompile) Symbol( + ctx sdk.Context, + _ *vm.Contract, + _ vm.StateDB, + method *abi.Method, + _ []interface{}, +) ([]byte, error) { + panic("not implemented") +} + +// Decimals returns the decimals places of the token. If the token metadata is registered in the +// bank module, it returns the display denomination exponent. Otherwise, it infers the decimal +// value from the first character of the base denomination (e.g. uatom -> 6). +func (p Precompile) Decimals( + ctx sdk.Context, + _ *vm.Contract, + _ vm.StateDB, + method *abi.Method, + _ []interface{}, +) ([]byte, error) { + panic("not implemented") +} + +// TotalSupply returns the amount of tokens in existence. It fetches the supply +// of the coin from the bank keeper and returns zero if not found. +func (p Precompile) TotalSupply( + ctx sdk.Context, + _ *vm.Contract, + _ vm.StateDB, + method *abi.Method, + _ []interface{}, +) ([]byte, error) { + supply := p.bankKeeper.GetSupply(ctx, p.funToken.BankDenom) + return method.Outputs.Pack(supply.Amount.BigInt()) +} + +// BalanceOf returns the amount of tokens owned by account. It fetches the balance +// of the coin from the bank keeper and returns zero if not found. +func (p Precompile) BalanceOf( + ctx sdk.Context, + _ *vm.Contract, + _ vm.StateDB, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 1 { + return nil, fmt.Errorf("invalid number of arguments; expected 1; got: %d", len(args)) + } + account, ok := args[0].(common.Address) + if !ok { + return nil, fmt.Errorf("invalid account address: %v", args[0]) + } + + balance := p.bankKeeper.GetBalance(ctx, account.Bytes(), p.funToken.BankDenom) + + return method.Outputs.Pack(balance.Amount.BigInt()) +} + +// Allowance returns the remaining allowance of a spender to the contract by +// checking the existence of a bank SendAuthorization. +func (p Precompile) Allowance( + ctx sdk.Context, + _ *vm.Contract, + _ vm.StateDB, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + panic("not implemented") +} + +// GetAuthzExpirationAndAllowance returns the authorization, its expiration as well as the amount of denom +// that the grantee is allowed to spend on behalf of the granter. +func GetAuthzExpirationAndAllowance( + authzKeeper authzkeeper.Keeper, + ctx sdk.Context, + grantee, granter common.Address, + denom string, +) (authz.Authorization, *time.Time, *big.Int, error) { + panic("not implemented") +} + +// getBaseDenomFromIBCVoucher returns the base denomination from the given IBC voucher denomination. +func (p Precompile) getBaseDenomFromIBCVoucher(ctx sdk.Context, denom string) (string, error) { + panic("not implemented") +} diff --git a/precompiles/erc20/tx.go b/precompiles/erc20/tx.go new file mode 100644 index 000000000..ac99367e6 --- /dev/null +++ b/precompiles/erc20/tx.go @@ -0,0 +1,61 @@ +package erc20 + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" +) + +const ( + // TransferMethod defines the ABI method name for the ERC-20 transfer + // transaction. + TransferMethod = "transfer" + // TransferFromMethod defines the ABI method name for the ERC-20 transferFrom + // transaction. + TransferFromMethod = "transferFrom" +) + +// SendMsgURL defines the authorization type for MsgSend +var SendMsgURL = sdk.MsgTypeURL(&banktypes.MsgSend{}) + +// Transfer executes a direct transfer from the caller address to the +// destination address. +func (p Precompile) Transfer( + ctx sdk.Context, + contract *vm.Contract, + stateDB vm.StateDB, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + panic("not implemented") +} + +// TransferFrom executes a transfer on behalf of the specified from address in +// the call data to the destination address. +func (p Precompile) TransferFrom( + ctx sdk.Context, + contract *vm.Contract, + stateDB vm.StateDB, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + panic("not implemented") +} + +// transfer is a common function that handles transfers for the ERC-20 Transfer +// and TransferFrom methods. It executes a bank Send message if the spender is +// the sender of the transfer, otherwise it executes an authorization. +func (p Precompile) transfer( + ctx sdk.Context, + contract *vm.Contract, + stateDB vm.StateDB, + method *abi.Method, + from, to common.Address, + amount *big.Int, +) (data []byte, err error) { + panic("not implemented") +} diff --git a/x/evm/cli/cli.go b/x/evm/cli/cli.go index 2fe7b80b8..a71f260ee 100644 --- a/x/evm/cli/cli.go +++ b/x/evm/cli/cli.go @@ -5,6 +5,8 @@ import ( "github.com/NibiruChain/nibiru/x/evm" "github.com/NibiruChain/nibiru/x/sudo/types" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/client" @@ -21,7 +23,9 @@ func GetTxCmd() *cobra.Command { RunE: client.ValidateCmd, } - cmds := []*cobra.Command{} + cmds := []*cobra.Command{ + CmdCreateFunTokenFromCoin(), + } for _, cmd := range cmds { txCmd.AddCommand(cmd) } @@ -48,3 +52,35 @@ func GetQueryCmd() *cobra.Command { return moduleQueryCmd } + +// CmdCreateFunTokenFromCoin broadcast MsgCreateFunTokenFromCoin +func CmdCreateFunTokenFromCoin() *cobra.Command { + cmd := &cobra.Command{ + Use: "create-fun-token-from-coin [denom] [flags]", + Short: `Create an evm fungible token from coin [denom]"`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + txFactory, err := tx.NewFactoryCLI(clientCtx, cmd.Flags()) + if err != nil { + return err + } + + txFactory = txFactory.WithTxConfig(clientCtx.TxConfig).WithAccountRetriever( + clientCtx.AccountRetriever) + + msg := &evm.MsgCreateFunTokenFromCoin{ + Sender: clientCtx.GetFromAddress().String(), + Denom: args[0], + } + return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txFactory, msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} diff --git a/x/evm/keeper/msg_fun_token.go b/x/evm/keeper/msg_fun_token.go index 18ba86565..127689a27 100644 --- a/x/evm/keeper/msg_fun_token.go +++ b/x/evm/keeper/msg_fun_token.go @@ -3,7 +3,9 @@ package keeper import ( "context" + "github.com/NibiruChain/nibiru/precompiles/erc20" sdk "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" "github.com/ethereum/go-ethereum/common" "github.com/NibiruChain/nibiru/eth/crypto/ethsecp256k1" @@ -17,17 +19,33 @@ func (k *Keeper) CreateFunTokenFromCoin( if err := msg.ValidateBasic(); err != nil { return nil, err } + // Generate contract address. TODO: consider using incremental addresses priv, err := ethsecp256k1.GenerateKey() if err != nil { return nil, err } newContractAddress := common.BytesToAddress(priv.PubKey().Address().Bytes()) ctx := sdk.UnwrapSDKContext(goCtx) + + // Save pair in fungible tokens mapping err = k.FunTokens.SafeInsert(ctx, newContractAddress, msg.Denom, true) if err != nil { return nil, err } + // Create and register new ERC-20 precompile + erc20Precompile, err := erc20.NewPrecompile( + evm.NewFunToken(newContractAddress, msg.Denom, true), + k.bankKeeper.(bankkeeper.Keeper), + ) + if err != nil { + return nil, err + } + err = k.AddEVMExtensions(ctx, erc20Precompile) + if err != nil { + return nil, err + } + _ = ctx.EventManager().EmitTypedEvent(&evm.EventCreateFunTokenFromCoin{ Creator: msg.Sender, Denom: msg.Denom, diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 93259f963..25073f46d 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -387,9 +387,11 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, if cfg.Params.HasCustomPrecompiles() { customPrecompiles := cfg.Params.GetActivePrecompilesAddrs() - activePrecompiles := make([]common.Address, len(vm.PrecompiledAddressesBerlin)+len(customPrecompiles)) - copy(activePrecompiles[:len(vm.PrecompiledAddressesBerlin)], vm.PrecompiledAddressesBerlin) - copy(activePrecompiles[len(vm.PrecompiledAddressesBerlin):], customPrecompiles) + //activePrecompiles := make([]common.Address, len(vm.PrecompiledAddressesBerlin)+len(customPrecompiles)) + activePrecompiles := make([]common.Address, len(customPrecompiles)) + //copy(activePrecompiles[:len(vm.PrecompiledAddressesBerlin)], vm.PrecompiledAddressesBerlin) + //copy(activePrecompiles[len(vm.PrecompiledAddressesBerlin):], customPrecompiles) + copy(activePrecompiles[:len(customPrecompiles)], customPrecompiles) // Check if the transaction is sent to an inactive precompile // diff --git a/x/evm/keeper/precompiles.go b/x/evm/keeper/precompiles.go index be7b0d0be..419d5ccf0 100644 --- a/x/evm/keeper/precompiles.go +++ b/x/evm/keeper/precompiles.go @@ -8,11 +8,16 @@ import ( "sort" sdk "github.com/cosmos/cosmos-sdk/types" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" ) -func AvailablePrecompiles() map[common.Address]vm.PrecompiledContract { +func AvailablePrecompiles( + bankKeeper bankkeeper.Keeper, + authzKeeper authzkeeper.Keeper, +) map[common.Address]vm.PrecompiledContract { contractMap := make(map[common.Address]vm.PrecompiledContract) // The following TODOs can go in an epic together. // TODO: feat(evm): implement precompiled contracts for fungible tokens @@ -60,7 +65,12 @@ func (k *Keeper) AddEVMExtensions( params := k.GetParams(ctx) addresses := make([]string, len(precompiles)) - precompilesMap := maps.Clone(k.precompiles) + var precompilesMap map[common.Address]vm.PrecompiledContract + if k.precompiles != nil { + precompilesMap = maps.Clone(k.precompiles) + } else { + precompilesMap = make(map[common.Address]vm.PrecompiledContract) + } for i, precompile := range precompiles { // add to active precompiles