Skip to content

Commit dad2658

Browse files
authored
accounts, signer: implement gnosis safe support (ethereum#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
1 parent 6c8310e commit dad2658

File tree

8 files changed

+396
-16
lines changed

8 files changed

+396
-16
lines changed

cmd/clef/extapi_changelog.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,64 @@ TL;DR: Given a version number MAJOR.MINOR.PATCH, increment the:
1010

1111
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
1212

13+
### 6.1.0
14+
15+
The API-method `account_signGnosisSafeTx` was added. This method takes two parameters,
16+
`[address, safeTx]`. The latter, `safeTx`, can be copy-pasted from the gnosis relay. For example:
17+
18+
```
19+
{
20+
"jsonrpc": "2.0",
21+
"method": "account_signGnosisSafeTx",
22+
"params": ["0xfd1c4226bfD1c436672092F4eCbfC270145b7256",
23+
{
24+
"safe": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3",
25+
"to": "0xB372a646f7F05Cc1785018dBDA7EBc734a2A20E2",
26+
"value": "20000000000000000",
27+
"data": null,
28+
"operation": 0,
29+
"gasToken": "0x0000000000000000000000000000000000000000",
30+
"safeTxGas": 27845,
31+
"baseGas": 0,
32+
"gasPrice": "0",
33+
"refundReceiver": "0x0000000000000000000000000000000000000000",
34+
"nonce": 2,
35+
"executionDate": null,
36+
"submissionDate": "2020-09-15T21:54:49.617634Z",
37+
"modified": "2020-09-15T21:54:49.617634Z",
38+
"blockNumber": null,
39+
"transactionHash": null,
40+
"safeTxHash": "0x2edfbd5bc113ff18c0631595db32eb17182872d88d9bf8ee4d8c2dd5db6d95e2",
41+
"executor": null,
42+
"isExecuted": false,
43+
"isSuccessful": null,
44+
"ethGasPrice": null,
45+
"gasUsed": null,
46+
"fee": null,
47+
"origin": null,
48+
"dataDecoded": null,
49+
"confirmationsRequired": null,
50+
"confirmations": [
51+
{
52+
"owner": "0xAd2e180019FCa9e55CADe76E4487F126Fd08DA34",
53+
"submissionDate": "2020-09-15T21:54:49.663299Z",
54+
"transactionHash": null,
55+
"confirmationType": "CONFIRMATION",
56+
"signature": "0x95a7250bb645f831c86defc847350e7faff815b2fb586282568e96cc859e39315876db20a2eed5f7a0412906ec5ab57652a6f645ad4833f345bda059b9da2b821c",
57+
"signatureType": "EOA"
58+
}
59+
],
60+
"signatures": null
61+
}
62+
],
63+
"id": 67
64+
}
65+
```
66+
67+
Not all fields are required, though. This method is really just a UX helper, which massages the
68+
input to conform to the `EIP-712` [specification](https://docs.gnosis.io/safe/docs/contracts_tx_execution/#transaction-hash)
69+
for the Gnosis Safe, and making the output be directly importable to by a relay service.
70+
1371

1472
### 6.0.0
1573

common/math/big.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,40 @@ func (i *HexOrDecimal256) MarshalText() ([]byte, error) {
6767
return []byte(fmt.Sprintf("%#x", (*big.Int)(i))), nil
6868
}
6969

70+
// Decimal256 unmarshals big.Int as a decimal string. When unmarshalling,
71+
// it however accepts either "0x"-prefixed (hex encoded) or non-prefixed (decimal)
72+
type Decimal256 big.Int
73+
74+
// NewHexOrDecimal256 creates a new Decimal256
75+
func NewDecimal256(x int64) *Decimal256 {
76+
b := big.NewInt(x)
77+
d := Decimal256(*b)
78+
return &d
79+
}
80+
81+
// UnmarshalText implements encoding.TextUnmarshaler.
82+
func (i *Decimal256) UnmarshalText(input []byte) error {
83+
bigint, ok := ParseBig256(string(input))
84+
if !ok {
85+
return fmt.Errorf("invalid hex or decimal integer %q", input)
86+
}
87+
*i = Decimal256(*bigint)
88+
return nil
89+
}
90+
91+
// MarshalText implements encoding.TextMarshaler.
92+
func (i *Decimal256) MarshalText() ([]byte, error) {
93+
return []byte(i.String()), nil
94+
}
95+
96+
// String implements Stringer.
97+
func (i *Decimal256) String() string {
98+
if i == nil {
99+
return "0"
100+
}
101+
return fmt.Sprintf("%#d", (*big.Int)(i))
102+
}
103+
70104
// ParseBig256 parses s as a 256 bit integer in decimal or hexadecimal syntax.
71105
// Leading zeros are accepted. The empty string parses as zero.
72106
func ParseBig256(s string) (*big.Int, bool) {

signer/core/api.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const (
4141
// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
4242
numberOfAccountsToDerive = 10
4343
// ExternalAPIVersion -- see extapi_changelog.md
44-
ExternalAPIVersion = "6.0.0"
44+
ExternalAPIVersion = "6.1.0"
4545
// InternalAPIVersion -- see intapi_changelog.md
4646
InternalAPIVersion = "7.0.1"
4747
)
@@ -62,6 +62,8 @@ type ExternalAPI interface {
6262
EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error)
6363
// Version info about the APIs
6464
Version(ctx context.Context) (string, error)
65+
// SignGnosisSafeTransaction signs/confirms a gnosis-safe multisig transaction
66+
SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error)
6567
}
6668

6769
// UIClientAPI specifies what method a UI needs to implement to be able to be used as a
@@ -234,6 +236,7 @@ type (
234236
Address common.MixedcaseAddress `json:"address"`
235237
Rawdata []byte `json:"raw_data"`
236238
Messages []*NameValueType `json:"messages"`
239+
Callinfo []ValidationInfo `json:"call_info"`
237240
Hash hexutil.Bytes `json:"hash"`
238241
Meta Metadata `json:"meta"`
239242
}
@@ -581,6 +584,33 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, meth
581584

582585
}
583586

587+
func (api *SignerAPI) SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) {
588+
// Do the usual validations, but on the last-stage transaction
589+
args := gnosisTx.ArgsForValidation()
590+
msgs, err := api.validator.ValidateTransaction(methodSelector, args)
591+
if err != nil {
592+
return nil, err
593+
}
594+
// If we are in 'rejectMode', then reject rather than show the user warnings
595+
if api.rejectMode {
596+
if err := msgs.getWarnings(); err != nil {
597+
return nil, err
598+
}
599+
}
600+
typedData := gnosisTx.ToTypedData()
601+
signature, preimage, err := api.signTypedData(ctx, signerAddress, typedData, msgs)
602+
if err != nil {
603+
return nil, err
604+
}
605+
checkSummedSender, _ := common.NewMixedcaseAddressFromString(signerAddress.Address().Hex())
606+
607+
gnosisTx.Signature = signature
608+
gnosisTx.SafeTxHash = common.BytesToHash(preimage)
609+
gnosisTx.Sender = *checkSummedSender // Must be checksumed to be accepted by relay
610+
611+
return &gnosisTx, nil
612+
}
613+
584614
// Returns the external api version. This method does not require user acceptance. Available methods are
585615
// available via enumeration anyway, and this info does not contain user-specific data
586616
func (api *SignerAPI) Version(ctx context.Context) (string, error) {

signer/core/auditlog.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package core
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122

2223
"github.com/ethereum/go-ethereum/common"
2324
"github.com/ethereum/go-ethereum/common/hexutil"
@@ -61,13 +62,32 @@ func (l *AuditLogger) SignTransaction(ctx context.Context, args SendTxArgs, meth
6162
}
6263

6364
func (l *AuditLogger) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) {
65+
marshalledData, _ := json.Marshal(data) // can ignore error, marshalling what we just unmarshalled
6466
l.log.Info("SignData", "type", "request", "metadata", MetadataFromContext(ctx).String(),
65-
"addr", addr.String(), "data", data, "content-type", contentType)
67+
"addr", addr.String(), "data", marshalledData, "content-type", contentType)
6668
b, e := l.api.SignData(ctx, contentType, addr, data)
6769
l.log.Info("SignData", "type", "response", "data", common.Bytes2Hex(b), "error", e)
6870
return b, e
6971
}
7072

73+
func (l *AuditLogger) SignGnosisSafeTx(ctx context.Context, addr common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) {
74+
sel := "<nil>"
75+
if methodSelector != nil {
76+
sel = *methodSelector
77+
}
78+
data, _ := json.Marshal(gnosisTx) // can ignore error, marshalling what we just unmarshalled
79+
l.log.Info("SignGnosisSafeTx", "type", "request", "metadata", MetadataFromContext(ctx).String(),
80+
"addr", addr.String(), "data", string(data), "selector", sel)
81+
res, e := l.api.SignGnosisSafeTx(ctx, addr, gnosisTx, methodSelector)
82+
if res != nil {
83+
data, _ := json.Marshal(res) // can ignore error, marshalling what we just unmarshalled
84+
l.log.Info("SignGnosisSafeTx", "type", "response", "data", string(data), "error", e)
85+
} else {
86+
l.log.Info("SignGnosisSafeTx", "type", "response", "data", res, "error", e)
87+
}
88+
return res, e
89+
}
90+
7191
func (l *AuditLogger) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error) {
7292
l.log.Info("SignTypedData", "type", "request", "metadata", MetadataFromContext(ctx).String(),
7393
"addr", addr.String(), "data", data)

signer/core/cliui.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp
148148

149149
fmt.Printf("-------- Sign data request--------------\n")
150150
fmt.Printf("Account: %s\n", request.Address.String())
151+
if len(request.Callinfo) != 0 {
152+
fmt.Printf("\nValidation messages:\n")
153+
for _, m := range request.Callinfo {
154+
fmt.Printf(" * %s : %s\n", m.Typ, m.Message)
155+
}
156+
fmt.Println()
157+
}
151158
fmt.Printf("messages:\n")
152159
for _, nvt := range request.Messages {
153160
fmt.Printf("\u00a0\u00a0%v\n", strings.TrimSpace(nvt.Pprint(1)))

signer/core/gnosis_safe.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package core
2+
3+
import (
4+
"fmt"
5+
"math/big"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/ethereum/go-ethereum/common/hexutil"
9+
"github.com/ethereum/go-ethereum/common/math"
10+
)
11+
12+
// GnosisSafeTx is a type to parse the safe-tx returned by the relayer,
13+
// it also conforms to the API required by the Gnosis Safe tx relay service.
14+
// See 'SafeMultisigTransaction' on https://safe-transaction.mainnet.gnosis.io/
15+
type GnosisSafeTx struct {
16+
// These fields are only used on output
17+
Signature hexutil.Bytes `json:"signature"`
18+
SafeTxHash common.Hash `json:"contractTransactionHash"`
19+
Sender common.MixedcaseAddress `json:"sender"`
20+
// These fields are used both on input and output
21+
Safe common.MixedcaseAddress `json:"safe"`
22+
To common.MixedcaseAddress `json:"to"`
23+
Value math.Decimal256 `json:"value"`
24+
GasPrice math.Decimal256 `json:"gasPrice"`
25+
Data *hexutil.Bytes `json:"data"`
26+
Operation uint8 `json:"operation"`
27+
GasToken common.Address `json:"gasToken"`
28+
RefundReceiver common.Address `json:"refundReceiver"`
29+
BaseGas big.Int `json:"baseGas"`
30+
SafeTxGas big.Int `json:"safeTxGas"`
31+
Nonce big.Int `json:"nonce"`
32+
InputExpHash common.Hash `json:"safeTxHash"`
33+
}
34+
35+
// ToTypedData converts the tx to a EIP-712 Typed Data structure for signing
36+
func (tx *GnosisSafeTx) ToTypedData() TypedData {
37+
var data hexutil.Bytes
38+
if tx.Data != nil {
39+
data = *tx.Data
40+
}
41+
gnosisTypedData := TypedData{
42+
Types: Types{
43+
"EIP712Domain": []Type{{Name: "verifyingContract", Type: "address"}},
44+
"SafeTx": []Type{
45+
{Name: "to", Type: "address"},
46+
{Name: "value", Type: "uint256"},
47+
{Name: "data", Type: "bytes"},
48+
{Name: "operation", Type: "uint8"},
49+
{Name: "safeTxGas", Type: "uint256"},
50+
{Name: "baseGas", Type: "uint256"},
51+
{Name: "gasPrice", Type: "uint256"},
52+
{Name: "gasToken", Type: "address"},
53+
{Name: "refundReceiver", Type: "address"},
54+
{Name: "nonce", Type: "uint256"},
55+
},
56+
},
57+
Domain: TypedDataDomain{
58+
VerifyingContract: tx.Safe.Address().Hex(),
59+
},
60+
PrimaryType: "SafeTx",
61+
Message: TypedDataMessage{
62+
"to": tx.To.Address().Hex(),
63+
"value": tx.Value.String(),
64+
"data": data,
65+
"operation": fmt.Sprintf("%d", tx.Operation),
66+
"safeTxGas": fmt.Sprintf("%#d", &tx.SafeTxGas),
67+
"baseGas": fmt.Sprintf("%#d", &tx.BaseGas),
68+
"gasPrice": tx.GasPrice.String(),
69+
"gasToken": tx.GasToken.Hex(),
70+
"refundReceiver": tx.RefundReceiver.Hex(),
71+
"nonce": fmt.Sprintf("%d", tx.Nonce.Uint64()),
72+
},
73+
}
74+
return gnosisTypedData
75+
}
76+
77+
// ArgsForValidation returns a SendTxArgs struct, which can be used for the
78+
// common validations, e.g. look up 4byte destinations
79+
func (tx *GnosisSafeTx) ArgsForValidation() *SendTxArgs {
80+
args := &SendTxArgs{
81+
From: tx.Safe,
82+
To: &tx.To,
83+
Gas: hexutil.Uint64(tx.SafeTxGas.Uint64()),
84+
GasPrice: hexutil.Big(tx.GasPrice),
85+
Value: hexutil.Big(tx.Value),
86+
Nonce: hexutil.Uint64(tx.Nonce.Uint64()),
87+
Data: tx.Data,
88+
Input: nil,
89+
}
90+
return args
91+
}

0 commit comments

Comments
 (0)