Skip to content

Commit

Permalink
feat(evm): add predeployed contracts (imua-xyz#279)
Browse files Browse the repository at this point in the history
* feat(evm): add Create2 and CreateX predeploys

Since the only way to deploy such contracts is through pre-EIP155
transactions which are not supported, include them in the genesis state
instead.

* fix(test): modify the local node chain id

To avoid confusion, use a different value for localnet CHAINID. Changing
233 to 232 also provides a different EVM chain-id, which is helpful when
using foundry with a well-configured `foundry.toml`

* chore: lint

* fix(ci): update docker versions to build

* remove superfluous comment

* update chainID in test-util

* fix(ci): pacify semgrep by specifying user first

* fix(evm): add unit test to validate defaults

* feat(evm): add multicall3 + safe-singleton-factory

* doc(evm): add a few comments

* doc(evm): update comment

* chore: gofumpt

* fix(evm): set nonce to 1 for predeploys

* test(evm): add predeploys test
  • Loading branch information
MaxMustermann2 authored Jan 6, 2025
1 parent a928315 commit de571b1
Show file tree
Hide file tree
Showing 17 changed files with 405 additions and 19 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ WORKDIR /go/src/github.com/ExocoreNetwork/exocore

COPY go.mod go.sum ./

RUN apk add --no-cache ca-certificates=20240226-r0 build-base=0.5-r3 git=2.43.5-r0 linux-headers=6.5-r0
RUN apk add --no-cache ca-certificates=20241121-r0 build-base=0.5-r3 git=2.43.5-r0 linux-headers=6.5-r0

RUN --mount=type=bind,target=. --mount=type=secret,id=GITHUB_TOKEN \
git config --global url."https://$(cat /run/secrets/GITHUB_TOKEN)@github.com/".insteadOf "https://github.com/"; \
Expand All @@ -23,7 +23,7 @@ WORKDIR /root
COPY --from=build-env /go/src/github.com/ExocoreNetwork/exocore/build/exocored /usr/bin/exocored
COPY --from=build-env /go/bin/toml-cli /usr/bin/toml-cli

RUN apk add --no-cache ca-certificates=20240226-r0 libstdc++=13.2.1_git20231014-r0 jq=1.7.1-r0 curl=8.9.1-r1 bash=5.2.21-r0 vim=9.0.2127-r0 lz4=1.9.4-r5 rclone=1.65.0-r3 \
RUN apk add --no-cache ca-certificates=20241121-r0 libstdc++=13.2.1_git20231014-r0 jq=1.7.1-r0 curl=8.9.1-r1 bash=5.2.21-r0 vim=9.0.2127-r0 lz4=1.9.4-r5 rclone=1.65.0-r3 \
&& addgroup -g 1000 exocore \
&& adduser -S -h /home/exocore -D exocore -u 1000 -G exocore

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ localnet-build:
# Generate multi node configuration files and initialize configurations
# TODO: exocore testnet chainid is still under consideration and need to be finalized later
localnet-init: localnet-stop
exocored testnet init-files --chain-id exocoretestnet_233-1 --v 4 -o $(CURDIR)/build/.testnets --starting-ip-address 192.168.0.2 --keyring-backend=test && \
exocored testnet init-files --chain-id exocorelocalnet_232-1 --v 4 -o $(CURDIR)/build/.testnets --starting-ip-address 192.168.0.2 --keyring-backend=test && \
./networks/init-node.sh

# Start a 4-node testnet locally
Expand Down
2 changes: 1 addition & 1 deletion cmd/exocored/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func InitCmd(mbm module.BasicManager, defaultNodeHome string) *cobra.Command {
chainID, _ := cmd.Flags().GetString(flags.FlagChainID)
if chainID == "" {
// TODO: default chainid is still under consideration and need to be finalized later
chainID = fmt.Sprintf("exocore_233-%v", tmrand.Str(6))
chainID = fmt.Sprintf("exocore_234-%v", tmrand.Str(6))
}

// Get bip39 mnemonic
Expand Down
2 changes: 1 addition & 1 deletion init.bat
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ rem C:\msys64\usr\bin

set KEY="dev0"
# TODO: exocore testnet chainid is still under consideration and need to be finalized later
set CHAINID="exocoretestnet_233-1"
set CHAINID="exocorelocalnet_232-1"
set MONIKER="localtestnet"
set KEYRING="test"
set ALGO="eth_secp256k1"
Expand Down
5 changes: 2 additions & 3 deletions local_node.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
KEYS[0]="dev0"
KEYS[1]="dev1"
KEYS[2]="dev2"
# TODO: exocore testnet chainid is still under consideration and need to be finalized later
CHAINID="exocoretestnet_233-1"
MONIKER="localtestnet"
CHAINID="exocorelocalnet_232-1"
MONIKER="localnet"
# Remember to change to other types of keyring like 'file' in-case exposing to outside world,
# otherwise your balance will be wiped quickly
# The keyring test does not require private key to steal tokens from you
Expand Down
7 changes: 2 additions & 5 deletions networks/local/exocore/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ RUN apk add --no-cache libstdc++=13.2.1_git20231014-r0 bash=5.2.21-r0 curl=8.9.1
&& addgroup -g 1000 exocore \
&& adduser -S -h /home/exocore -D exocore -u 1000 -G exocore
EXPOSE 26656 26657 1317 9090 8545 8546
# TODO: exocore testnet chainid is still under consideration and need to be finalized later
CMD ["start", "--log_format", "plain", "--chain-id", "exocoretestnet_233-1", "--metrics", "--json-rpc.api", "eth,txpool,personal,net,debug,web3", "--api.enable", "--json-rpc.enable", "true", "--minimum-gas-prices", "0.0001hua"]
USER exocore
CMD ["start", "--log_format", "plain", "--chain-id", "exocorelocalnet_232-1", "--metrics", "--json-rpc.api", "eth,txpool,personal,net,debug,web3", "--api.enable", "--json-rpc.enable", "true", "--minimum-gas-prices", "0.0001hua"]
# by default, a SIGKILL is sent after 10 seconds. We need to override this to allow graceful shutdown.
STOPSIGNAL SIGTERM
VOLUME /exocore
Expand All @@ -24,9 +24,6 @@ WORKDIR /exocore
COPY ./networks/local/exocore/wrapper.sh /usr/bin/wrapper.sh
COPY --from=build /go/work/build/exocored /exocore

# Use the created non-root user
USER exocore

HEALTHCHECK --interval=30s --timeout=30s --retries=3 CMD curl -f http://localhost:26657/health || exit 1

ENTRYPOINT ["/usr/bin/wrapper.sh"]
2 changes: 1 addition & 1 deletion scripts/start-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

KEY="dev0"
# TODO: exocore testnet chainid is still under consideration and need to be finalized later
CHAINID="exocoretestnet_233-1"
CHAINID="exocorelocalnet_232-1"
MONIKER="mymoniker"
DATA_DIR=$(mktemp -d -t exocore-datadir.XXXXX)

Expand Down
2 changes: 1 addition & 1 deletion testutil/batch/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ var DefaultTestToolConfig = TestToolConfig{
OperatorExoAmount: 10,
AVSExoAmount: 10,
ChainValidatorNumber: 1,
ChainID: "exocoretestnet_233-1",
ChainID: "exocorelocalnet_232-1",
DefaultClientChainID: 101,
NodesRPC: []string{"http://127.0.0.1:26657"},
NodesEVMRPCHTTP: []string{"http://127.0.0.1:8545"},
Expand Down
5 changes: 4 additions & 1 deletion testutil/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ type BaseTestSuite struct {

InitTime time.Time
OperatorMsgServer operatortypes.MsgServer

// tests may use this to allocate a genesis balance
Balances []banktypes.Balance
}

func (suite *BaseTestSuite) SetupTest() {
Expand Down Expand Up @@ -481,7 +484,7 @@ func (suite *BaseTestSuite) DoSetupTest() {

// Initialize an ExocoreApp for test
suite.SetupWithGenesisValSet(
[]authtypes.GenesisAccount{acc}, balance,
[]authtypes.GenesisAccount{acc}, append(suite.Balances, balance)...,
)

// Create StateDB
Expand Down
2 changes: 1 addition & 1 deletion utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
const (
// MainnetChainID defines the Evmos EIP155 chain ID for mainnet
// TODO: the mainnet chainid is still under consideration and need to be finalized later
MainnetChainID = "exocore_233"
MainnetChainID = "exocore_234"
// TestnetChainID defines the Evmos EIP155 chain ID for testnet
// TODO: the testnet chainid is still under consideration and need to be finalized later
TestnetChainID = "exocoretestnet_233"
Expand Down
2 changes: 1 addition & 1 deletion x/avs/keeper/avs.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func (k Keeper) RegisterAVSWithChainID(
statedb.Account{
Balance: big.NewInt(0),
CodeHash: types.ChainIDCodeHash[:],
Nonce: 1,
Nonce: k.evmKeeper.GetNewContractNonce(ctx),
},
); err != nil {
return common.Address{}, err
Expand Down
1 change: 1 addition & 0 deletions x/avs/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type BankKeeper interface {
type EVMKeeper interface {
SetAccount(ctx sdk.Context, addr common.Address, account statedb.Account) error
SetCode(ctx sdk.Context, codeHash, code []byte)
GetNewContractNonce(sdk.Context) uint64
}

// OperatorKeeper represents the expected keeper interface for the operator module.
Expand Down
25 changes: 24 additions & 1 deletion x/evm/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
"github.com/ethereum/go-ethereum/crypto"

"github.com/ExocoreNetwork/exocore/x/evm/keeper"
exocoreevmtypes "github.com/ExocoreNetwork/exocore/x/evm/types"
evmostypes "github.com/evmos/evmos/v16/types"
"github.com/evmos/evmos/v16/x/evm/statedb"
"github.com/evmos/evmos/v16/x/evm/types"
)

Expand All @@ -37,7 +39,7 @@ func InitGenesis(
for _, account := range data.Accounts {
address := common.HexToAddress(account.Address)
accAddress := sdk.AccAddress(address.Bytes())
// check that the EVM balance the matches the account balance
// check that the account is actually found in the account keeper
acc := accountKeeper.GetAccount(ctx, accAddress)
if acc == nil {
panic(fmt.Errorf("account not found for address %s", account.Address))
Expand Down Expand Up @@ -68,6 +70,27 @@ func InitGenesis(
}
}

nonce := k.GetNewContractNonce(ctx)
for _, predeploy := range exocoreevmtypes.DefaultPredeploys {
// load data from predeploys
addr := predeploy.GetByteAddress()
codeHash := predeploy.GetCodeHash()
// overwrite existing account but retain balance to avoid x/bank invariant breaking.
// the balance may be non-zero in the case of chain restarts, wherein someone has
// (accidentally?) sent funds to the predeployed contract.
balance := k.GetBalance(ctx, addr)
// set the evm account, which only contains the code hash and not the code
account := statedb.NewEmptyAccount()
account.CodeHash = codeHash.Bytes()
account.Balance = balance
account.Nonce = nonce
if err := k.SetAccount(ctx, addr, *account); err != nil {
panic(fmt.Errorf("error setting account at %s: %s", addr, err))
}
// set lookup from code hash to code
k.SetCode(ctx, account.CodeHash, predeploy.GetByteCode())
}

return []abci.ValidatorUpdate{}
}

Expand Down
15 changes: 15 additions & 0 deletions x/evm/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,18 @@ func (k Keeper) AddTransientGasUsed(ctx sdk.Context, gasUsed uint64) (uint64, er
k.SetTransientGasUsed(ctx, result)
return result, nil
}

// IsEIP158 returns true if the EIP158 is enabled at the current block height.
func (k Keeper) IsEIP158(ctx sdk.Context) bool {
evmConfig := k.GetParams(ctx).ChainConfig.EthereumConfig(k.ChainID())
return evmConfig.IsEIP158(big.NewInt(ctx.BlockHeight()))
}

// GetNewContractNonce returns the nonce for a new contract account.
func (k Keeper) GetNewContractNonce(ctx sdk.Context) uint64 {
// post EIP158, the nonce of contracts is 1 to prevent their deletion from the trie.
if k.IsEIP158(ctx) {
return 1
}
return 0
}
172 changes: 172 additions & 0 deletions x/evm/predeploys_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package evm_test

// This file contains tests for checking that:
// 1. All the contracts from DefaultPredeploys are included in genesis with nonce 1.
// 2. If a predeployed address has existing balance, it is retained.
// 3. CREATE2 can be used to deploy CREATE3 successfully.

import (
"math/big"
"testing"

sdkmath "cosmossdk.io/math"

sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"

"github.com/ExocoreNetwork/exocore/testutil"
testutiltx "github.com/ExocoreNetwork/exocore/testutil/tx"
"github.com/ExocoreNetwork/exocore/x/evm/types"
ethtypes "github.com/ethereum/go-ethereum/core/types"
evmostypes "github.com/evmos/evmos/v16/types"
"github.com/stretchr/testify/suite"
)

var s *KeeperTestSuite

type KeeperTestSuite struct {
testutil.BaseTestSuite
}

func TestKeeperTestSuite(t *testing.T) {
s = new(KeeperTestSuite)
suite.Run(t, s)
}

func (suite *KeeperTestSuite) SetupTest() {
suite.DoSetupTest()
}

func (suite *KeeperTestSuite) TestPredeploysExist() {
// reset test to ensure that balance retention running first does not impact us
suite.Balances = nil
suite.SetupTest()
expectedNonce := suite.App.EvmKeeper.GetNewContractNonce(suite.Ctx)
expectedBalance := big.NewInt(0)
evmParams := suite.App.EvmKeeper.GetParams(suite.Ctx)
evmDenom := evmParams.GetEvmDenom()
for _, predeploy := range types.DefaultPredeploys {
bytesAddr := predeploy.GetByteAddress()
acc := suite.App.AccountKeeper.GetAccount(suite.Ctx, bytesAddr[:])
suite.Require().NotNil(acc)

ethAcc, ok := acc.(evmostypes.EthAccountI)
suite.Require().True(ok)
// check code hash
suite.Require().Equal(predeploy.GetCodeHash(), ethAcc.GetCodeHash())
// check nonce
suite.Require().Equal(expectedNonce, ethAcc.GetSequence())
// check balance via evm keeper
suite.Require().Equal(
expectedBalance, suite.App.EvmKeeper.GetBalance(suite.Ctx, bytesAddr),
predeploy.Address,
)
// check balance via bank keeper
suite.Require().Equal(
expectedBalance,
suite.App.BankKeeper.GetBalance(
suite.Ctx, bytesAddr[:], evmDenom,
).Amount.BigInt(),
)
// check that code exists
suite.Require().NotNil(suite.App.EvmKeeper.GetCode(suite.Ctx, predeploy.GetCodeHash()))
}
}

func (suite *KeeperTestSuite) TestBalanceRetention() {
evmParams := suite.App.EvmKeeper.GetParams(suite.Ctx)
evmDenom := evmParams.GetEvmDenom()
targetBalance := sdkmath.NewInt(100)
// set balance > 0 for all of the predeployed address
for _, predeploy := range types.DefaultPredeploys {
addr := predeploy.GetByteAddress()
suite.Balances = append(
suite.Balances,
banktypes.Balance{
Address: sdk.AccAddress(addr.Bytes()).String(),
Coins: sdk.NewCoins(sdk.NewCoin(evmDenom, targetBalance)),
},
)
}
// now redo the genesis
suite.SetupTest()
// check the state of the predeploys
expectedNonce := suite.App.EvmKeeper.GetNewContractNonce(suite.Ctx)
for _, predeploy := range types.DefaultPredeploys {
addr := predeploy.GetByteAddress()
// check balance via evm keeper
suite.Require().Equal(
targetBalance.BigInt(), suite.App.EvmKeeper.GetBalance(suite.Ctx, addr),
)
// check balance via bank keeper
suite.Require().Equal(
targetBalance,
suite.App.BankKeeper.GetBalance(
suite.Ctx, addr[:], evmDenom,
).Amount,
)
// check nonce
acc := suite.App.AccountKeeper.GetAccount(suite.Ctx, addr[:])
suite.Require().NotNil(acc)
ethAcc, ok := acc.(evmostypes.EthAccountI)
suite.Require().True(ok)
suite.Require().Equal(expectedNonce, ethAcc.GetSequence())
// check code hash
suite.Require().Equal(predeploy.GetCodeHash(), ethAcc.GetCodeHash())
// check that code exists
suite.Require().NotNil(suite.App.EvmKeeper.GetCode(suite.Ctx, predeploy.GetCodeHash()))
}
}

func (suite *KeeperTestSuite) TestCreate3() {
// contract to call
create2 := common.HexToAddress("0x4e59b44847b379578588920cA78FbF26c0B4956C")
// blank salt
salt := common.Hash{}
// runCode is the code fetched via eth_getCode.
// it can be used directly in a predeploy but not to create a new contract, because
// it does not have the constructor.
runCode := "6080604052600436106100295760003560e01c806350f1c4641461002e578063cdcb760a14610077575b600080fd5b34801561003a57600080fd5b5061004e610049366004610489565b61008a565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b61004e6100853660046104fd565b6100ee565b6040517fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606084901b166020820152603481018290526000906054016040516020818303038152906040528051906020012091506100e78261014c565b9392505050565b6040517fffffffffffffffffffffffffffffffffffffffff0000000000000000000000003360601b166020820152603481018390526000906054016040516020818303038152906040528051906020012092506100e78383346102b2565b604080518082018252601081527f67363d3d37363d34f03d5260086018f30000000000000000000000000000000060209182015290517fff00000000000000000000000000000000000000000000000000000000000000918101919091527fffffffffffffffffffffffffffffffffffffffff0000000000000000000000003060601b166021820152603581018290527f21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f60558201526000908190610228906075015b6040516020818303038152906040528051906020012090565b6040517fd69400000000000000000000000000000000000000000000000000000000000060208201527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606083901b1660228201527f010000000000000000000000000000000000000000000000000000000000000060368201529091506100e79060370161020f565b6000806040518060400160405280601081526020017f67363d3d37363d34f03d5260086018f30000000000000000000000000000000081525090506000858251602084016000f5905073ffffffffffffffffffffffffffffffffffffffff811661037d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601160248201527f4445504c4f594d454e545f4641494c454400000000000000000000000000000060448201526064015b60405180910390fd5b6103868661014c565b925060008173ffffffffffffffffffffffffffffffffffffffff1685876040516103b091906105d6565b60006040518083038185875af1925050503d80600081146103ed576040519150601f19603f3d011682016040523d82523d6000602084013e6103f2565b606091505b50509050808015610419575073ffffffffffffffffffffffffffffffffffffffff84163b15155b61047f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601560248201527f494e495449414c495a4154494f4e5f4641494c454400000000000000000000006044820152606401610374565b5050509392505050565b6000806040838503121561049c57600080fd5b823573ffffffffffffffffffffffffffffffffffffffff811681146104c057600080fd5b946020939093013593505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000806040838503121561051057600080fd5b82359150602083013567ffffffffffffffff8082111561052f57600080fd5b818501915085601f83011261054357600080fd5b813581811115610555576105556104ce565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190838211818310171561059b5761059b6104ce565b816040528281528860208487010111156105b457600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b6000825160005b818110156105f757602081860181015185830152016105dd565b50600092019182525091905056fea2646970667358221220fd377c185926b3110b7e8a544f897646caf36a0e82b2629de851045e2a5f937764736f6c63430008100033"
runCodeBytes := common.Hex2Bytes(runCode)
// initCode includes constructor + some handling / prep + runCode
initCode := "608060405234801561001057600080fd5b5061063b806100206000396000f3fe" + runCode
initCodeBytes := common.Hex2Bytes(initCode)
// create3 destination
create3 := common.HexToAddress("0x6aA3D87e99286946161dCA02B97C5806fC5eD46F")
// check that this matches the derived create3 destination
derived := crypto.CreateAddress2(create2, salt, crypto.Keccak256Hash(initCodeBytes).Bytes())
suite.Require().Equal(create3, derived)
beforeBalance := suite.App.EvmKeeper.GetBalance(suite.Ctx, create3)
// any address that has fees can call this deterministically
addr := testutiltx.GenerateAddress()
// evm keeper can mint with impunity. use it to generate gas fees
err := suite.App.EvmKeeper.SetBalance(suite.Ctx, addr, big.NewInt(1000000000000000000))
suite.Require().NoError(err)
nonce, err := suite.App.AccountKeeper.GetSequence(suite.Ctx, sdk.AccAddress(addr.Bytes()))
suite.Require().NoError(err)
msg := ethtypes.NewMessage(
addr, &create2, nonce, big.NewInt(0),
2000000, big.NewInt(1), nil, nil,
append(salt[:], initCodeBytes...), nil, true,
)
rsp, err := suite.App.EvmKeeper.ApplyMessage(suite.Ctx, msg, nil, true)
suite.Require().NoError(err)
suite.Require().False(rsp.Failed())
// validate create3 destination
acc := suite.App.AccountKeeper.GetAccount(suite.Ctx, create3.Bytes())
suite.Require().NotNil(acc)
ethAcc, ok := acc.(evmostypes.EthAccountI)
suite.Require().True(ok)
suite.Require().Equal(
suite.App.EvmKeeper.GetNewContractNonce(suite.Ctx), ethAcc.GetSequence(),
)
suite.Require().Equal(crypto.Keccak256Hash(runCodeBytes), ethAcc.GetCodeHash())
suite.Require().Equal(
suite.App.EvmKeeper.GetCode(suite.Ctx, ethAcc.GetCodeHash()),
runCodeBytes,
)
// no funds are generated
suite.Require().Equal(beforeBalance, suite.App.EvmKeeper.GetBalance(suite.Ctx, create3))
}
Loading

0 comments on commit de571b1

Please sign in to comment.