From 848b275e111f735a15b0511881ac685992f5458b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 22 Jan 2024 09:42:19 +0100 Subject: [PATCH 01/18] initial commit --- pages/sdk-auth-kit/guides/safe-signatures.md | 814 +++++++++++++++++++ 1 file changed, 814 insertions(+) create mode 100644 pages/sdk-auth-kit/guides/safe-signatures.md diff --git a/pages/sdk-auth-kit/guides/safe-signatures.md b/pages/sdk-auth-kit/guides/safe-signatures.md new file mode 100644 index 00000000..c0762e8c --- /dev/null +++ b/pages/sdk-auth-kit/guides/safe-signatures.md @@ -0,0 +1,814 @@ +# Safe signatures + +Understanding and generating signatures is not an easy task. In the **Safe{Core} SDK** we have a set of utilities that can help using them with Safe. In this article we are going to explain how signatures work and look like and hor to generate them using the `protocol-kit` utilities. + +## Safe Accounts threshold setup + +Your Safe Account can be configured with different thresholds and different kind of owners. An owner can be any ethereum address such as a: + +- External Owned Account (EOA) +- Another Safe Account (Child signer Safe) + +When the owner is an EOA we generate a signature that is different that the signature we create using a Safe Account. The Safe Account is an Smart Contract Account to we have to take this into account when we try to gather the signatures as the Safe Contracts validate them in a different way. + +For the examples in this article we are going to use the following Safe Account setup, given we have 5 different ethereum addresses that can be signers (o*wner1 to owner5*): + +- `safeAddress3_4`: Safe Account with a 3/4 threshold configured + - `owner1` + - `owner2` + - `signerSafe1_1`: A 1/1 Safe Account as an owner + - `owner3` + - `signerSafe2_3`: A 2/3 Safe Account as another owner + - `owner4` + - `owner5` + +All the `ownerX` are basically ethereum addresses. For this example the addresses are these ones + +| Who | Address | +| ------------------ | ------------------------------------------ | +| **safeAddress3_4** | 0xb3b3862D8e38a1E965eb350B09f2167B2371D652 | +| **owner1** | 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 | +| **owner2** | 0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0 | +| **signerSafe1_1** | 0x215033cdE0619D60B7352348F4598316Cc39bC6E | +| **owner3** | 0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b | +| **signerSafe2_3** | 0xf75D61D6C27a7CC5788E633c1FC130f0F4a62D33 | +| **owner4** | 0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d | +| **owner5** | 0xd03ea8624C8C5987235048901fB614fDcA89b117 | + +## Transactions + +### Creating the transaction object + +We can sign a transactions using the `protocol-kit`. For that we need a `protocol-kit` instance. We can instantiate it like this: + +```typescript +const protocolKit = await Safe.create({ + ethAdapter: ethAdapter1, //EthAdapter, any compatible adapter as Web3Adapter or EthersAdapter can be used + safeAddress: safeAddress3_4, +}); +``` + +The `ethAdapter1` is bound to the `owner1`. For the examples in this article we are going to have 5 ethAdapters each one bound with an owner + +| Adapter | Owner | +| ----------- | ------ | +| ethAdapter1 | owner1 | +| ethAdapter2 | owner2 | +| ethAdapter3 | owner3 | +| ethAdapter4 | owner4 | +| ethAdapter5 | owner5 | + +Once we have the `protocolKit` instance we use `createTransaction()` to generate a transaction + +```typescript +// Create the transaction. Send 0.01 ETH to anyEthereumAddress +const safeTransactionData: SafeTransactionDataPartial = { + to: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', + value: '100000000000000000', // 0.01 ETH + data: '0x', +}; + +let safeTx = await protocolKit.createTransaction({ + transactions: [safeTransactionData], +}); +``` + +This `safeTx` is an `EthSafeTransaction` object that stores the transaction data (`data`) and a map of owner - signature (`signatures`) + +```typescript +class EthSafeTransaction implements SafeTransaction { + data: SafeTransactionData + signatures: Map = new Map() +... +// Other props and methods +} +``` + +### Generating the transaction signatures + +Now that we have the transaction object (`safeTx`) it’s time to add the required signatures + +#### Creating a ECDSA signature + +--- + +We are going to sign with `owner1` and `owner2`. For that we use the `signTransaction()` method that takes the tx `data` and add a new signature to the `signatures` map + +```typescript +safeTx = await protocolKit.signTransaction(safeTx, SigningMethod.ETH_SIGN); // owner1 EIP-191 signature +protocolKit = await protocolKit.connect({ ethAdapter: ethAdapter2 }); // Connect another owner +safeTx = await protocolKit.signTransaction( + safeTx, + SigningMethod.ETH_SIGN_TYPED_DATA_V4 +); // owner2 EIP-712 typed data signature (default) +``` + +In this snippet we are adding the signature for `owner1`, then we are connecting the `owner2` and signing again to add the second signature + +If we explore at this point the `safeTx` object we should see something like this: + +```json +EthSafeTransaction { + signatures: Map(2) { + '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1' => EthSafeSignature { + signer: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', + data: '0x969308e2abeda61a0c9c41b3c615012f50dd7456ca76ea39a18e3b975abeb67f275b07810dd59fc928f3f9103e52557c1578c7c5c171ffc983afa5306466b1261f', + isContractSignature: false + }, + '0xffcf8fdee72ac11b5c542428b35eef5769c409f0' => EthSafeSignature { + signer: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0', + data: '0x4d63c79cf9d743782bc31ad58c1a316020b39839ab164caee7ecac9829f685cc44ec0d066a5dfe646b2ffeeb37575df131daf9c96ced41b8c7c4aea8dc5461801c', + isContractSignature: false + } + }, + data: { ... } +} +``` + +Inside the signatures map, the `data` prop represents the signature and the `isContractSignature` flag indicates that this signature is a ECDSA signature. An Ethereum signature comprises two 32-byte integers (r, s) and an extra byte for recovery id (v), so in total a signature has 65 bytes. In a hexadecimal string representation, each byte uses 2 characters. Therefore, a 65-byte Ethereum signature will consist of 130 characters. Since signatures are often prefixed with '0x' for context, you can expect to see 132 characters in total that is the number of characters you can count in this produced signatures. More detailed explanation can be found [here](https://docs.safe.global/safe-smart-account/signatures). + +The last part of the signature `1f` or `1b` indicates the signature type: + +- The hexadecimal number "1f” converts to the decimal number 31 and indicates the signature is an `eth_sign` as the number is greater than 30 (This is being adjusted for the eth_sign flow [in the contracts](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347)) +- The hexadecimal number "1b" converts to the decimal number 27 and indicates the signature is a types data signature. + +For example, for the first signature `0x969308e2abeda61a0c9c41b3c615012f50dd7456ca76ea39a18e3b975abeb67f275b07810dd59fc928f3f9103e52557c1578c7c5c171ffc983afa5306466b1261f`: + +| Type | Bytes | Value | Description | +| -------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| Hex | 1 | 0x | Hex string characters | +| Signature | 64 | 969308e2abeda61a0c9c41b3c615012f50dd7456ca76ea39a18e3b975abeb67f275b07810dd59fc928f3f9103e52557c1578c7c5c171ffc983afa5306466b126 | Signature bytes | +| Signature Type | 1 | 1f | 1f hex is 31 in decimal | + +#### Creating Smart contract signatures (EIP-1271) + +--- + +**1/1 Safe Account** + +The Smart contract signatures supported by Safe look different than the regular ECDSA ones. In order to generate this kind of signatures we are going to use the special method `SigningMethod.ETH_SIGN_TYPED_DATA_V4`. + +We are going to start signing using the 1/1 Safe so we need the adapter for the `owner3` and we need to specify as well the new `safeAddress` on charge of the signature, the child Safe Accounn address. So, let’s connect the adapter and safe and sign: + +```typescript +// Create a new transaction object. Should produce same txHash but the signatures array is empty now +let signerSafeTx1_1 = await protocolKit.createTransaction({ + transactions: [safeTransactionData], +}); + +// Connect the adapter for owner3 and specify the address of the signer Safe Account +protocolKit = await protocolKit.connect({ + ethAdapter: ethAdapter3, + safeAddress: signerSafeAddress1_1, +}); + +// Sign the transaction +signerSafeTx1_1 = await protocolKit.signTransaction( + signerSafeTx1_1, + SigningMethod.SAFE_SIGNATURE, + safeAddress3_4 //Parent Safe address +); +``` + +Once signed, we are going to have an transaction object (`signerSafeTx1_1`) like this one: + +```json +EthSafeTransaction { + signatures: Map(1) { + '0x22d491bde2303f2f43325b2108d26f1eaba1e32b' => EthSafeSignature { + signer: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b', + data: '0x5edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f', + isContractSignature: false + } + }, + data: { ...} +} +``` + +Inside the signatures map there is a regular ECDSA signature (`isContractSignature = false`) and we can use it to produce the **EIP-1271** compatible signature by leveraging the `buildContractSignature()` [utility method](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L139-L150) that takes an array of signatures and creates a signature that Safe contracts can understand + +```typescript +const signerSafeSig1_1 = await buildContractSignature( + Array.from(signerSafeTx1_1.signatures.values()), + signerSafeAddress1_1 +); +``` + +The contract signature will look like like this + +``` +EthSafeSignature { + signer: '0x215033cdE0619D60B7352348F4598316Cc39bC6E', + data: '0x5edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f', + isContractSignature: true +} +``` + +Basically what changed is the signer and the `isContractSignature` method but this change will be the key one when joining all the signatures by calling the `buildSignatureBytes()` [method](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L152-L189) + +Let’s do that + +```typescript +console.log(buildSignatureBytes([signerSafeSig1_1]) +``` + +The output will be + +``` +0x000000000000000000000000215033cdE0619D60B7352348F4598316Cc39bC6E00000000000000000000000000000000000000000000000000000000000000410000000000000000000000000000000000000000000000000000000000000000415edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f +``` + +Let’s decompose this signature in parts + +| Type | Bytes | Value | Description | +| ---------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| Hex | 1 | 0x | Hex string characters | +| Verifier | 32 | 000000000000000000000000215033cdE0619D60B7352348F4598316Cc39bC6E | Padded address of the contract that implements the EIP-1271 interface to verify the signature. The Safe signer address | +| Data position | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | Position of the start of the signature data (offset relative to the beginning of the signature data). 41 hex is 65 in decimal | +| Signature Type | 1 | 00 | 00 for [Safe Accounts](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L322-L336) | +| Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | The length of the signature. 41 hex is 65 in decimal | +| Signature | 65 | 5edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f | Signature bytes that are verified by the signature verifier | + +And this is how looks like an **EIP-1271** contract signature for Safe contracts. Now that we demistified this long string let’s ad the signature to the original transaction + +```typescript +safeTx.addSignature(signerSafeSig1_1); +``` + +Let’s do the same for the other Safe signer + +**2/3 Safe Account** + +The 2/3 Safe Account need at least 2 signatures in order to consider it valid. Let’s sign with the owners `owner4` and `owner5` + +```typescript +// Create a new transaction object. Should produce same txHash but the signatures array is empty now +let signerSafeTx2_3 = await protocolKit.createTransaction({ + transactions: [safeTransactionData], +}); + +// Connect the adapter for owner4 and specify the address of the signer Safe Account +protocolKit = await protocolKit.connect({ + ethAdapter: ethAdapter4, + safeAddress: signerSafeAddress2_3, +}); + +// Sign the transaction +signerSafeTx2_3 = await protocolKit.signTransaction( + signerSafeTx2_3, + SigningMethod.SAFE_SIGNATURE, + safeAddress3_4 +); + +// Conects the adapter for owner5 +protocolKit = await protocolKit.connect({ ethAdapter: ethAdapter5 }); + +// Sign the transaction +signerSafeTx2_3 = await protocolKit.signTransaction( + signerSafeTx2_3, + SigningMethod.SAFE_SIGNATURE, + safeAddress +); +``` + +Once signed, we are going to have a transaction obecjet like this one: + +```javascript +EthSafeTransaction { + signatures: Map(2) { + '0xe11ba2b4d45eaed5996cd0823791e0c93114882d' => EthSafeSignature { + signer: '0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d', + data: '0xd3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f', + isContractSignature: false + }, + '0xd03ea8624c8c5987235048901fb614fdca89b117' => EthSafeSignature { + signer: '0xd03ea8624C8C5987235048901fB614fDcA89b117', + data: '0x023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520', + isContractSignature: false + } + }, + data: { ... } +} +``` + +Now we have 2 signatures from the owners `owner4` and `owner5`. Doing the same exercise we can build the contract signature and log the final one + +```typescript +const signerSafeSig2_3 = await buildContractSignature( + Array.from(signerSafeTx2_3.signatures.values()), + signerSafeAddress2_3 +) +console.log(buildSignatureBytes([signerSafeSig2_3]) +``` + +The output will be + +``` +0x000000000000000000000000f75D61D6C27a7CC5788E633c1FC130f0F4a62D330000000000000000000000000000000000000000000000000000000000000041000000000000000000000000000000000000000000000000000000000000000082023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f +``` + +Let’s decompose again this signature in parts + +| Type | Bytes | Value | Description | +| ---------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| Hex | 1 | 0x | Hex string characters | +| Verifier | 32 | 000000000000000000000000f75D61D6C27a7CC5788E633c1FC130f0F4a62D33 | Padded address of the contract that implements the EIP-1271 interface to verify the signature. The Safe signer address | +| Data position | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | Position of the start of the signature data (offset relative to the beginning of the signature data). 41 hex is 65 in decimal | +| Signature Type | 1 | 00 | 00 for [Safe Accounts](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L322-L336) | +| Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000082 | The length of the signature. 82 hex is 130 in decimal | +| Signature | 130 | 023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f | Signature bytes that are verified by the signature verifier (130 bytes are represented by 260 characters in an hex string) | + +The decomposition looks the same but what changed is the signature length (doubled as we have 2 signatures now) and the signature itself that have the 2 regular signatures concatenated. + +Let’s add the signature + +```typescript +safeTx.addSignature(signerSafeSig2_3); +``` + +Hooray! now we are done and we signed with all the owners. Even we have one more than required as the threshold is 3 but that’s ok. Let’s take a look to the final structure of the safeTx object + +```json +EthSafeTransaction { + signatures: Map(4) { + '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1' => EthSafeSignature { + signer: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', + data: '0x969308e2abeda61a0c9c41b3c615012f50dd7456ca76ea39a18e3b975abeb67f275b07810dd59fc928f3f9103e52557c1578c7c5c171ffc983afa5306466b1261f', + isContractSignature: false + }, + '0xffcf8fdee72ac11b5c542428b35eef5769c409f0' => EthSafeSignature { + signer: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0', + data: '0x4d63c79cf9d743782bc31ad58c1a316020b39839ab164caee7ecac9829f685cc44ec0d066a5dfe646b2ffeeb37575df131daf9c96ced41b8c7c4aea8dc5461801c', + isContractSignature: false + }, + '0x215033cde0619d60b7352348f4598316cc39bc6e' => EthSafeSignature { + signer: '0x215033cdE0619D60B7352348F4598316Cc39bC6E', + data: '0x5edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f', + isContractSignature: true + }, + '0xf75d61d6c27a7cc5788e633c1fc130f0f4a62d33' => EthSafeSignature { + signer: '0xf75D61D6C27a7CC5788E633c1FC130f0F4a62D33', + data: '0x023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f', + isContractSignature: true + } + }, + data: { + to: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', + value: '100000000000000000', + data: '0x', + operation: 0, + baseGas: '0', + gasPrice: '0', + gasToken: '0x0000000000000000000000000000000000000000', + refundReceiver: '0x0000000000000000000000000000000000000000', + nonce: 0, + safeTxGas: '0' + } +} +``` + +Four signatures (2 regular and 2 contract signatures) and the transaction data. As the threshold is 3 we can even remove one of the signatures but, anyway, the transaction is ready to be sent to the contract and to be executed. + +### Executing the transaction + +First, we need to connect to the original safe and to the desired adapter (must have funds to execute the transaction) as in the last step we have the `protocolKit` instance connected to the 2/3 Safe Account. Then we can execute the transaction. + +```typescript +protocolKit = await protocolKit.connect({ + ethAdapter: ethAdapter1, + safeAddress, +}); +const transactionResponse = await protocolKit.executeTransaction(safeTx); +``` + +When we call the `executeTransaction()` method, internally it uses the `safeTx.encodedSignatures()` [calls](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/adapters/ethers/contracts/Safe/SafeContractEthers.ts#L159-L171), that again internally, [calls](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts#L24-L26) the `buildSignatureBytes()` with the 4 signatures we generated using the different owners. Let’s log that. + +```typescript +console.log(safeTx.encodedSignatures()); // Calls buildSignatureBytes() +``` + +The final signature sent to the contract altogether with the transaction data will be: + +``` +0x000000000000000000000000215033cdE0619D60B7352348F4598316Cc39bC6E000000000000000000000000000000000000000000000000000000000000010400969308e2abeda61a0c9c41b3c615012f50dd7456ca76ea39a18e3b975abeb67f275b07810dd59fc928f3f9103e52557c1578c7c5c171ffc983afa5306466b1261f000000000000000000000000f75D61D6C27a7CC5788E633c1FC130f0F4a62D330000000000000000000000000000000000000000000000000000000000000165004d63c79cf9d743782bc31ad58c1a316020b39839ab164caee7ecac9829f685cc44ec0d066a5dfe646b2ffeeb37575df131daf9c96ced41b8c7c4aea8dc5461801c00000000000000000000000000000000000000000000000000000000000000415edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f0000000000000000000000000000000000000000000000000000000000000082023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f +``` + +The signatures are concatenated and sorted by signer address. + +Let’s play the game and decompose it again! + +| Type | Bytes | Value | Acc byte | Description | +| ------------------------------ | :---: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | ------------------------------------------------------- | +| Hex | 1 | 0x | - | Hex string characters | +| 1/1 Safe signer | 32 | 000000000000000000000000215033cdE0619D60B7352348F4598316Cc39bC6E | 32 | Safe Address | +| Data position for 1/1 Safe | 32 | 0000000000000000000000000000000000000000000000000000000000000104 | 64 | 104 hex = Signature data for 1/1 Safe start at byte 260 | +| Signature Type | 1 | 00 | 65 | Smart contract signature | +| Owner signature | 65 | 969308e2abeda61a0c9c41b3c615012f50dd7456ca76ea39a18e3b975abeb67f275b07810dd59fc928f3f9103e52557c1578c7c5c171ffc983afa5306466b1261f | 130 | `owner1` signature | +| 2/3 Safe signer | 32 | 000000000000000000000000f75D61D6C27a7CC5788E633c1FC130f0F4a62D33 | 162 | Safe Address | +| Data position for 2/3 Verifier | 32 | 0000000000000000000000000000000000000000000000000000000000000165 | 194 | 165 hex = Signature data for 2/3 Safe start at byte 357 | +| Signature Type | 1 | 00 | 195 | Smart contract signature | +| Owner signature | 65 | 4d63c79cf9d743782bc31ad58c1a316020b39839ab164caee7ecac9829f685cc44ec0d066a5dfe646b2ffeeb37575df131daf9c96ced41b8c7c4aea8dc5461801c | 260 | `owner2` signature | +| 1/1 Safe Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | 292 | Start of the 1/1 Safe Signature. 41 hex = 65 bytes | +| Signature | 65 | 5edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f | 357 | `owner3` signature | +| 2/3 Safe Signature length | 32 | 0000000000000000000000000000000000000000000000000000000000000082 | 389 | Start of the 2/3 Safe Signature. 82 hex = 130 bytes | +| Signature | 130 | 023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f | 519 | `owner4` and `owner5` concatenated signatures | + +Now, the transaction should be executed on-chain and the process is finished + +### Proposing transactions using Safe Services + +In the example we just saw until now we are using the `protocol-kit` utilities to create and compose a signature in order to execute a transaction with Safe. For doing this we don’t need any kind of service and all can be executed on-chain but if you imagine doing this in any application in implies gathering this signatures from different ethereum addresses, wallets, private keys … different user so you can create your own system (API, services …) to store each signature before sending it to the blockchain (We do not recommend 😰) or you can use Safe services to do that. + +We have deployed Safe services in the [main chains](https://docs.safe.global/safe-core-api/supported-networks): + +- [Safe Transaction Service](https://docs.safe.global/safe-core-api/service-architecture/safe-transaction-service) +- [Transaction Service Swagger](https://safe-transaction-mainnet.safe.global/) + +The API can be used directly to gather signatures but we have another package leveraging the usage of the transaction service, the `api-kit`. Using this kit you can propose transaction and add signatures to the existing ones before executing the transaction. + +How can we do that? In the previous steps we instantiated the `protocol-kit` and created a transaction using the `createTransaction()` method. In order to start gathering the signatures using our service we need a calculated safe transaction hash (we used this hash as a reference for requesting data in the transaction service) and a signature (The signatures will be calculated exactly the same as in the previous steps) + +```typescript +// Get the transaction hash of the safeTx +const txHash = await protocolKit.getTransactionHash(safeTx); + +// Instantiate the api-kit +const apiKit = new SafeApiKit({ chainId }); + +// Extract the signer address and the signature +const signerAddress = (await ethAdapter1.getSignerAddress()) || '0x'; +const ethSig1 = safeTx.getSignature(signerAddress) as EthSafeSignature; + +// Propose the transaction +await apiKit.proposeTransaction({ + safeAddress, + safeTransactionData: safeTx.data, + safeTxHash: txHash, + senderAddress: signerAddress, + senderSignature: buildSignatureBytes([ethSig1]), +}); +``` + +Now we have the transaction proposed and we can see it (once indexed) using Safe{Wallet} UI. But in order to execute the transaction we should gather the other signatures as well so we need the other owners to confirm the transaction. + +```typescript +// Extract the signer address and the signature +const signerAddress = (await ethAdapter2.getSignerAddress()) || '0x'; +const ethSig2 = safeTx.getSignature(signerAddress) as EthSafeSignature; + +// Confirm the transaction +await apiKit.confirmTransaction(txHash, buildSignatureBytes([ethSig2])); +``` + +`owner2` confirmed the transaction is valid!!. We need one extra confirmation to match the threshold. + +```typescript +// Confirm the transaction +await apiKit.confirmTransaction( + txHash, + buildSignatureBytes([signerSafeSig1_1]) +); +await apiKit.confirmTransaction( + txHash, + buildSignatureBytes([signerSafeSig2_3]) +); // Not really necessary as the threshold is matched +``` + +And we now matched the threshold!. We can confirm that retrieving the transaction and exploring the `confirmations` property + +```typescript +const confirmedTx = await api.getTransaction(txHash); +``` + +Now, we can execute the transaction just the same we did without using the services + +```typescript +// Use the retrieved confirmedTx to execute +const executedTxResponse = await safeSdk.executeTransaction(confirmedTx); +``` + +The transaction should be executed at this point + +## Messages + +Until now we were explaining how to work with transactions using the `protocol-kit` but we can work with messages as well, both plain string messages and **EIP-712** JSON ones. + +We are going to explain how to generate and sign messages but not as deep as with the transactions as the concept of signatures are exactly the same and the different tables explaining the signature parts applies to the messages so in this section we are explaining how to generate signed messages using snippets. If you want to know more in dept how signatures work read the [[/Generating the signatures]] section. + +### Creating the message object + +We already have a `protocolKit` instance right? So let’s use it to create a new message. A message can be a plain string or a valid EIP-712 typed data structure + +```json +// An example of string message +const STRING_MESSAGE = "I'm the owner of this Safe Account" + +// An example of typed data message +const TYPED_MESSAGE = { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' } + ], + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallets', type: 'address[]' } + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person[]' }, + { name: 'contents', type: 'string' } + ] + }, + domain: { + name: 'Ether Mail', + version: '1', + chainId: Number(chainId), + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' + }, + primaryType: 'Mail', + message: { + from: { + name: 'Cow', + wallets: [ + '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF' + ] + }, + to: [ + { + name: 'Bob', + wallets: [ + '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57', + '0xB0B0b0b0b0b0B000000000000000000000000000' + ] + } + ], + contents: 'Hello, Bob!' + } +} +``` + +```typescript +// Create the message. You can use the string or the typed JSON, it works the same +let safeMessage = protocolKit.createMessage(TYPED_MESSAGE); +``` + +This `safeMessage` is an `EthSafeMessage` object that stores the message data (`data`) and a map of owner - signature (`signatures`). Just the same the `EthSafeTransaction`. + +```typescript +class EthSafeMessage implements SafeMessage { + data: EIP712TypedData | string + signatures: Map = new Map() +... +// Other props and methods +} +``` + +### Generating the message signatures + +Now that we have the transaction object (`safeTx`) it’s time to add the required signatures + +#### Creating a ECDSA signature + +--- + +We are going to sign with `owner1` and `owner2`. For that we use the `signMessage()` method that takes the tx `data` and add a new signature to the `signatures` map + +```typescript +safeMessage = await protocolKit.signMessage( + safeMessage, + SigningMethod.ETH_SIGN +); +protocolKit = await protocolKit.connect({ ethAdapter: ethAdapter2 }); +safeMessage = await protocolKit.signMessage( + safeMessage, + SigningMethod.ETH_SIGN_TYPED_DATA_V4 +); +``` + +Looks similar to the transaction right? That’s because the process is exactly the same and the signatures are being accumuluted in the signatures Map of the `safeMessage` object. All the previous concepts around `data`,`signatures`, `isContractSignature` … applies + +#### Creating Smart contract signatures (EIP-1271) + +--- + +**1/1 Safe Account** + +We are going to sign the messafe using the 1/1 Safe. Let’s connect `owner3` and sign using the previous concepts: + +```typescript +// Create a new message object +let signerSafeMessage1_1 = await createMessage(TYPED_MESSAGE); + +// Connect the adapter for owner3 and specify the address of the signer Safe Account +protocolKit = await protocolKit.connect({ + ethAdapter: ethAdapter3, + safeAddress: signerSafeAddress1_1, +}); + +// Sign the message +signerSafeMessage1_1 = await signMessage( + signerSafeMessage1_1, + SigningMethod.SAFE_SIGNATURE, + safeAddress3_4 //Parent Safe address +); + +// Build contract signature +const signerSafeMessageSig1_1 = await buildContractSignature( + Array.from(signerSafeMessage1_1.signatures.values()), + signerSafeAddress1_1 +); + +// Add the signature +safeMessage.addSignature(signerSafeMessageSig1_1); +``` + +**2/3 Safe Account** + +The 2/3 Safe Account need at least 2 signatures in order to consider it valid. Let’s sign with the owners `owner4` and `owner5` + +```typescript +// Create a new transaction object. Should produce same txHash but the signatures array is empty now +let signerSafeMessage2_3 = await createMessage(TYPED_MESSAGE); + +// Connect the adapter for owner4 and specify the address of the signer Safe Account +protocolKit = await protocolKit.connect({ + ethAdapter: ethAdapter4, + safeAddress: signerSafeAddress2_3, +}); + +// Sign the message +signerSafeMessage2_3 = await protocolKit.signMessage( + signerSafeMessage2_3, + SigningMethod.SAFE_SIGNATURE, + safeAddress3_4 +); + +// Conects the adapter for owner5 +protocolKit = await protocolKit.connect({ ethAdapter: ethAdapter5 }); + +// Sign the transaction +signerSafeMessage2_3 = await protocolKit.signMessage( + signerSafeMessage2_3, + SigningMethod.SAFE_SIGNATURE, + safeAddress +); + +// Build contract signature +const signerSafeMessageSig2_3 = await buildContractSignature( + Array.from(signerSafeMessage2_3.signatures.values()), + signerSafeAddress2_3 +); + +// Add the signature +message.addSignature(signerSafeMessageSig2_3); +``` + +That’s it. We simplified the explanations because the concept is the same + +### Validating a message signature + +We can use the `isValidSignature()` method defined in the `CompatibilityFallbackHandler` [contract](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/handler/CompatibilityFallbackHandler.sol#L51-L68) to validate the signature of the previous generated message. + +```typescript +import { hashSafeMessage } from '@safe-global/protocol-kit'; + +await protocolKit.isValidSignature( + hashSafeMessage(MESSAGE), + safeMessage.encodedSignatures() +); +``` + +This would validate if the signatures we are creating are valid. + +### Utility of the messages + +We cannot “execute” a message as with transactions so what can do with this?. We can “store” messages in the Safe contract and we can check for their existence later. This is useful for example for flows coming from third party apps as declaring that i’m the owner of a Safe address and that i’m authorizing the Safe contract and the dApp to work together. + +Safe support this kind of messages: + +- **off-chain** : This is the default method and no on-chain interaction is required +- **on-chain** : Messages [stored](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L68-L69) in the Safe contract + +Safe contracts support the the signing of ~[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 al together with off-chain ~[EIP-1271](https://eips.ethereum.org/EIPS/eip-1271)~ validation for signatures. More about this topic [here](https://docs.safe.global/safe-smart-account/signatures/eip-1271). + +### Off-chain messages (default) + +Safe support off-chain messages by default. Using off-chain messages involves using the utilities we previously describe in this document and the transaction service api. There is no blockchain interaction for off-chain messages. + +We are going to do somehing very similar to the `proposeTransaction()` and `confirmTransaction()` methods + +#### Adding new messages using Safe services + +We are going to use the `api-kit` for adding a new message + +```typescript +const signerAddress = (await ethAdapter1.getSignerAddress()) || '0x'; +const ethSig1 = safeMessage.getSignature(signerAddress) as EthSafeSignature; + +apiKit.addMessage(safeAddress3_4, { + message: TYPED_MESSAGE, // or STRING_MESSAGE + signature: buildSignatureBytes([ethSig1]), +}); +``` + +We added the message!! + +#### Confirm messages by adding more signatures + +Same way we did with transactions, we need to confirm the message with other owners. The `id` of the stored message is the `safeMessageHash` and we include utilities to calculate it and to add more signatures. + +```typescript +// Get the safeMessageHash +const safeMessageHash = await protocolKit.getSafeMessageHash( + hashSafeMessage(TYPED_MESSAGE) +); + +// Get the signature for `owner2` +const signerAddress = (await ethAdapter2.getSignerAddress()) || '0x'; +const ethSig2 = safeMessage.getSignature(signerAddress) as EthSafeSignature; + +// Add the different signatures +await apiKit.addMessageSignature( + safeMessageHash, + buildSignatureBytes([ethSig2]) +); +await apiKit.addMessageSignature( + safeMessageHash, + buildSignatureBytes([signerSafeMessageSig1_1]) +); +await apiKit.addMessageSignature( + safeMessageHash, + buildSignatureBytes([signerSafeMessageSig2_3]) +); // Not really necessary as the threshold is matched +``` + +Now we have the message signed by all the owners of the Safe. We even get over the threshold 🥳 + +We can check the message status using `getMessage()` + +```typescript +const confirmedMessage = await apiKit.getMessage(safeMessageHash); +``` + +Or even better, go to the Safe{Wallet} UI to the off-chain messages section: + +``` +https://app.safe.global/transactions/messages?safe={myNetWorkPrefix}:{mySafeAddress} +``` + +### On-chain messages + +The former method Safe had for messages was signing on-chain. This is a step back from off-chain messages as it implies a transaction to store the message hash in the contract and the consequent gas cost. + +To sign messages on chain we need a special contract, the `SignMessageLib` library. We are using the `signMessage` method there to encode the message and execute a transaction to store it. Let’s do that. + +```typescript +// Get the contract with the correct version +const signMessageLibContract = await ethAdapter1.getSignMessageLibContract({ + safeVersion: '1.4.1', +}); +``` + +Now we need to encode it and create a transaction + +```typescript +const messageHash = hashSafeMessage(MESSAGE); +const txData = signMessageLibContract.encode('signMessage', [messageHash]); +const safeTransactionData: SafeTransactionDataPartial = { + to: customContract.signMessageLibAddress, + value: '0', + data: txData, + operation: OperationType.DelegateCall, +}; + +const signMessageTx = await protocolKit.createTransaction({ + transactions: [safeTransactionData], +}); +``` + +Now that we have a transaction we need the signatures for it. If you read until here you probably now how to gather them 😉, but if you started from this section … Please go to [[/Safe signatures]] and check how to produce all the required signatures. Remember, you can gather them yourself or use the transaction service. + +After that, execute the transaction and you will have the message stored in the contract. + +```typescript +// Gather signatures +... + +// Execute tre transaction +await protocolKit.executeTransaction(signMessageTx) +``` + +### Validating on-chain messages + +You can use the `isValidSignature()` method as well to validate on-chain messages, but the second parameter should be `0x ` + +```typescript +await protocolKit.isValidSignature(messageHash, '0x'); +``` + +This way, the method will check the stored hashes in the contract From 615995c66854ab32de0c30070f893f53228c931c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 22 Jan 2024 12:57:23 +0100 Subject: [PATCH 02/18] Improve syntax and grammar --- pages/sdk-auth-kit/guides/safe-signatures.md | 289 ++++++++++--------- 1 file changed, 160 insertions(+), 129 deletions(-) diff --git a/pages/sdk-auth-kit/guides/safe-signatures.md b/pages/sdk-auth-kit/guides/safe-signatures.md index c0762e8c..761eb124 100644 --- a/pages/sdk-auth-kit/guides/safe-signatures.md +++ b/pages/sdk-auth-kit/guides/safe-signatures.md @@ -1,28 +1,28 @@ -# Safe signatures +# Safe Signatures -Understanding and generating signatures is not an easy task. In the **Safe{Core} SDK** we have a set of utilities that can help using them with Safe. In this article we are going to explain how signatures work and look like and hor to generate them using the `protocol-kit` utilities. +Understanding and generating signatures can be challenging. In the **Safe{Core} SDK**, we provide a set of utilities that can assist in using signatures with Safe. In this article, we will explain how signatures work and how to generate them using the `protocol-kit` utilities. -## Safe Accounts threshold setup +## Setting up the Safe Accounts threshold -Your Safe Account can be configured with different thresholds and different kind of owners. An owner can be any ethereum address such as a: +Your Safe Account can be configured with various thresholds and different types of owners. An owner can be any Ethereum address, such as: - External Owned Account (EOA) -- Another Safe Account (Child signer Safe) +- Child Signer Safe (A Safe Account that is an owner of another Safe Account) -When the owner is an EOA we generate a signature that is different that the signature we create using a Safe Account. The Safe Account is an Smart Contract Account to we have to take this into account when we try to gather the signatures as the Safe Contracts validate them in a different way. +When the owner is an EOA, we generate a signature that is different from the signature created using a Safe Account. The Safe Account is a Smart Contract Account, so we need to consider this when gathering the signatures, as the Safe Contracts validate them differently. -For the examples in this article we are going to use the following Safe Account setup, given we have 5 different ethereum addresses that can be signers (o*wner1 to owner5*): +In this article, we will use the following Safe Account setup as examples. We have five different Ethereum addresses that can act as signers, namely _owner1_ to _owner5_. -- `safeAddress3_4`: Safe Account with a 3/4 threshold configured +- `safeAddress3_4`: 3/4 Safe Account (3 signatures required out of 4 owners) - `owner1` - `owner2` - - `signerSafe1_1`: A 1/1 Safe Account as an owner + - `signerSafe1_1`: 1/1 Safe Account as child owner - `owner3` - - `signerSafe2_3`: A 2/3 Safe Account as another owner + - `signerSafe2_3`: 2/3 Safe Account as another child owner - `owner4` - `owner5` -All the `ownerX` are basically ethereum addresses. For this example the addresses are these ones +All the owners are Ethereum addresses. Here are the addresses for this example: | Who | Address | | ------------------ | ------------------------------------------ | @@ -39,29 +39,31 @@ All the `ownerX` are basically ethereum addresses. For this example the addresse ### Creating the transaction object -We can sign a transactions using the `protocol-kit`. For that we need a `protocol-kit` instance. We can instantiate it like this: +We can sign transactions using the `protocol-kit` by creating a `protocol-kit` instance. Here's how we can do it: ```typescript +import Safe from '@safe-global/protocol-kit'; + const protocolKit = await Safe.create({ - ethAdapter: ethAdapter1, //EthAdapter, any compatible adapter as Web3Adapter or EthersAdapter can be used + ethAdapter: ethAdapter1, // You can use any compatible adapter, such as Web3Adapter or EthersAdapter safeAddress: safeAddress3_4, }); ``` -The `ethAdapter1` is bound to the `owner1`. For the examples in this article we are going to have 5 ethAdapters each one bound with an owner +The `ethAdapter1` is bound to `owner1`. In the examples provided in this article, we will have 5 ethAdapters, each one bound to an owner. -| Adapter | Owner | -| ----------- | ------ | -| ethAdapter1 | owner1 | -| ethAdapter2 | owner2 | -| ethAdapter3 | owner3 | -| ethAdapter4 | owner4 | -| ethAdapter5 | owner5 | +| Adapter | Bound to | +| ----------- | -------- | +| ethAdapter1 | owner1 | +| ethAdapter2 | owner2 | +| ethAdapter3 | owner3 | +| ethAdapter4 | owner4 | +| ethAdapter5 | owner5 | -Once we have the `protocolKit` instance we use `createTransaction()` to generate a transaction +After obtaining the `protocolKit` instance, we can use `createTransaction()` to generate a transaction. ```typescript -// Create the transaction. Send 0.01 ETH to anyEthereumAddress +// Create the transaction. Send 0.01 ETH const safeTransactionData: SafeTransactionDataPartial = { to: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', value: '100000000000000000', // 0.01 ETH @@ -73,7 +75,7 @@ let safeTx = await protocolKit.createTransaction({ }); ``` -This `safeTx` is an `EthSafeTransaction` object that stores the transaction data (`data`) and a map of owner - signature (`signatures`) +The `safeTx` is an `EthSafeTransaction` object that stores the transaction data (`data` prop) and a map of owner-signature pairs (`signatures` prop). ```typescript class EthSafeTransaction implements SafeTransaction { @@ -84,15 +86,17 @@ class EthSafeTransaction implements SafeTransaction { } ``` -### Generating the transaction signatures +### Generating transaction signatures -Now that we have the transaction object (`safeTx`) it’s time to add the required signatures +Now that we have the transaction object (`safeTx`), it's time to add the necessary signatures. -#### Creating a ECDSA signature +#### Creating an ECDSA signature --- -We are going to sign with `owner1` and `owner2`. For that we use the `signTransaction()` method that takes the tx `data` and add a new signature to the `signatures` map +We will sign with `owner1` and `owner2` using the `signTransaction()` method. This method adds 2 new signatures to the `signatures` map by using the transaction `data` as input. + +It's possible to use several signing methods, such as `ETH_SIGN`, `ETH_SIGN_TYPED_DATA_V4`, ...etc. The default signing method is `ETH_SIGN_TYPED_DATA_V4`. ```typescript safeTx = await protocolKit.signTransaction(safeTx, SigningMethod.ETH_SIGN); // owner1 EIP-191 signature @@ -103,9 +107,9 @@ safeTx = await protocolKit.signTransaction( ); // owner2 EIP-712 typed data signature (default) ``` -In this snippet we are adding the signature for `owner1`, then we are connecting the `owner2` and signing again to add the second signature +In this snippet, we add the signature for `owner1`. Then, we use the `connect()` method to connect the `owner2` adapter and sign again to add the second signature. -If we explore at this point the `safeTx` object we should see something like this: +If we examine the `safeTx` object at this point, we should observe something similar to the following: ```json EthSafeTransaction { @@ -125,14 +129,21 @@ EthSafeTransaction { } ``` -Inside the signatures map, the `data` prop represents the signature and the `isContractSignature` flag indicates that this signature is a ECDSA signature. An Ethereum signature comprises two 32-byte integers (r, s) and an extra byte for recovery id (v), so in total a signature has 65 bytes. In a hexadecimal string representation, each byte uses 2 characters. Therefore, a 65-byte Ethereum signature will consist of 130 characters. Since signatures are often prefixed with '0x' for context, you can expect to see 132 characters in total that is the number of characters you can count in this produced signatures. More detailed explanation can be found [here](https://docs.safe.global/safe-smart-account/signatures). +The `data` prop in the `signatures` map represents the concrete signature. The `isContractSignature` flag (false) indicates if the signature is an Ethereum signature or a Contract signature (A signer Safe Account). + +An Ethereum signature is composed of two 32-byte integers (r, s) and an extra byte for recovery id (v), making a total of 65 bytes. In hexadecimal string format, each byte is represented by 2 characters. Hence, a 65-byte Ethereum signature will be 130 characters long. Including the '0x' prefix commonly used with signatures, the total character count for such a signature would be 132. For a more detailed explanation, you can refer to [this link](https://docs.safe.global/safe-smart-account/signatures) for more information. + +> To represent a byte (8 bits) in hexadecimal, you need 2 characters. Each hexadecimal character represents 4 bits. Therefore, 2 hexadecimal characters (2 x 4 bits) are able to represent a byte (8 bits). -The last part of the signature `1f` or `1b` indicates the signature type: +The final part of the signature, either `1f` or `1b`, indicates the signature type. -- The hexadecimal number "1f” converts to the decimal number 31 and indicates the signature is an `eth_sign` as the number is greater than 30 (This is being adjusted for the eth_sign flow [in the contracts](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347)) -- The hexadecimal number "1b" converts to the decimal number 27 and indicates the signature is a types data signature. +The hexadecimal number `1f` converts to the decimal number 31. It indicates that the signature is an `eth_sign` because the number is greater than 30. This adjustment is made for the `eth_sign` flow in the contracts. You can find the relevant code [here](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347). -For example, for the first signature `0x969308e2abeda61a0c9c41b3c615012f50dd7456ca76ea39a18e3b975abeb67f275b07810dd59fc928f3f9103e52557c1578c7c5c171ffc983afa5306466b1261f`: +The hexadecimal number `1c` converts to the decimal number 28, indicating that the signature is a data signature of types. + +For instance, in the case of the initial signature: + +`0x969308e2abeda61a0c9c41b3c615012f50dd7456ca76ea39a18e3b975abeb67f275b07810dd59fc928f3f9103e52557c1578c7c5c171ffc983afa5306466b1261f`: | Type | Bytes | Value | Description | | -------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | @@ -146,31 +157,33 @@ For example, for the first signature `0x969308e2abeda61a0c9c41b3c615012f50dd7456 **1/1 Safe Account** -The Smart contract signatures supported by Safe look different than the regular ECDSA ones. In order to generate this kind of signatures we are going to use the special method `SigningMethod.ETH_SIGN_TYPED_DATA_V4`. +The smart contract signatures supported by Safe differ from regular ECDSA signatures. We will use the special method `SigningMethod.ETH_SIGN_TYPED_DATA_V4` to generate these signatures. -We are going to start signing using the 1/1 Safe so we need the adapter for the `owner3` and we need to specify as well the new `safeAddress` on charge of the signature, the child Safe Accounn address. So, let’s connect the adapter and safe and sign: +To start signing with the 1/1 Safe, we need the adapter for `owner3` and the new `safeAddress` for the signature. The new `safeAddress` is associated with the child Safe Account. Let's connect the adapter and safe, and continue with the signing process: ```typescript -// Create a new transaction object. Should produce same txHash but the signatures array is empty now +// Create a new transaction object with an empty signatures array. The txHash should remain the same. You can copy the safeTx object and remove the contents of the signatures array let signerSafeTx1_1 = await protocolKit.createTransaction({ transactions: [safeTransactionData], }); -// Connect the adapter for owner3 and specify the address of the signer Safe Account +// Connect the adapter for owner3 and provide the address of the signer Safe Account protocolKit = await protocolKit.connect({ ethAdapter: ethAdapter3, - safeAddress: signerSafeAddress1_1, + safeAddress: signerSafe1_1, }); // Sign the transaction signerSafeTx1_1 = await protocolKit.signTransaction( signerSafeTx1_1, SigningMethod.SAFE_SIGNATURE, - safeAddress3_4 //Parent Safe address + safeAddress3_4 // Parent Safe address ); ``` -Once signed, we are going to have an transaction object (`signerSafeTx1_1`) like this one: +> When signing with a child Safe Account, it is important to specify the parent Safe address. The parent Safe address is used to generate the signature verifier address based on the version of the contracts you are using + +Once signed, we will have a transaction object (`signerSafeTx1_1`) similar to this one: ```json EthSafeTransaction { @@ -185,16 +198,16 @@ EthSafeTransaction { } ``` -Inside the signatures map there is a regular ECDSA signature (`isContractSignature = false`) and we can use it to produce the **EIP-1271** compatible signature by leveraging the `buildContractSignature()` [utility method](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L139-L150) that takes an array of signatures and creates a signature that Safe contracts can understand +Inside the signatures map, there is a regular ECDSA signature (`isContractSignature = false`). We can use this signature to generate an EIP-1271 compatible signature. This can be achieved by using the `buildContractSignature()` utility method, which can be found [here](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L139-L150). This method takes an array of signatures and output another signature that is ready to be used with Safe contracts. ```typescript const signerSafeSig1_1 = await buildContractSignature( Array.from(signerSafeTx1_1.signatures.values()), - signerSafeAddress1_1 + signerSafe1_1 ); ``` -The contract signature will look like like this +The contract signature will look like like this: ``` EthSafeSignature { @@ -204,9 +217,15 @@ EthSafeSignature { } ``` -Basically what changed is the signer and the `isContractSignature` method but this change will be the key one when joining all the signatures by calling the `buildSignatureBytes()` [method](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L152-L189) +The main changes are as follows: -Let’s do that +- Specify the correct `signer` (the Safe owner Account) +- Set `isContractSignature` to `true` +- Build the `data` by considering all the individual signatures of the child Safe + +After signing the contract, we can generate the final contract compatible with Safe Accounts using the `buildSignatureBytes()` method. This method is also used internally in the `buildContractSignature()` method. You can find the implementation of the `buildSignatureBytes()` method [here](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L152-L189). + +Let’s examine the output of the do `buildSignatureBytes()` method. We can log the output of the method by doing ```typescript console.log(buildSignatureBytes([signerSafeSig1_1]) @@ -218,7 +237,7 @@ The output will be 0x000000000000000000000000215033cdE0619D60B7352348F4598316Cc39bC6E00000000000000000000000000000000000000000000000000000000000000410000000000000000000000000000000000000000000000000000000000000000415edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f ``` -Let’s decompose this signature in parts +Let's break down this signature into parts: | Type | Bytes | Value | Description | | ---------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -229,20 +248,18 @@ Let’s decompose this signature in parts | Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | The length of the signature. 41 hex is 65 in decimal | | Signature | 65 | 5edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f | Signature bytes that are verified by the signature verifier | -And this is how looks like an **EIP-1271** contract signature for Safe contracts. Now that we demistified this long string let’s ad the signature to the original transaction +This is what an **EIP-1271** contract signature for Safe contracts looks like. Now that we have explained this lengthy string, let's add the signature to the original transaction. ```typescript safeTx.addSignature(signerSafeSig1_1); ``` -Let’s do the same for the other Safe signer - **2/3 Safe Account** -The 2/3 Safe Account need at least 2 signatures in order to consider it valid. Let’s sign with the owners `owner4` and `owner5` +The 2/3 Safe Account requires a minimum of 2 signatures to be considered valid. Let's sign it with `owner4` and `owner5`. ```typescript -// Create a new transaction object. Should produce same txHash but the signatures array is empty now +// Create a new transaction object let signerSafeTx2_3 = await protocolKit.createTransaction({ transactions: [safeTransactionData], }); @@ -253,17 +270,17 @@ protocolKit = await protocolKit.connect({ safeAddress: signerSafeAddress2_3, }); -// Sign the transaction +// Sign the transaction with owner4 signerSafeTx2_3 = await protocolKit.signTransaction( signerSafeTx2_3, SigningMethod.SAFE_SIGNATURE, safeAddress3_4 ); -// Conects the adapter for owner5 +// Connect the adapter for owner5 protocolKit = await protocolKit.connect({ ethAdapter: ethAdapter5 }); -// Sign the transaction +// Sign the transaction with owner5 signerSafeTx2_3 = await protocolKit.signTransaction( signerSafeTx2_3, SigningMethod.SAFE_SIGNATURE, @@ -271,7 +288,7 @@ signerSafeTx2_3 = await protocolKit.signTransaction( ); ``` -Once signed, we are going to have a transaction obecjet like this one: +Once signed, we will have a transaction object like this one: ```javascript EthSafeTransaction { @@ -291,23 +308,23 @@ EthSafeTransaction { } ``` -Now we have 2 signatures from the owners `owner4` and `owner5`. Doing the same exercise we can build the contract signature and log the final one +We now have two signatures from the owners, `owner4` and `owner5`. By following the same process, we can create the contract signature and examine the result. ```typescript const signerSafeSig2_3 = await buildContractSignature( Array.from(signerSafeTx2_3.signatures.values()), - signerSafeAddress2_3 + signerSafe2_3 ) console.log(buildSignatureBytes([signerSafeSig2_3]) ``` -The output will be +The output will be: ``` 0x000000000000000000000000f75D61D6C27a7CC5788E633c1FC130f0F4a62D330000000000000000000000000000000000000000000000000000000000000041000000000000000000000000000000000000000000000000000000000000000082023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f ``` -Let’s decompose again this signature in parts +Let's break down this signature into parts again: | Type | Bytes | Value | Description | | ---------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -318,15 +335,15 @@ Let’s decompose again this signature in parts | Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000082 | The length of the signature. 82 hex is 130 in decimal | | Signature | 130 | 023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f | Signature bytes that are verified by the signature verifier (130 bytes are represented by 260 characters in an hex string) | -The decomposition looks the same but what changed is the signature length (doubled as we have 2 signatures now) and the signature itself that have the 2 regular signatures concatenated. +The decomposition appears unchanged, but there are two changes: the signature length has doubled because there are now two signatures, and the signature itself is a concatenation of the two regular signatures. -Let’s add the signature +We're finished. Let's add the signature to the original transaction. ```typescript safeTx.addSignature(signerSafeSig2_3); ``` -Hooray! now we are done and we signed with all the owners. Even we have one more than required as the threshold is 3 but that’s ok. Let’s take a look to the final structure of the safeTx object +🚀🚀🚀 We are now done!!, we have signed with all the owners. We even have one more owner than the required threshold of 3 💃🏻, but that's okay. Now, let's take a look at the final structure of the `safeTx` object. ```json EthSafeTransaction { @@ -367,11 +384,11 @@ EthSafeTransaction { } ``` -Four signatures (2 regular and 2 contract signatures) and the transaction data. As the threshold is 3 we can even remove one of the signatures but, anyway, the transaction is ready to be sent to the contract and to be executed. +The transaction includes four signatures: two regular and two contract signatures. Since the threshold is three, we can even remove one signature 😉. Nevertheless, the transaction is prepared for sending to the contract and execution. ### Executing the transaction -First, we need to connect to the original safe and to the desired adapter (must have funds to execute the transaction) as in the last step we have the `protocolKit` instance connected to the 2/3 Safe Account. Then we can execute the transaction. +To start, connect to the original safe and the desired adapter. Ensure sufficient funds are available in the underlying owner account to execute the transaction. In the previous step, we connected the `protocolKit` instance to the 2/3 Safe Account. Once connected, proceed with executing the transaction. ```typescript protocolKit = await protocolKit.connect({ @@ -381,21 +398,19 @@ protocolKit = await protocolKit.connect({ const transactionResponse = await protocolKit.executeTransaction(safeTx); ``` -When we call the `executeTransaction()` method, internally it uses the `safeTx.encodedSignatures()` [calls](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/adapters/ethers/contracts/Safe/SafeContractEthers.ts#L159-L171), that again internally, [calls](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts#L24-L26) the `buildSignatureBytes()` with the 4 signatures we generated using the different owners. Let’s log that. +When we call the `executeTransaction()` method, it internally uses the [`safeTx.encodedSignatures()`](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/adapters/ethers/contracts/Safe/SafeContractEthers.ts#L159-L171) function, which in turn calls [`buildSignatureBytes()`](<(https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts#L24-L26)>) with the four signatures generated by the different owners. + +Let’s log that. ```typescript -console.log(safeTx.encodedSignatures()); // Calls buildSignatureBytes() +console.log(safeTx.encodedSignatures()); // Call buildSignatureBytes() ``` -The final signature sent to the contract altogether with the transaction data will be: - ``` 0x000000000000000000000000215033cdE0619D60B7352348F4598316Cc39bC6E000000000000000000000000000000000000000000000000000000000000010400969308e2abeda61a0c9c41b3c615012f50dd7456ca76ea39a18e3b975abeb67f275b07810dd59fc928f3f9103e52557c1578c7c5c171ffc983afa5306466b1261f000000000000000000000000f75D61D6C27a7CC5788E633c1FC130f0F4a62D330000000000000000000000000000000000000000000000000000000000000165004d63c79cf9d743782bc31ad58c1a316020b39839ab164caee7ecac9829f685cc44ec0d066a5dfe646b2ffeeb37575df131daf9c96ced41b8c7c4aea8dc5461801c00000000000000000000000000000000000000000000000000000000000000415edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f0000000000000000000000000000000000000000000000000000000000000082023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f ``` -The signatures are concatenated and sorted by signer address. - -Let’s play the game and decompose it again! +This is the final signature we sent to the contract. The signatures are concatenated and sorted by signer address. Let’s decompose it. | Type | Bytes | Value | Acc byte | Description | | ------------------------------ | :---: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | ------------------------------------------------------- | @@ -413,27 +428,31 @@ Let’s play the game and decompose it again! | 2/3 Safe Signature length | 32 | 0000000000000000000000000000000000000000000000000000000000000082 | 389 | Start of the 2/3 Safe Signature. 82 hex = 130 bytes | | Signature | 130 | 023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f | 519 | `owner4` and `owner5` concatenated signatures | -Now, the transaction should be executed on-chain and the process is finished +At this point, The transaction should be executed on-chain and the process is finished. ### Proposing transactions using Safe Services -In the example we just saw until now we are using the `protocol-kit` utilities to create and compose a signature in order to execute a transaction with Safe. For doing this we don’t need any kind of service and all can be executed on-chain but if you imagine doing this in any application in implies gathering this signatures from different ethereum addresses, wallets, private keys … different user so you can create your own system (API, services …) to store each signature before sending it to the blockchain (We do not recommend 😰) or you can use Safe services to do that. +In the example we just saw, we used the `protocol-kit` utilities to create and compose a signature for executing a transaction with Safe. This process can be done entirely on-chain without the need for any external service. However, in a real-world application, you would typically gather signatures from multiple Ethereum addresses, wallets, ... and private keys belonging to different users. To handle this, you can either develop your own system (such as an API or services) to store each signature before sending it to the blockchain (although we don't recommend it 😰), or you can utilize Safe services for this purpose. -We have deployed Safe services in the [main chains](https://docs.safe.global/safe-core-api/supported-networks): +We have already deployed Safe services on the main chains. More information can be found in: +- [Safe Core API documentation](https://docs.safe.global/safe-core-api/supported-networks). - [Safe Transaction Service](https://docs.safe.global/safe-core-api/service-architecture/safe-transaction-service) - [Transaction Service Swagger](https://safe-transaction-mainnet.safe.global/) -The API can be used directly to gather signatures but we have another package leveraging the usage of the transaction service, the `api-kit`. Using this kit you can propose transaction and add signatures to the existing ones before executing the transaction. +You can use the API directly to gather signatures. Alternatively, you can use the `api-kit` package, which utilizes the transaction service. With the kit, you can propose transactions and add signatures to existing ones before executing them. + +How can we do that? In the previous steps we instantiated the `protocol-kit` and created a transaction using the `createTransaction()` method. In order to start gathering the signatures using our services we need: -How can we do that? In the previous steps we instantiated the `protocol-kit` and created a transaction using the `createTransaction()` method. In order to start gathering the signatures using our service we need a calculated safe transaction hash (we used this hash as a reference for requesting data in the transaction service) and a signature (The signatures will be calculated exactly the same as in the previous steps) +- A calculated safe transaction hash. We used this hash as a reference (id) for requesting data in the transaction service +- The signature. Can be calculated in the same way as in the previous steps. ```typescript // Get the transaction hash of the safeTx const txHash = await protocolKit.getTransactionHash(safeTx); // Instantiate the api-kit -const apiKit = new SafeApiKit({ chainId }); +const apiKit = new SafeApiKit({ chainId }); // Use the chainId where you have the Safe Account deployed // Extract the signer address and the signature const signerAddress = (await ethAdapter1.getSignerAddress()) || '0x'; @@ -449,7 +468,7 @@ await apiKit.proposeTransaction({ }); ``` -Now we have the transaction proposed and we can see it (once indexed) using Safe{Wallet} UI. But in order to execute the transaction we should gather the other signatures as well so we need the other owners to confirm the transaction. +Now that we have the proposed transaction, we can view it in the Safe{Wallet} UI. However, to execute the transaction, we need the other owners to confirm it by providing their signatures. ```typescript // Extract the signer address and the signature @@ -460,40 +479,44 @@ const ethSig2 = safeTx.getSignature(signerAddress) as EthSafeSignature; await apiKit.confirmTransaction(txHash, buildSignatureBytes([ethSig2])); ``` -`owner2` confirmed the transaction is valid!!. We need one extra confirmation to match the threshold. +`owner2` has confirmed that the transaction is valid at this point! We only need one more confirmation to meet the threshold. ```typescript -// Confirm the transaction +// Confirm the transaction with the 1/1 signer Safe await apiKit.confirmTransaction( txHash, buildSignatureBytes([signerSafeSig1_1]) ); + +// Confirm the transaction with the 2/3 signer Safe +// Not really necessary as the threshold is matched at this point await apiKit.confirmTransaction( txHash, buildSignatureBytes([signerSafeSig2_3]) -); // Not really necessary as the threshold is matched +); ``` -And we now matched the threshold!. We can confirm that retrieving the transaction and exploring the `confirmations` property +We have now reached the threshold! We can confirm it by retrieving the transaction and examine the `confirmations` property. ```typescript const confirmedTx = await api.getTransaction(txHash); +// Examine the confirmedTx.confirmations property ``` -Now, we can execute the transaction just the same we did without using the services +Now, we can execute the transaction just as we did without using the services. ```typescript // Use the retrieved confirmedTx to execute const executedTxResponse = await safeSdk.executeTransaction(confirmedTx); ``` -The transaction should be executed at this point +The transaction should be executed now. ## Messages -Until now we were explaining how to work with transactions using the `protocol-kit` but we can work with messages as well, both plain string messages and **EIP-712** JSON ones. +Previously, we explained how to work with transactions using the `protocol-kit`. However, we can also work with messages, including plain string messages and **EIP-712** JSON messages. -We are going to explain how to generate and sign messages but not as deep as with the transactions as the concept of signatures are exactly the same and the different tables explaining the signature parts applies to the messages so in this section we are explaining how to generate signed messages using snippets. If you want to know more in dept how signatures work read the [[/Generating the signatures]] section. +In this section, we will explain how to generate and sign messages. The concept of signatures is the same as with transactions, so the tables explaining the signature parts also apply to messages. We will focus on generating signed messages using snippets. For a more detailed understanding of how signatures work, please refer to the **Generating the signatures** section above. ### Creating the message object @@ -552,12 +575,13 @@ const TYPED_MESSAGE = { } ``` +Let's compose the message. You can use either the string or the typed JSON format: + ```typescript -// Create the message. You can use the string or the typed JSON, it works the same let safeMessage = protocolKit.createMessage(TYPED_MESSAGE); ``` -This `safeMessage` is an `EthSafeMessage` object that stores the message data (`data`) and a map of owner - signature (`signatures`). Just the same the `EthSafeTransaction`. +The `safeMessage` is an object of type `EthSafeMessage` that contains the message data (`data`) and a map of owner-signature pairs (`signatures`). It is similar to the `EthSafeTransaction`. ```typescript class EthSafeMessage implements SafeMessage { @@ -570,13 +594,13 @@ class EthSafeMessage implements SafeMessage { ### Generating the message signatures -Now that we have the transaction object (`safeTx`) it’s time to add the required signatures +Now that we have the transaction object (`safeTx`), it is time to collect signatures. #### Creating a ECDSA signature --- -We are going to sign with `owner1` and `owner2`. For that we use the `signMessage()` method that takes the tx `data` and add a new signature to the `signatures` map +We are going to sign with `owner1` and `owner2`. For that we use the `signMessage()` method that takes the tx `data` and add a new signature to the `signatures` map. ```typescript safeMessage = await protocolKit.signMessage( @@ -590,7 +614,7 @@ safeMessage = await protocolKit.signMessage( ); ``` -Looks similar to the transaction right? That’s because the process is exactly the same and the signatures are being accumuluted in the signatures Map of the `safeMessage` object. All the previous concepts around `data`,`signatures`, `isContractSignature` … applies +It feels very similar to the transactions, right? That's because the process is basically the same and the signatures are being accumulated in the signatures Map of the `safeMessage` object. The previous concepts related to `data`, `signatures`, and `isContractSignature` still apply. #### Creating Smart contract signatures (EIP-1271) @@ -598,7 +622,7 @@ Looks similar to the transaction right? That’s because the process is exactly **1/1 Safe Account** -We are going to sign the messafe using the 1/1 Safe. Let’s connect `owner3` and sign using the previous concepts: +We will sign the message using the 1/1 Safe. Connect `owner3` and sign using the concepts explained earlier. ```typescript // Create a new message object @@ -607,20 +631,20 @@ let signerSafeMessage1_1 = await createMessage(TYPED_MESSAGE); // Connect the adapter for owner3 and specify the address of the signer Safe Account protocolKit = await protocolKit.connect({ ethAdapter: ethAdapter3, - safeAddress: signerSafeAddress1_1, + safeAddress: signerSafe1_1, }); // Sign the message signerSafeMessage1_1 = await signMessage( signerSafeMessage1_1, SigningMethod.SAFE_SIGNATURE, - safeAddress3_4 //Parent Safe address + safeAddress3_4 // Parent Safe address ); // Build contract signature const signerSafeMessageSig1_1 = await buildContractSignature( Array.from(signerSafeMessage1_1.signatures.values()), - signerSafeAddress1_1 + signerSafe1_1 ); // Add the signature @@ -629,10 +653,10 @@ safeMessage.addSignature(signerSafeMessageSig1_1); **2/3 Safe Account** -The 2/3 Safe Account need at least 2 signatures in order to consider it valid. Let’s sign with the owners `owner4` and `owner5` +The 2/3 Safe Account requires a minimum of 2 signatures to be considered valid. Let's sign it with the owners `owner4` and `owner5`. ```typescript -// Create a new transaction object. Should produce same txHash but the signatures array is empty now +// Create a new message object let signerSafeMessage2_3 = await createMessage(TYPED_MESSAGE); // Connect the adapter for owner4 and specify the address of the signer Safe Account @@ -648,27 +672,27 @@ signerSafeMessage2_3 = await protocolKit.signMessage( safeAddress3_4 ); -// Conects the adapter for owner5 +// Connect the adapter for owner5 protocolKit = await protocolKit.connect({ ethAdapter: ethAdapter5 }); -// Sign the transaction +// Sign the message signerSafeMessage2_3 = await protocolKit.signMessage( signerSafeMessage2_3, SigningMethod.SAFE_SIGNATURE, - safeAddress + safeAddress3_4 ); // Build contract signature const signerSafeMessageSig2_3 = await buildContractSignature( Array.from(signerSafeMessage2_3.signatures.values()), - signerSafeAddress2_3 + signerSafe2_3 ); // Add the signature message.addSignature(signerSafeMessageSig2_3); ``` -That’s it. We simplified the explanations because the concept is the same +That's it. We simplified the explanations because the concept remains the same. ### Validating a message signature @@ -683,28 +707,32 @@ await protocolKit.isValidSignature( ); ``` -This would validate if the signatures we are creating are valid. +This would validate the validity of the signatures we create. + +### What are the messages about ? -### Utility of the messages +We cannot execute a message like transactions. Instead, we can store messages in the Safe contract and later check for their existence. This is useful, for example, for flows from third-party apps. It declares ownership of a Safe address and authorizes the Safe contract and the dApp to collaborate. -We cannot “execute” a message as with transactions so what can do with this?. We can “store” messages in the Safe contract and we can check for their existence later. This is useful for example for flows coming from third party apps as declaring that i’m the owner of a Safe address and that i’m authorizing the Safe contract and the dApp to work together. +We cannot “execute” a message like transactions so what can do ?. Instead, we can store messages in the Safe contract and later check for their existence. This is useful for example for flows from third-party apps where the app ask the Safe owner to declare ownership and authorize the Safe Account and dApp to work together. -Safe support this kind of messages: +Safe supports these two kinds of messages: -- **off-chain** : This is the default method and no on-chain interaction is required +- **Off-chain**: This is the default method and does not require any on-chain interaction. - **on-chain** : Messages [stored](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L68-L69) in the Safe contract -Safe contracts support the the signing of ~[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 al together with off-chain ~[EIP-1271](https://eips.ethereum.org/EIPS/eip-1271)~ validation for signatures. More about this topic [here](https://docs.safe.global/safe-smart-account/signatures/eip-1271). +Safe Accounts support signing of ~[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 all together with off-chain ~[EIP-1271](https://eips.ethereum.org/EIPS/eip-1271)~ validation for signatures. More about this topic [here](https://docs.safe.global/safe-smart-account/signatures/eip-1271). ### Off-chain messages (default) -Safe support off-chain messages by default. Using off-chain messages involves using the utilities we previously describe in this document and the transaction service api. There is no blockchain interaction for off-chain messages. +By default, Safe supports off-chain messages. To use off-chain messages, you need to use the utilities described in this document and the transaction service API. Off-chain messages do not require any interaction with the blockchain. -We are going to do somehing very similar to the `proposeTransaction()` and `confirmTransaction()` methods +We mentioned the utility of storing messages in the contract. Off-chain messages have the same purpose, but they are stored in the transaction service. The transaction service stores the messages and signatures in a database. It is a centralized service, but it is open-source and can be deployed by anyone. The transaction service is used by the Safe{Wallet} UI to store messages and signatures by default. -#### Adding new messages using Safe services +We will perform a task similar to the `proposeTransaction()` and `confirmTransaction()` methods. -We are going to use the `api-kit` for adding a new message +#### Adding new messages using Safe services. + +We will use the `api-kit` to add a new message. The process is similar to the one we used for transactions. We need to calculate the `safeMessageHash` and add the message to the service. ```typescript const signerAddress = (await ethAdapter1.getSignerAddress()) || '0x'; @@ -716,11 +744,11 @@ apiKit.addMessage(safeAddress3_4, { }); ``` -We added the message!! +We have added the message! -#### Confirm messages by adding more signatures +#### Confirm messages by adding additional signatures -Same way we did with transactions, we need to confirm the message with other owners. The `id` of the stored message is the `safeMessageHash` and we include utilities to calculate it and to add more signatures. +We should confirm the message with other owners, just like we did with transactions. The `id` of the stored message is the `safeMessageHash`, and we have utilities available to calculate it and add more signatures. ```typescript // Get the safeMessageHash @@ -728,7 +756,7 @@ const safeMessageHash = await protocolKit.getSafeMessageHash( hashSafeMessage(TYPED_MESSAGE) ); -// Get the signature for `owner2` +// Get the signature for owner2 const signerAddress = (await ethAdapter2.getSignerAddress()) || '0x'; const ethSig2 = safeMessage.getSignature(signerAddress) as EthSafeSignature; @@ -737,25 +765,28 @@ await apiKit.addMessageSignature( safeMessageHash, buildSignatureBytes([ethSig2]) ); + await apiKit.addMessageSignature( safeMessageHash, buildSignatureBytes([signerSafeMessageSig1_1]) ); + +// The threshold has already been matched, so it is not necessary. await apiKit.addMessageSignature( safeMessageHash, buildSignatureBytes([signerSafeMessageSig2_3]) -); // Not really necessary as the threshold is matched +); ``` -Now we have the message signed by all the owners of the Safe. We even get over the threshold 🥳 +Now we have the message signed by all Safe owners. We have even "crossed the threshold" 🥳 -We can check the message status using `getMessage()` +We can check the status of the message by using the `getMessage()` function. ```typescript const confirmedMessage = await apiKit.getMessage(safeMessageHash); ``` -Or even better, go to the Safe{Wallet} UI to the off-chain messages section: +Or, better yet, visit the SafeWallet UI and navigate to the off-chain messages section. ``` https://app.safe.global/transactions/messages?safe={myNetWorkPrefix}:{mySafeAddress} @@ -763,9 +794,9 @@ https://app.safe.global/transactions/messages?safe={myNetWorkPrefix}:{mySafeAddr ### On-chain messages -The former method Safe had for messages was signing on-chain. This is a step back from off-chain messages as it implies a transaction to store the message hash in the contract and the consequent gas cost. +The previous method used by Safe for messages involved on-chain signing. This is less efficient than off-chain messages because it requires a transaction to store the message hash in the contract, resulting in additional gas costs. -To sign messages on chain we need a special contract, the `SignMessageLib` library. We are using the `signMessage` method there to encode the message and execute a transaction to store it. Let’s do that. +We require a special contract, the `SignMessageLib` library, to sign messages on the chain. In this library, we use the `signMessage` method in the contract to encode the message and execute a transaction for storing it. Let's proceed with this process: ```typescript // Get the contract with the correct version @@ -774,7 +805,7 @@ const signMessageLibContract = await ethAdapter1.getSignMessageLibContract({ }); ``` -Now we need to encode it and create a transaction +Now we need to encode it and create a transaction: ```typescript const messageHash = hashSafeMessage(MESSAGE); @@ -791,12 +822,12 @@ const signMessageTx = await protocolKit.createTransaction({ }); ``` -Now that we have a transaction we need the signatures for it. If you read until here you probably now how to gather them 😉, but if you started from this section … Please go to [[/Safe signatures]] and check how to produce all the required signatures. Remember, you can gather them yourself or use the transaction service. +Now that we have a transaction, we need its signatures. If you've read this far, you probably know how to gather them 😉. If you started from this section, please go to the beginning and learn how to generate all the necessary signatures. Remember, you can gather them on your own or use the transaction service. After that, execute the transaction and you will have the message stored in the contract. ```typescript -// Gather signatures +// Gather signatures using signTransaction() ... // Execute tre transaction @@ -805,10 +836,10 @@ await protocolKit.executeTransaction(signMessageTx) ### Validating on-chain messages -You can use the `isValidSignature()` method as well to validate on-chain messages, but the second parameter should be `0x ` +You can also use the `isValidSignature()` method to validate on-chain messages. However, make sure to pass `0x` as the second parameter. ```typescript await protocolKit.isValidSignature(messageHash, '0x'); ``` -This way, the method will check the stored hashes in the contract +This way, the method will check the stored hashes in the contract. From bc20c788c6183c98155014b08b0d6c4c01494a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 22 Jan 2024 13:00:02 +0100 Subject: [PATCH 03/18] Add comments --- pages/sdk-auth-kit/guides/safe-signatures.md | 54 ++++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/pages/sdk-auth-kit/guides/safe-signatures.md b/pages/sdk-auth-kit/guides/safe-signatures.md index 761eb124..06190ad2 100644 --- a/pages/sdk-auth-kit/guides/safe-signatures.md +++ b/pages/sdk-auth-kit/guides/safe-signatures.md @@ -131,13 +131,13 @@ EthSafeTransaction { The `data` prop in the `signatures` map represents the concrete signature. The `isContractSignature` flag (false) indicates if the signature is an Ethereum signature or a Contract signature (A signer Safe Account). -An Ethereum signature is composed of two 32-byte integers (r, s) and an extra byte for recovery id (v), making a total of 65 bytes. In hexadecimal string format, each byte is represented by 2 characters. Hence, a 65-byte Ethereum signature will be 130 characters long. Including the '0x' prefix commonly used with signatures, the total character count for such a signature would be 132. For a more detailed explanation, you can refer to [this link](https://docs.safe.global/safe-smart-account/signatures) for more information. +An Ethereum signature is composed of two 32-byte integers (r, s) and an extra byte for recovery id (v), making a total of 65 bytes. In hexadecimal string format, each byte is represented by 2 characters. Hence, a 65-byte Ethereum signature will be 130 characters long. Including the '0x' prefix commonly used with signatures, the total character count for such a signature would be 132. For a more detailed explanation, you can refer to [~this link~](https://docs.safe.global/safe-smart-account/signatures) for more information. > To represent a byte (8 bits) in hexadecimal, you need 2 characters. Each hexadecimal character represents 4 bits. Therefore, 2 hexadecimal characters (2 x 4 bits) are able to represent a byte (8 bits). The final part of the signature, either `1f` or `1b`, indicates the signature type. -The hexadecimal number `1f` converts to the decimal number 31. It indicates that the signature is an `eth_sign` because the number is greater than 30. This adjustment is made for the `eth_sign` flow in the contracts. You can find the relevant code [here](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347). +The hexadecimal number `1f` converts to the decimal number 31. It indicates that the signature is an `eth_sign` because the number is greater than 30. This adjustment is made for the `eth_sign` flow in the contracts. You can find the relevant code [~here~](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347). The hexadecimal number `1c` converts to the decimal number 28, indicating that the signature is a data signature of types. @@ -198,7 +198,7 @@ EthSafeTransaction { } ``` -Inside the signatures map, there is a regular ECDSA signature (`isContractSignature = false`). We can use this signature to generate an EIP-1271 compatible signature. This can be achieved by using the `buildContractSignature()` utility method, which can be found [here](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L139-L150). This method takes an array of signatures and output another signature that is ready to be used with Safe contracts. +Inside the signatures map, there is a regular ECDSA signature (`isContractSignature = false`). We can use this signature to generate an EIP-1271 compatible signature. This can be achieved by using the `buildContractSignature()` utility method, which can be found [~here~](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L139-L150). This method takes an array of signatures and output another signature that is ready to be used with Safe contracts. ```typescript const signerSafeSig1_1 = await buildContractSignature( @@ -223,7 +223,7 @@ The main changes are as follows: - Set `isContractSignature` to `true` - Build the `data` by considering all the individual signatures of the child Safe -After signing the contract, we can generate the final contract compatible with Safe Accounts using the `buildSignatureBytes()` method. This method is also used internally in the `buildContractSignature()` method. You can find the implementation of the `buildSignatureBytes()` method [here](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L152-L189). +After signing the contract, we can generate the final contract compatible with Safe Accounts using the `buildSignatureBytes()` method. This method is also used internally in the `buildContractSignature()` method. You can find the implementation of the `buildSignatureBytes()` method [~here~](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L152-L189). Let’s examine the output of the do `buildSignatureBytes()` method. We can log the output of the method by doing @@ -239,14 +239,14 @@ The output will be Let's break down this signature into parts: -| Type | Bytes | Value | Description | -| ---------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| Hex | 1 | 0x | Hex string characters | -| Verifier | 32 | 000000000000000000000000215033cdE0619D60B7352348F4598316Cc39bC6E | Padded address of the contract that implements the EIP-1271 interface to verify the signature. The Safe signer address | -| Data position | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | Position of the start of the signature data (offset relative to the beginning of the signature data). 41 hex is 65 in decimal | -| Signature Type | 1 | 00 | 00 for [Safe Accounts](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L322-L336) | -| Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | The length of the signature. 41 hex is 65 in decimal | -| Signature | 65 | 5edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f | Signature bytes that are verified by the signature verifier | +| Type | Bytes | Value | Description | +| ---------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| Hex | 1 | 0x | Hex string characters | +| Verifier | 32 | 000000000000000000000000215033cdE0619D60B7352348F4598316Cc39bC6E | Padded address of the contract that implements the EIP-1271 interface to verify the signature. The Safe signer address | +| Data position | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | Start position of the signature data (offset relative to the beginning of the signature data). 41 hex is 65 in decimal | +| Signature Type | 1 | 00 | 00 for [~Safe Accounts~](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L322-L336) | +| Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | The length of the signature. 41 hex is 65 in decimal | +| Signature | 65 | 5edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f | Signature bytes that are verified by the signature verifier | This is what an **EIP-1271** contract signature for Safe contracts looks like. Now that we have explained this lengthy string, let's add the signature to the original transaction. @@ -326,14 +326,14 @@ The output will be: Let's break down this signature into parts again: -| Type | Bytes | Value | Description | -| ---------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| Hex | 1 | 0x | Hex string characters | -| Verifier | 32 | 000000000000000000000000f75D61D6C27a7CC5788E633c1FC130f0F4a62D33 | Padded address of the contract that implements the EIP-1271 interface to verify the signature. The Safe signer address | -| Data position | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | Position of the start of the signature data (offset relative to the beginning of the signature data). 41 hex is 65 in decimal | -| Signature Type | 1 | 00 | 00 for [Safe Accounts](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L322-L336) | -| Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000082 | The length of the signature. 82 hex is 130 in decimal | -| Signature | 130 | 023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f | Signature bytes that are verified by the signature verifier (130 bytes are represented by 260 characters in an hex string) | +| Type | Bytes | Value | Description | +| ---------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| Hex | 1 | 0x | Hex string characters | +| Verifier | 32 | 000000000000000000000000f75D61D6C27a7CC5788E633c1FC130f0F4a62D33 | Padded address of the contract that implements the EIP-1271 interface to verify the signature. The Safe signer address | +| Data position | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | Start position of the signature data (offset relative to the beginning of the signature data). 41 hex is 65 in decimal | +| Signature Type | 1 | 00 | 00 for [~Safe Accounts~](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L322-L336) | +| Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000082 | The length of the signature. 82 hex is 130 in decimal | +| Signature | 130 | 023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f | Signature bytes that are verified by the signature verifier (130 bytes are represented by 260 characters in an hex string) | The decomposition appears unchanged, but there are two changes: the signature length has doubled because there are now two signatures, and the signature itself is a concatenation of the two regular signatures. @@ -398,7 +398,7 @@ protocolKit = await protocolKit.connect({ const transactionResponse = await protocolKit.executeTransaction(safeTx); ``` -When we call the `executeTransaction()` method, it internally uses the [`safeTx.encodedSignatures()`](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/adapters/ethers/contracts/Safe/SafeContractEthers.ts#L159-L171) function, which in turn calls [`buildSignatureBytes()`](<(https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts#L24-L26)>) with the four signatures generated by the different owners. +When we call the `executeTransaction()` method, it internally uses the [~`safeTx.encodedSignatures()`~](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/adapters/ethers/contracts/Safe/SafeContractEthers.ts#L159-L171) function, which in turn calls [~`buildSignatureBytes()`~](<(https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts#L24-L26)>) with the four signatures generated by the different owners. Let’s log that. @@ -436,9 +436,9 @@ In the example we just saw, we used the `protocol-kit` utilities to create and c We have already deployed Safe services on the main chains. More information can be found in: -- [Safe Core API documentation](https://docs.safe.global/safe-core-api/supported-networks). -- [Safe Transaction Service](https://docs.safe.global/safe-core-api/service-architecture/safe-transaction-service) -- [Transaction Service Swagger](https://safe-transaction-mainnet.safe.global/) +- [~Safe Core API documentation~](https://docs.safe.global/safe-core-api/supported-networks). +- [~Safe Transaction Service~](https://docs.safe.global/safe-core-api/service-architecture/safe-transaction-service) +- [~Transaction Service Swagger~](https://safe-transaction-mainnet.safe.global/) You can use the API directly to gather signatures. Alternatively, you can use the `api-kit` package, which utilizes the transaction service. With the kit, you can propose transactions and add signatures to existing ones before executing them. @@ -696,7 +696,7 @@ That's it. We simplified the explanations because the concept remains the same. ### Validating a message signature -We can use the `isValidSignature()` method defined in the `CompatibilityFallbackHandler` [contract](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/handler/CompatibilityFallbackHandler.sol#L51-L68) to validate the signature of the previous generated message. +We can use the `isValidSignature()` method defined in the `CompatibilityFallbackHandler` [~contract~](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/handler/CompatibilityFallbackHandler.sol#L51-L68) to validate the signature of the previous generated message. ```typescript import { hashSafeMessage } from '@safe-global/protocol-kit'; @@ -718,9 +718,9 @@ We cannot “execute” a message like transactions so what can do ?. Instead, w Safe supports these two kinds of messages: - **Off-chain**: This is the default method and does not require any on-chain interaction. -- **on-chain** : Messages [stored](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L68-L69) in the Safe contract +- **on-chain** : Messages [~stored~](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L68-L69) in the Safe contract -Safe Accounts support signing of ~[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 all together with off-chain ~[EIP-1271](https://eips.ethereum.org/EIPS/eip-1271)~ validation for signatures. More about this topic [here](https://docs.safe.global/safe-smart-account/signatures/eip-1271). +Safe Accounts support signing of ~[~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 all together with off-chain ~[~EIP-1271~](https://eips.ethereum.org/EIPS/eip-1271)~ validation for signatures. More about this topic [~here~](https://docs.safe.global/safe-smart-account/signatures/eip-1271). ### Off-chain messages (default) From 0432942a8eec33817cc3f184e9790c13fae76a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 22 Jan 2024 14:44:30 +0100 Subject: [PATCH 04/18] Change folder --- .../{sdk-auth-kit => sdk-protocol-kit}/guides/safe-signatures.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pages/{sdk-auth-kit => sdk-protocol-kit}/guides/safe-signatures.md (100%) diff --git a/pages/sdk-auth-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md similarity index 100% rename from pages/sdk-auth-kit/guides/safe-signatures.md rename to pages/sdk-protocol-kit/guides/safe-signatures.md From 8d925b6373b7b611b7199f9c36d1c8c112912465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 22 Jan 2024 14:51:21 +0100 Subject: [PATCH 05/18] Fix some vale issues --- .../guides/safe-signatures.md | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index 06190ad2..b656affd 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -7,11 +7,11 @@ Understanding and generating signatures can be challenging. In the **Safe{Core} Your Safe Account can be configured with various thresholds and different types of owners. An owner can be any Ethereum address, such as: - External Owned Account (EOA) -- Child Signer Safe (A Safe Account that is an owner of another Safe Account) +- Child Signer Safe (A Safe Account that's an owner of another Safe Account) -When the owner is an EOA, we generate a signature that is different from the signature created using a Safe Account. The Safe Account is a Smart Contract Account, so we need to consider this when gathering the signatures, as the Safe Contracts validate them differently. +When the owner is an EOA, we generate a signature that's different from the signature created using a Safe Account. The Safe Account is a Smart Contract Account, so we need to consider this when gathering the signatures, as the Safe Contracts validate them differently. -In this article, we will use the following Safe Account setup as examples. We have five different Ethereum addresses that can act as signers, namely _owner1_ to _owner5_. +In this article, we will use the following Safe Account setup as examples. We've five different Ethereum addresses that can act as signers, namely _owner1_ to _owner5_. - `safeAddress3_4`: 3/4 Safe Account (3 signatures required out of 4 owners) - `owner1` @@ -88,7 +88,7 @@ class EthSafeTransaction implements SafeTransaction { ### Generating transaction signatures -Now that we have the transaction object (`safeTx`), it's time to add the necessary signatures. +Now that we've the transaction object (`safeTx`), it's time to add the necessary signatures. #### Creating an ECDSA signature @@ -181,7 +181,7 @@ signerSafeTx1_1 = await protocolKit.signTransaction( ); ``` -> When signing with a child Safe Account, it is important to specify the parent Safe address. The parent Safe address is used to generate the signature verifier address based on the version of the contracts you are using +> When signing with a child Safe Account, it's important to specify the parent Safe address. The parent Safe address is used to generate the signature verifier address based on the version of the contracts you are using Once signed, we will have a transaction object (`signerSafeTx1_1`) similar to this one: @@ -198,7 +198,7 @@ EthSafeTransaction { } ``` -Inside the signatures map, there is a regular ECDSA signature (`isContractSignature = false`). We can use this signature to generate an EIP-1271 compatible signature. This can be achieved by using the `buildContractSignature()` utility method, which can be found [~here~](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L139-L150). This method takes an array of signatures and output another signature that is ready to be used with Safe contracts. +Inside the signatures map, there is a regular ECDSA signature (`isContractSignature = false`). We can use this signature to generate an EIP-1271 compatible signature. This can be achieved by using the `buildContractSignature()` utility method, which can be found [~here~](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L139-L150). This method takes an array of signatures and output another signature that's ready to be used with Safe contracts. ```typescript const signerSafeSig1_1 = await buildContractSignature( @@ -207,7 +207,7 @@ const signerSafeSig1_1 = await buildContractSignature( ); ``` -The contract signature will look like like this: +The contract signature will look like this: ``` EthSafeSignature { @@ -248,7 +248,7 @@ Let's break down this signature into parts: | Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | The length of the signature. 41 hex is 65 in decimal | | Signature | 65 | 5edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f | Signature bytes that are verified by the signature verifier | -This is what an **EIP-1271** contract signature for Safe contracts looks like. Now that we have explained this lengthy string, let's add the signature to the original transaction. +This is what an **EIP-1271** contract signature for Safe contracts looks like. Now that we've explained this lengthy string, let's add the signature to the original transaction. ```typescript safeTx.addSignature(signerSafeSig1_1); @@ -343,7 +343,7 @@ We're finished. Let's add the signature to the original transaction. safeTx.addSignature(signerSafeSig2_3); ``` -🚀🚀🚀 We are now done!!, we have signed with all the owners. We even have one more owner than the required threshold of 3 💃🏻, but that's okay. Now, let's take a look at the final structure of the `safeTx` object. +🚀🚀🚀 We are now done!!, we've signed with all the owners. We even have one more owner than the required threshold of 3 💃🏻, but that's okay. Now, let's take a look at the final structure of the `safeTx` object. ```json EthSafeTransaction { @@ -434,17 +434,17 @@ At this point, The transaction should be executed on-chain and the process is fi In the example we just saw, we used the `protocol-kit` utilities to create and compose a signature for executing a transaction with Safe. This process can be done entirely on-chain without the need for any external service. However, in a real-world application, you would typically gather signatures from multiple Ethereum addresses, wallets, ... and private keys belonging to different users. To handle this, you can either develop your own system (such as an API or services) to store each signature before sending it to the blockchain (although we don't recommend it 😰), or you can utilize Safe services for this purpose. -We have already deployed Safe services on the main chains. More information can be found in: +We've already deployed Safe services on the main chains. More information can be found in: - [~Safe Core API documentation~](https://docs.safe.global/safe-core-api/supported-networks). - [~Safe Transaction Service~](https://docs.safe.global/safe-core-api/service-architecture/safe-transaction-service) - [~Transaction Service Swagger~](https://safe-transaction-mainnet.safe.global/) -You can use the API directly to gather signatures. Alternatively, you can use the `api-kit` package, which utilizes the transaction service. With the kit, you can propose transactions and add signatures to existing ones before executing them. +You can use the API directly to gather signatures. Alternatively, you can use the `api-kit` package, which uses the Transaction Service. With the kit, you can propose transactions and add signatures to existing ones before executing them. How can we do that? In the previous steps we instantiated the `protocol-kit` and created a transaction using the `createTransaction()` method. In order to start gathering the signatures using our services we need: -- A calculated safe transaction hash. We used this hash as a reference (id) for requesting data in the transaction service +- A calculated safe transaction hash. We used this hash as a reference (id) for requesting data in the Transaction Service - The signature. Can be calculated in the same way as in the previous steps. ```typescript @@ -468,7 +468,7 @@ await apiKit.proposeTransaction({ }); ``` -Now that we have the proposed transaction, we can view it in the Safe{Wallet} UI. However, to execute the transaction, we need the other owners to confirm it by providing their signatures. +Now that we've the proposed transaction, we can view it in the Safe{Wallet} UI. However, to execute the transaction, we need the other owners to confirm it by providing their signatures. ```typescript // Extract the signer address and the signature @@ -496,7 +496,7 @@ await apiKit.confirmTransaction( ); ``` -We have now reached the threshold! We can confirm it by retrieving the transaction and examine the `confirmations` property. +We've now reached the threshold! We can confirm it by retrieving the transaction and examine the `confirmations` property. ```typescript const confirmedTx = await api.getTransaction(txHash); @@ -520,7 +520,7 @@ In this section, we will explain how to generate and sign messages. The concept ### Creating the message object -We already have a `protocolKit` instance right? So let’s use it to create a new message. A message can be a plain string or a valid EIP-712 typed data structure +We already have a `protocolKit` instance right?, let’s use it to create a new message. A message can be a plain string or a valid EIP-712 typed data structure ```json // An example of string message @@ -581,7 +581,7 @@ Let's compose the message. You can use either the string or the typed JSON forma let safeMessage = protocolKit.createMessage(TYPED_MESSAGE); ``` -The `safeMessage` is an object of type `EthSafeMessage` that contains the message data (`data`) and a map of owner-signature pairs (`signatures`). It is similar to the `EthSafeTransaction`. +The `safeMessage` is an object of type `EthSafeMessage` that contains the message data (`data`) and a map of owner-signature pairs (`signatures`). It's similar to the `EthSafeTransaction`. ```typescript class EthSafeMessage implements SafeMessage { @@ -594,7 +594,7 @@ class EthSafeMessage implements SafeMessage { ### Generating the message signatures -Now that we have the transaction object (`safeTx`), it is time to collect signatures. +Now that we've the transaction object (`safeTx`), it's time to collect signatures. #### Creating a ECDSA signature @@ -711,22 +711,20 @@ This would validate the validity of the signatures we create. ### What are the messages about ? -We cannot execute a message like transactions. Instead, we can store messages in the Safe contract and later check for their existence. This is useful, for example, for flows from third-party apps. It declares ownership of a Safe address and authorizes the Safe contract and the dApp to collaborate. - -We cannot “execute” a message like transactions so what can do ?. Instead, we can store messages in the Safe contract and later check for their existence. This is useful for example for flows from third-party apps where the app ask the Safe owner to declare ownership and authorize the Safe Account and dApp to work together. +We can't execute a message like transactions. Instead, we can store messages in the Safe contract and later check for their existence. This is useful, for example, for flows from third-party apps. It declares ownership of a Safe address and authorizes the Safe contract and the dApp to collaborate. Safe supports these two kinds of messages: -- **Off-chain**: This is the default method and does not require any on-chain interaction. +- **Off-chain**: This is the default method and doesn't require any on-chain interaction. - **on-chain** : Messages [~stored~](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L68-L69) in the Safe contract Safe Accounts support signing of ~[~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 all together with off-chain ~[~EIP-1271~](https://eips.ethereum.org/EIPS/eip-1271)~ validation for signatures. More about this topic [~here~](https://docs.safe.global/safe-smart-account/signatures/eip-1271). ### Off-chain messages (default) -By default, Safe supports off-chain messages. To use off-chain messages, you need to use the utilities described in this document and the transaction service API. Off-chain messages do not require any interaction with the blockchain. +By default, Safe supports off-chain messages. To use off-chain messages, you need to use the utilities described in this document and the Transaction Service API. Off-chain messages do not require any interaction with the blockchain. -We mentioned the utility of storing messages in the contract. Off-chain messages have the same purpose, but they are stored in the transaction service. The transaction service stores the messages and signatures in a database. It is a centralized service, but it is open-source and can be deployed by anyone. The transaction service is used by the Safe{Wallet} UI to store messages and signatures by default. +We mentioned the utility of storing messages in the contract. Off-chain messages have the same purpose, but they're stored in the Transaction Service. It stores the messages and signatures in a database. It's a centralized service, but it's open-source and can be deployed by anyone. The Transaction Service is used by the Safe{Wallet} UI to store messages and signatures by default. We will perform a task similar to the `proposeTransaction()` and `confirmTransaction()` methods. @@ -744,11 +742,11 @@ apiKit.addMessage(safeAddress3_4, { }); ``` -We have added the message! +We've added the message! #### Confirm messages by adding additional signatures -We should confirm the message with other owners, just like we did with transactions. The `id` of the stored message is the `safeMessageHash`, and we have utilities available to calculate it and add more signatures. +We should confirm the message with other owners, just like we did with transactions. The `id` of the stored message is the `safeMessageHash`, and we've utilities available to calculate it and add more signatures. ```typescript // Get the safeMessageHash @@ -771,14 +769,14 @@ await apiKit.addMessageSignature( buildSignatureBytes([signerSafeMessageSig1_1]) ); -// The threshold has already been matched, so it is not necessary. +// The threshold has already been matched, so it's not necessary. await apiKit.addMessageSignature( safeMessageHash, buildSignatureBytes([signerSafeMessageSig2_3]) ); ``` -Now we have the message signed by all Safe owners. We have even "crossed the threshold" 🥳 +Now we've the message signed by all Safe owners. We've even "crossed the threshold" 🥳 We can check the status of the message by using the `getMessage()` function. @@ -822,7 +820,7 @@ const signMessageTx = await protocolKit.createTransaction({ }); ``` -Now that we have a transaction, we need its signatures. If you've read this far, you probably know how to gather them 😉. If you started from this section, please go to the beginning and learn how to generate all the necessary signatures. Remember, you can gather them on your own or use the transaction service. +Now that we've a transaction, we need its signatures. If you've read this far, you probably know how to gather them 😉. If you started from this section, please go to the beginning and learn how to generate all the necessary signatures. Remember, you can gather them on your own or use the Transaction Service. After that, execute the transaction and you will have the message stored in the contract. From 75da9b7c60eab9d2ed791bb394b591e8044c1207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 22 Jan 2024 14:53:37 +0100 Subject: [PATCH 06/18] Fix some vale issues --- pages/sdk-protocol-kit/guides/safe-signatures.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index b656affd..209471bf 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -343,7 +343,7 @@ We're finished. Let's add the signature to the original transaction. safeTx.addSignature(signerSafeSig2_3); ``` -🚀🚀🚀 We are now done!!, we've signed with all the owners. We even have one more owner than the required threshold of 3 💃🏻, but that's okay. Now, let's take a look at the final structure of the `safeTx` object. +🚀🚀🚀 We're now done!!, we've signed with all the owners. We even have one more owner than the required threshold of 3 💃🏻, but that's okay. Now, let's take a look at the final structure of the `safeTx` object. ```json EthSafeTransaction { @@ -600,7 +600,7 @@ Now that we've the transaction object (`safeTx`), it's time to collect signature --- -We are going to sign with `owner1` and `owner2`. For that we use the `signMessage()` method that takes the tx `data` and add a new signature to the `signatures` map. +We're going to sign with `owner1` and `owner2`. For that we use the `signMessage()` method that takes the transaction `data` and add a new signature to the `signatures` map. ```typescript safeMessage = await protocolKit.signMessage( @@ -722,7 +722,7 @@ Safe Accounts support signing of ~[~EIP-191~](https://eips.ethereum.org/EIPS/eip ### Off-chain messages (default) -By default, Safe supports off-chain messages. To use off-chain messages, you need to use the utilities described in this document and the Transaction Service API. Off-chain messages do not require any interaction with the blockchain. +By default, Safe supports off-chain messages. To use off-chain messages, you need to use the utilities described in this document and the Transaction Service API. Off-chain messages don't require any interaction with the blockchain. We mentioned the utility of storing messages in the contract. Off-chain messages have the same purpose, but they're stored in the Transaction Service. It stores the messages and signatures in a database. It's a centralized service, but it's open-source and can be deployed by anyone. The Transaction Service is used by the Safe{Wallet} UI to store messages and signatures by default. From 43dac1651d9ada0c470c26b0ac35c40cda369b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 22 Jan 2024 15:09:33 +0100 Subject: [PATCH 07/18] Some improvements --- .../guides/safe-signatures.md | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index 209471bf..b4bd0951 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -1,17 +1,17 @@ # Safe Signatures -Understanding and generating signatures can be challenging. In the **Safe{Core} SDK**, we provide a set of utilities that can assist in using signatures with Safe. In this article, we will explain how signatures work and how to generate them using the `protocol-kit` utilities. +Understanding and generating signatures can be challenging. In the **Safe{Core} SDK**, we provide a set of utilities that can assist in using signatures with Safe. In this article, we will explain how signatures work and how to generate them using the `@safe-global/protocol-kit` package. -## Setting up the Safe Accounts threshold +## Setting up the example Safe Account -Your Safe Account can be configured with various thresholds and different types of owners. An owner can be any Ethereum address, such as: +Your Safe Account can be configured with various thresholds and different owner types. An owner can be any Ethereum address, such as: - External Owned Account (EOA) - Child Signer Safe (A Safe Account that's an owner of another Safe Account) -When the owner is an EOA, we generate a signature that's different from the signature created using a Safe Account. The Safe Account is a Smart Contract Account, so we need to consider this when gathering the signatures, as the Safe Contracts validate them differently. +When the owner is an EOA, we generate a signature that's different from the signature created using a Safe Account. The Safe Account is a Smart Contract Account, so we need to consider this when collecting the signatures, as the Safe Accounts validate them differently. -In this article, we will use the following Safe Account setup as examples. We've five different Ethereum addresses that can act as signers, namely _owner1_ to _owner5_. +In this article, we will use the following Safe Account setup. We've five different Ethereum addresses that can act as signers, namely _owner1_ to _owner5_. - `safeAddress3_4`: 3/4 Safe Account (3 signatures required out of 4 owners) - `owner1` @@ -39,18 +39,19 @@ All the owners are Ethereum addresses. Here are the addresses for this example: ### Creating the transaction object -We can sign transactions using the `protocol-kit` by creating a `protocol-kit` instance. Here's how we can do it: +We can sign transactions using the `protocol-kit` by creating an instance of the `Safe` class. Here's how we can do it: ```typescript import Safe from '@safe-global/protocol-kit'; +// You can use any compatible adapter, such as Web3Adapter or EthersAdapter const protocolKit = await Safe.create({ - ethAdapter: ethAdapter1, // You can use any compatible adapter, such as Web3Adapter or EthersAdapter + ethAdapter: ethAdapter1, safeAddress: safeAddress3_4, }); ``` -The `ethAdapter1` is bound to `owner1`. In the examples provided in this article, we will have 5 ethAdapters, each one bound to an owner. +The `ethAdapter1` is bound to `owner1`. In the examples provided in this article, we will have 5 adapters, each one bound to an owner. | Adapter | Bound to | | ----------- | -------- | @@ -60,7 +61,7 @@ The `ethAdapter1` is bound to `owner1`. In the examples provided in this article | ethAdapter4 | owner4 | | ethAdapter5 | owner5 | -After obtaining the `protocolKit` instance, we can use `createTransaction()` to generate a transaction. +After obtaining the `protocolKit` instance, we can use `createTransaction()` to generate a transaction object. ```typescript // Create the transaction. Send 0.01 ETH @@ -94,17 +95,19 @@ Now that we've the transaction object (`safeTx`), it's time to add the necessary --- -We will sign with `owner1` and `owner2` using the `signTransaction()` method. This method adds 2 new signatures to the `signatures` map by using the transaction `data` as input. +We will sign with `owner1` and `owner2` using the `signTransaction()` method. This method add new signatures to the `signatures` map by using the transaction `data` as input. + +It's possible to use several signing methods, such as `ETH_SIGN` (eth_sign), `ETH_SIGN_TYPED_DATA_V4` (eth_signTypedData_v4), ...etc. The default signing method is `ETH_SIGN_TYPED_DATA_V4`. -It's possible to use several signing methods, such as `ETH_SIGN`, `ETH_SIGN_TYPED_DATA_V4`, ...etc. The default signing method is `ETH_SIGN_TYPED_DATA_V4`. +`ETH_SIGN` produces a regular [EIP-191](https://eips.ethereum.org/EIPS/eip-191) signature. `ETH_SIGN_TYPED_DATA_V4` produces a signature that's compatible with the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) standard. The EIP-712 standard is used by Safe contracts to verify the signatures. ```typescript -safeTx = await protocolKit.signTransaction(safeTx, SigningMethod.ETH_SIGN); // owner1 EIP-191 signature +safeTx = await protocolKit.signTransaction(safeTx, SigningMethod.ETH_SIGN); // EIP-191 signature from owner1 protocolKit = await protocolKit.connect({ ethAdapter: ethAdapter2 }); // Connect another owner safeTx = await protocolKit.signTransaction( safeTx, SigningMethod.ETH_SIGN_TYPED_DATA_V4 -); // owner2 EIP-712 typed data signature (default) +); // owner2 EIP-712 typed data signature ``` In this snippet, we add the signature for `owner1`. Then, we use the `connect()` method to connect the `owner2` adapter and sign again to add the second signature. From eb4b0b00d3a183decc5168c5fa5186659afd48c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 22 Jan 2024 15:18:19 +0100 Subject: [PATCH 08/18] Improve grammar --- .../guides/safe-signatures.md | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index b4bd0951..bf91b3c9 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -112,9 +112,9 @@ safeTx = await protocolKit.signTransaction( In this snippet, we add the signature for `owner1`. Then, we use the `connect()` method to connect the `owner2` adapter and sign again to add the second signature. -If we examine the `safeTx` object at this point, we should observe something similar to the following: +If we examine the `safeTx` object at this point, we should see something similar to the following: -```json +```javascript EthSafeTransaction { signatures: Map(2) { '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1' => EthSafeSignature { @@ -132,19 +132,17 @@ EthSafeTransaction { } ``` -The `data` prop in the `signatures` map represents the concrete signature. The `isContractSignature` flag (false) indicates if the signature is an Ethereum signature or a Contract signature (A signer Safe Account). +The `data` prop in the `signatures` map represents the concrete signature. The `isContractSignature` flag (_false_) indicates if the signature is an Ethereum signature or a Contract signature (A signer Safe Account). An Ethereum signature is composed of two 32-byte integers (r, s) and an extra byte for recovery id (v), making a total of 65 bytes. In hexadecimal string format, each byte is represented by 2 characters. Hence, a 65-byte Ethereum signature will be 130 characters long. Including the '0x' prefix commonly used with signatures, the total character count for such a signature would be 132. For a more detailed explanation, you can refer to [~this link~](https://docs.safe.global/safe-smart-account/signatures) for more information. > To represent a byte (8 bits) in hexadecimal, you need 2 characters. Each hexadecimal character represents 4 bits. Therefore, 2 hexadecimal characters (2 x 4 bits) are able to represent a byte (8 bits). -The final part of the signature, either `1f` or `1b`, indicates the signature type. +The final part of the signature, either `1f` or `1c`, indicates the signature type. The hexadecimal number `1f` converts to the decimal number 31. It indicates that the signature is an `eth_sign` because the number is greater than 30. This adjustment is made for the `eth_sign` flow in the contracts. You can find the relevant code [~here~](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347). -The hexadecimal number `1c` converts to the decimal number 28, indicating that the signature is a data signature of types. - -For instance, in the case of the initial signature: +The hexadecimal number `1c` converts to the decimal number 28, indicating that the signature is a typed data signature. For instance, in the case of the initial signature: `0x969308e2abeda61a0c9c41b3c615012f50dd7456ca76ea39a18e3b975abeb67f275b07810dd59fc928f3f9103e52557c1578c7c5c171ffc983afa5306466b1261f`: @@ -154,13 +152,13 @@ For instance, in the case of the initial signature: | Signature | 64 | 969308e2abeda61a0c9c41b3c615012f50dd7456ca76ea39a18e3b975abeb67f275b07810dd59fc928f3f9103e52557c1578c7c5c171ffc983afa5306466b126 | Signature bytes | | Signature Type | 1 | 1f | 1f hex is 31 in decimal | -#### Creating Smart contract signatures (EIP-1271) +#### Creating Smart contract signatures --- **1/1 Safe Account** -The smart contract signatures supported by Safe differ from regular ECDSA signatures. We will use the special method `SigningMethod.ETH_SIGN_TYPED_DATA_V4` to generate these signatures. +The smart contract signatures supported by Safe differ from regular ECDSA signatures. We will use the special method `SigningMethod.SAFE_SIGNATURE` to generate these kind of signatures. To start signing with the 1/1 Safe, we need the adapter for `owner3` and the new `safeAddress` for the signature. The new `safeAddress` is associated with the child Safe Account. Let's connect the adapter and safe, and continue with the signing process: @@ -184,11 +182,11 @@ signerSafeTx1_1 = await protocolKit.signTransaction( ); ``` -> When signing with a child Safe Account, it's important to specify the parent Safe address. The parent Safe address is used to generate the signature verifier address based on the version of the contracts you are using +> When signing with a child Safe Account, it's important to specify the parent Safe address. The parent Safe address is used internally to generate the signature based on the version of the contracts you are using Once signed, we will have a transaction object (`signerSafeTx1_1`) similar to this one: -```json +```javascript EthSafeTransaction { signatures: Map(1) { '0x22d491bde2303f2f43325b2108d26f1eaba1e32b' => EthSafeSignature { @@ -201,7 +199,7 @@ EthSafeTransaction { } ``` -Inside the signatures map, there is a regular ECDSA signature (`isContractSignature = false`). We can use this signature to generate an EIP-1271 compatible signature. This can be achieved by using the `buildContractSignature()` utility method, which can be found [~here~](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L139-L150). This method takes an array of signatures and output another signature that's ready to be used with Safe contracts. +Inside the signatures map, there is a regular ECDSA signature (`isContractSignature=false`). We can use this signature to generate an Safe Account smart contract compatible signature. This can be achieved by using the `buildContractSignature()` utility method, which can be found [~here~](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L139-L150). This method takes an array of signatures and output another signature that's ready to be used with Safe Accounts. ```typescript const signerSafeSig1_1 = await buildContractSignature( @@ -220,7 +218,7 @@ EthSafeSignature { } ``` -The main changes are as follows: +The main changes from the one in the `EthSafeTransaction` object are the following ones: - Specify the correct `signer` (the Safe owner Account) - Set `isContractSignature` to `true` @@ -228,7 +226,7 @@ The main changes are as follows: After signing the contract, we can generate the final contract compatible with Safe Accounts using the `buildSignatureBytes()` method. This method is also used internally in the `buildContractSignature()` method. You can find the implementation of the `buildSignatureBytes()` method [~here~](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L152-L189). -Let’s examine the output of the do `buildSignatureBytes()` method. We can log the output of the method by doing +Let’s examine the output of the do `buildSignatureBytes()` method. We can log it: ```typescript console.log(buildSignatureBytes([signerSafeSig1_1]) @@ -348,7 +346,7 @@ safeTx.addSignature(signerSafeSig2_3); 🚀🚀🚀 We're now done!!, we've signed with all the owners. We even have one more owner than the required threshold of 3 💃🏻, but that's okay. Now, let's take a look at the final structure of the `safeTx` object. -```json +```javascript EthSafeTransaction { signatures: Map(4) { '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1' => EthSafeSignature { From b1074eafcd2abaf6378c7306f6c62407b26e6e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 22 Jan 2024 15:25:43 +0100 Subject: [PATCH 09/18] Remove ~ --- .../guides/safe-signatures.md | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index bf91b3c9..e8e8ddd4 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -134,13 +134,13 @@ EthSafeTransaction { The `data` prop in the `signatures` map represents the concrete signature. The `isContractSignature` flag (_false_) indicates if the signature is an Ethereum signature or a Contract signature (A signer Safe Account). -An Ethereum signature is composed of two 32-byte integers (r, s) and an extra byte for recovery id (v), making a total of 65 bytes. In hexadecimal string format, each byte is represented by 2 characters. Hence, a 65-byte Ethereum signature will be 130 characters long. Including the '0x' prefix commonly used with signatures, the total character count for such a signature would be 132. For a more detailed explanation, you can refer to [~this link~](https://docs.safe.global/safe-smart-account/signatures) for more information. +An Ethereum signature is composed of two 32-byte integers (r, s) and an extra byte for recovery id (v), making a total of 65 bytes. In hexadecimal string format, each byte is represented by 2 characters. Hence, a 65-byte Ethereum signature will be 130 characters long. Including the '0x' prefix commonly used with signatures, the total character count for such a signature would be 132. For a more detailed explanation, you can refer to [this link](https://docs.safe.global/safe-smart-account/signatures) for more information. > To represent a byte (8 bits) in hexadecimal, you need 2 characters. Each hexadecimal character represents 4 bits. Therefore, 2 hexadecimal characters (2 x 4 bits) are able to represent a byte (8 bits). The final part of the signature, either `1f` or `1c`, indicates the signature type. -The hexadecimal number `1f` converts to the decimal number 31. It indicates that the signature is an `eth_sign` because the number is greater than 30. This adjustment is made for the `eth_sign` flow in the contracts. You can find the relevant code [~here~](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347). +The hexadecimal number `1f` converts to the decimal number 31. It indicates that the signature is an `eth_sign` because the number is greater than 30. This adjustment is made for the `eth_sign` flow in the contracts. You can find the relevant code [here](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347). The hexadecimal number `1c` converts to the decimal number 28, indicating that the signature is a typed data signature. For instance, in the case of the initial signature: @@ -199,7 +199,7 @@ EthSafeTransaction { } ``` -Inside the signatures map, there is a regular ECDSA signature (`isContractSignature=false`). We can use this signature to generate an Safe Account smart contract compatible signature. This can be achieved by using the `buildContractSignature()` utility method, which can be found [~here~](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L139-L150). This method takes an array of signatures and output another signature that's ready to be used with Safe Accounts. +Inside the signatures map, there is a regular ECDSA signature (`isContractSignature=false`). We can use this signature to generate an Safe Account smart contract compatible signature. This can be achieved by using the `buildContractSignature()` utility method, which can be found [here](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L139-L150). This method takes an array of signatures and output another signature that's ready to be used with Safe Accounts. ```typescript const signerSafeSig1_1 = await buildContractSignature( @@ -224,7 +224,7 @@ The main changes from the one in the `EthSafeTransaction` object are the followi - Set `isContractSignature` to `true` - Build the `data` by considering all the individual signatures of the child Safe -After signing the contract, we can generate the final contract compatible with Safe Accounts using the `buildSignatureBytes()` method. This method is also used internally in the `buildContractSignature()` method. You can find the implementation of the `buildSignatureBytes()` method [~here~](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L152-L189). +After signing the contract, we can generate the final contract compatible with Safe Accounts using the `buildSignatureBytes()` method. This method is also used internally in the `buildContractSignature()` method. You can find the implementation of the `buildSignatureBytes()` method [here](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/signatures/utils.ts#L152-L189). Let’s examine the output of the do `buildSignatureBytes()` method. We can log it: @@ -240,14 +240,14 @@ The output will be Let's break down this signature into parts: -| Type | Bytes | Value | Description | -| ---------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| Hex | 1 | 0x | Hex string characters | -| Verifier | 32 | 000000000000000000000000215033cdE0619D60B7352348F4598316Cc39bC6E | Padded address of the contract that implements the EIP-1271 interface to verify the signature. The Safe signer address | -| Data position | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | Start position of the signature data (offset relative to the beginning of the signature data). 41 hex is 65 in decimal | -| Signature Type | 1 | 00 | 00 for [~Safe Accounts~](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L322-L336) | -| Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | The length of the signature. 41 hex is 65 in decimal | -| Signature | 65 | 5edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f | Signature bytes that are verified by the signature verifier | +| Type | Bytes | Value | Description | +| ---------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| Hex | 1 | 0x | Hex string characters | +| Verifier | 32 | 000000000000000000000000215033cdE0619D60B7352348F4598316Cc39bC6E | Padded address of the contract that implements the EIP-1271 interface to verify the signature. The Safe signer address | +| Data position | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | Start position of the signature data (offset relative to the beginning of the signature data). 41 hex is 65 in decimal | +| Signature Type | 1 | 00 | 00 for [Safe Accounts](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L322-L336) | +| Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | The length of the signature. 41 hex is 65 in decimal | +| Signature | 65 | 5edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f | Signature bytes that are verified by the signature verifier | This is what an **EIP-1271** contract signature for Safe contracts looks like. Now that we've explained this lengthy string, let's add the signature to the original transaction. @@ -327,14 +327,14 @@ The output will be: Let's break down this signature into parts again: -| Type | Bytes | Value | Description | -| ---------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| Hex | 1 | 0x | Hex string characters | -| Verifier | 32 | 000000000000000000000000f75D61D6C27a7CC5788E633c1FC130f0F4a62D33 | Padded address of the contract that implements the EIP-1271 interface to verify the signature. The Safe signer address | -| Data position | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | Start position of the signature data (offset relative to the beginning of the signature data). 41 hex is 65 in decimal | -| Signature Type | 1 | 00 | 00 for [~Safe Accounts~](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L322-L336) | -| Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000082 | The length of the signature. 82 hex is 130 in decimal | -| Signature | 130 | 023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f | Signature bytes that are verified by the signature verifier (130 bytes are represented by 260 characters in an hex string) | +| Type | Bytes | Value | Description | +| ---------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| Hex | 1 | 0x | Hex string characters | +| Verifier | 32 | 000000000000000000000000f75D61D6C27a7CC5788E633c1FC130f0F4a62D33 | Padded address of the contract that implements the EIP-1271 interface to verify the signature. The Safe signer address | +| Data position | 32 | 0000000000000000000000000000000000000000000000000000000000000041 | Start position of the signature data (offset relative to the beginning of the signature data). 41 hex is 65 in decimal | +| Signature Type | 1 | 00 | 00 for [Safe Accounts](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L322-L336) | +| Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000082 | The length of the signature. 82 hex is 130 in decimal | +| Signature | 130 | 023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f | Signature bytes that are verified by the signature verifier (130 bytes are represented by 260 characters in an hex string) | The decomposition appears unchanged, but there are two changes: the signature length has doubled because there are now two signatures, and the signature itself is a concatenation of the two regular signatures. @@ -399,7 +399,7 @@ protocolKit = await protocolKit.connect({ const transactionResponse = await protocolKit.executeTransaction(safeTx); ``` -When we call the `executeTransaction()` method, it internally uses the [~`safeTx.encodedSignatures()`~](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/adapters/ethers/contracts/Safe/SafeContractEthers.ts#L159-L171) function, which in turn calls [~`buildSignatureBytes()`~](<(https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts#L24-L26)>) with the four signatures generated by the different owners. +When we call the `executeTransaction()` method, it internally uses the [`safeTx.encodedSignatures()`](https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/adapters/ethers/contracts/Safe/SafeContractEthers.ts#L159-L171) function, which in turn calls [`buildSignatureBytes()`](<(https://github.com/safe-global/safe-core-sdk/blob/cce519b4204b2c54ae0c3d2295ab6031332c0fe7/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts#L24-L26)>) with the four signatures generated by the different owners. Let’s log that. @@ -437,9 +437,9 @@ In the example we just saw, we used the `protocol-kit` utilities to create and c We've already deployed Safe services on the main chains. More information can be found in: -- [~Safe Core API documentation~](https://docs.safe.global/safe-core-api/supported-networks). -- [~Safe Transaction Service~](https://docs.safe.global/safe-core-api/service-architecture/safe-transaction-service) -- [~Transaction Service Swagger~](https://safe-transaction-mainnet.safe.global/) +- [Safe Core API documentation](https://docs.safe.global/safe-core-api/supported-networks). +- [Safe Transaction Service](https://docs.safe.global/safe-core-api/service-architecture/safe-transaction-service) +- [Transaction Service Swagger](https://safe-transaction-mainnet.safe.global/) You can use the API directly to gather signatures. Alternatively, you can use the `api-kit` package, which uses the Transaction Service. With the kit, you can propose transactions and add signatures to existing ones before executing them. @@ -697,7 +697,7 @@ That's it. We simplified the explanations because the concept remains the same. ### Validating a message signature -We can use the `isValidSignature()` method defined in the `CompatibilityFallbackHandler` [~contract~](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/handler/CompatibilityFallbackHandler.sol#L51-L68) to validate the signature of the previous generated message. +We can use the `isValidSignature()` method defined in the `CompatibilityFallbackHandler` [contract](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/handler/CompatibilityFallbackHandler.sol#L51-L68) to validate the signature of the previous generated message. ```typescript import { hashSafeMessage } from '@safe-global/protocol-kit'; @@ -717,9 +717,9 @@ We can't execute a message like transactions. Instead, we can store messages in Safe supports these two kinds of messages: - **Off-chain**: This is the default method and doesn't require any on-chain interaction. -- **on-chain** : Messages [~stored~](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L68-L69) in the Safe contract +- **on-chain** : Messages [stored](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L68-L69) in the Safe contract -Safe Accounts support signing of ~[~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 all together with off-chain ~[~EIP-1271~](https://eips.ethereum.org/EIPS/eip-1271)~ validation for signatures. More about this topic [~here~](https://docs.safe.global/safe-smart-account/signatures/eip-1271). +Safe Accounts support signing of [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 all together with off-chain [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) validation for signatures. More about this topic [here](https://docs.safe.global/safe-smart-account/signatures/eip-1271). ### Off-chain messages (default) From 932b693147ccecccc315d6437a7f188948b7e928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 22 Jan 2024 15:29:38 +0100 Subject: [PATCH 10/18] improve grammar --- pages/sdk-protocol-kit/guides/safe-signatures.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index e8e8ddd4..f22e0fe2 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -210,7 +210,7 @@ const signerSafeSig1_1 = await buildContractSignature( The contract signature will look like this: -``` +```javascript EthSafeSignature { signer: '0x215033cdE0619D60B7352348F4598316Cc39bC6E', data: '0x5edb6ffe67dd935d93d07c634970944ba0b096f767b92018ad635e8b28effeea5a1e512f1ad6f886690e0e30a3fae2c8c61d3f83d24d43276acdb3254b92ea5b1f', @@ -433,7 +433,7 @@ At this point, The transaction should be executed on-chain and the process is fi ### Proposing transactions using Safe Services -In the example we just saw, we used the `protocol-kit` utilities to create and compose a signature for executing a transaction with Safe. This process can be done entirely on-chain without the need for any external service. However, in a real-world application, you would typically gather signatures from multiple Ethereum addresses, wallets, ... and private keys belonging to different users. To handle this, you can either develop your own system (such as an API or services) to store each signature before sending it to the blockchain (although we don't recommend it 😰), or you can utilize Safe services for this purpose. +Until now, in the previous examples, we used the `protocol-kit` utilities to create and compose a signature for executing a transaction with Safe. This process can be done entirely on-chain without the need for any external service. However, in a real-world application, you would typically gather signatures from multiple Ethereum addresses, wallets, ... and private keys belonging to different users. To handle this, you can either develop your own system (such as an API or services) to store each signature before sending it to the blockchain (although we don't recommend it 😰), or you can utilize Safe services for this purpose. We've already deployed Safe services on the main chains. More information can be found in: From 08b504b80ae15d2b8cbeec84695f0ddbaec04169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 22 Jan 2024 15:34:47 +0100 Subject: [PATCH 11/18] Improve grammar --- pages/sdk-protocol-kit/guides/safe-signatures.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index f22e0fe2..33964f45 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -433,7 +433,7 @@ At this point, The transaction should be executed on-chain and the process is fi ### Proposing transactions using Safe Services -Until now, in the previous examples, we used the `protocol-kit` utilities to create and compose a signature for executing a transaction with Safe. This process can be done entirely on-chain without the need for any external service. However, in a real-world application, you would typically gather signatures from multiple Ethereum addresses, wallets, ... and private keys belonging to different users. To handle this, you can either develop your own system (such as an API or services) to store each signature before sending it to the blockchain (although we don't recommend it 😰), or you can utilize Safe services for this purpose. +Until now, in the previous examples, we used the `protocol-kit` utilities to create and compose a signature for executing a transaction with Safe. This process can be done entirely on-chain without the need for any external service. However, in a real-world application, you would typically collect signatures from multiple Ethereum addresses (wallets), and there will be private keys belonging to different users. To handle this situation, you can either develop your own system (such as an API or services) to store each signature before sending it along the transaction to the blockchain (although we don't recommend it 😰), or you can utilize Safe services for this purpose 🚀. We've already deployed Safe services on the main chains. More information can be found in: @@ -441,9 +441,9 @@ We've already deployed Safe services on the main chains. More information can be - [Safe Transaction Service](https://docs.safe.global/safe-core-api/service-architecture/safe-transaction-service) - [Transaction Service Swagger](https://safe-transaction-mainnet.safe.global/) -You can use the API directly to gather signatures. Alternatively, you can use the `api-kit` package, which uses the Transaction Service. With the kit, you can propose transactions and add signatures to existing ones before executing them. +You can use our APIs directly to collect signatures. Alternatively, you can use the `api-kit` package, which uses the Transaction Service under the hood. With the `api-kit`, you can propose transactions and add signatures to existing ones before execution. -How can we do that? In the previous steps we instantiated the `protocol-kit` and created a transaction using the `createTransaction()` method. In order to start gathering the signatures using our services we need: +How can we do that? In the previous steps we instantiated the `protocol-kit` and created a transaction using the `createTransaction()` method. In order to start collecting signatures using our services we'll need: - A calculated safe transaction hash. We used this hash as a reference (id) for requesting data in the Transaction Service - The signature. Can be calculated in the same way as in the previous steps. @@ -480,7 +480,7 @@ const ethSig2 = safeTx.getSignature(signerAddress) as EthSafeSignature; await apiKit.confirmTransaction(txHash, buildSignatureBytes([ethSig2])); ``` -`owner2` has confirmed that the transaction is valid at this point! We only need one more confirmation to meet the threshold. +`owner2` has confirmed that the transaction is valid at this point! We only need one more confirmation to meet the configured threshold. ```typescript // Confirm the transaction with the 1/1 signer Safe From 50981dd47da9fe212c5da7b375ec8421bd9405cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 22 Jan 2024 15:37:07 +0100 Subject: [PATCH 12/18] Improve syntax --- pages/sdk-protocol-kit/guides/safe-signatures.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index 33964f45..2960c1b9 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -490,18 +490,18 @@ await apiKit.confirmTransaction( ); // Confirm the transaction with the 2/3 signer Safe -// Not really necessary as the threshold is matched at this point +// It's not really necessary as the threshold is matched at this point await apiKit.confirmTransaction( txHash, buildSignatureBytes([signerSafeSig2_3]) ); ``` -We've now reached the threshold! We can confirm it by retrieving the transaction and examine the `confirmations` property. +We've now reached the threshold! We can check it by retrieving the transaction and examine the `confirmations` property. ```typescript const confirmedTx = await api.getTransaction(txHash); -// Examine the confirmedTx.confirmations property +// Examine the `confirmedTx.confirmations` property ``` Now, we can execute the transaction just as we did without using the services. @@ -511,7 +511,7 @@ Now, we can execute the transaction just as we did without using the services. const executedTxResponse = await safeSdk.executeTransaction(confirmedTx); ``` -The transaction should be executed now. +The transaction should be executed now and after the Transaction Service indexes it we should be able to see it using the Safe{Wallet} UI. ## Messages From 7ffa5b0f4f2064b1f66479628a4ca0e7081807bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Tue, 23 Jan 2024 08:55:50 +0100 Subject: [PATCH 13/18] Apply suggestions from code review Co-authored-by: Daniel <25051234+dasanra@users.noreply.github.com> --- pages/sdk-protocol-kit/guides/safe-signatures.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index 2960c1b9..2cef94b4 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -140,9 +140,9 @@ An Ethereum signature is composed of two 32-byte integers (r, s) and an extra by The final part of the signature, either `1f` or `1c`, indicates the signature type. -The hexadecimal number `1f` converts to the decimal number 31. It indicates that the signature is an `eth_sign` because the number is greater than 30. This adjustment is made for the `eth_sign` flow in the contracts. You can find the relevant code [here](https://github.com/safe-global/safe-contracts/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347). +The hexadecimal value `1f` equals to the decimal number 31. Because the decimal value is greater than 30, it [indicates to the Safe smart contract]()(https://github.com/safe-global/safe-smart-account/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347) that the signature is an `eth_sign`. -The hexadecimal number `1c` converts to the decimal number 28, indicating that the signature is a typed data signature. For instance, in the case of the initial signature: +The hexadecimal value `1c` equals to the decimal number 28, indicating that the signature is a typed data signature. For instance, in the case of the initial signature: `0x969308e2abeda61a0c9c41b3c615012f50dd7456ca76ea39a18e3b975abeb67f275b07810dd59fc928f3f9103e52557c1578c7c5c171ffc983afa5306466b1261f`: @@ -336,7 +336,11 @@ Let's break down this signature into parts again: | Signature Length | 32 | 0000000000000000000000000000000000000000000000000000000000000082 | The length of the signature. 82 hex is 130 in decimal | | Signature | 130 | 023d1746ed548e90f387a6b8ddba26e6b80a78d5bfbc36e5bfcbfd63e136f8071db6e91c037fa36bde72159138bbb74fc359b35eb515e276a7c0547d5eaa042520d3e6565e5590641db447277243cf24711dce533cfcaaf3a64415dcb9fa309fbf2de1ae4709c6450752acc0d45e01b67b55379bdf4e3dc32b2d89ad0a60c231d61f | Signature bytes that are verified by the signature verifier (130 bytes are represented by 260 characters in an hex string) | -The decomposition appears unchanged, but there are two changes: the signature length has doubled because there are now two signatures, and the signature itself is a concatenation of the two regular signatures. +The table looks very similar to the previous one, but there are two changes: + +- The _signature length_ has doubled because this Safe needs two signatures. +- The _signature_ value is a concatenation of the two regular signatures. + We're finished. Let's add the signature to the original transaction. From 4ab75c95b5b20f181e8f8d627a1997150541eeb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Tue, 23 Jan 2024 11:04:53 +0100 Subject: [PATCH 14/18] Apply suggestions from code review Co-authored-by: Mikhail <16622558+mmv08@users.noreply.github.com> --- pages/sdk-protocol-kit/guides/safe-signatures.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index 2cef94b4..20e838a1 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -9,7 +9,7 @@ Your Safe Account can be configured with various thresholds and different owner - External Owned Account (EOA) - Child Signer Safe (A Safe Account that's an owner of another Safe Account) -When the owner is an EOA, we generate a signature that's different from the signature created using a Safe Account. The Safe Account is a Smart Contract Account, so we need to consider this when collecting the signatures, as the Safe Accounts validate them differently. +When the owner is an EOA, an ECDSA signature is generated. When the signer is a smart account, the exact signature algorithm depends on the signer itself. In this article, we will use the following Safe Account setup. We've five different Ethereum addresses that can act as signers, namely _owner1_ to _owner5_. @@ -39,7 +39,7 @@ All the owners are Ethereum addresses. Here are the addresses for this example: ### Creating the transaction object -We can sign transactions using the `protocol-kit` by creating an instance of the `Safe` class. Here's how we can do it: +We can sign transactions using the `protocol-kit` by creating an instance of the `Safe` class: ```typescript import Safe from '@safe-global/protocol-kit'; @@ -134,7 +134,7 @@ EthSafeTransaction { The `data` prop in the `signatures` map represents the concrete signature. The `isContractSignature` flag (_false_) indicates if the signature is an Ethereum signature or a Contract signature (A signer Safe Account). -An Ethereum signature is composed of two 32-byte integers (r, s) and an extra byte for recovery id (v), making a total of 65 bytes. In hexadecimal string format, each byte is represented by 2 characters. Hence, a 65-byte Ethereum signature will be 130 characters long. Including the '0x' prefix commonly used with signatures, the total character count for such a signature would be 132. For a more detailed explanation, you can refer to [this link](https://docs.safe.global/safe-smart-account/signatures) for more information. +An ECDSA signature is composed of two 32-byte integers (r, s) and an extra byte for recovery id (v), making a total of 65 bytes. In hexadecimal string format, each byte is represented by 2 characters. Hence, a 65-byte Ethereum signature will be 130 characters long. Including the '0x' prefix commonly used with signatures, the total character count for such a signature would be 132. For a more detailed explanation, you can refer to [this link](https://docs.safe.global/safe-smart-account/signatures) for more information. > To represent a byte (8 bits) in hexadecimal, you need 2 characters. Each hexadecimal character represents 4 bits. Therefore, 2 hexadecimal characters (2 x 4 bits) are able to represent a byte (8 bits). From 98ec6d2fc7c31c9f5df3071f4c13a7d021861c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Tue, 23 Jan 2024 11:51:25 +0100 Subject: [PATCH 15/18] Improve signers scope definition --- pages/sdk-protocol-kit/guides/safe-signatures.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index 20e838a1..86ef76c9 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -7,7 +7,7 @@ Understanding and generating signatures can be challenging. In the **Safe{Core} Your Safe Account can be configured with various thresholds and different owner types. An owner can be any Ethereum address, such as: - External Owned Account (EOA) -- Child Signer Safe (A Safe Account that's an owner of another Safe Account) +- An smart contract that implements [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) for signature validation, such as Safe Account When the owner is an EOA, an ECDSA signature is generated. When the signer is a smart account, the exact signature algorithm depends on the signer itself. From eb7ea5fd2d016abd2aeb48c393fb374d0daae4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Tue, 23 Jan 2024 11:55:22 +0100 Subject: [PATCH 16/18] Removed section --- pages/sdk-protocol-kit/guides/safe-signatures.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index 86ef76c9..e77f8508 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -99,15 +99,13 @@ We will sign with `owner1` and `owner2` using the `signTransaction()` method. Th It's possible to use several signing methods, such as `ETH_SIGN` (eth_sign), `ETH_SIGN_TYPED_DATA_V4` (eth_signTypedData_v4), ...etc. The default signing method is `ETH_SIGN_TYPED_DATA_V4`. -`ETH_SIGN` produces a regular [EIP-191](https://eips.ethereum.org/EIPS/eip-191) signature. `ETH_SIGN_TYPED_DATA_V4` produces a signature that's compatible with the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) standard. The EIP-712 standard is used by Safe contracts to verify the signatures. - ```typescript -safeTx = await protocolKit.signTransaction(safeTx, SigningMethod.ETH_SIGN); // EIP-191 signature from owner1 +safeTx = await protocolKit.signTransaction(safeTx, SigningMethod.ETH_SIGN); protocolKit = await protocolKit.connect({ ethAdapter: ethAdapter2 }); // Connect another owner safeTx = await protocolKit.signTransaction( safeTx, SigningMethod.ETH_SIGN_TYPED_DATA_V4 -); // owner2 EIP-712 typed data signature +); ``` In this snippet, we add the signature for `owner1`. Then, we use the `connect()` method to connect the `owner2` adapter and sign again to add the second signature. @@ -140,7 +138,7 @@ An ECDSA signature is composed of two 32-byte integers (r, s) and an extra byte The final part of the signature, either `1f` or `1c`, indicates the signature type. -The hexadecimal value `1f` equals to the decimal number 31. Because the decimal value is greater than 30, it [indicates to the Safe smart contract]()(https://github.com/safe-global/safe-smart-account/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347) that the signature is an `eth_sign`. +The hexadecimal value `1f` equals to the decimal number 31. Because the decimal value is greater than 30, it [indicates to the Safe smart contract]()(https://github.com/safe-global/safe-smart-account/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347) that the signature is an `eth_sign`. The hexadecimal value `1c` equals to the decimal number 28, indicating that the signature is a typed data signature. For instance, in the case of the initial signature: @@ -341,7 +339,6 @@ The table looks very similar to the previous one, but there are two changes: - The _signature length_ has doubled because this Safe needs two signatures. - The _signature_ value is a concatenation of the two regular signatures. - We're finished. Let's add the signature to the original transaction. ```typescript From 2724eecd56c962e7b99d7a486370c6b17b7e786c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Tue, 23 Jan 2024 12:08:20 +0100 Subject: [PATCH 17/18] Add info about signature types --- pages/sdk-protocol-kit/guides/safe-signatures.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index e77f8508..66db0c2d 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -138,6 +138,14 @@ An ECDSA signature is composed of two 32-byte integers (r, s) and an extra byte The final part of the signature, either `1f` or `1c`, indicates the signature type. +> There are the following V values supported by Safe: +> +> - 0: Contract signature +> - 1: Approved hash +> - {27, 28} + 4: Ethereum adjusted ECDSA recovery byte for eip-191 signed message +> It's important that for the EIP-191 signed message the V is adjusted to the ecdsa V + 4. If the generated value is 28 and you adjust it to 0x1f, the signature verification will fail, it should be 0x20 (28+4=32) instead. So, if v > 30 then default v (27,28) has been adjusted for eth_sign flow. This is automatically done in the SDK +> - Other: Ethereum adjusted ECDSA recovery byte for raw signed hash + The hexadecimal value `1f` equals to the decimal number 31. Because the decimal value is greater than 30, it [indicates to the Safe smart contract]()(https://github.com/safe-global/safe-smart-account/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347) that the signature is an `eth_sign`. The hexadecimal value `1c` equals to the decimal number 28, indicating that the signature is a typed data signature. For instance, in the case of the initial signature: From 914bf7cb0cd4823bb1111ac85af34a505c79bc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Tue, 23 Jan 2024 15:32:19 +0100 Subject: [PATCH 18/18] vale --- pages/sdk-protocol-kit/guides/safe-signatures.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/sdk-protocol-kit/guides/safe-signatures.md b/pages/sdk-protocol-kit/guides/safe-signatures.md index 66db0c2d..de030490 100644 --- a/pages/sdk-protocol-kit/guides/safe-signatures.md +++ b/pages/sdk-protocol-kit/guides/safe-signatures.md @@ -138,12 +138,12 @@ An ECDSA signature is composed of two 32-byte integers (r, s) and an extra byte The final part of the signature, either `1f` or `1c`, indicates the signature type. -> There are the following V values supported by Safe: +> Safe supports the following V values: > > - 0: Contract signature > - 1: Approved hash -> - {27, 28} + 4: Ethereum adjusted ECDSA recovery byte for eip-191 signed message -> It's important that for the EIP-191 signed message the V is adjusted to the ecdsa V + 4. If the generated value is 28 and you adjust it to 0x1f, the signature verification will fail, it should be 0x20 (28+4=32) instead. So, if v > 30 then default v (27,28) has been adjusted for eth_sign flow. This is automatically done in the SDK +> - {27, 28} + 4: Ethereum adjusted ECDSA recovery byte for EIP-191 signed message +> It's important that for the EIP-191 signed message the V is adjusted to the ECDSA V + 4. If the generated value is 28 and you adjust it to 0x1f, the signature verification will fail, it should be 0x20 (28+4=32) instead, so, if v > 30 then default v (27,28) has been adjusted for eth_sign flow. This is automatically done in the SDK > - Other: Ethereum adjusted ECDSA recovery byte for raw signed hash The hexadecimal value `1f` equals to the decimal number 31. Because the decimal value is greater than 30, it [indicates to the Safe smart contract]()(https://github.com/safe-global/safe-smart-account/blob/f03dfae65fd1d085224b00a10755c509a4eaacfe/contracts/Safe.sol#L344-L347) that the signature is an `eth_sign`.