Skip to content

Commit

Permalink
accounts, signer: implement gnosis safe support (#21593)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
holiman committed Sep 29, 2020
1 parent 6c8310e commit dad2658
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 16 deletions.
58 changes: 58 additions & 0 deletions cmd/clef/extapi_changelog.md
Expand Up @@ -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

Expand Down
34 changes: 34 additions & 0 deletions common/math/big.go
Expand Up @@ -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) {
Expand Down
32 changes: 31 additions & 1 deletion signer/core/api.go
Expand Up @@ -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"
)
Expand All @@ -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
Expand Down Expand Up @@ -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"`
}
Expand Down Expand Up @@ -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) {
Expand Down
22 changes: 21 additions & 1 deletion signer/core/auditlog.go
Expand Up @@ -18,6 +18,7 @@ package core

import (
"context"
"encoding/json"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions signer/core/cliui.go
Expand Up @@ -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)))
Expand Down
91 changes: 91 additions & 0 deletions signer/core/gnosis_safe.go
@@ -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
}

0 comments on commit dad2658

Please sign in to comment.