Skip to content

Commit

Permalink
op-chain-ops: forge script fixes and improvements (ethereum-optimism#…
Browse files Browse the repository at this point in the history
  • Loading branch information
protolambda authored and samlaf committed Nov 10, 2024
1 parent 5d946a1 commit 3637e1c
Show file tree
Hide file tree
Showing 13 changed files with 524 additions and 186 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
3 changes: 2 additions & 1 deletion op-chain-ops/script/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
17 changes: 15 additions & 2 deletions op-chain-ops/script/cheatcodes_environment.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions op-chain-ops/script/cheatcodes_external.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package script

import (
"bytes"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand Down
22 changes: 2 additions & 20 deletions op-chain-ops/script/cheatcodes_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion op-chain-ops/script/cheatcodes_utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 20 additions & 18 deletions op-chain-ops/script/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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{},
}
150 changes: 150 additions & 0 deletions op-chain-ops/script/prank.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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
)
Loading

0 comments on commit 3637e1c

Please sign in to comment.