diff --git a/signer/core/gnosis_safe.go b/signer/core/gnosis_safe.go index 016b1fff3..1b88db1af 100644 --- a/signer/core/gnosis_safe.go +++ b/signer/core/gnosis_safe.go @@ -31,6 +31,7 @@ type GnosisSafeTx struct { SafeTxGas big.Int `json:"safeTxGas"` Nonce big.Int `json:"nonce"` InputExpHash common.Hash `json:"safeTxHash"` + ChainId *math.HexOrDecimal256 `json:"chainId,omitempty"` } // ToTypedData converts the tx to a EIP-712 Typed Data structure for signing @@ -39,9 +40,14 @@ func (tx *GnosisSafeTx) ToTypedData() apitypes.TypedData { if tx.Data != nil { data = *tx.Data } + var domainType = []apitypes.Type{{Name: "verifyingContract", Type: "address"}} + if tx.ChainId != nil { + domainType = append([]apitypes.Type{{Name: "chainId", Type: "uint256"}}, domainType[0]) + } + gnosisTypedData := apitypes.TypedData{ Types: apitypes.Types{ - "EIP712Domain": []apitypes.Type{{Name: "verifyingContract", Type: "address"}}, + "EIP712Domain": domainType, "SafeTx": []apitypes.Type{ {Name: "to", Type: "address"}, {Name: "value", Type: "uint256"}, @@ -57,6 +63,7 @@ func (tx *GnosisSafeTx) ToTypedData() apitypes.TypedData { }, Domain: apitypes.TypedDataDomain{ VerifyingContract: tx.Safe.Address().Hex(), + ChainId: tx.ChainId, }, PrimaryType: "SafeTx", Message: apitypes.TypedDataMessage{ @@ -88,6 +95,7 @@ func (tx *GnosisSafeTx) ArgsForValidation() *apitypes.SendTxArgs { Nonce: hexutil.Uint64(tx.Nonce.Uint64()), Data: tx.Data, Input: nil, + ChainID: (*hexutil.Big)(tx.ChainId), } return args } diff --git a/signer/core/signed_data_test.go b/signer/core/signed_data_test.go index 1d972d296..e2aff09b9 100644 --- a/signer/core/signed_data_test.go +++ b/signer/core/signed_data_test.go @@ -532,3 +532,133 @@ func TestGnosisCustomData(t *testing.T) { t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) } } + +var gnosisTypedDataWithChainId = ` +{ + "types": { + "EIP712Domain": [ + { "type": "uint256", "name": "chainId" }, + { "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": "0x111dAE35D176A9607053e0c46e91F36AFbC1dc57", + "chainId": "4" + }, + "primaryType": "SafeTx", + "message": { + "to": "0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa", + "value": "0", + "data": "0xa9059cbb00000000000000000000000099d580d3a7fe7bd183b2464517b2cd7ce5a8f15a0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "operation": 0, + "safeTxGas": 0, + "baseGas": 0, + "gasPrice": "0", + "gasToken": "0x0000000000000000000000000000000000000000", + "refundReceiver": "0x0000000000000000000000000000000000000000", + "nonce": 15 + } +}` + +var gnosisTxWithChainId = ` +{ + "safe": "0x111dAE35D176A9607053e0c46e91F36AFbC1dc57", + "to": "0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa", + "value": "0", + "data": "0xa9059cbb00000000000000000000000099d580d3a7fe7bd183b2464517b2cd7ce5a8f15a0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "operation": 0, + "gasToken": "0x0000000000000000000000000000000000000000", + "safeTxGas": 0, + "baseGas": 0, + "gasPrice": "0", + "refundReceiver": "0x0000000000000000000000000000000000000000", + "nonce": 15, + "executionDate": "2022-01-10T20:00:12Z", + "submissionDate": "2022-01-10T19:59:59.689989Z", + "modified": "2022-01-10T20:00:31.903635Z", + "blockNumber": 9968802, + "transactionHash": "0xc9fef30499ee8984974ab9dddd9d15c2a97c1a4393935dceed5efc3af9fc41a4", + "safeTxHash": "0x6619dab5401503f2735256e12b898e69eb701d6a7e0d07abf1be4bb8aebfba29", + "executor": "0xbc2BB26a6d821e69A38016f3858561a1D80d4182", + "isExecuted": true, + "isSuccessful": true, + "ethGasPrice": "2500000009", + "gasUsed": 82902, + "fee": "207255000746118", + "chainId": "4", + "origin": null, + "dataDecoded": { + "method": "transfer", + "parameters": [ + { + "name": "to", + "type": "address", + "value": "0x99D580d3a7FE7BD183b2464517B2cD7ce5A8F15A" + }, + { + "name": "value", + "type": "uint256", + "value": "1000000000000000000" + } + ] + }, + "confirmationsRequired": 1, + "confirmations": [ + { + "owner": "0xbc2BB26a6d821e69A38016f3858561a1D80d4182", + "submissionDate": "2022-01-10T19:59:59.722500Z", + "transactionHash": null, + "signature": "0x5ca34641bcdee06e7b99143bfe34778195ca41022bd35837b96c204c7786be9d6dfa6dba43b53cd92da45ac728899e1561b232d28f38ba82df45f164caba38be1b", + "signatureType": "EOA" + } + ], + "signatures": "0x5ca34641bcdee06e7b99143bfe34778195ca41022bd35837b96c204c7786be9d6dfa6dba43b53cd92da45ac728899e1561b232d28f38ba82df45f164caba38be1b" +} +` + +func TestGnosisTypedDataWithChainId(t *testing.T) { + var td apitypes.TypedData + err := json.Unmarshal([]byte(gnosisTypedDataWithChainId), &td) + if err != nil { + t.Fatalf("unmarshalling failed '%v'", err) + } + _, sighash, err := sign(td) + if err != nil { + t.Fatal(err) + } + expSigHash := common.FromHex("0x6619dab5401503f2735256e12b898e69eb701d6a7e0d07abf1be4bb8aebfba29") + 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 TestGnosisCustomDataWithChainId(t *testing.T) { + var tx core.GnosisSafeTx + err := json.Unmarshal([]byte(gnosisTxWithChainId), &tx) + if err != nil { + t.Fatal(err) + } + var td = tx.ToTypedData() + _, sighash, err := sign(td) + if err != nil { + t.Fatal(err) + } + expSigHash := common.FromHex("0x6619dab5401503f2735256e12b898e69eb701d6a7e0d07abf1be4bb8aebfba29") + if !bytes.Equal(expSigHash, sighash) { + t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) + } +}