Commit ddeb96b4 authored by protolambda's avatar protolambda Committed by GitHub

op-chain-ops: forge script cheatcodes (#11465)

* op-chain-ops: forge script cheatcodes, work in progress

incl prank cheatcodes
incl env var handling
incl serialize utils
incl some json/toml utils
incl go.mod: tidy

* op-chain-ops: fix state dumping

* op-chain-ops: remove debug print line
parent 30b3cddf
......@@ -18,6 +18,7 @@ tests/
# Semgrep-action log folder
.semgrep_logs/
op-chain-ops/script/testdata
packages/*/node_modules
packages/*/test
......@@ -3,6 +3,7 @@ module github.com/ethereum-optimism/optimism
go 1.21
require (
github.com/BurntSushi/toml v1.4.0
github.com/andybalholm/brotli v1.1.0
github.com/btcsuite/btcd v0.24.2
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
......@@ -50,7 +51,6 @@ require (
)
require (
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/VictoriaMetrics/fastcache v1.12.2 // indirect
......
package foundry
import (
"encoding/json"
"fmt"
"maps"
"math/big"
"os"
"github.com/holiman/uint256"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
)
type ForgeAllocs struct {
Accounts types.GenesisAlloc
}
// FromState takes a geth StateDB, and dumps the accounts into the ForgeAllocs.
// Any previous allocs contents are removed.
// Warning: the state must be committed first, trie-key preimages must be present for iteration,
// and a fresh state around the committed state-root must be presented, for the latest state-contents to be dumped.
func (f *ForgeAllocs) FromState(stateDB StateDB) {
f.Accounts = make(types.GenesisAlloc)
stateDB.DumpToCollector((*forgeAllocsDump)(f), &state.DumpConfig{
OnlyWithAddresses: true,
})
}
// StateDB is a minimal interface to support dumping of Geth EVM state to ForgeAllocs.
type StateDB interface {
DumpToCollector(c state.DumpCollector, conf *state.DumpConfig) (nextKey []byte)
}
// Assert that the Geth StateDB implements this interface still.
var _ StateDB = (*state.StateDB)(nil)
// forgeAllocsDump is a wrapper to hide the error-prone state-dumping interface from public API.
// Use ForgeAllocs.FromState to dump a state to forge-allocs.
type forgeAllocsDump ForgeAllocs
// ForgeAllocs implements state.DumpAllocator, such that the EVM state can be dumped into it:
// with a StateDB.DumpToCollector call.
var _ state.DumpCollector = (*forgeAllocsDump)(nil)
func (d *forgeAllocsDump) OnRoot(hash common.Hash) {
// Unlike the geth raw-state-dump, forge-allocs do not reference the state trie root.
}
func (d *forgeAllocsDump) OnAccount(address *common.Address, account state.DumpAccount) {
if address == nil {
return
}
if _, ok := d.Accounts[*address]; ok {
panic(fmt.Errorf("cannot dump account %s twice", *address))
}
balance, ok := new(big.Int).SetString(account.Balance, 0)
if !ok {
panic("invalid balance")
}
var storage map[common.Hash]common.Hash
if len(account.Storage) > 0 {
storage = make(map[common.Hash]common.Hash, len(account.Storage))
for k, v := range account.Storage {
storage[k] = common.HexToHash(v)
}
}
d.Accounts[*address] = types.Account{
Code: account.Code,
Storage: storage,
Balance: balance,
Nonce: account.Nonce,
}
}
func (d *ForgeAllocs) Copy() *ForgeAllocs {
out := make(types.GenesisAlloc, len(d.Accounts))
maps.Copy(out, d.Accounts)
return &ForgeAllocs{Accounts: out}
}
func (d *ForgeAllocs) UnmarshalJSON(b []byte) error {
// forge, since integrating Alloy, likes to hex-encode everything.
type forgeAllocAccount struct {
Balance hexutil.U256 `json:"balance"`
Nonce hexutil.Uint64 `json:"nonce"`
Code hexutil.Bytes `json:"code,omitempty"`
Storage map[common.Hash]common.Hash `json:"storage,omitempty"`
}
var allocs map[common.Address]forgeAllocAccount
if err := json.Unmarshal(b, &allocs); err != nil {
return err
}
d.Accounts = make(types.GenesisAlloc, len(allocs))
for addr, acc := range allocs {
acc := acc
d.Accounts[addr] = types.Account{
Code: acc.Code,
Storage: acc.Storage,
Balance: (*uint256.Int)(&acc.Balance).ToBig(),
Nonce: (uint64)(acc.Nonce),
PrivateKey: nil,
}
}
return nil
}
func LoadForgeAllocs(allocsPath string) (*ForgeAllocs, error) {
f, err := os.OpenFile(allocsPath, os.O_RDONLY, 0644)
if err != nil {
return nil, fmt.Errorf("failed to open forge allocs %q: %w", allocsPath, err)
}
defer f.Close()
var out ForgeAllocs
if err := json.NewDecoder(f).Decode(&out); err != nil {
return nil, fmt.Errorf("failed to json-decode forge allocs %q: %w", allocsPath, err)
}
return &out, nil
}
package foundry
import (
"os"
"testing"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/triedb"
"github.com/ethereum/go-ethereum/triedb/hashdb"
)
func TestForgeAllocs_FromState(t *testing.T) {
// Internals of state-dumping of Geth have silent errors.
cfg := oplog.DefaultCLIConfig()
cfg.Level = log.LevelTrace
oplog.SetGlobalLogHandler(oplog.NewLogHandler(os.Stdout, cfg))
rawDB := rawdb.NewMemoryDatabase()
stateDB := state.NewDatabaseWithConfig(rawDB, &triedb.Config{
Preimages: true,
IsVerkle: false,
HashDB: hashdb.Defaults,
PathDB: nil,
})
st, err := state.New(types.EmptyRootHash, stateDB, nil)
require.NoError(t, err)
alice := common.HexToAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
st.CreateAccount(alice)
st.SetBalance(alice, uint256.NewInt(123), tracing.BalanceChangeUnspecified)
st.SetNonce(alice, 42)
bob := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")
st.CreateAccount(bob)
st.CreateContract(bob)
st.SetBalance(bob, uint256.NewInt(100), tracing.BalanceChangeUnspecified)
st.SetNonce(bob, 1)
st.SetState(bob, common.Hash{0: 0x42}, common.Hash{0: 7})
contract := common.HexToAddress("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC")
st.CreateAccount(contract)
st.CreateContract(contract)
st.SetNonce(contract, 30)
st.SetBalance(contract, uint256.NewInt(0), tracing.BalanceChangeUnspecified)
st.SetCode(contract, []byte{10, 11, 12, 13, 14})
// Commit and make a new state, we cannot reuse the state after Commit
// (see doc-comment in Commit, absolute footgun)
root, err := st.Commit(0, false)
require.NoError(t, err)
st, err = state.New(root, stateDB, nil)
require.NoError(t, err)
st.SetState(contract, common.Hash{0: 0xa}, common.Hash{0: 1})
st.SetState(contract, crypto.Keccak256Hash([]byte("hello")), crypto.Keccak256Hash([]byte("world")))
root, err = st.Commit(0, false)
require.NoError(t, err)
st, err = state.New(root, stateDB, nil)
require.NoError(t, err)
var allocs ForgeAllocs
allocs.FromState(st)
require.Len(t, allocs.Accounts, 3)
require.Contains(t, allocs.Accounts, alice)
require.Nil(t, allocs.Accounts[alice].Code)
require.Nil(t, allocs.Accounts[alice].Storage)
require.Equal(t, "123", allocs.Accounts[alice].Balance.String())
require.Equal(t, uint64(42), allocs.Accounts[alice].Nonce)
require.Contains(t, allocs.Accounts, bob)
require.Nil(t, allocs.Accounts[bob].Code)
require.Len(t, allocs.Accounts[bob].Storage, 1)
require.Equal(t, common.Hash{0: 7}, allocs.Accounts[bob].Storage[common.Hash{0: 0x42}])
require.Equal(t, "100", allocs.Accounts[bob].Balance.String())
require.Equal(t, uint64(1), allocs.Accounts[bob].Nonce)
require.Contains(t, allocs.Accounts, contract)
require.Equal(t, []byte{10, 11, 12, 13, 14}, allocs.Accounts[contract].Code)
require.Len(t, allocs.Accounts[contract].Storage, 2)
require.Equal(t, common.Hash{0: 1}, allocs.Accounts[contract].Storage[common.Hash{0: 0xa}])
require.Equal(t, crypto.Keccak256Hash([]byte("world")),
allocs.Accounts[contract].Storage[crypto.Keccak256Hash([]byte("hello"))])
require.Equal(t, "0", allocs.Accounts[contract].Balance.String())
require.Equal(t, uint64(30), allocs.Accounts[contract].Nonce)
}
......@@ -8,14 +8,10 @@ import (
"os"
"strings"
"github.com/holiman/uint256"
"golang.org/x/exp/maps"
"github.com/ethereum-optimism/optimism/op-chain-ops/solc"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
)
// Artifact represents a foundry compilation artifact.
......@@ -157,52 +153,3 @@ func ReadArtifact(path string) (*Artifact, error) {
}
return &artifact, nil
}
type ForgeAllocs struct {
Accounts types.GenesisAlloc
}
func (d *ForgeAllocs) Copy() *ForgeAllocs {
out := make(types.GenesisAlloc, len(d.Accounts))
maps.Copy(out, d.Accounts)
return &ForgeAllocs{Accounts: out}
}
func (d *ForgeAllocs) UnmarshalJSON(b []byte) error {
// forge, since integrating Alloy, likes to hex-encode everything.
type forgeAllocAccount struct {
Balance hexutil.U256 `json:"balance"`
Nonce hexutil.Uint64 `json:"nonce"`
Code hexutil.Bytes `json:"code,omitempty"`
Storage map[common.Hash]common.Hash `json:"storage,omitempty"`
}
var allocs map[common.Address]forgeAllocAccount
if err := json.Unmarshal(b, &allocs); err != nil {
return err
}
d.Accounts = make(types.GenesisAlloc, len(allocs))
for addr, acc := range allocs {
acc := acc
d.Accounts[addr] = types.Account{
Code: acc.Code,
Storage: acc.Storage,
Balance: (*uint256.Int)(&acc.Balance).ToBig(),
Nonce: (uint64)(acc.Nonce),
PrivateKey: nil,
}
}
return nil
}
func LoadForgeAllocs(allocsPath string) (*ForgeAllocs, error) {
f, err := os.OpenFile(allocsPath, os.O_RDONLY, 0644)
if err != nil {
return nil, fmt.Errorf("failed to open forge allocs %q: %w", allocsPath, err)
}
defer f.Close()
var out ForgeAllocs
if err := json.NewDecoder(f).Decode(&out); err != nil {
return nil, fmt.Errorf("failed to json-decode forge allocs %q: %w", allocsPath, err)
}
return &out, nil
}
package script
import (
"github.com/ethereum/go-ethereum/common"
)
// CheatCodesPrecompile implements the Forge vm cheatcodes.
// Note that forge-std wraps these cheatcodes,
// and provides additional convenience functions that use these cheatcodes.
type CheatCodesPrecompile struct {
h *Host
}
func (c *CheatCodesPrecompile) GetNonce(addr common.Address) uint64 {
return c.h.state.GetNonce(addr)
}
package script
import (
"math/big"
"github.com/holiman/uint256"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/vm"
)
// Warp implements https://book.getfoundry.sh/cheatcodes/warp
func (c *CheatCodesPrecompile) Warp(timestamp *big.Int) {
c.h.env.Context.Time = timestamp.Uint64()
}
// Roll implements https://book.getfoundry.sh/cheatcodes/roll
func (c *CheatCodesPrecompile) Roll(num *big.Int) {
c.h.env.Context.BlockNumber = num
}
// Fee implements https://book.getfoundry.sh/cheatcodes/fee
func (c *CheatCodesPrecompile) Fee(fee *big.Int) {
c.h.env.Context.BaseFee = fee
}
// GetBlockTimestamp implements https://book.getfoundry.sh/cheatcodes/get-block-timestamp
func (c *CheatCodesPrecompile) GetBlockTimestamp() *big.Int {
return new(big.Int).SetUint64(c.h.env.Context.Time)
}
// GetBlockNumber implements https://book.getfoundry.sh/cheatcodes/get-block-number
func (c *CheatCodesPrecompile) GetBlockNumber() *big.Int {
return c.h.env.Context.BlockNumber
}
// Difficulty implements https://book.getfoundry.sh/cheatcodes/difficulty
func (c *CheatCodesPrecompile) Difficulty(_ *big.Int) error {
return vm.ErrExecutionReverted // only post-merge is supported
}
// Prevrandao implements https://book.getfoundry.sh/cheatcodes/prevrandao
func (c *CheatCodesPrecompile) Prevrandao(v [32]byte) {
c.h.env.Context.Random = (*common.Hash)(&v)
}
// ChainId implements https://book.getfoundry.sh/cheatcodes/chain-id
func (c *CheatCodesPrecompile) ChainId(id *big.Int) {
c.h.env.ChainConfig().ChainID = id
c.h.chainCfg.ChainID = id
// c.h.env.rules.ChainID is unused, but should maybe also be modified
}
// Store implements https://book.getfoundry.sh/cheatcodes/store
func (c *CheatCodesPrecompile) Store(account common.Address, slot [32]byte, value [32]byte) {
c.h.state.SetState(account, slot, value)
}
// Load implements https://book.getfoundry.sh/cheatcodes/load
func (c *CheatCodesPrecompile) Load(account common.Address, slot [32]byte) [32]byte {
return c.h.state.GetState(account, slot)
}
// Etch implements https://book.getfoundry.sh/cheatcodes/etch
func (c *CheatCodesPrecompile) Etch(who common.Address, code []byte) {
c.h.state.SetCode(who, code)
}
// Deal implements https://book.getfoundry.sh/cheatcodes/deal
func (c *CheatCodesPrecompile) Deal(who common.Address, newBalance *big.Int) {
c.h.state.SetBalance(who, uint256.MustFromBig(newBalance), tracing.BalanceChangeUnspecified)
}
// Prank_ca669fa7 implements https://book.getfoundry.sh/cheatcodes/prank
func (c *CheatCodesPrecompile) Prank_ca669fa7(sender common.Address) error {
return c.h.Prank(&sender, nil, false, false)
}
// Prank_47e50cce implements https://book.getfoundry.sh/cheatcodes/prank
func (c *CheatCodesPrecompile) Prank_47e50cce(sender common.Address, origin common.Address) error {
return c.h.Prank(&sender, &origin, false, false)
}
// StartPrank_06447d56 implements https://book.getfoundry.sh/cheatcodes/start-prank
func (c *CheatCodesPrecompile) StartPrank_06447d56(sender common.Address) error {
return c.h.Prank(&sender, nil, true, false)
}
// StartPrank_45b56078 implements https://book.getfoundry.sh/cheatcodes/start-prank
func (c *CheatCodesPrecompile) StartPrank_45b56078(sender common.Address, origin common.Address) error {
return c.h.Prank(&sender, &origin, true, false)
}
// StopPrank implements https://book.getfoundry.sh/cheatcodes/stop-prank
func (c *CheatCodesPrecompile) StopPrank() error {
return c.h.StopPrank(false)
}
// ReadCallers implements https://book.getfoundry.sh/cheatcodes/read-callers
func (c *CheatCodesPrecompile) ReadCallers() (callerMode *big.Int, msgSender common.Address, txOrigin common.Address) {
return c.h.CallerMode().Big(), c.h.MsgSender(), c.h.env.TxContext.Origin
}
// Record implements https://book.getfoundry.sh/cheatcodes/record
func (c *CheatCodesPrecompile) Record() error {
panic("vm.record not supported")
}
// Accesses implements https://book.getfoundry.sh/cheatcodes/accesses
func (c *CheatCodesPrecompile) Accesses() (reads [][32]byte, writes [][32]byte, err error) {
panic("vm.accesses not supported")
}
// RecordLogs implements https://book.getfoundry.sh/cheatcodes/record-logs
func (c *CheatCodesPrecompile) RecordLogs() error {
panic("vm.recordLogs not supported")
}
type Log struct {
Topics [][32]byte
Data []byte
Emitter common.Address
}
// GetRecordedLogs implements https://book.getfoundry.sh/cheatcodes/get-recorded-logs
//func (c *CheatCodesPrecompile) GetRecordedLogs() []Log {
// return nil // TODO
//}
// SetNonce implements https://book.getfoundry.sh/cheatcodes/set-nonce
func (c *CheatCodesPrecompile) SetNonce(account common.Address, nonce uint64) {
c.h.state.SetNonce(account, nonce)
}
// GetNonce implements https://book.getfoundry.sh/cheatcodes/get-nonce
func (c *CheatCodesPrecompile) GetNonce(addr common.Address) uint64 {
return c.h.state.GetNonce(addr)
}
// MockCall_b96213e4 implements https://book.getfoundry.sh/cheatcodes/mock-call
func (c *CheatCodesPrecompile) MockCall_b96213e4(where common.Address, data []byte, retdata []byte) error {
panic("mockCall not supported")
}
// MockCall_81409b91 implements https://book.getfoundry.sh/cheatcodes/mock-call
func (c *CheatCodesPrecompile) MockCall_81409b91(where common.Address, value *big.Int, data []byte, retdata []byte) error {
panic("vm.mockCall not supported")
}
// MockCallRevert_dbaad147 implements https://book.getfoundry.sh/cheatcodes/mock-call-revert
func (c *CheatCodesPrecompile) MockCallRevert_dbaad147(where common.Address, data []byte, retdata []byte) error {
panic("vm.mockCall not supported")
}
// MockCallRevert_d23cd037 implements https://book.getfoundry.sh/cheatcodes/mock-call-revert
func (c *CheatCodesPrecompile) MockCallRevert_d23cd037(where common.Address, value *big.Int, data []byte, retdata []byte) error {
panic("vm.mockCall not supported")
}
// ClearMockedCalls implements https://book.getfoundry.sh/cheatcodes/clear-mocked-calls
func (c *CheatCodesPrecompile) ClearMockedCalls() error {
panic("vm.clearMockedCalls not supported")
}
// Coinbase implements https://book.getfoundry.sh/cheatcodes/coinbase
func (c *CheatCodesPrecompile) Coinbase(addr common.Address) {
c.h.env.Context.Coinbase = addr
}
// Broadcast_afc98040 implements https://book.getfoundry.sh/cheatcodes/broadcast
func (c *CheatCodesPrecompile) Broadcast_afc98040() error {
c.h.log.Info("broadcasting next call")
return c.h.Prank(nil, nil, false, true)
}
// Broadcast_e6962cdb implements https://book.getfoundry.sh/cheatcodes/broadcast
func (c *CheatCodesPrecompile) Broadcast_e6962cdb(who common.Address) error {
c.h.log.Info("broadcasting next call", "who", who)
return c.h.Prank(&who, nil, false, true)
}
// StartBroadcast_7fb5297f implements https://book.getfoundry.sh/cheatcodes/start-broadcast
func (c *CheatCodesPrecompile) StartBroadcast_7fb5297f() error {
c.h.log.Info("starting repeat-broadcast")
return c.h.Prank(nil, nil, true, true)
}
// StartBroadcast_7fec2a8d implements https://book.getfoundry.sh/cheatcodes/start-broadcast
func (c *CheatCodesPrecompile) StartBroadcast_7fec2a8d(who common.Address) error {
c.h.log.Info("starting repeat-broadcast", "who", who)
return c.h.Prank(nil, nil, true, true)
}
// StopBroadcast implements https://book.getfoundry.sh/cheatcodes/stop-broadcast
func (c *CheatCodesPrecompile) StopBroadcast() error {
c.h.log.Info("stopping repeat-broadcast")
return c.h.StopPrank(true)
}
// PauseGasMetering implements https://book.getfoundry.sh/cheatcodes/pause-gas-metering
func (c *CheatCodesPrecompile) PauseGasMetering() error {
panic("vm.pauseGasMetering not supported")
}
// ResumeGasMetering implements https://book.getfoundry.sh/cheatcodes/resume-gas-metering
func (c *CheatCodesPrecompile) ResumeGasMetering() error {
panic("vm.resumeGasMetering not supported")
}
// TxGasPrice implements https://book.getfoundry.sh/cheatcodes/tx-gas-price
func (c *CheatCodesPrecompile) TxGasPrice(newGasPrice *big.Int) {
c.h.env.TxContext.GasPrice = newGasPrice
}
// StartStateDiffRecording implements https://book.getfoundry.sh/cheatcodes/start-state-diff-recording
func (c *CheatCodesPrecompile) StartStateDiffRecording() error {
panic("vm.startStateDiffRecording not supported")
}
// StopAndReturnStateDiff implements https://book.getfoundry.sh/cheatcodes/stop-and-return-state-diff
func (c *CheatCodesPrecompile) StopAndReturnStateDiff() error {
panic("vm.stopAndReturnStateDiff not supported")
}
This diff is collapsed.
package script
import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum-optimism/optimism/op-chain-ops/foundry"
)
func (c *CheatCodesPrecompile) LoadAllocs(pathToAllocsJson string) error {
c.h.log.Info("loading state", "target", pathToAllocsJson)
return errors.New("state-loading is not supported")
}
func (c *CheatCodesPrecompile) DumpState(pathToStateJson string) error {
c.h.log.Info("dumping state", "target", pathToStateJson)
// We have to commit the existing state to the trie,
// for all the state-changes to be captured by the trie iterator.
root, err := c.h.state.Commit(c.h.env.Context.BlockNumber.Uint64(), true)
if err != nil {
return fmt.Errorf("failed to commit state: %w", err)
}
// We need a state object around the state DB
st, err := state.New(root, c.h.stateDB, nil)
if err != nil {
return fmt.Errorf("failed to create state object for state-dumping: %w", err)
}
// After Commit we cannot reuse the old State, so we update the host to use the new one
c.h.state = st
c.h.env.StateDB = st
var allocs foundry.ForgeAllocs
allocs.FromState(st)
// This may be written somewhere in the future (or run some callback to collect the state dump)
_ = allocs
c.h.log.Info("state-dumping is not supported, but have state",
"path", pathToStateJson, "accounts", len(allocs.Accounts))
return nil
}
package script
import (
"fmt"
"math/big"
"strconv"
"strings"
hdwallet "github.com/ethereum-optimism/go-ethereum-hdwallet"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
)
// Addr implements https://book.getfoundry.sh/cheatcodes/addr
func (c *CheatCodesPrecompile) Addr(privateKey *big.Int) (common.Address, error) {
priv, err := crypto.ToECDSA(leftPad32(privateKey.Bytes()))
if err != nil {
return common.Address{}, err
}
return crypto.PubkeyToAddress(priv.PublicKey), nil
}
// Sign implements https://book.getfoundry.sh/cheatcodes/sign
func (c *CheatCodesPrecompile) Sign() error {
return vm.ErrExecutionReverted
}
// Skip implements https://book.getfoundry.sh/cheatcodes/skip
func (c *CheatCodesPrecompile) Skip() error {
return vm.ErrExecutionReverted
}
// Label implements https://book.getfoundry.sh/cheatcodes/label
func (c *CheatCodesPrecompile) Label(addr common.Address, label string) {
c.h.labels[addr] = label
}
// GetLabel implements https://book.getfoundry.sh/cheatcodes/get-label
func (c *CheatCodesPrecompile) GetLabel(addr common.Address) string {
label, ok := c.h.labels[addr]
if !ok {
return "unlabeled:" + addr.String()
}
return label
}
// DeriveKey_6229498b implements https://book.getfoundry.sh/cheatcodes/derive-key
func (c *CheatCodesPrecompile) DeriveKey_6229498b(mnemonic string, index uint32) (*big.Int, error) {
return c.DeriveKey_6bcb2c1b(mnemonic, "m/44'/60'/0'/0/", index)
}
// DeriveKey_6bcb2c1b implements https://book.getfoundry.sh/cheatcodes/derive-key
func (c *CheatCodesPrecompile) DeriveKey_6bcb2c1b(mnemonic string, path string, index uint32) (*big.Int, error) {
w, err := hdwallet.NewFromMnemonic(mnemonic)
if err != nil {
return nil, fmt.Errorf("invalid mnemonic: %w", err)
}
account := accounts.Account{URL: accounts.URL{Path: path + strconv.FormatInt(int64(index), 10)}}
priv, err := w.PrivateKey(account)
if err != nil {
return nil, fmt.Errorf("failed to derive key of path %s: %w", account.URL.Path, err)
}
return common.Hash(crypto.FromECDSA(priv)).Big(), nil
}
// ParseBytes implements https://book.getfoundry.sh/cheatcodes/parse-bytes
func (c *CheatCodesPrecompile) ParseBytes(stringifiedValue string) ([]byte, error) {
return hexutil.Decode(stringifiedValue)
}
// ParseAddress implements https://book.getfoundry.sh/cheatcodes/parse-address
func (c *CheatCodesPrecompile) ParseAddress(stringifiedValue string) (common.Address, error) {
var out common.Address
err := out.UnmarshalText([]byte(stringifiedValue))
return out, err
}
// ParseUint implements https://book.getfoundry.sh/cheatcodes/parse-uint
func (c *CheatCodesPrecompile) ParseUint(stringifiedValue string) (*big.Int, error) {
out := new(big.Int)
err := out.UnmarshalText([]byte(stringifiedValue))
if err != nil {
return big.NewInt(0), err
}
if out.BitLen() > 256 {
return big.NewInt(0), fmt.Errorf("value %d is not a uint256, got %d bits", out, out.BitLen())
}
if out.Sign() < 0 {
return big.NewInt(0), fmt.Errorf("value %d is not a uint256, it has a negative sign", out)
}
return out, nil
}
var (
topBit = math.BigPow(2, 255)
maxInt256 = new(big.Int).Sub(topBit, big.NewInt(1))
minInt256 = new(big.Int).Neg(topBit)
)
// ParseInt implements https://book.getfoundry.sh/cheatcodes/parse-int
func (c *CheatCodesPrecompile) ParseInt(stringifiedValue string) (*ABIInt256, error) {
out := new(big.Int)
err := out.UnmarshalText([]byte(stringifiedValue))
if err != nil {
return (*ABIInt256)(big.NewInt(0)), err
}
if out.Cmp(minInt256) < 0 || out.Cmp(maxInt256) > 0 {
return (*ABIInt256)(big.NewInt(0)), fmt.Errorf("input %q out of int256 bounds", stringifiedValue)
}
return (*ABIInt256)(out), nil
}
// ParseBytes32 implements https://book.getfoundry.sh/cheatcodes/parse-bytes32
func (c *CheatCodesPrecompile) ParseBytes32(stringifiedValue string) ([32]byte, error) {
var out common.Hash
err := out.UnmarshalText([]byte(stringifiedValue))
return out, err
}
// ParseBool implements https://book.getfoundry.sh/cheatcodes/parse-bool
func (c *CheatCodesPrecompile) ParseBool(stringifiedValue string) (bool, error) {
switch strings.ToLower(stringifiedValue) {
case "true", "1":
return true, nil
case "false", "0":
return false, nil
default:
return false, fmt.Errorf("failed parsing %q as type `bool`", stringifiedValue)
}
}
// RememberKey implements https://book.getfoundry.sh/cheatcodes/remember-key
func (c *CheatCodesPrecompile) RememberKey(privateKey *big.Int) (common.Address, error) {
// We don't store the key, but we can return the address of it, to not break compat
return c.Addr(privateKey)
}
// ToString_56ca623e implements https://book.getfoundry.sh/cheatcodes/to-string
func (c *CheatCodesPrecompile) ToString_56ca623e(v common.Address) string {
return v.String()
}
// ToString_71dce7da implements https://book.getfoundry.sh/cheatcodes/to-string
func (c *CheatCodesPrecompile) ToString_71dce7da(v bool) string {
if v {
return "true"
} else {
return "false"
}
}
// ToString_6900a3ae implements https://book.getfoundry.sh/cheatcodes/to-string
func (c *CheatCodesPrecompile) ToString_6900a3ae(v *big.Int) string {
return v.String()
}
// ToString_a322c40e implements https://book.getfoundry.sh/cheatcodes/to-string
func (c *CheatCodesPrecompile) ToString_a322c40e(v *ABIInt256) string {
return (*big.Int)(v).String()
}
// ToString_b11a19e8 implements https://book.getfoundry.sh/cheatcodes/to-string
func (c *CheatCodesPrecompile) ToString_b11a19e8(v [32]byte) string {
return common.Hash(v).String()
}
// ToString_71aad10d implements https://book.getfoundry.sh/cheatcodes/to-string
func (c *CheatCodesPrecompile) ToString_71aad10d(v []byte) string {
return hexutil.Bytes(v).String()
}
// Breakpoint_f0259e92 implements https://book.getfoundry.sh/cheatcodes/breakpoint
func (c *CheatCodesPrecompile) Breakpoint_f0259e92(name string) {
c.h.log.Debug("breakpoint hit", "name", name)
}
// Breakpoint_f7d39a8d implements https://book.getfoundry.sh/cheatcodes/breakpoint
func (c *CheatCodesPrecompile) Breakpoint_f7d39a8d(name string, v bool) {
if v {
c.h.log.Debug("breakpoint set", "name", name)
} else {
c.h.log.Debug("breakpoint unset", "name", name)
}
}
// unsupported
//func (c *CheatCodesPrecompile) CreateWallet() {}
......@@ -2,6 +2,8 @@ package script
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"math/big"
......@@ -20,15 +22,36 @@ import (
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/triedb"
"github.com/ethereum/go-ethereum/triedb/hashdb"
"github.com/ethereum-optimism/optimism/op-chain-ops/foundry"
)
// Prank represents an active prank task for the next sub-call.
type Prank struct {
// Sender overrides msg.sender
Sender *common.Address
// Origin overrides tx.origin (set to actual origin if not part of the prank)
Origin *common.Address
// PrevOrigin is the tx.origin to restore after the prank
PrevOrigin common.Address
// Repeat is true if the prank persists after returning from a sub-call
Repeat bool
// A Prank may be a broadcast also.
Broadcast bool
}
// CallFrame encodes the scope context of the current call
type CallFrame struct {
Depth int
Opener vm.OpCode
Ctx *vm.ScopeContext
// Prank overrides the msg.sender, and optionally the origin.
// Forge script does not support nested pranks on the same call-depth.
// Pranks can also be broadcasting.
Prank *Prank
}
// Host is an EVM executor that runs Forge scripts.
......@@ -45,6 +68,14 @@ type Host struct {
console *Precompile[*ConsolePrecompile]
callStack []CallFrame
// serializerStates are in-progress JSON payloads by name,
// for the serializeX family of cheat codes, see:
// https://book.getfoundry.sh/cheatcodes/serialize-json
serializerStates map[string]json.RawMessage
envVars map[string]string
labels map[common.Address]string
}
// NewHost creates a Host that can load contracts from the given Artifacts FS,
......@@ -53,6 +84,9 @@ func NewHost(logger log.Logger, fs *foundry.ArtifactsFS, executionContext Contex
h := &Host{
log: logger,
af: fs,
serializerStates: make(map[string]json.RawMessage),
envVars: make(map[string]string),
labels: make(map[common.Address]string),
}
// Init a default chain config, with all the mainnet L1 forks activated
......@@ -93,7 +127,12 @@ func NewHost(logger log.Logger, fs *foundry.ArtifactsFS, executionContext Contex
// Create an in-memory database, to host our temporary script state changes
h.rawDB = rawdb.NewMemoryDatabase()
h.stateDB = state.NewDatabase(h.rawDB)
h.stateDB = state.NewDatabaseWithConfig(h.rawDB, &triedb.Config{
Preimages: true, // To be able to iterate the state we need the Preimages
IsVerkle: false,
HashDB: hashdb.Defaults,
PathDB: nil,
})
var err error
h.state, err = state.New(types.EmptyRootHash, h.stateDB, nil)
if err != nil {
......@@ -239,6 +278,21 @@ func (h *Host) onFault(pc uint64, op byte, gas, cost uint64, scope tracing.OpCon
func (h *Host) unwindCallstack(depth int) {
// pop the callstack until the depth matches
for len(h.callStack) > 0 && h.callStack[len(h.callStack)-1].Depth > depth {
// unset the prank, if the parent call-frame had set up a prank that does not repeat
if len(h.callStack) > 1 {
parentCallFrame := h.callStack[len(h.callStack)-2]
if parentCallFrame.Prank != nil {
// While going back to the parent, restore the tx.origin.
// It will later be re-applied on sub-calls if the prank persists (if Repeat == true).
if parentCallFrame.Prank.Origin != nil {
h.env.TxContext.Origin = parentCallFrame.Prank.PrevOrigin
}
if !parentCallFrame.Prank.Repeat {
parentCallFrame.Prank = nil
}
}
}
// Now pop the call-frame
h.callStack[len(h.callStack)-1] = CallFrame{} // don't hold on to the underlying call-frame resources
h.callStack = h.callStack[:len(h.callStack)-1]
}
......@@ -256,6 +310,18 @@ func (h *Host) onOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpCo
Opener: vm.OpCode(op),
Ctx: scopeCtx,
})
// apply prank, if parent call-frame set up a prank
if len(h.callStack) > 1 {
parentCallFrame := h.callStack[len(h.callStack)-2]
if parentCallFrame.Prank != nil {
if parentCallFrame.Prank.Sender != nil {
scopeCtx.Contract.CallerAddress = *parentCallFrame.Prank.Sender
}
if parentCallFrame.Prank.Origin != nil {
h.env.TxContext.Origin = *parentCallFrame.Prank.Origin
}
}
}
}
// Sanity check that top of the call-stack matches the scope context now
if len(h.callStack) == 0 || h.callStack[len(h.callStack)-1].Ctx != scopeCtx {
......@@ -307,3 +373,96 @@ func (h *Host) SelfAddress() common.Address {
}
return cf.Ctx.Address()
}
// Prank applies a prank to the current call-frame.
// Any sub-call will apply the prank to their frame context.
func (h *Host) Prank(msgSender *common.Address, txOrigin *common.Address, repeat bool, broadcast bool) error {
if len(h.callStack) == 0 {
h.log.Warn("no call stack")
return nil // cannot prank while not in a call.
}
cf := &h.callStack[len(h.callStack)-1]
if cf.Prank != nil {
if cf.Prank.Broadcast && !broadcast {
return errors.New("you have an active broadcast; broadcasting and pranks are not compatible")
}
if !cf.Prank.Broadcast && broadcast {
return errors.New("you have an active prank; broadcasting and pranks are not compatible")
}
}
cf.Prank = &Prank{
Sender: msgSender,
Origin: txOrigin,
PrevOrigin: h.env.TxContext.Origin,
Repeat: repeat,
Broadcast: broadcast,
}
return nil
}
// StopPrank disables the current prank. Any sub-call will not be pranked.
func (h *Host) StopPrank(broadcast bool) error {
if len(h.callStack) == 0 {
return nil
}
cf := &h.callStack[len(h.callStack)-1]
if cf.Prank == nil {
if broadcast {
return errors.New("no broadcast in progress to stop")
}
return nil
}
if cf.Prank.Broadcast && !broadcast {
// stopPrank on active broadcast is silent and no-op
return nil
}
if !cf.Prank.Broadcast && broadcast {
return errors.New("no broadcast in progress to stop")
}
cf.Prank = nil
return nil
}
func (h *Host) CallerMode() CallerMode {
if len(h.callStack) == 0 {
return CallerModeNone
}
cf := &h.callStack[len(h.callStack)-1]
if cf.Prank != nil {
if cf.Prank.Broadcast {
if cf.Prank.Repeat {
return CallerModeRecurrentBroadcast
}
return CallerModeBroadcast
}
if cf.Prank.Repeat {
return CallerModeRecurrentPrank
}
return CallerModePrank
}
return CallerModeNone
}
type CallerMode uint8
func (cm CallerMode) Big() *big.Int {
return big.NewInt(int64(cm))
}
// CallerMode matches the CallerMode forge cheatcode enum.
const (
CallerModeNone CallerMode = iota
CallerModeBroadcast
CallerModeRecurrentBroadcast
CallerModePrank
CallerModeRecurrentPrank
)
func (h *Host) GetEnvVar(key string) (value string, ok bool) {
value, ok = h.envVars[key]
return
}
func (h *Host) SetEnvVar(key string, value string) {
h.envVars[key] = value
}
......@@ -25,10 +25,15 @@ func TestScript(t *testing.T) {
require.NoError(t, h.EnableCheats())
h.SetEnvVar("EXAMPLE_BOOL", "true")
input := bytes4("run()")
returnData, _, err := h.Call(scriptContext.sender, addr, input[:], DefaultFoundryGasLimit, uint256.NewInt(0))
require.NoError(t, err, "call failed: %x", string(returnData))
require.NotNil(t, captLog.FindLog(
testlog.NewAttributesFilter("p0", "sender nonce"),
testlog.NewAttributesFilter("p1", "1")))
require.NoError(t, h.cheatcodes.Precompile.DumpState("noop"))
// and a second time, to see if we can revisit the host state.
require.NoError(t, h.cheatcodes.Precompile.DumpState("noop"))
}
......@@ -3,7 +3,11 @@ pragma solidity 0.8.15;
// Vm is a minimal interface to the forge cheatcode precompile
interface Vm {
function envOr(string calldata name, bool defaultValue) external view returns (bool value);
function getNonce(address account) external view returns (uint64 nonce);
function parseJsonKeys(string calldata json, string calldata key) external pure returns (string[] memory keys);
function startPrank(address msgSender) external;
function stopPrank() external;
}
// console is a minimal version of the console2 lib.
......@@ -36,6 +40,10 @@ library console {
_sendLogPayload(abi.encodeWithSignature("log(string)", p0));
}
function log(string memory p0, bool p1) internal pure {
_sendLogPayload(abi.encodeWithSignature("log(string,bool)", p0, p1));
}
function log(string memory p0, uint256 p1) internal pure {
_sendLogPayload(abi.encodeWithSignature("log(string,uint256)", p0, p1));
}
......@@ -43,6 +51,10 @@ library console {
function log(string memory p0, address p1) internal pure {
_sendLogPayload(abi.encodeWithSignature("log(string,address)", p0, p1));
}
function log(string memory p0, string memory p1, string memory p2) internal pure {
_sendLogPayload(abi.encodeWithSignature("log(string,string,string)", p0, p1, p2));
}
}
/// @title ScriptExample
......@@ -53,11 +65,34 @@ contract ScriptExample {
Vm internal constant vm = Vm(VM_ADDRESS);
/// @notice example function, runs through basic cheat-codes and console logs.
function run() public view {
function run() public {
bool x = vm.envOr("EXAMPLE_BOOL", false);
console.log("bool value from env", x);
console.log("contract addr", address(this));
console.log("contract nonce", vm.getNonce(address(this)));
console.log("sender addr", address(msg.sender));
console.log("sender nonce", vm.getNonce(address(msg.sender)));
string memory json = '{"root_key": [{"a": 1, "b": 2}]}';
string[] memory keys = vm.parseJsonKeys(json, ".root_key[0]");
console.log("keys", keys[0], keys[1]);
this.hello("from original");
vm.startPrank(address(uint160(0x42)));
this.hello("from prank 1");
console.log("parent scope msg.sender", address(msg.sender));
console.log("parent scope contract.addr", address(this));
this.hello("from prank 2");
vm.stopPrank();
this.hello("from original again");
console.log("done!");
}
/// @notice example external function, to force a CALL, and test vm.startPrank with.
function hello(string calldata _v) external view {
console.log(_v);
console.log("hello msg.sender", address(msg.sender));
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment