Skip to content

Commit

Permalink
more tests and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Unique-Divine committed Oct 1, 2024
1 parent cde8803 commit 24c07cf
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 108 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): remove unnecessary interface code
- [#2044](https://github.com/NibiruChain/nibiru/pull/2044) - feat(evm): evm tx indexer service implemented
- [#2045](https://github.com/NibiruChain/nibiru/pull/2045) - test(evm): backend tests with test network and real txs
- [#2xxx](https://github.com/NibiruChain/nibiru/pull/2xxx) - feat(evm-precompile): Precompile for one-way EVM calls to invoke/execute Wasm contracts.
- [#2054](https://github.com/NibiruChain/nibiru/pull/2054) - feat(evm-precompile): Precompile for one-way EVM calls to invoke/execute Wasm contracts.

#### Dapp modules: perp, spot, oracle, etc

Expand Down
17 changes: 6 additions & 11 deletions x/evm/precompile/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"

gethabi "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/core/vm"
)

Expand All @@ -17,6 +18,10 @@ func ErrInvalidArgs(err error) error {
return fmt.Errorf("invalid method args: %w", err)
}

func ErrMethodCalled(method *gethabi.Method, wrapped error) error {
return fmt.Errorf("%s method called: %w", method.Name, wrapped)
}

// Check required for transactions but not needed for queries
func assertNotReadonlyTx(readOnly bool, isTx bool) error {
if readOnly && isTx {
Expand All @@ -32,19 +37,9 @@ func assertContractQuery(contract *vm.Contract) error {
weiValue := contract.Value()
if weiValue != nil && weiValue.Sign() != 0 {
return fmt.Errorf(
"funds (value) must not be expended calling a query function; received wei value %s", weiValue,
"funds (wei value) must not be expended calling a query function; received wei value %s", weiValue,
)

Check warning on line 41 in x/evm/precompile/errors.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/errors.go#L39-L41

Added lines #L39 - L41 were not covered by tests
}

return nil
}

// assertNumArgs checks if the number of provided arguments matches the expected
// count. If lenArgs does not equal wantArgsLen, it returns an error describing
// the mismatch between expected and actual argument counts.
func assertNumArgs(lenArgs, wantArgsLen int) error {
if lenArgs != wantArgsLen {
return fmt.Errorf("expected %d arguments but got %d", wantArgsLen, lenArgs)
}
return nil
}
47 changes: 21 additions & 26 deletions x/evm/precompile/funtoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ func (p precompileFunToken) Address() gethcommon.Address {
return PrecompileAddr_FunToken
}

// RequiredGas calculates the contract gas used
func (p precompileFunToken) RequiredGas(input []byte) (gasPrice uint64) {
// RequiredGas calculates the cost of calling the precompile in gas units.
func (p precompileFunToken) RequiredGas(input []byte) (gasCost uint64) {
// Since [gethparams.TxGas] is the cost per (Ethereum) transaction that does not create
// a contract, it's value can be used to derive an appropriate value for the
// precompile call. The FunToken precompile performs 3 operations, labeled 1-3
Expand Down Expand Up @@ -79,11 +79,10 @@ func (p precompileFunToken) Run(

switch PrecompileMethod(method.Name) {
case FunTokenMethod_BankSend:
// TODO: UD-DEBUG: Test that calling non-method on the right address does
// nothing.
bz, err = p.bankSend(ctx, contract.CallerAddress, method, args, readonly)
default:
// TODO: UD-DEBUG: test invalid method called
// Note that this code path should be impossible to reach since
// "DecomposeInput" parses methods directly from the ABI.
err = fmt.Errorf("invalid method called with name \"%s\"", method.Name)
return
}
Expand All @@ -105,28 +104,27 @@ type precompileFunToken struct {

var executionGuard sync.Mutex

/*
bankSend: Implements "IFunToken.bankSend"
The "args" populate the following function signature in Solidity:
```solidity
/// @dev bankSend sends ERC20 tokens as coins to a Nibiru base account
/// @param erc20 the address of the ERC20 token contract
/// @param amount the amount of tokens to send
/// @param to the receiving Nibiru base account address as a string
function bankSend(address erc20, uint256 amount, string memory to) external;
```
*/
// bankSend: Implements "IFunToken.bankSend"
//
// The "args" populate the following function signature in Solidity:
//
// ```solidity
// /// @dev bankSend sends ERC20 tokens as coins to a Nibiru base account
// /// @param erc20 the address of the ERC20 token contract
// /// @param amount the amount of tokens to send
// /// @param to the receiving Nibiru base account address as a string
// function bankSend(address erc20, uint256 amount, string memory to) external;
// ```
func (p precompileFunToken) bankSend(
ctx sdk.Context,
caller gethcommon.Address,
method *gethabi.Method,
args []any,
readOnly bool,
) (bz []byte, err error) {
if readOnly {
// Check required for transactions but not needed for queries
return nil, fmt.Errorf("cannot write state from staticcall (a read-only call)")
if e := assertNotReadonlyTx(readOnly, true); e != nil {
err = e
return

Check warning on line 127 in x/evm/precompile/funtoken.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/funtoken.go#L126-L127

Added lines #L126 - L127 were not covered by tests
}
if !executionGuard.TryLock() {
return nil, fmt.Errorf("bankSend is already in progress")
Expand Down Expand Up @@ -197,7 +195,6 @@ func (p precompileFunToken) bankSend(
}

// TODO: UD-DEBUG: feat: Emit EVM events
// TODO: UD-DEBUG: feat: Emit ABCI events

return method.Outputs.Pack()
}
Expand All @@ -208,11 +205,9 @@ func (p precompileFunToken) decomposeBankSendArgs(args []any) (
to string,
err error,
) {
wantArgsLen := 3
if len(args) != wantArgsLen {
err = fmt.Errorf("expected %d arguments but got %d", wantArgsLen, len(args))
return
}
// Note: The number of arguments is valiated before this function is called
// during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack",
// which validates against the the structure of the precompile's ABI.

erc20, ok := args[0].(gethcommon.Address)
if !ok {
Expand Down
27 changes: 27 additions & 0 deletions x/evm/precompile/precompile.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import (
"fmt"

"github.com/NibiruChain/collections"
store "github.com/cosmos/cosmos-sdk/store/types"
gethabi "github.com/ethereum/go-ethereum/accounts/abi"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
gethparams "github.com/ethereum/go-ethereum/params"

"github.com/NibiruChain/nibiru/v2/app/keepers"
)
Expand Down Expand Up @@ -102,3 +104,28 @@ func DecomposeInput(

return method, args, nil
}

func RequiredGas(input []byte, abi *gethabi.ABI) uint64 {
method, _, err := DecomposeInput(abi, input)
if err != nil {
// It's appropriate to return a reasonable default here
// because the error from DecomposeInput will be handled automatically by
// "Run". In go-ethereum/core/vm/contracts.go, you can see the execution
// order of a precompile in the "runPrecompiledContract" function.
return gethparams.TxGas // return reasonable default

Check warning on line 115 in x/evm/precompile/precompile.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/precompile.go#L115

Added line #L115 was not covered by tests
}
gasCfg := store.KVGasConfig()

// Map access could panic. We know that it won't panic because all methods
// are in the map, which is verified by unit tests.
methodIsTx := precompileMethodIsTxMap[PrecompileMethod(method.Name)]
var costPerByte, costFlat uint64
if methodIsTx {
costPerByte, costFlat = gasCfg.WriteCostPerByte, gasCfg.WriteCostFlat
} else {
costPerByte, costFlat = gasCfg.ReadCostPerByte, gasCfg.ReadCostFlat
}

argsBzLen := uint64(len(input[4:]))
return (costPerByte * argsBzLen) + costFlat
}
88 changes: 60 additions & 28 deletions x/evm/precompile/wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
gethabi "github.com/ethereum/go-ethereum/accounts/abi"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
gethparams "github.com/ethereum/go-ethereum/params"

"github.com/NibiruChain/nibiru/v2/x/evm/statedb"
)
Expand All @@ -33,6 +32,16 @@ const (
WasmMethod_queryRaw PrecompileMethod = "queryRaw"
)

var precompileMethodIsTxMap map[PrecompileMethod]bool = map[PrecompileMethod]bool{
WasmMethod_execute: true,
WasmMethod_instantiate: true,
WasmMethod_executeMulti: true,
WasmMethod_query: false,
WasmMethod_queryRaw: false,

FunTokenMethod_BankSend: true,
}

// Wasm: A struct embedding keepers for read and write operations in Wasm, such
// as execute, query, and instantiate.
type Wasm struct {
Expand All @@ -57,10 +66,9 @@ func (p precompileWasm) Address() gethcommon.Address {
return PrecompileAddr_Wasm
}

// RequiredGas calculates the contract gas used
func (p precompileWasm) RequiredGas(input []byte) (gasPrice uint64) {
return gethparams.TxGas
// TODO: Lower gas requirement for queries in comparison to txs
// RequiredGas calculates the cost of calling the precompile in gas units.
func (p precompileWasm) RequiredGas(input []byte) (gasCost uint64) {
return RequiredGas(input, embeds.SmartContract_Wasm.ABI)
}

// Run runs the precompiled contract
Expand Down Expand Up @@ -100,38 +108,47 @@ func (p precompileWasm) Run(
case WasmMethod_queryRaw:
bz, err = p.queryRaw(ctx, method, args, contract)
default:

Check warning on line 110 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L110

Added line #L110 was not covered by tests
// Note that this code path should be impossible to reach since
// "DecomposeInput" parses methods directly from the ABI.
err = fmt.Errorf("invalid method called with name \"%s\"", method.Name)
return

Check warning on line 114 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L113-L114

Added lines #L113 - L114 were not covered by tests
}

return
}

// execute invokes a Wasm contract's "ExecuteMsg", which corresponds to
// execute invokes a Wasm contract's "ExecuteMsg", which corresponds to
// "wasm/types/MsgExecuteContract". This enables arbitrary smart contract
// execution using the Wasm VM from the EVM.
//
// Implements "execute" from evm/embeds/contracts/Wasm.sol:
//
// ```solidity
// /// @param contractAddr nibi-prefixed Bech32 address of the wasm contract
// /// @param msgArgs JSON encoded wasm execute invocation
// /// @param funds Optional funds to supply during the execute call. It's
// /// uncommon to use this field, so you'll pass an empty array most of the time.
// /// @dev The three non-struct arguments are more gas efficient than encoding a
// /// single argument as a WasmExecuteMsg.
// function query(
// string memory contractAddr,
// bytes memory req
// ) external view returns (bytes memory response);
// ```
// ```solidity
// function execute(
// string memory contractAddr,
// bytes memory msgArgs,
// BankCoin[] memory funds
// ) payable external returns (bytes memory response);
// ```
//
// Contract Args:
// - contractAddr: nibi-prefixed Bech32 address of the wasm contract
// - msgArgs: JSON encoded wasm execute invocation
// - funds: Optional funds to supply during the execute call. It's
// uncommon to use this field, so you'll pass an empty array most of the time.
func (p precompileWasm) execute(
ctx sdk.Context,
caller gethcommon.Address,
method *gethabi.Method,
args []any,
readOnly bool,
) (bz []byte, err error) {
defer func() {
if err != nil {
err = ErrMethodCalled(method, err)
}
}()

if err := assertNotReadonlyTx(readOnly, true); err != nil {
return bz, err

Check warning on line 153 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L153

Added line #L153 was not covered by tests
}
Expand All @@ -143,7 +160,6 @@ func (p precompileWasm) execute(
callerBech32 := eth.EthAddrToNibiruAddr(caller)
data, err := p.Wasm.Execute(ctx, wasmContract, callerBech32, msgArgs, funds)
if err != nil {
err = fmt.Errorf("Execute failed: %w", err)
return
}
return method.Outputs.Pack(data)
Expand All @@ -167,6 +183,11 @@ func (p precompileWasm) query(
args []any,
contract *vm.Contract,
) (bz []byte, err error) {
defer func() {
if err != nil {
err = ErrMethodCalled(method, err)

Check warning on line 188 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L188

Added line #L188 was not covered by tests
}
}()
if err := assertContractQuery(contract); err != nil {
return bz, err

Check warning on line 192 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L192

Added line #L192 was not covered by tests
}
Expand All @@ -177,7 +198,6 @@ func (p precompileWasm) query(
}
respBz, err := p.Wasm.QuerySmart(ctx, wasmContract, req)
if err != nil {
err = fmt.Errorf("Query failed: %w", err)
return

Check warning on line 201 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L201

Added line #L201 was not covered by tests
}
return method.Outputs.Pack(respBz)
Expand Down Expand Up @@ -209,6 +229,11 @@ func (p precompileWasm) instantiate(
args []any,
readOnly bool,
) (bz []byte, err error) {
defer func() {
if err != nil {
err = ErrMethodCalled(method, err)

Check warning on line 234 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L234

Added line #L234 was not covered by tests
}
}()
if err := assertNotReadonlyTx(readOnly, true); err != nil {
return bz, err

Check warning on line 238 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L238

Added line #L238 was not covered by tests
}
Expand All @@ -228,7 +253,6 @@ func (p precompileWasm) instantiate(
ctx, txMsg.CodeID, callerBech32, adminAddr, txMsg.Msg, txMsg.Label, txMsg.Funds,
)
if err != nil {
err = fmt.Errorf("Instantiate failed: %w", err)
return

Check warning on line 256 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L256

Added line #L256 was not covered by tests
}

Expand Down Expand Up @@ -257,6 +281,11 @@ func (p precompileWasm) executeMulti(
args []any,
readOnly bool,
) (bz []byte, err error) {
defer func() {
if err != nil {
err = ErrMethodCalled(method, err)

Check warning on line 286 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L286

Added line #L286 was not covered by tests
}
}()
if err := assertNotReadonlyTx(readOnly, true); err != nil {
return bz, err

Check warning on line 290 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L290

Added line #L290 was not covered by tests
}
Expand Down Expand Up @@ -284,7 +313,7 @@ func (p precompileWasm) executeMulti(
}
respBz, e := p.Wasm.Execute(ctx, wasmContract, callerBech32, m.MsgArgs, funds)
if e != nil {
err = fmt.Errorf("Execute failed: %w", e)
err = e
return

Check warning on line 317 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L316-L317

Added lines #L316 - L317 were not covered by tests
}
responses = append(responses, respBz)
Expand Down Expand Up @@ -317,18 +346,21 @@ func (p precompileWasm) queryRaw(
args []any,
contract *vm.Contract,
) (bz []byte, err error) {
defer func() {
if err != nil {
err = ErrMethodCalled(method, err)

Check warning on line 351 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L351

Added line #L351 was not covered by tests
}
}()
if err := assertContractQuery(contract); err != nil {
return bz, err

Check warning on line 355 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L355

Added line #L355 was not covered by tests
}

wantArgsLen := 2
if len(args) != wantArgsLen {
err = fmt.Errorf("expected %d arguments but got %d", wantArgsLen, len(args))
return
}
// Note: The number of arguments is valiated before this function is called
// during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack",
// which validates against the the structure of the precompile's ABI.

argIdx := 0
wasmContract, e := parseContraAddrArg(args[argIdx])
wasmContract, e := parseContractAddrArg(args[argIdx])
if e != nil {
err = e
return

Check warning on line 366 in x/evm/precompile/wasm.go

View check run for this annotation

Codecov / codecov/patch

x/evm/precompile/wasm.go#L365-L366

Added lines #L365 - L366 were not covered by tests
Expand Down
Loading

0 comments on commit 24c07cf

Please sign in to comment.