Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Append all signatures and sort them via script #4

Merged
merged 4 commits into from
Apr 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
RPC_URL=
SAFE=0x0000000000000000000000000000000000000000
TO=0x0000000000000000000000000000000000000000
SENDER=0x0000000000000000000000000000000000000000
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ docs/

# Dotenv file
.env

# IDE
.vscode
26 changes: 26 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-include .env
.EXPORT_ALL_VARIABLES:
MAKEFLAGS += --no-print-directory

NETWORK ?= ethereum-mainnet


install:
foundryup
forge install

hash:
forge script script/HashData.s.sol --rpc-url rpc

sign\:%:
cast wallet sign --$* $$(cat signatures/hashData.txt)

exec\:%:
forge script script/ExecTransaction.s.sol --$* --broadcast --rpc-url rpc >> signatures/signatures.txt

clean:
> signatures/hashData.txt
> signatures/signatures.txt


.PHONY: contracts test coverage
65 changes: 46 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
# Safer

## Tutorial: sign data

1. Put the data to sign in `/signatures/data.txt`
2. To sign the data with a Ledger, run:
```bash
cast wallet sign $(cat ./signatures/data.txt) --ledger
```
Keep the signed data or send it to the signer that will execute the transaction on the Safe.

## Tutorial: batch signatures and execute transaction

1. Populate your `.env` file as described in `.env.example`.
1. The data to sign should be put in `/signatures/data.txt`.
2. Each required signer must sign the data. Signatures must be put in `/signature/0.txt`, `/signature/1.txt`, etc.
3. To send the transaction to the Safe with a Ledger, run:
```bash
forge script script/BatchSignaturesAndExecuteOnSafe.s.sol --ledger --broadcast --rpc-url $RPC_URL
```
4. Approve the transaction on your Ledger.
## Getting Started

- Install [Foundry](https://github.com/foundry-rs/foundry).
- Run `make` to initialize the repository.
- Create a `.env` file from the template [`.env.example`](./.env.example) file.

You can customize the RPC url used in [`foundry.tml`](./foundry.toml) under the `rpc_endpoint` section. This is useful if your Safe is not deployed on mainnet (which is the default chain used).

### Sign a Safe tx

1. Put the transaction's raw data in `signatures/tx.json`
2. Hash the transaction's raw data: `make hash`
3. To sign the data with a Ledger, run: `make sign:ledger`
4. Share the content of `signatures.txt` with the signer who will execute the transaction on the Safe.

### Batch signatures and execute transaction

1. Make at least `threshold` signatures are available in `/signatures/signatures.txt`, each one per line
2. To execute the transaction on the Safe with a Ledger, run: `make exec:ledger`

## Advanced options

### Wallet support

With `make sign` & `make exec`, one can also use any other wallet provider available with `cast`:
- `make cmd:interactive` to input the private key to the command prompt
- `make cmd:ledger` to use a Ledger
- `make cmd:trezor` to use a Trezor
- `make cmd:keystore` to use a keystore
- `make cmd:"private-key 0x..."` if you really want to save your private key to your shell's history...

### Transaction details

```json
{
"to": "0x0000000000000000000000000000000000000000",
"value": 0,
"data": "0x", // the raw tx data
"operation": 0, // 0 for a call, 1 for a delegatecall
"safeTxGas": 0,
"baseGas": 0,
"gasPrice": 0,
"gasToken": "0x0000000000000000000000000000000000000000", // indicates the tx will consume the chain's default gas token (ETH on mainnet)
"refundReceiver": "0x0000000000000000000000000000000000000000" // indicates the tx's refund receiver will be the address executing the tx
}
```
9 changes: 7 additions & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
src = "src"
out = "out"
libs = ["lib"]
fs_permissions = [{ access = "read-write", path = "./"}]
fs_permissions = [{ access = "read-write", path = "signatures"}]
ffi = true

# See more config options https://github.com/foundry-rs/foundry/tree/master/config

[rpc_endpoints]
rpc = "https://eth.llamarpc.com" # change this to match the chain on which the Safe is deployed

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
65 changes: 0 additions & 65 deletions script/BatchSignaturesAndExecuteOnSafe.s.sol
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved

This file was deleted.

77 changes: 77 additions & 0 deletions script/ExecTransaction.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {QuickSort} from "./libraries/QuickSort.sol";
import {SafeTxDataBuilder, Enum} from "./SafeTxDataBuilder.sol";

contract ExecTransaction is SafeTxDataBuilder {
using QuickSort for address[];

mapping(address => bytes) signatureOf;

constructor() SafeTxDataBuilder(payable(vm.envAddress("SAFE"))) {}

function run() public {
SafeTxData memory txData = loadSafeTxData();
bytes[] memory signatures = loadSignatures();

bytes32 dataHash = hashData(txData);
address[] memory signers = new address[](signatures.length);
for (uint256 i; i < signatures.length; i++) {
(address signer, bytes32 r, bytes32 s, uint8 v) = decode(dataHash, signatures[i]);

signers[i] = signer;
signatureOf[signer] = abi.encodePacked(r, s, v + 4);
}

signers.sort();

for (uint256 i; i < signers.length; ++i) {
txData.signatures = bytes.concat(txData.signatures, signatureOf[signers[i]]);
}

// Execute tx.
vm.broadcast(vm.envAddress("SENDER"));
SAFE.execTransaction(
txData.to,
txData.value,
txData.data,
txData.operation,
txData.safeTxGas,
txData.baseGas,
txData.gasPrice,
txData.gasToken,
txData.refundReceiver,
txData.signatures
);
}

function loadSignatures() internal returns (bytes[] memory signatures) {
string[] memory cmd = new string[](2);
cmd[0] = "cat";
cmd[1] = SIGNATURES_FILE;

bytes memory res = vm.ffi(cmd); // TODO: use vm.readFile

// If the file only contains a single signature, ffi converts it to bytes and can be used as is.
if (res.length == 32) {
signatures = new bytes[](1);
signatures[0] = res;
} else {
// Otherwise, each signature is (2 bytes 0x prefix + 64 bytes data =) 66 bytes long and suffixed by 1 byte of newline character.
uint256 nbSignatures = (res.length + 1) / 67; // The last 1 byte newline character is trimmed by ffi.
signatures = new bytes[](nbSignatures);

for (uint256 i; i < nbSignatures; ++i) {
uint256 start = i * 67 + 2; // Don't read the first 2 bytes of 0x prefix.

bytes memory signature = new bytes(64);
for (uint256 j; j < 64; ++j) {
signature[j] = res[start + j];
}

signatures[i] = vm.parseBytes(string(signature));
}
}
}
}
16 changes: 16 additions & 0 deletions script/HashData.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {SafeTxDataBuilder, console2} from "./SafeTxDataBuilder.sol";

contract HashData is SafeTxDataBuilder {
constructor() SafeTxDataBuilder(payable(vm.envAddress("SAFE"))) {}

function run() public {
SafeTxData memory txData = loadSafeTxData();

bytes32 dataHash = hashData(txData);

vm.writeFile(HASH_DATA_FILE, vm.toString(dataHash));
}
}
91 changes: 91 additions & 0 deletions script/SafeTxDataBuilder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {SignatureDecoder} from "safe/common/SignatureDecoder.sol";
import {GnosisSafe, Enum} from "safe/GnosisSafe.sol";

import "forge-std/console2.sol";
import "forge-std/StdJson.sol";
import "forge-std/Script.sol";

contract SafeTxDataBuilder is Script, SignatureDecoder {
using stdJson for string;

struct SafeTxData {
address to;
uint256 value;
bytes data;
Enum.Operation operation;
uint256 safeTxGas;
uint256 baseGas;
uint256 gasPrice;
address gasToken;
address payable refundReceiver;
bytes signatures;
}

// keccak256(
// "SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)"
// );
bytes32 private constant SAFE_TX_TYPEHASH = 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8;
bytes NEWLINE_CHAR = bytes("\n");

string ROOT = vm.projectRoot();
string SIGNATURES_DIR = string.concat(ROOT, "/signatures/");

string TX_FILE = string.concat(SIGNATURES_DIR, "tx.json");
string HASH_DATA_FILE = string.concat(SIGNATURES_DIR, "hashData.txt");
string SIGNATURES_FILE = string.concat(SIGNATURES_DIR, "signatures.txt");

GnosisSafe immutable SAFE;

constructor(address payable safe) {
SAFE = GnosisSafe(safe);
}

function loadSafeTxData() internal returns (SafeTxData memory txData) {
string memory json = vm.readFile(TX_FILE);

txData.to = json.readAddress("$.to");
txData.value = json.readUint("$.value");
txData.data = json.readBytes("$.data");
txData.operation = Enum.Operation(json.readUint("$.operation"));
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
txData.safeTxGas = json.readUint("$.safeTxGas");
txData.baseGas = json.readUint("$.baseGas");
txData.gasPrice = json.readUint("$.gasPrice");
txData.gasToken = json.readAddress("$.gasToken");
txData.refundReceiver = payable(json.readAddress("$.refundReceiver"));
}

function hashData(SafeTxData memory txData) internal view returns (bytes32) {
bytes32 safeTxHash = keccak256(
abi.encode(
SAFE_TX_TYPEHASH,
txData.to,
txData.value,
keccak256(txData.data),
txData.operation,
txData.safeTxGas,
txData.baseGas,
txData.gasPrice,
txData.gasToken,
txData.refundReceiver,
SAFE.nonce()
)
);

bytes memory txHashData = abi.encodePacked(bytes1(0x19), bytes1(0x01), SAFE.domainSeparator(), safeTxHash);

return keccak256(txHashData);
}

function decode(bytes32 dataHash, bytes memory signature)
internal
pure
returns (address signer, bytes32 r, bytes32 s, uint8 v)
{
(v, r, s) = signatureSplit(signature, 0);

signer = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v, r, s);
}
}
Loading