batch.go 8.74 KB
Newer Older
1 2 3 4 5 6
// Package safe contains types for working with Safe smart contract wallets. These are used to
// build batch transactions for the tx-builder app. The types are based on
// https://github.com/safe-global/safe-react-apps/blob/development/apps/tx-builder/src/typings/models.ts.
package safe

import (
7
	"bytes"
8 9
	"encoding/json"
	"fmt"
Mark Tyneway's avatar
Mark Tyneway committed
10
	"math/big"
Mark Tyneway's avatar
Mark Tyneway committed
11
	"strings"
Mark Tyneway's avatar
Mark Tyneway committed
12

Mark Tyneway's avatar
Mark Tyneway committed
13 14
	"golang.org/x/exp/maps"

Mark Tyneway's avatar
Mark Tyneway committed
15
	"github.com/ethereum/go-ethereum/accounts/abi"
16 17
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
18
	"github.com/ethereum/go-ethereum/crypto"
19 20
)

Mark Tyneway's avatar
Mark Tyneway committed
21 22
// Batch represents a Safe tx-builder transaction.
type Batch struct {
23 24 25
	Version      string             `json:"version"`
	ChainID      *big.Int           `json:"chainId"`
	CreatedAt    uint64             `json:"createdAt"`
Mark Tyneway's avatar
Mark Tyneway committed
26
	Meta         BatchMeta          `json:"meta"`
27 28 29
	Transactions []BatchTransaction `json:"transactions"`
}

Mark Tyneway's avatar
Mark Tyneway committed
30 31
// AddCall will add a call to the batch. After a series of calls are
// added to the batch, it can be serialized to JSON.
Mark Tyneway's avatar
Mark Tyneway committed
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
func (b *Batch) AddCall(to common.Address, value *big.Int, sig string, args []any, iface abi.ABI) error {
	// Attempt to pull out the signature from the top level methods.
	// The abi package uses normalization that we do not want to be
	// coupled to, so attempt to search for the raw name if the top
	// level name is not found to handle overloading more gracefully.
	method, ok := iface.Methods[sig]
	if !ok {
		for _, m := range iface.Methods {
			if m.RawName == sig || m.Sig == sig {
				method = m
				ok = true
			}
		}
	}
	if !ok {
		keys := maps.Keys(iface.Methods)
		methods := strings.Join(keys, ",")
		return fmt.Errorf("%s not found in abi, options are %s", sig, methods)
	}

Mark Tyneway's avatar
Mark Tyneway committed
52
	if len(args) != len(method.Inputs) {
Mark Tyneway's avatar
Mark Tyneway committed
53 54 55 56 57 58 59 60 61
		return fmt.Errorf("requires %d inputs but got %d for %s", len(method.Inputs), len(args), method.RawName)
	}

	contractMethod := ContractMethod{
		Name:    method.RawName,
		Payable: method.Payable,
	}

	inputValues := make(map[string]string)
Mark Tyneway's avatar
Mark Tyneway committed
62
	contractInputs := make([]ContractInput, 0)
Mark Tyneway's avatar
Mark Tyneway committed
63 64

	for i, input := range method.Inputs {
Mark Tyneway's avatar
Mark Tyneway committed
65
		contractInput, err := createContractInput(input, contractInputs)
Mark Tyneway's avatar
Mark Tyneway committed
66 67 68
		if err != nil {
			return err
		}
Mark Tyneway's avatar
Mark Tyneway committed
69
		contractMethod.Inputs = append(contractMethod.Inputs, contractInput...)
Mark Tyneway's avatar
Mark Tyneway committed
70 71 72 73 74 75 76 77

		str, err := stringifyArg(args[i])
		if err != nil {
			return err
		}
		inputValues[input.Name] = str
	}

Mark Tyneway's avatar
Mark Tyneway committed
78
	encoded, err := method.Inputs.PackValues(args)
Mark Tyneway's avatar
Mark Tyneway committed
79 80 81
	if err != nil {
		return err
	}
Mark Tyneway's avatar
Mark Tyneway committed
82 83 84
	data := make([]byte, len(method.ID)+len(encoded))
	copy(data, method.ID)
	copy(data[len(method.ID):], encoded)
Mark Tyneway's avatar
Mark Tyneway committed
85

Mark Tyneway's avatar
Mark Tyneway committed
86 87 88 89
	batchTransaction := BatchTransaction{
		To:          to,
		Value:       value,
		Method:      contractMethod,
Mark Tyneway's avatar
Mark Tyneway committed
90
		Data:        data,
Mark Tyneway's avatar
Mark Tyneway committed
91 92 93 94 95 96 97 98
		InputValues: inputValues,
	}

	b.Transactions = append(b.Transactions, batchTransaction)

	return nil
}

99 100 101 102 103 104 105 106 107 108
// Check will check the batch for errors
func (b *Batch) Check() error {
	for _, tx := range b.Transactions {
		if err := tx.Check(); err != nil {
			return err
		}
	}
	return nil
}

109
// bathcFileMarshaling is a helper type used for JSON marshaling.
Mark Tyneway's avatar
Mark Tyneway committed
110
type batchMarshaling struct {
111 112 113
	Version      string             `json:"version"`
	ChainID      string             `json:"chainId"`
	CreatedAt    uint64             `json:"createdAt"`
Mark Tyneway's avatar
Mark Tyneway committed
114
	Meta         BatchMeta          `json:"meta"`
115 116 117
	Transactions []BatchTransaction `json:"transactions"`
}

Mark Tyneway's avatar
Mark Tyneway committed
118 119 120
// MarshalJSON will marshal a Batch to JSON.
func (b *Batch) MarshalJSON() ([]byte, error) {
	batch := batchMarshaling{
121 122 123 124
		Version:      b.Version,
		CreatedAt:    b.CreatedAt,
		Meta:         b.Meta,
		Transactions: b.Transactions,
Mark Tyneway's avatar
Mark Tyneway committed
125 126 127 128 129
	}
	if b.ChainID != nil {
		batch.ChainID = b.ChainID.String()
	}
	return json.Marshal(batch)
130 131
}

Mark Tyneway's avatar
Mark Tyneway committed
132 133 134
// UnmarshalJSON will unmarshal a Batch from JSON.
func (b *Batch) UnmarshalJSON(data []byte) error {
	var bf batchMarshaling
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
	if err := json.Unmarshal(data, &bf); err != nil {
		return err
	}
	b.Version = bf.Version
	chainId, ok := new(big.Int).SetString(bf.ChainID, 10)
	if !ok {
		return fmt.Errorf("cannot set chainId to %s", bf.ChainID)
	}
	b.ChainID = chainId
	b.CreatedAt = bf.CreatedAt
	b.Meta = bf.Meta
	b.Transactions = bf.Transactions
	return nil
}

Mark Tyneway's avatar
Mark Tyneway committed
150
// BatchMeta contains metadata about a Batch. Not all
151
// of the fields are required.
Mark Tyneway's avatar
Mark Tyneway committed
152
type BatchMeta struct {
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
	TxBuilderVersion        string `json:"txBuilderVersion,omitempty"`
	Checksum                string `json:"checksum,omitempty"`
	CreatedFromSafeAddress  string `json:"createdFromSafeAddress"`
	CreatedFromOwnerAddress string `json:"createdFromOwnerAddress"`
	Name                    string `json:"name"`
	Description             string `json:"description"`
}

// BatchTransaction represents a single call in a tx-builder transaction.
type BatchTransaction struct {
	To          common.Address    `json:"to"`
	Value       *big.Int          `json:"value"`
	Data        []byte            `json:"data"`
	Method      ContractMethod    `json:"contractMethod"`
	InputValues map[string]string `json:"contractInputsValues"`
}

170 171 172 173
// Check will check the batch transaction for errors.
// An error is defined by:
// - incorrectly encoded calldata
// - mismatch in number of arguments
Mark Tyneway's avatar
Mark Tyneway committed
174 175
// It does not currently work on structs, will return no error if a "tuple"
// is used as an argument. Need to find a generic way to work with structs.
176 177 178 179 180 181 182 183 184 185 186 187 188 189
func (bt *BatchTransaction) Check() error {
	if len(bt.Method.Inputs) != len(bt.InputValues) {
		return fmt.Errorf("expected %d inputs but got %d", len(bt.Method.Inputs), len(bt.InputValues))
	}

	if len(bt.Data) > 0 && bt.Method.Name != "fallback" {
		if len(bt.Data) < 4 {
			return fmt.Errorf("must have at least 4 bytes of calldata, got %d", len(bt.Data))
		}
		sig := bt.Signature()
		selector := crypto.Keccak256([]byte(sig))[0:4]
		if !bytes.Equal(bt.Data[0:4], selector) {
			return fmt.Errorf("data does not match signature")
		}
Mark Tyneway's avatar
Mark Tyneway committed
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215

		// Check the calldata
		values := make([]any, len(bt.Method.Inputs))
		for i, input := range bt.Method.Inputs {
			value, ok := bt.InputValues[input.Name]
			if !ok {
				return fmt.Errorf("missing input %s", input.Name)
			}
			// Need to figure out better way to handle tuples in a generic way
			if input.Type == "tuple" {
				return nil
			}
			arg, err := unstringifyArg(value, input.Type)
			if err != nil {
				return err
			}
			values[i] = arg
		}

		calldata, err := bt.Arguments().PackValues(values)
		if err != nil {
			return err
		}
		if !bytes.Equal(bt.Data[4:], calldata) {
			return fmt.Errorf("calldata does not match inputs, expected %s, got %s", hexutil.Encode(bt.Data[4:]), hexutil.Encode(calldata))
		}
216 217 218 219 220 221 222 223 224 225 226 227 228
	}
	return nil
}

// Signature returns the function signature of the batch transaction.
func (bt *BatchTransaction) Signature() string {
	types := make([]string, len(bt.Method.Inputs))
	for i, input := range bt.Method.Inputs {
		types[i] = buildFunctionSignature(input)
	}
	return fmt.Sprintf("%s(%s)", bt.Method.Name, strings.Join(types, ","))
}

Mark Tyneway's avatar
Mark Tyneway committed
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
func (bt *BatchTransaction) Arguments() abi.Arguments {
	arguments := make(abi.Arguments, len(bt.Method.Inputs))
	for i, input := range bt.Method.Inputs {
		serialized, err := json.Marshal(input)
		if err != nil {
			panic(err)
		}
		var arg abi.Argument
		if err := json.Unmarshal(serialized, &arg); err != nil {
			panic(err)
		}
		arguments[i] = arg
	}
	return arguments
}

245 246 247 248 249 250 251 252
// UnmarshalJSON will unmarshal a BatchTransaction from JSON.
func (b *BatchTransaction) UnmarshalJSON(data []byte) error {
	var bt batchTransactionMarshaling
	if err := json.Unmarshal(data, &bt); err != nil {
		return err
	}
	b.To = common.HexToAddress(bt.To)
	b.Value = new(big.Int).SetUint64(bt.Value)
253 254 255
	if bt.Data != nil {
		b.Data = common.CopyBytes(*bt.Data)
	}
256 257 258 259 260 261 262 263 264 265 266 267 268 269
	b.Method = bt.Method
	b.InputValues = bt.InputValues
	return nil
}

// MarshalJSON will marshal a BatchTransaction to JSON.
func (b *BatchTransaction) MarshalJSON() ([]byte, error) {
	batch := batchTransactionMarshaling{
		To:          b.To.Hex(),
		Value:       b.Value.Uint64(),
		Method:      b.Method,
		InputValues: b.InputValues,
	}
	if len(b.Data) != 0 {
Mark Tyneway's avatar
Mark Tyneway committed
270 271
		data := hexutil.Bytes(b.Data)
		batch.Data = &data
272 273 274 275 276 277 278 279
	}
	return json.Marshal(batch)
}

// batchTransactionMarshaling is a helper type used for JSON marshaling.
type batchTransactionMarshaling struct {
	To          string            `json:"to"`
	Value       uint64            `json:"value,string"`
Mark Tyneway's avatar
Mark Tyneway committed
280
	Data        *hexutil.Bytes    `json:"data"`
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
	Method      ContractMethod    `json:"contractMethod"`
	InputValues map[string]string `json:"contractInputsValues"`
}

// ContractMethod represents a method call in a tx-builder transaction.
type ContractMethod struct {
	Inputs  []ContractInput `json:"inputs"`
	Name    string          `json:"name"`
	Payable bool            `json:"payable"`
}

// ContractInput represents an input to a contract method.
type ContractInput struct {
	InternalType string          `json:"internalType"`
	Name         string          `json:"name"`
	Type         string          `json:"type"`
	Components   []ContractInput `json:"components,omitempty"`
}