Skip to content

Commit

Permalink
Fix conflicts with signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
germartinez committed Jan 12, 2024
1 parent ea8bf9f commit 14adbd0
Show file tree
Hide file tree
Showing 5 changed files with 338 additions and 0 deletions.
1 change: 1 addition & 0 deletions pages/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"smart-account-modules": "Safe Modules",
"smart-account-guards": "Safe Guards",
"smart-account-audits": "Audits",
"smart-account-signatures": "Signatures",
"smart-account-supported-networks": "Supported Networks",
"-- Safe{Core} AA SDK": {
"type": "separator",
Expand Down
92 changes: 92 additions & 0 deletions pages/smart-account-signatures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Signatures

The Safe supports different types of signatures. All signatures are combined into a single `bytes` and transmitted to the contract when a transaction should be executed.

### Encoding

Each signature has a constant length of 65 bytes. If more data is necessary it can be appended to the end of concatenated constant data of all signatures. The position is encoded into the constant length data.

Constant part per signature: `{(max) 64-bytes signature data}{1-byte signature type}`

All the signatures are sorted by the signer address and concatenated.

#### ECDSA signature

`31 > signature type > 26`

To be able to have the ECDSA signature without the need of additional data we use the signature type byte to encode `v`.

**Constant part:**

`{32-bytes r}{32-bytes s}{1-byte v}`

`r`, `s` and `v` are the required parts of the ECDSA signature to recover the signer.

#### `eth_sign` signature

`signature type > 30`

To be able to use `eth_sign` we need to take the parameters `r`, `s` and `v` from calling `eth_sign` and set `v = v + 4`

**Constant part:**

`{32-bytes r}{32-bytes s}{1-byte v}`

`r`, `s` and `v`are the required parts of the ECDSA signature to recover the signer. `v` will be substracted by `4` to calculate the signature.

#### Contract signature (EIP-1271)

`signature type == 0`

**Constant part:**

`{32-bytes signature verifier}{32-bytes data position}{1-byte signature type}`

**Signature verifier** - Padded address of the contract that implements the EIP 1271 interface to verify the signature

**Data position** - Position of the start of the signature data (offset relative to the beginning of the signature data)

**Signature type** - 0

**Dynamic part (solidity bytes):**

`{32-bytes signature length}{bytes signature data}`

**Signature data** - Signature bytes that are verified by the signature verifier

The method `signMessage` can be used to mark a message as signed on-chain.

#### Pre-validated signatures

`signature type == 1`

**Constant Part:**

`{32-bytes hash validator}{32-bytes ignored}{1-byte signature type}`

**Hash validator** - Padded address of the account that pre-validated the hash that should be validated. The Safe keeps track of all hashes that have been pre validated. This is done with a **mapping address to mapping of bytes32 to boolean** where it is possible to set a hash as validated by a certain address (hash validator). To add an entry to this mapping use `approveHash`. Also if the validator is the sender of transaction that executed the Safe transaction it is **not** required to use `approveHash` to add an entry to the mapping. (This can be seen in the [Team Edition tests](https://github.com/gnosis/safe-contracts/blob/v1.0.0/test/gnosisSafeTeamEdition.js))

**Signature type** - 1

### Examples

Assuming that three signatures are required to confirm a transaction where one signer uses an EOA to generate a ECDSA signature, another a contract signature and the last a pre-validated signature:

We assume that the following addresses generate the following signatures:

1. `0x3` (EOA address) -> `bde0b9f486b1960454e326375d0b1680243e031fd4fb3f070d9a3ef9871ccfd5` (r) + `7d1a653cffb6321f889169f08e548684e005f2b0c3a6c06fba4c4a68f5e00624` (s) + `1c` (v)
2. `0x1` (EIP-1271 validator contract address) -> `0000000000000000000000000000000000000000000000000000000000000001` (address) + `00000000000000000000000000000000000000000000000000000000000000c3` (dynamic position) + `00` (signature type)
* The contract takes the following `bytes` (dynamic part) for verification `00000000000000000000000000000000000000000000000000000000deadbeef`
3. `0x2` (Validator address) -> `0000000000000000000000000000000000000000000000000000000000000002` (address) +`0000000000000000000000000000000000000000000000000000000000000000` (padding - not used) + `01` (signature type)

The constant parts need to be sorted so that the recovered signers are sorted **ascending** (natural order) by address (not checksummed).

The signatures bytes used for `execTransaction` would therefore be the following:

```text
"0x" +
"000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c300" + // encoded EIP-1271 signature
"0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000001" + // encoded pre-validated signature
"bde0b9f486b1960454e326375d0b1680243e031fd4fb3f070d9a3ef9871ccfd57d1a653cffb6321f889169f08e548684e005f2b0c3a6c06fba4c4a68f5e006241c" + // encoded ECDSA signature
"000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000deadbeef" // length of bytes + data of bytes
```
3 changes: 3 additions & 0 deletions pages/smart-account-signatures/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"eip-1271": "EIP-1271"
}
232 changes: 232 additions & 0 deletions pages/smart-account-signatures/eip-1271.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# EIP-1271 Off-chain signatures

The Safe contracts and interface support off-chain [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) signatures. However, because Safe is a smart account, the flow is slightly different from simple EOA signatures.

This doc explains signing and verifying messages off-chain. All examples use [WalletConnect](https://walletconnect.com/) to connect to the Safe and [ethers](https://ethers.io).

## Signing messages

It is possible to sign [EIP-191](https://eips.ethereum.org/EIPS/eip-191) compliant messages as well as [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data messages.

### Restrictions

- Only Safe contracts of version `>=1.1.0` are supported.
- Signing off-chain messages with smart contract wallets is not yet supported.

### Enabling off-chain signing

Multiple dApps rely on on-chain signing.
However, off-chain signing is the new default for Safe Apps that use the [safe-apps-sdk](https://www.npmjs.com/package/@safe-global/safe-apps-sdk) version `>=7.11`.
In order to enable off-chain signing in a Safe App, the `safe-apps-sdk` package needs to be updated.

### EIP-191 messages

To sign a message we have to call the `signMessage` function and pass in the message as hex string.

The signing request will be blocked until the message is fully signed and then return the `signature` as a string.
As Safe{Wallet} is a multi signature wallet, this process can take some time because multiple signers may need to sign the message.

#### Example: sign message

```typescript
import { hashMessage, hexlify, toUtf8Bytes } from 'ethers/lib/utils'

const signMessage = async (message: string) => {
const hexMessage = hexlify(toUtf8Bytes(message))
const signature = await connector.signMessage([safeAddress, hexMessage])
}
```

After signing a message it will be available in the Safe's Message list (Transactions -> Messages).

### EIP-712 typed data

To sign typed data we have to call the `signTypedData` function and pass in the typed data object.

The signing request will be blocked until the message is fully signed and then return the `signature` as a string.
As Safe{Wallet} is a multi signature wallet, this process can take some time because multiple signers may need to sign the message.

### Example: sign typed data

```typescript
const getExampleData = () => {
return {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Example: [{ name: 'content', type: 'string' }],
},
primaryType: 'Example',
domain: {
name: 'EIP-1271 Example DApp',
version: '1.0',
chainId: 1,
verifyingContract: '0x123..456',
},
message: {
content: 'Hello World!',
},
}
}

const signTypedData = async () => {
const typedData = getExampleData()
const signature = await connector.signTypedData([
safeAddress,
JSON.stringify(typedData),
])
}
```

After signing, the message will be available in the Safe's Message list (Transactions -> Messages).

## Fetching the signature asynchronously

You can fetch the signature asynchronously instead of waiting for the RPC response via the [Safe Transaction Service](https://github.com/safe-global/safe-transaction-service).

To do so we have to generate a hash of the `message` or `typedData` using ethers `hashMessage(message)` or `_TypedDataEncoder.hash(domain, types, message)` and then compute the `Safe message hash` by calling `getMessageHash(messageHash)` on the Safe contract.

### Example: get Safe message hash

```typescript
const getSafeInterface = () => {
const SAFE_ABI = [
'function getThreshold() public view returns (uint256)',
'function getMessageHash(bytes memory message) public view returns (bytes32)',
'function isValidSignature(bytes calldata _data, bytes calldata _signature) public view returns (bytes4)'
]

return new Interface(SAFE_ABI)
}

const getSafeMessageHash = async (
connector: WalletConnect,
safeAddress: string,
messageHash: string
) => {
// https://github.com/safe-global/safe-contracts/blob/main/contracts/handler/CompatibilityFallbackHandler.sol#L43
const getMessageHash = getSafeInterface().encodeFunctionData(
'getMessageHash',
[messageHash]
)

return connector.sendCustomRequest({
method: 'eth_call',
params: [{ to: safeAddress, data: getMessageHash }]
})
}
```

Then we can query the state of the message from the network-specific transaction service endpoint for messages: `https://safe-transaction-<NETWORK>.safe.global/api/v1/messages/<SAFE_MSG_HASH>`.

An example message on Goerli can be found here: `https://safe-transaction-goerli.safe.global/api/v1/messages/0x7f4032fd13f27c5fce4694a1a6b79f65c68656da4eee4347a414f5bcec915b39/`.

For other network endpoints, see [Available Services](../../safe-core-api/supported-networks.md).

### Example: Loading message from transaction service

```typescript
const fetchMessage = async (
safeMessageHash: string
): Promise<TransactionServiceSafeMessage | undefined> => {
const safeMessage = await fetch(
`https://safe-transaction-goerli.safe.global/api/v1/messages/${safeMessageHash}`,
{
headers: { 'Content-Type': 'application/json' }
}
).then((res) => {
if (!res.ok) {
return Promise.reject('Invalid response when fetching SafeMessage')
}
return res.json() as Promise<TransactionServiceSafeMessage>
})

return safeMessage
}
```

A Safe message has the following format:

```typescript
{
"messageHash": string,
"status": string,
"logoUri": string | null,
"name": string | null,
"message": string | EIP712TypedData,
"creationTimestamp": number,
"modifiedTimestamp": number,
"confirmationsSubmitted": number,
"confirmationsRequired": number,
"proposedBy": { "value": string },
"confirmations": [
{
"owner": { "value": string },
"signature": string
}
],
"preparedSignature": string | null
}
```

A fully signed message will have the status `CONFIRMED`, `confirmationsSubmitted >= confirmationsRequired` and a `preparedSignature !== null`.

The signature of the message will be returned in the `preparedSignature` field.

## Verifying a signature

We verify the signature by calling the Safe contract's `isValidSignature(hash, signature)` function on-chain. This function returns the `MAGIC VALUE BYTES 0x20c13b0b` if the `signature` is correct for the `messageHash`.

_Note: A common pitfall is to pass the `safeMessageHash` to the `isValidSignature` call which is not correct. It needs to be the hash of the original message._

### Example: verify signature

```typescript
const MAGIC_VALUE_BYTES = '0x20c13b0b'

const isValidSignature = async (
connector: WalletConnect,
safeAddress: string,
messageHash: string,
signature: string
) => {
// https://github.com/safe-global/safe-contracts/blob/main/contracts/handler/CompatibilityFallbackHandler.sol#L28
const isValidSignatureData = getSafeInterface().encodeFunctionData(
'isValidSignature',
[messageHash, signature]
)

const isValidSignature = (await connector.sendCustomRequest({
method: 'eth_call',
params: [{ to: safeAddress, data: isValidSignatureData }]
})) as string

return isValidSignature?.slice(0, 10).toLowerCase() === MAGIC_VALUE_BYTES
}
```

### Example dApps

- [Small test dApp](https://github.com/5afe/eip-1271-dapp)

## Troubleshooting

### Off-chain signing is not being used

If your signing requests fallback to on-chain signing this could be because of multiple reasons:

- The Safe App is not using `safe-apps-sdk` version `>=7.11.0`.
- The Safe{Wallet} is set to always use on-chain signing. This can be toggled in the Settings of the Safe{Wallet} (Settings -> Safe Apps).
- The connected Safe does not have a _fallback handler_ set. This can happen if Safes were not created through the official interface such as a CLI or third party interface.
- The Safe version is not compatible - off-chain signing is only available for Safes with version `>1.0.0`

### Confusion of messageHash and safeMessageHash

`message`, `messageHash` and `safeMessageHash` often get mixed up:

- `message` or `messageHash` is used to verify or sign messages.
- `safeMessageHash` is used to fetch data from the `safe-transaction-service`.
10 changes: 10 additions & 0 deletions redirects.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@
"destination": "/smart-account-audits",
"permanent": true
},
{
"source": "/safe-smart-account/signatures",
"destination": "/smart-account-signatures",
"permanent": true
},
{
"source": "/safe-smart-account/signatures/eip-1271",
"destination": "/smart-account-signatures/eip-1271",
"permanent": true
},
{
"source": "/safe-smart-account/supported-networks",
"destination": "/smart-account-supported-networks",
Expand Down

0 comments on commit 14adbd0

Please sign in to comment.