Skip to content

Commit

Permalink
Refactor safe use in e2e tests (#1944)
Browse files Browse the repository at this point in the history
# Description
Implements some [suggestion from
Felix](#1938 (review))
on how we abstract a Safe multisig.

Note that some of the new abstractions are already used in #1939.

# Changes

In the first commit, some new abstractions are created under the `Safe`
struct to reduce some of the boilerplate in the tests.
In the second commit, all code related to the safe is moved to its own
submodule `onchain_components::safe`; the only changes are imports and
`GnosisSafeInfrastructure` → `safe::Infrastructure`.

## How to test

See that all e2e tests continue to work.
  • Loading branch information
fedgiac authored Oct 12, 2023
1 parent dea8df3 commit ced5c5b
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 279 deletions.
256 changes: 5 additions & 251 deletions crates/e2e/src/setup/onchain_components.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
use {
crate::setup::deploy::Contracts,
contracts::{
CowProtocolToken,
ERC20Mintable,
GnosisSafe,
GnosisSafeCompatibilityFallbackHandler,
GnosisSafeProxy,
GnosisSafeProxyFactory,
},
ethcontract::{transaction::TransactionBuilder, Account, Bytes, PrivateKey, H160, H256, U256},
contracts::{CowProtocolToken, ERC20Mintable},
ethcontract::{transaction::TransactionBuilder, Account, Bytes, PrivateKey, H160, U256},
hex_literal::hex,
model::{
order::Hook,
Expand All @@ -19,13 +12,11 @@ use {
secp256k1::SecretKey,
shared::ethrpc::Web3,
std::{borrow::BorrowMut, ops::Deref},
web3::{
signing,
signing::{Key, SecretKeyRef},
Transport,
},
web3::{signing, signing::SecretKeyRef, Transport},
};

pub mod safe;

#[macro_export]
macro_rules! tx_value {
($acc:expr, $value:expr, $call:expr) => {{
Expand All @@ -46,127 +37,6 @@ macro_rules! tx {
};
}

#[macro_export]
macro_rules! tx_safe {
($acc:expr, $safe:ident, $call:expr) => {{
let call = $call;
$crate::tx!(
$acc,
$safe.exec_transaction(
call.tx.to.unwrap(),
call.tx.value.unwrap_or_default(),
::ethcontract::Bytes(call.tx.data.unwrap_or_default().0),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
$crate::setup::gnosis_safe_prevalidated_signature($acc.address()),
)
);
}};
}

pub struct GnosisSafeInfrastructure {
pub factory: GnosisSafeProxyFactory,
pub fallback: GnosisSafeCompatibilityFallbackHandler,
pub singleton: GnosisSafe,
web3: Web3,
}

impl GnosisSafeInfrastructure {
pub async fn new(web3: &Web3) -> Self {
let singleton = GnosisSafe::builder(web3).deploy().await.unwrap();
let fallback = GnosisSafeCompatibilityFallbackHandler::builder(web3)
.deploy()
.await
.unwrap();
let factory = GnosisSafeProxyFactory::builder(web3)
.deploy()
.await
.unwrap();
Self {
web3: web3.clone(),
singleton,
fallback,
factory,
}
}

pub async fn deploy_safe(&self, owners: Vec<H160>, threshold: usize) -> GnosisSafe {
let safe_proxy = GnosisSafeProxy::builder(&self.web3, self.singleton.address())
.deploy()
.await
.unwrap();
let safe = GnosisSafe::at(&self.web3, safe_proxy.address());
safe.setup(
owners,
threshold.into(),
H160::default(), // delegate call
Bytes::default(), // delegate call bytes
self.fallback.address(),
H160::default(), // relayer payment token
0.into(), // relayer payment amount
H160::default(), // relayer address
)
.send()
.await
.unwrap();
safe
}
}

/// Generate a Safe "pre-validated" signature.
///
/// This is a special "marker" signature that can be used if the account that
/// is executing the transaction is an owner. For single owner safes, this is
/// the easiest way to execute a transaction as it does not involve any ECDSA
/// signing.
///
/// See:
/// - Documentation: <https://docs.gnosis-safe.io/contracts/signatures#pre-validated-signatures>
/// - Code: <https://github.com/safe-global/safe-contracts/blob/c36bcab46578a442862d043e12a83fec41143dec/contracts/GnosisSafe.sol#L287-L291>
pub fn gnosis_safe_prevalidated_signature(owner: H160) -> Bytes<Vec<u8>> {
let mut signature = vec![0; 65];
signature[12..32].copy_from_slice(owner.as_bytes());
signature[64] = 1;
Bytes(signature)
}

/// Generate an owner signature for EIP-1271.
///
/// The Gnosis Safe uses off-chain ECDSA signatures from its owners as the
/// signature bytes when validating EIP-1271 signatures. Specifically, it
/// expects a signed EIP-712 `SafeMessage(bytes message)` (where `message` is
/// the 32-byte hash of the data being verified).
///
/// See:
/// - Code: <https://github.com/safe-global/safe-contracts/blob/c36bcab46578a442862d043e12a83fec41143dec/contracts/handler/CompatibilityFallbackHandler.sol#L66-L70>
pub async fn gnosis_safe_eip1271_signature(
key: SecretKeyRef<'_>,
safe: &GnosisSafe,
message_hash: H256,
) -> Vec<u8> {
let handler =
GnosisSafeCompatibilityFallbackHandler::at(&safe.raw_instance().web3(), safe.address());

let signing_hash = handler
.get_message_hash(Bytes(message_hash.as_bytes().to_vec()))
.call()
.await
.unwrap();

let signature = key.sign(&signing_hash.0, None).unwrap();

let mut bytes = vec![0u8; 65];
bytes[0..32].copy_from_slice(signature.r.as_bytes());
bytes[32..64].copy_from_slice(signature.s.as_bytes());
bytes[64] = signature.v as _;

bytes
}

pub fn to_wei(base: u32) -> U256 {
U256::from(base) * U256::exp10(18)
}
Expand Down Expand Up @@ -322,122 +192,6 @@ impl Deref for CowToken {
}
}

/// Wrapper over a deployed Safe.
pub struct Safe {
chain_id: U256,
contract: GnosisSafe,
owner: TestAccount,
}

impl Safe {
/// Return a wrapper at the deployed address.
pub fn deployed(chain_id: U256, contract: GnosisSafe, owner: TestAccount) -> Self {
Self {
chain_id,
contract,
owner,
}
}

/// Returns the address of the Safe.
pub fn address(&self) -> H160 {
self.contract.address()
}

/// Returns a signed transaction ready for execution.
pub fn sign_transaction(
&self,
to: H160,
data: Vec<u8>,
nonce: U256,
) -> ethcontract::dyns::DynMethodBuilder<bool> {
let signature = self.sign({
// `SafeTx` struct hash computation ported from the Safe Solidity code:
// <https://etherscan.io/address/0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552#code#F1#L377>

let mut buffer = [0_u8; 352];
buffer[0..32].copy_from_slice(&hex!(
// `SafeTx` type hash:
// <https://etherscan.io/address/0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552#code#F1#L43>
"bb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8"
));
buffer[44..64].copy_from_slice(to.as_bytes());
buffer[96..128].copy_from_slice(&signing::keccak256(&data));
nonce.to_big_endian(&mut buffer[320..352]);

// Since the [`sign_transaction`] transaction method only accepts
// a limited number of parameters and defaults to 0 for the others,
// We can leave the rest of the buffer 0-ed out (as we have 0
// values for those fields).

signing::keccak256(&buffer)
});

self.contract.exec_transaction(
to,
Default::default(), // value
Bytes(data),
Default::default(), // operation (= CALL)
Default::default(), // safe tx gas
Default::default(), // base gas
Default::default(), // gas price
Default::default(), // gas token
Default::default(), // refund receiver
Bytes(signature),
)
}

/// Returns the ERC-1271 signature bytes for the specified message.
pub fn sign_message(&self, message: &[u8; 32]) -> Vec<u8> {
self.sign({
// `SafeMessage` struct hash computation ported from the Safe Solidity code:
// <https://etherscan.io/address/0xf48f2b2d2a534e402487b3ee7c18c33aec0fe5e4#code#F1#L52>

let mut buffer = [0_u8; 64];
buffer[0..32].copy_from_slice(&hex!(
// `SafeMessage` type hash:
// <https://etherscan.io/address/0xf48f2b2d2a534e402487b3ee7c18c33aec0fe5e4#code#F1#L14>
"60b3cbf8b4a223d68d641b3b6ddf9a298e7f33710cf3d3a9d1146b5a6150fbca"
));
buffer[32..64].copy_from_slice(&signing::keccak256(message));

signing::keccak256(&buffer)
})
}

/// Returns the domain separator for the Safe.
fn domain_separator(&self) -> DomainSeparator {
// Domain separator computation ported from the Safe Solidity code:
// <https://etherscan.io/address/0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552#code#F1#L350>

let mut buffer = [0_u8; 96];
buffer[0..32].copy_from_slice(&hex!(
// The domain separator type hash:
// <https://etherscan.io/address/0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552#code#F1#L38>
"47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218"
));
self.chain_id.to_big_endian(&mut buffer[32..64]);
buffer[76..96].copy_from_slice(self.contract.address().as_bytes());

DomainSeparator(signing::keccak256(&buffer))
}

/// Creates an ECDSA signature with the [`Safe`]'s `owner` and encodes to
/// bytes in the format expected by the Safe contract.
fn sign(&self, hash: [u8; 32]) -> Vec<u8> {
let signature = self.owner.sign_typed_data(&self.domain_separator(), &hash);

// Signature format specified here:
// <https://etherscan.io/address/0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552#code#F11#L20>
[
signature.r.as_bytes(),
signature.s.as_bytes(),
&[signature.v],
]
.concat()
}
}

/// Wrapper over deployed [Contracts].
/// Exposes various utility methods for tests.
/// Deterministically generates unique accounts.
Expand Down
Loading

0 comments on commit ced5c5b

Please sign in to comment.