Commit 85d8c86e authored by protolambda's avatar protolambda Committed by GitHub

op-chain-ops: forge script fixes and improvements (#11577)

parent 36279371
......@@ -234,7 +234,7 @@ require (
rsc.io/tmplfunc v0.0.3 // indirect
)
replace github.com/ethereum/go-ethereum v1.14.8 => github.com/ethereum-optimism/op-geth v1.101408.0-rc.4
replace github.com/ethereum/go-ethereum v1.14.8 => github.com/ethereum-optimism/op-geth v1.101408.0-rc.4.0.20240822213944-6c8de76e0720
// replace github.com/ethereum/go-ethereum => ../op-geth
......
......@@ -176,8 +176,8 @@ github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/
github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 h1:RWHKLhCrQThMfch+QJ1Z8veEq5ZO3DfIhZ7xgRP9WTc=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3/go.mod h1:QziizLAiF0KqyLdNJYD7O5cpDlaFMNZzlxYNcWsJUxs=
github.com/ethereum-optimism/op-geth v1.101408.0-rc.4 h1:OhiSpP+IOoKe+9chfHYjQFFwGruLT9Uh52+LFk4y6ms=
github.com/ethereum-optimism/op-geth v1.101408.0-rc.4/go.mod h1:Mk8AhvlqFbjI9oW2ymThSSoqc6kiEH0/tCmHGMEu6ac=
github.com/ethereum-optimism/op-geth v1.101408.0-rc.4.0.20240822213944-6c8de76e0720 h1:PlMldvODGzwEBLRpK/mVUdrVa9LEN1cC0j5nKk5q7Jg=
github.com/ethereum-optimism/op-geth v1.101408.0-rc.4.0.20240822213944-6c8de76e0720/go.mod h1:Mk8AhvlqFbjI9oW2ymThSSoqc6kiEH0/tCmHGMEu6ac=
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240821192748-42bd03ba8313 h1:SVSFg8ccdRBJxOdRS1pK8oIHvMufiPAQz1gkQsEPnZc=
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240821192748-42bd03ba8313/go.mod h1:XaVXL9jg8BcyOeugECgIUGa9Y3DjYJj71RHmb5qon6M=
github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
......
......@@ -153,7 +153,8 @@ func hydrateBindingsField(
// Decodes the result of the backend into values to return as function, including error/revert handling.
outDecodeFn := func(result []byte, resultErr error) []reflect.Value {
if resultErr != nil {
if errors.Is(resultErr, vm.ErrExecutionReverted) {
// Empty return-data might happen on a regular description-less revert. No need to unpack in that case.
if len(result) > 0 && errors.Is(resultErr, vm.ErrExecutionReverted) {
msg, err := abi.UnpackRevert(result)
if err != nil {
return returnErr(fmt.Errorf("failed to unpack result args: %w", err))
......
package script
import (
"bytes"
"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/types"
"github.com/ethereum/go-ethereum/core/vm"
)
......@@ -64,7 +66,7 @@ func (c *CheatCodesPrecompile) Load(account common.Address, slot [32]byte) [32]b
// Etch implements https://book.getfoundry.sh/cheatcodes/etch
func (c *CheatCodesPrecompile) Etch(who common.Address, code []byte) {
c.h.state.SetCode(who, code)
c.h.state.SetCode(who, bytes.Clone(code)) // important to clone; geth EVM will reuse the calldata memory.
}
// Deal implements https://book.getfoundry.sh/cheatcodes/deal
......@@ -138,6 +140,17 @@ func (c *CheatCodesPrecompile) GetNonce(addr common.Address) uint64 {
return c.h.state.GetNonce(addr)
}
func (c *CheatCodesPrecompile) ResetNonce(addr common.Address) {
// Undocumented cheatcode of forge, but used a lot.
// Resets nonce to 0 if EOA, or 1 if contract.
// In scripts often set code to empty first when using it, it then becomes 0.
if c.h.state.GetCodeHash(addr) == types.EmptyCodeHash {
c.h.state.SetNonce(addr, 0)
} else {
c.h.state.SetNonce(addr, 1)
}
}
// 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")
......@@ -189,7 +202,7 @@ func (c *CheatCodesPrecompile) StartBroadcast_7fb5297f() error {
// 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)
return c.h.Prank(&who, nil, true, true)
}
// StopBroadcast implements https://book.getfoundry.sh/cheatcodes/stop-broadcast
......
package script
import (
"bytes"
"encoding/json"
"errors"
"fmt"
......@@ -36,7 +37,7 @@ func (c *CheatCodesPrecompile) ProjectRoot() string {
func (c *CheatCodesPrecompile) getArtifact(input string) (*foundry.Artifact, error) {
// fetching by relative file path, or using a contract version, is not supported
parts := strings.SplitN(input, ":", 1)
parts := strings.SplitN(input, ":", 2)
name := parts[0] + ".sol"
contract := parts[0]
if len(parts) == 2 {
......@@ -52,7 +53,7 @@ func (c *CheatCodesPrecompile) GetCode(input string) ([]byte, error) {
if err != nil {
return nil, err
}
return artifact.Bytecode.Object, nil
return bytes.Clone(artifact.Bytecode.Object), nil
}
// GetDeployedCode implements https://book.getfoundry.sh/cheatcodes/get-deployed-code
......@@ -61,7 +62,7 @@ func (c *CheatCodesPrecompile) GetDeployedCode(input string) ([]byte, error) {
if err != nil {
return nil, err
}
return artifact.DeployedBytecode.Object, nil
return bytes.Clone(artifact.DeployedBytecode.Object), nil
}
// Sleep implements https://book.getfoundry.sh/cheatcodes/sleep
......
......@@ -2,11 +2,6 @@ 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 {
......@@ -17,23 +12,10 @@ func (c *CheatCodesPrecompile) LoadAllocs(pathToAllocsJson string) error {
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)
allocs, err := c.h.StateDump()
if err != nil {
return fmt.Errorf("failed to commit state: %w", err)
return 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",
......
......@@ -37,7 +37,7 @@ func (c *CheatCodesPrecompile) Skip() error {
// Label implements https://book.getfoundry.sh/cheatcodes/label
func (c *CheatCodesPrecompile) Label(addr common.Address, label string) {
c.h.labels[addr] = label
c.h.Label(addr, label)
}
// GetLabel implements https://book.getfoundry.sh/cheatcodes/get-label
......
......@@ -17,6 +17,8 @@ var (
// ConsoleAddr is known as CONSOLE, "console.log" in ascii.
// Utils like console.sol and console2.sol work by executing a staticcall to this address.
ConsoleAddr = common.HexToAddress("0x000000000000000000636F6e736F6c652e6c6f67")
// ScriptDeployer is used for temporary scripts address(uint160(uint256(keccak256("op-stack script deployer"))))
ScriptDeployer = common.HexToAddress("0x76Ce131128F3616871f8CDA86d18fAB44E4d0D8B")
)
const (
......@@ -25,25 +27,25 @@ const (
)
type Context struct {
chainID *big.Int
sender common.Address
origin common.Address
feeRecipient common.Address
gasLimit uint64
blockNum uint64
timestamp uint64
prevRandao common.Hash
blobHashes []common.Hash
ChainID *big.Int
Sender common.Address
Origin common.Address
FeeRecipient common.Address
GasLimit uint64
BlockNum uint64
Timestamp uint64
PrevRandao common.Hash
BlobHashes []common.Hash
}
var DefaultContext = Context{
chainID: big.NewInt(1337),
sender: DefaultSenderAddr,
origin: DefaultSenderAddr,
feeRecipient: common.Address{},
gasLimit: DefaultFoundryGasLimit,
blockNum: 0,
timestamp: 0,
prevRandao: common.Hash{},
blobHashes: []common.Hash{},
ChainID: big.NewInt(1337),
Sender: DefaultSenderAddr,
Origin: DefaultSenderAddr,
FeeRecipient: common.Address{},
GasLimit: DefaultFoundryGasLimit,
BlockNum: 0,
Timestamp: 0,
PrevRandao: common.Hash{},
BlobHashes: []common.Hash{},
}
package script
import (
"errors"
"math/big"
"github.com/holiman/uint256"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
)
// Prank represents an active prank task for the next sub-call.
// This is embedded into a call-frame, to then influence the sub-call through a caller-override.
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
}
// prankRef implements the vm.ContractRef interface, to mock a caller.
type prankRef struct {
prank common.Address
ref vm.ContractRef
}
var _ vm.ContractRef = (*prankRef)(nil)
func (p *prankRef) Address() common.Address {
return p.prank
}
// Value returns the value send into this contract context.
// The delegate call tracer implicitly relies on this being implemented on ContractRef
func (p *prankRef) Value() *uint256.Int {
return p.ref.(interface{ Value() *uint256.Int }).Value()
}
func (h *Host) handleCaller(caller vm.ContractRef) vm.ContractRef {
// apply prank, if top call-frame had set up a prank
if len(h.callStack) > 0 {
parentCallFrame := h.callStack[len(h.callStack)-1]
if parentCallFrame.Prank != nil && caller.Address() != VMAddr { // pranks do not apply to the cheatcode precompile
if parentCallFrame.Prank.Sender != nil {
return &prankRef{
prank: *parentCallFrame.Prank.Sender,
ref: caller,
}
}
if parentCallFrame.Prank.Origin != nil {
h.env.TxContext.Origin = *parentCallFrame.Prank.Origin
}
}
}
return caller
}
// 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")
}
}
h.log.Warn("prank", "sender", msgSender)
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
}
// CallerMode returns the type of the top-most callframe,
// i.e. if we are in regular operation, a prank, or a broadcast (special kind of prank).
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
}
// CallerMode matches the CallerMode forge cheatcode enum.
type CallerMode uint8
func (cm CallerMode) Big() *big.Int {
return big.NewInt(int64(cm))
}
const (
CallerModeNone CallerMode = iota
CallerModeBroadcast
CallerModeRecurrentBroadcast
CallerModePrank
CallerModeRecurrentPrank
)
......@@ -10,6 +10,7 @@ import (
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
)
......@@ -55,22 +56,37 @@ func rightPad32(data []byte) []byte {
type Precompile[E any] struct {
Precompile E
fieldsOnly bool
// abiMethods is effectively the jump-table for 4-byte ABI calls to the precompile.
abiMethods map[[4]byte]*precompileFunc
}
var _ vm.PrecompiledContract = (*Precompile[struct{}])(nil)
type PrecompileOption[E any] func(p *Precompile[E])
func WithFieldsOnly[E any](p *Precompile[E]) {
p.fieldsOnly = true
}
// NewPrecompile wraps a Go object into a Precompile.
// All exported fields and methods will have a corresponding ABI interface.
// Fields with a tag `evm:"-"` will be ignored.
// Fields with a tag `evm:"-"` will be ignored, or can override their ABI name to x with this tag: `evm:"x"`.
// Field names and method names are adjusted to start with a lowercase character in the ABI signature.
// Method names may end with a `_X` where X must be the 4byte selector (this is sanity-checked),
// to support multiple variants of the same method with different ABI input parameters.
// Methods may return an error, which will result in a revert, rather than become an ABI encoded arg, if not nil.
// All precompile methods have 0 gas cost.
func NewPrecompile[E any](e E) (*Precompile[E], error) {
out := &Precompile[E]{Precompile: e, abiMethods: make(map[[4]byte]*precompileFunc)}
func NewPrecompile[E any](e E, opts ...PrecompileOption[E]) (*Precompile[E], error) {
out := &Precompile[E]{
Precompile: e,
abiMethods: make(map[[4]byte]*precompileFunc),
fieldsOnly: false,
}
for _, opt := range opts {
opt(out)
}
elemVal := reflect.ValueOf(e)
// setup methods (and if pointer, the indirect methods also)
if err := out.setupMethods(&elemVal); err != nil {
......@@ -85,6 +101,9 @@ func NewPrecompile[E any](e E) (*Precompile[E], error) {
// setupMethods iterates through all exposed methods of val, and sets them all up as ABI methods.
func (p *Precompile[E]) setupMethods(val *reflect.Value) error {
if p.fieldsOnly {
return nil
}
typ := val.Type()
methodCount := val.NumMethod()
for i := 0; i < methodCount; i++ {
......@@ -431,6 +450,10 @@ func (p *Precompile[E]) setupStructField(fieldDef *reflect.StructField, fieldVal
if lo := strings.ToLower(abiFunctionName[:1]); lo != abiFunctionName[:1] {
abiFunctionName = lo + abiFunctionName[1:]
}
// The tag can override the field name
if v, ok := fieldDef.Tag.Lookup("evm"); ok {
abiFunctionName = v
}
// The ABI signature of public fields in solidity is simply a getter function of the same name.
// The return type is not part of the ABI signature. So we just append "()" to turn it into a function.
methodSig := abiFunctionName + "()"
......@@ -455,7 +478,14 @@ func (p *Precompile[E]) setupStructField(fieldDef *reflect.StructField, fieldVal
if len(input) != 0 { // 4 byte selector is already trimmed
return nil, fmt.Errorf("unexpected input: %x", input)
}
outData, err := outArgs.PackValues([]any{fieldVal.Interface()})
v := fieldVal.Interface()
if abiVal, ok := v.(interface{ ToABI() []byte }); ok {
return abiVal.ToABI(), nil
}
if bigInt, ok := v.(*hexutil.Big); ok { // We can change this to use convertType later, if we need more generic type handling.
v = (*big.Int)(bigInt)
}
outData, err := outArgs.PackValues([]any{v})
if err != nil {
return nil, fmt.Errorf("method %s failed to pack return data: %w", methodSig, err)
}
......
package script
import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"math/big"
......@@ -26,27 +26,22 @@ import (
"github.com/ethereum/go-ethereum/triedb/hashdb"
"github.com/ethereum-optimism/optimism/op-chain-ops/foundry"
"github.com/ethereum-optimism/optimism/op-chain-ops/srcmap"
)
// 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
Depth int
LastOp vm.OpCode
LastPC uint64
// Reverts often happen in generated code.
// We want to fallback to logging the source-map position of
// the non-generated code, i.e. the origin of the last successful jump.
LastJumpPC uint64
Ctx *vm.ScopeContext
// Prank overrides the msg.sender, and optionally the origin.
// Forge script does not support nested pranks on the same call-depth.
......@@ -67,6 +62,8 @@ type Host struct {
cheatcodes *Precompile[*CheatCodesPrecompile]
console *Precompile[*ConsolePrecompile]
precompiles map[common.Address]vm.PrecompiledContract
callStack []CallFrame
// serializerStates are in-progress JSON payloads by name,
......@@ -76,22 +73,32 @@ type Host struct {
envVars map[string]string
labels map[common.Address]string
// srcFS enables src-map loading;
// this is a bit more expensive, but provides useful debug information.
// src-maps are disabled if this is nil.
srcFS *foundry.SourceMapFS
srcMaps map[common.Address]*srcmap.SourceMap
}
// NewHost creates a Host that can load contracts from the given Artifacts FS,
// and with an EVM initialized to the given executionContext.
func NewHost(logger log.Logger, fs *foundry.ArtifactsFS, executionContext Context) *Host {
// Optionally src-map loading may be enabled, by providing a non-nil srcFS to read sources from.
func NewHost(logger log.Logger, fs *foundry.ArtifactsFS, srcFS *foundry.SourceMapFS, executionContext Context) *Host {
h := &Host{
log: logger,
af: fs,
serializerStates: make(map[string]json.RawMessage),
envVars: make(map[string]string),
labels: make(map[common.Address]string),
precompiles: make(map[common.Address]vm.PrecompiledContract),
srcFS: srcFS,
srcMaps: make(map[common.Address]*srcmap.SourceMap),
}
// Init a default chain config, with all the mainnet L1 forks activated
h.chainCfg = &params.ChainConfig{
ChainID: executionContext.chainID,
ChainID: executionContext.ChainID,
// Ethereum forks in proof-of-work era.
HomesteadBlock: big.NewInt(0),
EIP150Block: big.NewInt(0),
......@@ -149,22 +156,22 @@ func NewHost(logger log.Logger, fs *foundry.ArtifactsFS, executionContext Contex
return crypto.Keccak256Hash(out[:])
},
L1CostFunc: nil,
Coinbase: executionContext.feeRecipient,
GasLimit: executionContext.gasLimit,
BlockNumber: new(big.Int).SetUint64(executionContext.blockNum),
Time: executionContext.timestamp,
Coinbase: executionContext.FeeRecipient,
GasLimit: executionContext.GasLimit,
BlockNumber: new(big.Int).SetUint64(executionContext.BlockNum),
Time: executionContext.Timestamp,
Difficulty: nil, // not used anymore post-merge
BaseFee: big.NewInt(0),
BlobBaseFee: big.NewInt(0),
Random: &executionContext.prevRandao,
Random: &executionContext.PrevRandao,
}
// Initialize a transaction-context for the EVM to access environment variables.
// The transaction context (after embedding inside of the EVM environment) may be mutated later.
txContext := vm.TxContext{
Origin: executionContext.origin,
Origin: executionContext.Origin,
GasPrice: big.NewInt(0),
BlobHashes: executionContext.blobHashes,
BlobHashes: executionContext.BlobHashes,
BlobFeeCap: big.NewInt(0),
AccessEvents: state.NewAccessEvents(h.stateDB.PointCache()),
}
......@@ -183,6 +190,7 @@ func NewHost(logger log.Logger, fs *foundry.ArtifactsFS, executionContext Contex
NoBaseFee: true,
Tracer: trHooks,
PrecompileOverrides: h.getPrecompile,
CallerOverride: h.handleCaller,
}
h.env = vm.NewEVM(blockContext, txContext, h.state, h.chainCfg, vmCfg)
......@@ -201,6 +209,7 @@ func (h *Host) EnableCheats() error {
// We need to insert some placeholder code to prevent it from aborting calls.
// Emulates Forge script: https://github.com/foundry-rs/foundry/blob/224fe9cbf76084c176dabf7d3b2edab5df1ab818/crates/evm/evm/src/executors/mod.rs#L108
h.state.SetCode(VMAddr, []byte{0x00})
h.precompiles[VMAddr] = h.cheatcodes
consolePrecompile, err := NewPrecompile[*ConsolePrecompile](&ConsolePrecompile{
logger: h.log,
......@@ -210,6 +219,7 @@ func (h *Host) EnableCheats() error {
return fmt.Errorf("failed to init console precompile: %w", err)
}
h.console = consolePrecompile
h.precompiles[ConsoleAddr] = h.console
// The Console precompile does not need bytecode,
// calls all go through a console lib, which avoids the EXTCODESIZE.
return nil
......@@ -234,44 +244,106 @@ func (h *Host) LoadContract(artifactName, contractName string) (common.Address,
if err != nil {
return common.Address{}, fmt.Errorf("failed to load %s / %s: %w", artifactName, contractName, err)
}
h.prelude(h.env.TxContext.Origin, nil)
ret, addr, _, err := h.env.Create(vm.AccountRef(h.env.TxContext.Origin),
artifact.Bytecode.Object, DefaultFoundryGasLimit, uint256.NewInt(0))
addr, err := h.Create(h.TxOrigin(), artifact.Bytecode.Object)
if err != nil {
return common.Address{}, err
}
h.RememberArtifact(addr, artifact, contractName)
return addr, nil
}
// RememberArtifact registers an address as originating from a particular artifact.
// This register a source-map, if the Host is configured with a source-map FS.
func (h *Host) RememberArtifact(addr common.Address, artifact *foundry.Artifact, contract string) {
if h.srcFS == nil {
return
}
code := h.state.GetCode(addr)
if !bytes.Equal(code, artifact.DeployedBytecode.Object) {
h.log.Warn("src map warning: state bytecode does not match artifact deployed bytecode", "addr", addr)
}
srcMap, err := h.srcFS.SourceMap(artifact, contract)
if err != nil {
return common.Address{}, fmt.Errorf("failed to create contract, return: %x, err: %w", ret, err)
h.log.Warn("failed to load srcmap", "addr", addr, "err", err)
return
}
h.srcMaps[addr] = srcMap
}
// Create a contract with unlimited gas, and 0 ETH value.
// This create function helps deploy contracts quickly for scripting etc.
func (h *Host) Create(from common.Address, initCode []byte) (common.Address, error) {
h.prelude(from, nil)
ret, addr, _, err := h.env.Create(vm.AccountRef(from),
initCode, DefaultFoundryGasLimit, uint256.NewInt(0))
if err != nil {
retStr := fmt.Sprintf("%x", ret)
if len(retStr) > 20 {
retStr = retStr[:20] + "..."
}
return common.Address{}, fmt.Errorf("failed to create contract, return: %s, err: %w", retStr, err)
}
return addr, nil
}
// Wipe an account: removing the code, and setting address and balance to 0. This makes the account "empty".
// Note that storage is not removed.
func (h *Host) Wipe(addr common.Address) {
if h.state.GetCodeSize(addr) > 0 {
h.state.SetCode(addr, nil)
}
h.state.SetNonce(addr, 0)
h.state.SetBalance(addr, uint256.NewInt(0), tracing.BalanceChangeUnspecified)
}
// getPrecompile overrides any accounts during runtime, to insert special precompiles, if activated.
func (h *Host) getPrecompile(rules params.Rules, original vm.PrecompiledContract, addr common.Address) vm.PrecompiledContract {
switch addr {
case VMAddr:
return h.cheatcodes // nil if cheats are not enabled
case ConsoleAddr:
return h.console // nil if cheats are not enabled
default:
return original
if p, ok := h.precompiles[addr]; ok {
return p
}
return original
}
// SetPrecompile inserts a precompile at the given address.
// If the precompile is nil, it removes the precompile override from that address, and wipes the account.
func (h *Host) SetPrecompile(addr common.Address, precompile vm.PrecompiledContract) {
if precompile == nil {
h.log.Debug("removing precompile", "addr", addr)
delete(h.precompiles, addr)
h.Wipe(addr)
return
}
h.log.Debug("adding precompile", "addr", addr)
h.precompiles[addr] = precompile
// insert non-empty placeholder bytecode, so EXTCODESIZE checks pass
h.state.SetCode(addr, []byte{0})
}
// HasPrecompileOverride inspects if there exists an active precompile-override at the given address.
func (h *Host) HasPrecompileOverride(addr common.Address) bool {
_, ok := h.precompiles[addr]
return ok
}
// onExit is a trace-hook, which we use to maintain an accurate view of functions, and log any revert warnings.
func (h *Host) onExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) {
// Note: onExit runs also when going deeper, exiting the context into a nested context.
addr := h.SelfAddress()
h.unwindCallstack(depth)
if reverted {
h.LogCallStack()
if msg, revertInspectErr := abi.UnpackRevert(output); revertInspectErr == nil {
h.log.Warn("Revert", "addr", addr, "err", err, "revertMsg", msg)
h.log.Warn("Revert", "addr", addr, "err", err, "revertMsg", msg, "depth", depth)
} else {
h.log.Warn("Revert", "addr", addr, "err", err, "revertData", hexutil.Bytes(output))
h.log.Warn("Revert", "addr", addr, "err", err, "revertData", hexutil.Bytes(output), "depth", depth)
}
}
h.unwindCallstack(depth)
}
// onFault is a trace-hook, catches things more generic than regular EVM reverts.
func (h *Host) onFault(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, depth int, err error) {
h.log.Warn("Fault", "addr", scope.Address(), "err", err)
h.log.Warn("Fault", "addr", scope.Address(), "err", err, "depth", depth)
}
// unwindCallstack is a helper to remove call-stack entries.
......@@ -306,27 +378,23 @@ func (h *Host) onOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpCo
// We do this here, instead of onEnter, to capture an initialized scope.
if len(h.callStack) == 0 || h.callStack[len(h.callStack)-1].Depth < depth {
h.callStack = append(h.callStack, CallFrame{
Depth: depth,
Opener: vm.OpCode(op),
Ctx: scopeCtx,
Depth: depth,
LastOp: vm.OpCode(op),
LastPC: pc,
LastJumpPC: pc,
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 {
panic("scope context changed without call-frame pop/push")
}
cf := &h.callStack[len(h.callStack)-1]
if vm.OpCode(op) == vm.JUMPDEST { // remember the last PC before successful jump
cf.LastJumpPC = cf.LastPC
}
cf.LastOp = vm.OpCode(op)
cf.LastPC = pc
}
// onStorageChange is a trace-hook to capture state changes
......@@ -374,95 +442,110 @@ 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
func (h *Host) GetEnvVar(key string) (value string, ok bool) {
value, ok = h.envVars[key]
return
}
// 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
func (h *Host) SetEnvVar(key string, value string) {
h.envVars[key] = value
}
// StateDump turns the current EVM state into a foundry-allocs dump
// (wrapping a geth Account allocs type). This is used to export the state.
// Note that upon dumping, the state-DB is committed and flushed.
// This affects any remaining self-destructs, as all accounts are flushed to persistent state.
// After flushing the EVM state also cannot revert to a previous snapshot state:
// the state should not be dumped within contract-execution that needs to revert.
func (h *Host) StateDump() (*foundry.ForgeAllocs, error) {
// We have to commit the existing state to the trie,
// for all the state-changes to be captured by the trie iterator.
root, err := h.state.Commit(h.env.Context.BlockNumber.Uint64(), true)
if err != nil {
return nil, fmt.Errorf("failed to commit state: %w", err)
}
cf := &h.callStack[len(h.callStack)-1]
if cf.Prank == nil {
if broadcast {
return errors.New("no broadcast in progress to stop")
}
return nil
// We need a state object around the state DB
st, err := state.New(root, h.stateDB, nil)
if err != nil {
return nil, fmt.Errorf("failed to create state object for state-dumping: %w", err)
}
if cf.Prank.Broadcast && !broadcast {
// stopPrank on active broadcast is silent and no-op
return nil
// After Commit we cannot reuse the old State, so we update the host to use the new one
h.state = st
h.env.StateDB = st
var allocs foundry.ForgeAllocs
allocs.FromState(st)
// Sanity check we have no lingering scripts.
for i := uint64(0); i <= allocs.Accounts[ScriptDeployer].Nonce; i++ {
scriptAddr := crypto.CreateAddress(ScriptDeployer, i)
h.log.Info("removing script from state-dump", "addr", scriptAddr, "label", h.labels[scriptAddr])
delete(allocs.Accounts, scriptAddr)
}
if !cf.Prank.Broadcast && broadcast {
return errors.New("no broadcast in progress to stop")
// Remove the script deployer from the output
delete(allocs.Accounts, ScriptDeployer)
// The cheatcodes VM has a placeholder bytecode,
// because solidity checks if the code exists prior to regular EVM-calls to it.
delete(allocs.Accounts, VMAddr)
// Precompile overrides come with temporary state account placeholders. Ignore those.
for addr := range h.precompiles {
delete(allocs.Accounts, addr)
}
cf.Prank = nil
return nil
return &allocs, 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
func (h *Host) SetTxOrigin(addr common.Address) {
h.env.TxContext.Origin = addr
}
type CallerMode uint8
func (h *Host) TxOrigin() common.Address {
return h.env.TxContext.Origin
}
func (cm CallerMode) Big() *big.Int {
return big.NewInt(int64(cm))
// ScriptBackendFn is a convenience method for scripts to attach to the Host.
// It return a function pre-configured with the given destination-address,
// to call the destination script.
func (h *Host) ScriptBackendFn(to common.Address) CallBackendFn {
return func(data []byte) ([]byte, error) {
ret, _, err := h.Call(h.env.TxContext.Origin, to, data, DefaultFoundryGasLimit, uint256.NewInt(0))
return ret, err
}
}
// CallerMode matches the CallerMode forge cheatcode enum.
const (
CallerModeNone CallerMode = iota
CallerModeBroadcast
CallerModeRecurrentBroadcast
CallerModePrank
CallerModeRecurrentPrank
)
// EnforceMaxCodeSize configures the EVM to enforce (if true), or not enforce (if false),
// the maximum contract bytecode size.
func (h *Host) EnforceMaxCodeSize(v bool) {
h.env.Config.NoMaxCodeSize = !v
}
func (h *Host) GetEnvVar(key string) (value string, ok bool) {
value, ok = h.envVars[key]
return
// LogCallStack is a convenience method for debugging,
// to log details of each call-frame (from bottom to top) to the logger.
func (h *Host) LogCallStack() {
for _, cf := range h.callStack {
callsite := ""
if srcMap, ok := h.srcMaps[cf.Ctx.Address()]; ok {
callsite = srcMap.FormattedInfo(cf.LastPC)
if callsite == "unknown:0:0" {
callsite = srcMap.FormattedInfo(cf.LastJumpPC)
}
}
input := cf.Ctx.CallInput()
byte4 := ""
if len(input) >= 4 {
byte4 = fmt.Sprintf("0x%x", input[:4])
}
h.log.Debug("callframe", "depth", cf.Depth, "input", hexutil.Bytes(input), "pc", cf.LastPC, "op", cf.LastOp)
h.log.Warn("callframe", "depth", cf.Depth, "byte4", byte4,
"addr", cf.Ctx.Address(), "callsite", callsite, "label", h.labels[cf.Ctx.Address()])
}
}
func (h *Host) SetEnvVar(key string, value string) {
h.envVars[key] = value
// Label an address with a name, like the foundry vm.label cheatcode.
func (h *Host) Label(addr common.Address, label string) {
h.log.Debug("labeling", "addr", addr, "label", label)
h.labels[addr] = label
}
......@@ -19,7 +19,7 @@ func TestScript(t *testing.T) {
af := foundry.OpenArtifactsDir("./testdata/test-artifacts")
scriptContext := DefaultContext
h := NewHost(logger, af, scriptContext)
h := NewHost(logger, af, nil, scriptContext)
addr, err := h.LoadContract("ScriptExample.s.sol", "ScriptExample")
require.NoError(t, err)
......@@ -27,7 +27,7 @@ func TestScript(t *testing.T) {
h.SetEnvVar("EXAMPLE_BOOL", "true")
input := bytes4("run()")
returnData, _, err := h.Call(scriptContext.sender, addr, input[:], DefaultFoundryGasLimit, uint256.NewInt(0))
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"),
......
package script
import (
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
func checkABI(abiData *abi.ABI, methodSignature string) bool {
for _, m := range abiData.Methods {
if m.Sig == methodSignature {
return true
}
}
return false
}
// WithScript deploys a script contract, at a create-address based on the ScriptDeployer.
// The returned cleanup function wipes the script account again (but not the storage).
func WithScript[B any](h *Host, name string, contract string) (b *B, cleanup func(), err error) {
// load contract artifact
artifact, err := h.af.ReadArtifact(name, contract)
if err != nil {
return nil, nil, fmt.Errorf("could not load script artifact: %w", err)
}
deployer := ScriptDeployer
deployNonce := h.state.GetNonce(deployer)
// compute address of script contract to be deployed
addr := crypto.CreateAddress(deployer, deployNonce)
// init bindings (with ABI check)
bindings, err := MakeBindings[B](h.ScriptBackendFn(addr), func(abiDef string) bool {
return checkABI(&artifact.ABI, abiDef)
})
if err != nil {
return nil, nil, fmt.Errorf("failed to make bindings: %w", err)
}
// Scripts can be very large
h.EnforceMaxCodeSize(false)
defer h.EnforceMaxCodeSize(true)
// deploy the script contract
deployedAddr, err := h.Create(deployer, artifact.Bytecode.Object)
if err != nil {
return nil, nil, fmt.Errorf("failed to deploy script: %w", err)
}
if deployedAddr != addr {
return nil, nil, fmt.Errorf("deployed to unexpected address %s, expected %s", deployedAddr, addr)
}
h.RememberArtifact(addr, artifact, contract)
h.Label(addr, contract)
return bindings, func() {
h.Wipe(addr)
}, nil
}
// WithPrecompileAtAddress turns a struct into a precompile,
// and inserts it as override at the given address in the host.
// A cleanup function is returned, to remove the precompile override again.
func WithPrecompileAtAddress[E any](h *Host, addr common.Address, elem E, opts ...PrecompileOption[E]) (cleanup func(), err error) {
if h.HasPrecompileOverride(addr) {
return nil, fmt.Errorf("already have existing precompile override at %s", addr)
}
precompile, err := NewPrecompile[E](elem, opts...)
if err != nil {
return nil, fmt.Errorf("failed to construct precompile: %w", err)
}
h.SetPrecompile(addr, precompile)
h.Label(addr, fmt.Sprintf("%T", precompile.Precompile))
return func() {
h.SetPrecompile(addr, nil)
}, nil
}
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