accounts, signer: implement gnosis safe support (#21593)
* accounts, signer: implement gnosis safe support * common/math: add type for marshalling big to dec * accounts, signer: properly sign gnosis requests * signer, clef: implement account_signGnosisTx * signer: fix auditlog print, change rpc-name (signGnosisTx to signGnosisSafeTx) * signer: pass validation-messages/warnings to the UI for gnonsis-safe txs * signer/core: minor change to validationmessages of typed data
This commit is contained in:
		
							parent
							
								
									6c8310ebb4
								
							
						
					
					
						commit
						dad26582b6
					
				| @ -10,6 +10,64 @@ TL;DR: Given a version number MAJOR.MINOR.PATCH, increment the: | ||||
| 
 | ||||
| Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. | ||||
| 
 | ||||
| ### 6.1.0 | ||||
| 
 | ||||
| The API-method `account_signGnosisSafeTx` was added. This method takes two parameters,  | ||||
| `[address, safeTx]`. The latter, `safeTx`, can be copy-pasted from the gnosis relay. For example:  | ||||
| 
 | ||||
| ``` | ||||
| { | ||||
|   "jsonrpc": "2.0", | ||||
|   "method": "account_signGnosisSafeTx", | ||||
|   "params": ["0xfd1c4226bfD1c436672092F4eCbfC270145b7256", | ||||
|     { | ||||
|       "safe": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3", | ||||
|       "to": "0xB372a646f7F05Cc1785018dBDA7EBc734a2A20E2", | ||||
|       "value": "20000000000000000", | ||||
|       "data": null, | ||||
|       "operation": 0, | ||||
|       "gasToken": "0x0000000000000000000000000000000000000000", | ||||
|       "safeTxGas": 27845, | ||||
|       "baseGas": 0, | ||||
|       "gasPrice": "0", | ||||
|       "refundReceiver": "0x0000000000000000000000000000000000000000", | ||||
|       "nonce": 2, | ||||
|       "executionDate": null, | ||||
|       "submissionDate": "2020-09-15T21:54:49.617634Z", | ||||
|       "modified": "2020-09-15T21:54:49.617634Z", | ||||
|       "blockNumber": null, | ||||
|       "transactionHash": null, | ||||
|       "safeTxHash": "0x2edfbd5bc113ff18c0631595db32eb17182872d88d9bf8ee4d8c2dd5db6d95e2", | ||||
|       "executor": null, | ||||
|       "isExecuted": false, | ||||
|       "isSuccessful": null, | ||||
|       "ethGasPrice": null, | ||||
|       "gasUsed": null, | ||||
|       "fee": null, | ||||
|       "origin": null, | ||||
|       "dataDecoded": null, | ||||
|       "confirmationsRequired": null, | ||||
|       "confirmations": [ | ||||
|         { | ||||
|           "owner": "0xAd2e180019FCa9e55CADe76E4487F126Fd08DA34", | ||||
|           "submissionDate": "2020-09-15T21:54:49.663299Z", | ||||
|           "transactionHash": null, | ||||
|           "confirmationType": "CONFIRMATION", | ||||
|           "signature": "0x95a7250bb645f831c86defc847350e7faff815b2fb586282568e96cc859e39315876db20a2eed5f7a0412906ec5ab57652a6f645ad4833f345bda059b9da2b821c", | ||||
|           "signatureType": "EOA" | ||||
|         } | ||||
|       ], | ||||
|       "signatures": null | ||||
|     } | ||||
|   ], | ||||
|   "id": 67 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| Not all fields are required, though. This method is really just a UX helper, which massages the  | ||||
| input to conform to the `EIP-712` [specification](https://docs.gnosis.io/safe/docs/contracts_tx_execution/#transaction-hash)  | ||||
| for the Gnosis Safe, and making the output be directly importable to by a relay service.  | ||||
| 
 | ||||
| 
 | ||||
| ### 6.0.0 | ||||
| 
 | ||||
|  | ||||
| @ -67,6 +67,40 @@ func (i *HexOrDecimal256) MarshalText() ([]byte, error) { | ||||
| 	return []byte(fmt.Sprintf("%#x", (*big.Int)(i))), nil | ||||
| } | ||||
| 
 | ||||
| // Decimal256 unmarshals big.Int as a decimal string. When unmarshalling,
 | ||||
| // it however accepts either "0x"-prefixed (hex encoded) or non-prefixed (decimal)
 | ||||
| type Decimal256 big.Int | ||||
| 
 | ||||
| // NewHexOrDecimal256 creates a new Decimal256
 | ||||
| func NewDecimal256(x int64) *Decimal256 { | ||||
| 	b := big.NewInt(x) | ||||
| 	d := Decimal256(*b) | ||||
| 	return &d | ||||
| } | ||||
| 
 | ||||
| // UnmarshalText implements encoding.TextUnmarshaler.
 | ||||
| func (i *Decimal256) UnmarshalText(input []byte) error { | ||||
| 	bigint, ok := ParseBig256(string(input)) | ||||
| 	if !ok { | ||||
| 		return fmt.Errorf("invalid hex or decimal integer %q", input) | ||||
| 	} | ||||
| 	*i = Decimal256(*bigint) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // MarshalText implements encoding.TextMarshaler.
 | ||||
| func (i *Decimal256) MarshalText() ([]byte, error) { | ||||
| 	return []byte(i.String()), nil | ||||
| } | ||||
| 
 | ||||
| // String implements Stringer.
 | ||||
| func (i *Decimal256) String() string { | ||||
| 	if i == nil { | ||||
| 		return "0" | ||||
| 	} | ||||
| 	return fmt.Sprintf("%#d", (*big.Int)(i)) | ||||
| } | ||||
| 
 | ||||
| // ParseBig256 parses s as a 256 bit integer in decimal or hexadecimal syntax.
 | ||||
| // Leading zeros are accepted. The empty string parses as zero.
 | ||||
| func ParseBig256(s string) (*big.Int, bool) { | ||||
|  | ||||
| @ -41,7 +41,7 @@ const ( | ||||
| 	// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
 | ||||
| 	numberOfAccountsToDerive = 10 | ||||
| 	// ExternalAPIVersion -- see extapi_changelog.md
 | ||||
| 	ExternalAPIVersion = "6.0.0" | ||||
| 	ExternalAPIVersion = "6.1.0" | ||||
| 	// InternalAPIVersion -- see intapi_changelog.md
 | ||||
| 	InternalAPIVersion = "7.0.1" | ||||
| ) | ||||
| @ -62,6 +62,8 @@ type ExternalAPI interface { | ||||
| 	EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) | ||||
| 	// Version info about the APIs
 | ||||
| 	Version(ctx context.Context) (string, error) | ||||
| 	// SignGnosisSafeTransaction signs/confirms a gnosis-safe multisig transaction
 | ||||
| 	SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) | ||||
| } | ||||
| 
 | ||||
| // UIClientAPI specifies what method a UI needs to implement to be able to be used as a
 | ||||
| @ -234,6 +236,7 @@ type ( | ||||
| 		Address     common.MixedcaseAddress `json:"address"` | ||||
| 		Rawdata     []byte                  `json:"raw_data"` | ||||
| 		Messages    []*NameValueType        `json:"messages"` | ||||
| 		Callinfo    []ValidationInfo        `json:"call_info"` | ||||
| 		Hash        hexutil.Bytes           `json:"hash"` | ||||
| 		Meta        Metadata                `json:"meta"` | ||||
| 	} | ||||
| @ -581,6 +584,33 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, meth | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func (api *SignerAPI) SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) { | ||||
| 	// Do the usual validations, but on the last-stage transaction
 | ||||
| 	args := gnosisTx.ArgsForValidation() | ||||
| 	msgs, err := api.validator.ValidateTransaction(methodSelector, args) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	// If we are in 'rejectMode', then reject rather than show the user warnings
 | ||||
| 	if api.rejectMode { | ||||
| 		if err := msgs.getWarnings(); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 	typedData := gnosisTx.ToTypedData() | ||||
| 	signature, preimage, err := api.signTypedData(ctx, signerAddress, typedData, msgs) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	checkSummedSender, _ := common.NewMixedcaseAddressFromString(signerAddress.Address().Hex()) | ||||
| 
 | ||||
| 	gnosisTx.Signature = signature | ||||
| 	gnosisTx.SafeTxHash = common.BytesToHash(preimage) | ||||
| 	gnosisTx.Sender = *checkSummedSender // Must be checksumed to be accepted by relay
 | ||||
| 
 | ||||
| 	return &gnosisTx, nil | ||||
| } | ||||
| 
 | ||||
| // Returns the external api version. This method does not require user acceptance. Available methods are
 | ||||
| // available via enumeration anyway, and this info does not contain user-specific data
 | ||||
| func (api *SignerAPI) Version(ctx context.Context) (string, error) { | ||||
|  | ||||
| @ -18,6 +18,7 @@ package core | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 
 | ||||
| 	"github.com/ethereum/go-ethereum/common" | ||||
| 	"github.com/ethereum/go-ethereum/common/hexutil" | ||||
| @ -61,13 +62,32 @@ func (l *AuditLogger) SignTransaction(ctx context.Context, args SendTxArgs, meth | ||||
| } | ||||
| 
 | ||||
| func (l *AuditLogger) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) { | ||||
| 	marshalledData, _ := json.Marshal(data) // can ignore error, marshalling what we just unmarshalled
 | ||||
| 	l.log.Info("SignData", "type", "request", "metadata", MetadataFromContext(ctx).String(), | ||||
| 		"addr", addr.String(), "data", data, "content-type", contentType) | ||||
| 		"addr", addr.String(), "data", marshalledData, "content-type", contentType) | ||||
| 	b, e := l.api.SignData(ctx, contentType, addr, data) | ||||
| 	l.log.Info("SignData", "type", "response", "data", common.Bytes2Hex(b), "error", e) | ||||
| 	return b, e | ||||
| } | ||||
| 
 | ||||
| func (l *AuditLogger) SignGnosisSafeTx(ctx context.Context, addr common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) { | ||||
| 	sel := "<nil>" | ||||
| 	if methodSelector != nil { | ||||
| 		sel = *methodSelector | ||||
| 	} | ||||
| 	data, _ := json.Marshal(gnosisTx) // can ignore error, marshalling what we just unmarshalled
 | ||||
| 	l.log.Info("SignGnosisSafeTx", "type", "request", "metadata", MetadataFromContext(ctx).String(), | ||||
| 		"addr", addr.String(), "data", string(data), "selector", sel) | ||||
| 	res, e := l.api.SignGnosisSafeTx(ctx, addr, gnosisTx, methodSelector) | ||||
| 	if res != nil { | ||||
| 		data, _ := json.Marshal(res) // can ignore error, marshalling what we just unmarshalled
 | ||||
| 		l.log.Info("SignGnosisSafeTx", "type", "response", "data", string(data), "error", e) | ||||
| 	} else { | ||||
| 		l.log.Info("SignGnosisSafeTx", "type", "response", "data", res, "error", e) | ||||
| 	} | ||||
| 	return res, e | ||||
| } | ||||
| 
 | ||||
| func (l *AuditLogger) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error) { | ||||
| 	l.log.Info("SignTypedData", "type", "request", "metadata", MetadataFromContext(ctx).String(), | ||||
| 		"addr", addr.String(), "data", data) | ||||
|  | ||||
| @ -148,6 +148,13 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp | ||||
| 
 | ||||
| 	fmt.Printf("-------- Sign data request--------------\n") | ||||
| 	fmt.Printf("Account:  %s\n", request.Address.String()) | ||||
| 	if len(request.Callinfo) != 0 { | ||||
| 		fmt.Printf("\nValidation messages:\n") | ||||
| 		for _, m := range request.Callinfo { | ||||
| 			fmt.Printf("  * %s : %s\n", m.Typ, m.Message) | ||||
| 		} | ||||
| 		fmt.Println() | ||||
| 	} | ||||
| 	fmt.Printf("messages:\n") | ||||
| 	for _, nvt := range request.Messages { | ||||
| 		fmt.Printf("\u00a0\u00a0%v\n", strings.TrimSpace(nvt.Pprint(1))) | ||||
|  | ||||
							
								
								
									
										91
									
								
								signer/core/gnosis_safe.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								signer/core/gnosis_safe.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,91 @@ | ||||
| package core | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math/big" | ||||
| 
 | ||||
| 	"github.com/ethereum/go-ethereum/common" | ||||
| 	"github.com/ethereum/go-ethereum/common/hexutil" | ||||
| 	"github.com/ethereum/go-ethereum/common/math" | ||||
| ) | ||||
| 
 | ||||
| // GnosisSafeTx is a type to parse the safe-tx returned by the relayer,
 | ||||
| // it also conforms to the API required by the Gnosis Safe tx relay service.
 | ||||
| // See 'SafeMultisigTransaction' on https://safe-transaction.mainnet.gnosis.io/
 | ||||
| type GnosisSafeTx struct { | ||||
| 	// These fields are only used on output
 | ||||
| 	Signature  hexutil.Bytes           `json:"signature"` | ||||
| 	SafeTxHash common.Hash             `json:"contractTransactionHash"` | ||||
| 	Sender     common.MixedcaseAddress `json:"sender"` | ||||
| 	// These fields are used both on input and output
 | ||||
| 	Safe           common.MixedcaseAddress `json:"safe"` | ||||
| 	To             common.MixedcaseAddress `json:"to"` | ||||
| 	Value          math.Decimal256         `json:"value"` | ||||
| 	GasPrice       math.Decimal256         `json:"gasPrice"` | ||||
| 	Data           *hexutil.Bytes          `json:"data"` | ||||
| 	Operation      uint8                   `json:"operation"` | ||||
| 	GasToken       common.Address          `json:"gasToken"` | ||||
| 	RefundReceiver common.Address          `json:"refundReceiver"` | ||||
| 	BaseGas        big.Int                 `json:"baseGas"` | ||||
| 	SafeTxGas      big.Int                 `json:"safeTxGas"` | ||||
| 	Nonce          big.Int                 `json:"nonce"` | ||||
| 	InputExpHash   common.Hash             `json:"safeTxHash"` | ||||
| } | ||||
| 
 | ||||
| // ToTypedData converts the tx to a EIP-712 Typed Data structure for signing
 | ||||
| func (tx *GnosisSafeTx) ToTypedData() TypedData { | ||||
| 	var data hexutil.Bytes | ||||
| 	if tx.Data != nil { | ||||
| 		data = *tx.Data | ||||
| 	} | ||||
| 	gnosisTypedData := TypedData{ | ||||
| 		Types: Types{ | ||||
| 			"EIP712Domain": []Type{{Name: "verifyingContract", Type: "address"}}, | ||||
| 			"SafeTx": []Type{ | ||||
| 				{Name: "to", Type: "address"}, | ||||
| 				{Name: "value", Type: "uint256"}, | ||||
| 				{Name: "data", Type: "bytes"}, | ||||
| 				{Name: "operation", Type: "uint8"}, | ||||
| 				{Name: "safeTxGas", Type: "uint256"}, | ||||
| 				{Name: "baseGas", Type: "uint256"}, | ||||
| 				{Name: "gasPrice", Type: "uint256"}, | ||||
| 				{Name: "gasToken", Type: "address"}, | ||||
| 				{Name: "refundReceiver", Type: "address"}, | ||||
| 				{Name: "nonce", Type: "uint256"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		Domain: TypedDataDomain{ | ||||
| 			VerifyingContract: tx.Safe.Address().Hex(), | ||||
| 		}, | ||||
| 		PrimaryType: "SafeTx", | ||||
| 		Message: TypedDataMessage{ | ||||
| 			"to":             tx.To.Address().Hex(), | ||||
| 			"value":          tx.Value.String(), | ||||
| 			"data":           data, | ||||
| 			"operation":      fmt.Sprintf("%d", tx.Operation), | ||||
| 			"safeTxGas":      fmt.Sprintf("%#d", &tx.SafeTxGas), | ||||
| 			"baseGas":        fmt.Sprintf("%#d", &tx.BaseGas), | ||||
| 			"gasPrice":       tx.GasPrice.String(), | ||||
| 			"gasToken":       tx.GasToken.Hex(), | ||||
| 			"refundReceiver": tx.RefundReceiver.Hex(), | ||||
| 			"nonce":          fmt.Sprintf("%d", tx.Nonce.Uint64()), | ||||
| 		}, | ||||
| 	} | ||||
| 	return gnosisTypedData | ||||
| } | ||||
| 
 | ||||
| // ArgsForValidation returns a SendTxArgs struct, which can be used for the
 | ||||
| // common validations, e.g. look up 4byte destinations
 | ||||
| func (tx *GnosisSafeTx) ArgsForValidation() *SendTxArgs { | ||||
| 	args := &SendTxArgs{ | ||||
| 		From:     tx.Safe, | ||||
| 		To:       &tx.To, | ||||
| 		Gas:      hexutil.Uint64(tx.SafeTxGas.Uint64()), | ||||
| 		GasPrice: hexutil.Big(tx.GasPrice), | ||||
| 		Value:    hexutil.Big(tx.Value), | ||||
| 		Nonce:    hexutil.Uint64(tx.Nonce.Uint64()), | ||||
| 		Data:     tx.Data, | ||||
| 		Input:    nil, | ||||
| 	} | ||||
| 	return args | ||||
| } | ||||
| @ -125,7 +125,7 @@ var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Z](\w*)(\[\])?$`) | ||||
| //
 | ||||
| // Note, the produced signature conforms to the secp256k1 curve R, S and V values,
 | ||||
| // where the V value will be 27 or 28 for legacy reasons, if legacyV==true.
 | ||||
| func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) { | ||||
| func (api *SignerAPI) sign(req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) { | ||||
| 	// We make the request prior to looking up if we actually have the account, to prevent
 | ||||
| 	// account-enumeration via the API
 | ||||
| 	res, err := api.UI.ApproveSignData(req) | ||||
| @ -136,7 +136,7 @@ func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest, l | ||||
| 		return nil, ErrRequestDenied | ||||
| 	} | ||||
| 	// Look up the wallet containing the requested signer
 | ||||
| 	account := accounts.Account{Address: addr.Address()} | ||||
| 	account := accounts.Account{Address: req.Address.Address()} | ||||
| 	wallet, err := api.am.Find(account) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| @ -167,7 +167,7 @@ func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr com | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	signature, err := api.sign(addr, req, transformV) | ||||
| 	signature, err := api.sign(req, transformV) | ||||
| 	if err != nil { | ||||
| 		api.UI.ShowError(err.Error()) | ||||
| 		return nil, err | ||||
| @ -312,28 +312,47 @@ func cliqueHeaderHashAndRlp(header *types.Header) (hash, rlp []byte, err error) | ||||
| 
 | ||||
| // SignTypedData signs EIP-712 conformant typed data
 | ||||
| // hash = keccak256("\x19${byteVersion}${domainSeparator}${hashStruct(message)}")
 | ||||
| // It returns
 | ||||
| // - the signature,
 | ||||
| // - and/or any error
 | ||||
| func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, typedData TypedData) (hexutil.Bytes, error) { | ||||
| 	signature, _, err := api.signTypedData(ctx, addr, typedData, nil) | ||||
| 	return signature, err | ||||
| } | ||||
| 
 | ||||
| // signTypedData is identical to the capitalized version, except that it also returns the hash (preimage)
 | ||||
| // - the signature preimage (hash)
 | ||||
| func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAddress, | ||||
| 	typedData TypedData, validationMessages *ValidationMessages) (hexutil.Bytes, hexutil.Bytes, error) { | ||||
| 	domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 	typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 	rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) | ||||
| 	sighash := crypto.Keccak256(rawData) | ||||
| 	messages, err := typedData.Format() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 	req := &SignDataRequest{ContentType: DataTyped.Mime, Rawdata: rawData, Messages: messages, Hash: sighash} | ||||
| 	signature, err := api.sign(addr, req, true) | ||||
| 	req := &SignDataRequest{ | ||||
| 		ContentType: DataTyped.Mime, | ||||
| 		Rawdata:     rawData, | ||||
| 		Messages:    messages, | ||||
| 		Hash:        sighash, | ||||
| 		Address:     addr} | ||||
| 	if validationMessages != nil { | ||||
| 		req.Callinfo = validationMessages.Messages | ||||
| 	} | ||||
| 	signature, err := api.sign(req, true) | ||||
| 	if err != nil { | ||||
| 		api.UI.ShowError(err.Error()) | ||||
| 		return nil, err | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 	return signature, nil | ||||
| 	return signature, sighash, nil | ||||
| } | ||||
| 
 | ||||
| // HashStruct generates a keccak256 hash of the encoding of the provided data
 | ||||
| @ -420,8 +439,8 @@ func (typedData *TypedData) EncodeData(primaryType string, data map[string]inter | ||||
| 	buffer := bytes.Buffer{} | ||||
| 
 | ||||
| 	// Verify extra data
 | ||||
| 	if len(typedData.Types[primaryType]) < len(data) { | ||||
| 		return nil, errors.New("there is extra data provided in the message") | ||||
| 	if exp, got := len(typedData.Types[primaryType]), len(data); exp < got { | ||||
| 		return nil, fmt.Errorf("there is extra data provided in the message (%d < %d)", exp, got) | ||||
| 	} | ||||
| 
 | ||||
| 	// Add typehash
 | ||||
| @ -834,7 +853,11 @@ func (nvt *NameValueType) Pprint(depth int) string { | ||||
| 			output.WriteString(sublevel) | ||||
| 		} | ||||
| 	} else { | ||||
| 		output.WriteString(fmt.Sprintf("%q\n", nvt.Value)) | ||||
| 		if nvt.Value != nil { | ||||
| 			output.WriteString(fmt.Sprintf("%q\n", nvt.Value)) | ||||
| 		} else { | ||||
| 			output.WriteString("\n") | ||||
| 		} | ||||
| 	} | ||||
| 	return output.String() | ||||
| } | ||||
|  | ||||
| @ -17,6 +17,7 @@ | ||||
| package core_test | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| @ -414,3 +415,119 @@ func TestFuzzerFiles(t *testing.T) { | ||||
| 		typedData.Format() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| var gnosisTypedData = ` | ||||
| { | ||||
| 	"types": { | ||||
| 		"EIP712Domain": [ | ||||
| 			{ "type": "address", "name": "verifyingContract" } | ||||
| 		], | ||||
| 		"SafeTx": [ | ||||
| 			{ "type": "address", "name": "to" }, | ||||
| 			{ "type": "uint256", "name": "value" }, | ||||
| 			{ "type": "bytes", "name": "data" }, | ||||
| 			{ "type": "uint8", "name": "operation" }, | ||||
| 			{ "type": "uint256", "name": "safeTxGas" }, | ||||
| 			{ "type": "uint256", "name": "baseGas" }, | ||||
| 			{ "type": "uint256", "name": "gasPrice" }, | ||||
| 			{ "type": "address", "name": "gasToken" }, | ||||
| 			{ "type": "address", "name": "refundReceiver" }, | ||||
| 			{ "type": "uint256", "name": "nonce" } | ||||
| 		] | ||||
| 	}, | ||||
| 	"domain": { | ||||
| 		"verifyingContract": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3" | ||||
| 	}, | ||||
| 	"primaryType": "SafeTx", | ||||
| 	"message": { | ||||
| 		"to": "0x9eE457023bB3De16D51A003a247BaEaD7fce313D", | ||||
| 		"value": "20000000000000000", | ||||
| 		"data": "0x", | ||||
| 		"operation": 0, | ||||
| 		"safeTxGas": 27845, | ||||
| 		"baseGas": 0, | ||||
| 		"gasPrice": "0", | ||||
| 		"gasToken": "0x0000000000000000000000000000000000000000", | ||||
| 		"refundReceiver": "0x0000000000000000000000000000000000000000", | ||||
| 		"nonce": 3 | ||||
| 	} | ||||
| }` | ||||
| 
 | ||||
| var gnosisTx = ` | ||||
| { | ||||
|       "safe": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3", | ||||
|       "to": "0x9eE457023bB3De16D51A003a247BaEaD7fce313D", | ||||
|       "value": "20000000000000000", | ||||
|       "data": null, | ||||
|       "operation": 0, | ||||
|       "gasToken": "0x0000000000000000000000000000000000000000", | ||||
|       "safeTxGas": 27845, | ||||
|       "baseGas": 0, | ||||
|       "gasPrice": "0", | ||||
|       "refundReceiver": "0x0000000000000000000000000000000000000000", | ||||
|       "nonce": 3, | ||||
|       "executionDate": null, | ||||
|       "submissionDate": "2020-09-15T21:59:23.815748Z", | ||||
|       "modified": "2020-09-15T21:59:23.815748Z", | ||||
|       "blockNumber": null, | ||||
|       "transactionHash": null, | ||||
|       "safeTxHash": "0x28bae2bd58d894a1d9b69e5e9fde3570c4b98a6fc5499aefb54fb830137e831f", | ||||
|       "executor": null, | ||||
|       "isExecuted": false, | ||||
|       "isSuccessful": null, | ||||
|       "ethGasPrice": null, | ||||
|       "gasUsed": null, | ||||
|       "fee": null, | ||||
|       "origin": null, | ||||
|       "dataDecoded": null, | ||||
|       "confirmationsRequired": null, | ||||
|       "confirmations": [ | ||||
|         { | ||||
|           "owner": "0xAd2e180019FCa9e55CADe76E4487F126Fd08DA34", | ||||
|           "submissionDate": "2020-09-15T21:59:28.281243Z", | ||||
|           "transactionHash": null, | ||||
|           "confirmationType": "CONFIRMATION", | ||||
|           "signature": "0x5e562065a0cb15d766dac0cd49eb6d196a41183af302c4ecad45f1a81958d7797753f04424a9b0aa1cb0448e4ec8e189540fbcdda7530ef9b9d95dfc2d36cb521b", | ||||
|           "signatureType": "EOA" | ||||
|         } | ||||
|       ], | ||||
|       "signatures": null | ||||
|     } | ||||
| ` | ||||
| 
 | ||||
| // TestGnosisTypedData tests the scenario where a user submits a full EIP-712
 | ||||
| // struct without using the gnosis-specific endpoint
 | ||||
| func TestGnosisTypedData(t *testing.T) { | ||||
| 	var td core.TypedData | ||||
| 	err := json.Unmarshal([]byte(gnosisTypedData), &td) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unmarshalling failed '%v'", err) | ||||
| 	} | ||||
| 	_, sighash, err := sign(td) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	expSigHash := common.FromHex("0x28bae2bd58d894a1d9b69e5e9fde3570c4b98a6fc5499aefb54fb830137e831f") | ||||
| 	if !bytes.Equal(expSigHash, sighash) { | ||||
| 		t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // TestGnosisCustomData tests the scenario where a user submits only the gnosis-safe
 | ||||
| // specific data, and we fill the TypedData struct on our side
 | ||||
| func TestGnosisCustomData(t *testing.T) { | ||||
| 	var tx core.GnosisSafeTx | ||||
| 	err := json.Unmarshal([]byte(gnosisTx), &tx) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	var td = tx.ToTypedData() | ||||
| 	_, sighash, err := sign(td) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	expSigHash := common.FromHex("0x28bae2bd58d894a1d9b69e5e9fde3570c4b98a6fc5499aefb54fb830137e831f") | ||||
| 	if !bytes.Equal(expSigHash, sighash) { | ||||
| 		t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) | ||||
| 	} | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user