-
Notifications
You must be signed in to change notification settings - Fork 195
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(evm): draft of the erc-20 precompile
- Loading branch information
1 parent
202a3bd
commit 8a6a59a
Showing
9 changed files
with
946 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
||
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 | ||
} |
Oops, something went wrong.