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 1 commit
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
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
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
cast wallet sign $(cat ./signatures/data.txt) --ledger > signatures/signatures.txt
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
```
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.
2. Each required signer must sign the data. Signatures must be written one per line in `/signature/signatures.txt`.
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
forge script script/BatchSignaturesAndExecuteOnSafe.s.sol --ledger --broadcast --rpc-url $RPC_URL
```
4. Approve the transaction on your Ledger.
3 changes: 2 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ src = "src"
out = "out"
libs = ["lib"]
fs_permissions = [{ access = "read-write", path = "./"}]
ffi = true

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
119 changes: 102 additions & 17 deletions script/BatchSignaturesAndExecuteOnSafe.s.sol
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Script.sol";
import {QuickSort} from "./libraries/QuickSort.sol";
import {SignatureDecoder} from "safe/common/SignatureDecoder.sol";
import {GnosisSafe, Enum} from "safe/GnosisSafe.sol";

import "forge-std/console2.sol";
import "safe/GnosisSafe.sol";
import "forge-std/Script.sol";

contract BatchSignaturesAndExecuteOnSafe is Script, SignatureDecoder {
using QuickSort for address[];

contract BatchSignaturesAndExecuteOnSafe is Script {
// Avoid stack too deep error.
struct TxData {
address to;
Expand All @@ -20,36 +25,55 @@ contract BatchSignaturesAndExecuteOnSafe is Script {
bytes signatures;
}

function run() public {
string memory root = vm.projectRoot();
string memory path = string.concat(root, "/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;

uint256 nbOfSignatures = 2; // TODO: automatically fetch the number of signatures?
TxData memory txData;
string ROOT = vm.projectRoot();
string SIGNATURES_DIR = string.concat(ROOT, "/signatures/");
string SIGNATURES_FILE = string.concat(SIGNATURES_DIR, "signatures.txt");
string DATA_FILE = string.concat(SIGNATURES_DIR, "data.txt");

// Build signatures payload.
for (uint256 i; i < nbOfSignatures; i++) {
txData.signatures =
bytes.concat(txData.signatures, vm.parseBytes(vm.readFile(string.concat(path, vm.toString(i), ".txt"))));
}
GnosisSafe immutable SAFE = GnosisSafe(payable(vm.envAddress("SAFE")));

// Fetch Safe contract.
GnosisSafe safe = GnosisSafe(payable(vm.envAddress("SAFE")));
mapping(address => bytes) signatureOf;

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

// Build tx data.
TxData memory txData;

txData.to = vm.envAddress("TO");
txData.value = 0;
txData.data = vm.parseBytes(vm.readFile(string.concat(path, "data.txt")));
txData.data = vm.parseBytes(vm.readFile(DATA_FILE));
txData.operation = Enum.Operation.Call;
txData.safeTxGas = 0;
txData.baseGas = 0;
txData.gasPrice = 0;
txData.gasToken = address(0); // ETH
txData.refundReceiver = payable(address(0)); // tx.origin

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(
SAFE.execTransaction(
txData.to,
txData.value,
txData.data,
Expand All @@ -62,4 +86,65 @@ contract BatchSignaturesAndExecuteOnSafe is Script {
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);

// 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));
}
}
}

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);
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
}

function hashData(TxData 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);
}
}
31 changes: 31 additions & 0 deletions script/libraries/QuickSort.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

function quickSortAddress(address[] memory arr, int256 left, int256 right) pure {
int256 i = left;
int256 j = right;
if (i == j) return;

address pivot = arr[uint256(left + (right - left) / 2)];
while (i <= j) {
while (arr[uint256(i)] < pivot) i++;
while (pivot < arr[uint256(j)]) j--;

if (i <= j) {
(arr[uint256(i)], arr[uint256(j)]) = (arr[uint256(j)], arr[uint256(i)]);
i++;
j--;
}
}

if (left < j) quickSortAddress(arr, left, j);
if (i < right) quickSortAddress(arr, i, right);
}

library QuickSort {
function sort(address[] memory data) internal pure returns (address[] memory) {
quickSortAddress(data, int256(0), int256(data.length - 1));
MerlinEgalite marked this conversation as resolved.
Show resolved Hide resolved

return data;
}
}
1 change: 0 additions & 1 deletion signatures/0.txt

This file was deleted.

1 change: 0 additions & 1 deletion signatures/1.txt

This file was deleted.

3 changes: 3 additions & 0 deletions signatures/signatures.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
0x0000000000000000000000000000000000000000000000000000000000000000
0x0000000000000000000000000000000000000000000000000000000000000001
0x0000000000000000000000000000000000000000000000000000000000000002