-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ea8bf9f
commit 14adbd0
Showing
5 changed files
with
338 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"eip-1271": "EIP-1271" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters