Skip to content

Commit

Permalink
feat(evm): draft of the erc-20 precompile
Browse files Browse the repository at this point in the history
  • Loading branch information
onikonychev committed Jul 1, 2024
1 parent 202a3bd commit 8a6a59a
Show file tree
Hide file tree
Showing 9 changed files with 946 additions and 6 deletions.
169 changes: 169 additions & 0 deletions precompiles/common/precompile.go
Original file line number Diff line number Diff line change
@@ -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
}
200 changes: 200 additions & 0 deletions precompiles/erc20/erc20.go
Original file line number Diff line number Diff line change
@@ -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"

Check failure on line 21 in precompiles/erc20/erc20.go

View workflow job for this annotation

GitHub Actions / lint

const `abiPath` is unused (unused)

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
}
Loading

0 comments on commit 8a6a59a

Please sign in to comment.