Skip to content

Commit

Permalink
chore: improve e2e base (#14)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes SAF-XXX
  • Loading branch information
0xOneTony authored Nov 27, 2023
1 parent ab26a81 commit 82bb744
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 102 deletions.
15 changes: 14 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
## For e2e tests and deployment scripts
# For anvil to fork from
MAINNET_RPC=
GOERLI_RPC=
OPTIMISM_RPC=
OPTIMISM_GOERLI_RPC=

# Deployer address for the E2E Tests
MAINNET_DEPLOYER_ADDR=
# Safe owner address for the E2E Tests
MAINNET_SAFE_OWNER_ADDR=
# Deployer private key for the E2E Tests
MAINNET_DEPLOYER_PK=
# Safe owner private key for the E2E Tests
MAINNET_SAFE_OWNER_PK=
# Mainnet rpc for the E2E Tests, should be the anvil url
MAINNET_E2E_RPC=
# Optimism rpc for the E2E Tests, should be the anvil url
OPTIMISM_E2E_RPC=

## For deployment scripts
DEPLOYER_MAINNNET_PRIVATE_KEY=
DEPLOYER_GOERLI_PRIVATE_KEY=
Expand Down
12 changes: 9 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,25 @@ jobs:
- name: Install dependencies
run: yarn --frozen-lockfile --network-concurrency 1

- name: Precompile using 0.8.14 and via-ir=false
- name: Precompile using 0.8.19 and via-ir=false
run: yarn build

- name: "Create env file"
run: |
touch .env
echo MAINNET_RPC="${{ secrets.MAINNET_RPC }}" >> .env
echo GOERLI_RPC="${{ secrets.GOERLI_RPC }}" >> .env
echo OPTIMISM_RPC="${{ secrets.OPTIMISM_RPC }}" >> .env
echo MAINNET_E2E_RPC="${{ secrets.MAINNET_E2E_RPC }}" >> .env
echo OPTIMISM_E2E_RPC="${{ secrets.OPTIMISM_E2E_RPC }}" >> .env
echo MAINNET_SAFE_OWNER_ADDR="${{ secrets.MAINNET_SAFE_OWNER_ADDR }}" >> .env
echo MAINNET_SAFE_OWNER_PK="${{ secrets.MAINNET_SAFE_OWNER_PK }}" >> .env
echo MAINNET_DEPLOYER_ADDR="${{ secrets.MAINNET_DEPLOYER_ADDR }}" >> .env
echo MAINNET_DEPLOYER_PK="${{ secrets.MAINNET_DEPLOYER_PK }}" >> .env
cat .env
- name: Run tests
shell: bash
run: yarn test
run: yarn test:e2e-workflow

forge-optimized:
name: Run Optimized Unit Tests
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ yarn build

### Available Commands

Make sure to set `MAINNET_RPC` environment variable before running end-to-end tests.
Make sure to set `MAINNET_RPC` and `OPTIMISM_RPC` environment variable before running end-to-end tests.

| Yarn Command | Description |
| ----------------------- | ---------------------------------------------------------- |
Expand Down
75 changes: 75 additions & 0 deletions e2e-tests-with-nodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const { spawn } = require('child_process');
require('dotenv').config(); // Initialize dotenv to load environment variables

(async() => {
// Starting Anvil nodes for 'mainnet' and 'optimism'
console.debug(`Starting Anvil nodes`);
const anvilMainnetPromise = runAnvilNode('mainnet');
const anvilOptimismPromise = runAnvilNode('optimism');

// Waiting for both nodes to be fully operational
console.debug(`Waiting for nodes to be up and running...`);
const [anvilMainnet, anvilOptimism] = await Promise.all([anvilMainnetPromise, anvilOptimismPromise]);

// Running end-to-end tests
console.debug(`Running tests`);
const testProcess = spawn('yarn', [`test:e2e`]);

// Handle test errors
testProcess.stderr.on('data', (data) => console.error(`Test error: ${data}`));

// Track the test result
let testPassed = true;
testProcess.stdout.on('data', (data) => {
console.info(String(data));
if (String(data).includes('Failing tests:')) testPassed = false;
});

// When tests are complete, kill the Anvil nodes
testProcess.on('close', (code) => {
console.debug(`Tests finished running, killing anvil nodes...`);
try {
anvilMainnet.kill();
anvilOptimism.kill();
console.debug('Anvil nodes terminated successfully.');
} catch (error) {
console.error(`Error terminating Anvil nodes: ${error}`);
}

// Exit with an error code if tests failed
if (!testPassed) {
console.error('Tests failed. Setting exit code to 1.');
process.exit(1);
}

// Exit with a success code if tests passed
process.exit(0);
});
})();


/**
* Start an Anvil node for a given network
*
* @param network: name of the network to run (string)
* @returns promise which resolves when the node is running
*/
function runAnvilNode(network) {
return new Promise((resolve, reject) => {
const node = spawn('yarn', [`anvil:${network}`]);

// Handle errors without exposing sensitive information
node.stderr.on('data', () => {
console.error(`Anvil ${network} node errored! Not showing the error log since it could reveal the RPC url.`);
reject();
});

// Resolve the promise when the node is up
node.stdout.on('data', (data) => {
if (String(data).includes('Listening on')) {
console.debug(`Anvil ${network} node up and running`);
resolve(node);
}
});
});
}
3 changes: 3 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ src = './solidity/interfaces/'
[rpc_endpoints]
mainnet = "${MAINNET_RPC}"
goerli = "${GOERLI_RPC}"
optimism = "${OPTIMISM_RPC}"
mainnet_e2e = "${MAINNET_E2E_RPC}"
optimism_e2e = "${OPTIMISM_E2E_RPC}"
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
},
"author": "Wonderland",
"scripts": {
"anvil:mainnet": "anvil --port 8545 -f $MAINNET_RPC --fork-block-number 18621047 --chain-id 1",
"anvil:optimism": "anvil --port 9545 -f $OPTIMISM_RPC --fork-block-number 112491451 --chain-id 10",
"build": "forge build",
"build:optimized": "FOUNDRY_PROFILE=optimized forge build",
"coverage": "forge coverage --match-contract Unit",
Expand All @@ -27,6 +29,7 @@
"proof": "python3 proofs/generate_proof.py",
"test": "forge test -vvv",
"test:e2e": "forge test --match-contract E2E -vvv",
"test:e2e-workflow": "node e2e-tests-with-nodes.js",
"test:unit": "forge test --match-contract Unit -vvv",
"test:unit:deep": "FOUNDRY_FUZZ_RUNS=5000 yarn test:unit"
},
Expand All @@ -38,6 +41,7 @@
"dependencies": {
"@defi-wonderland/solidity-utils": "0.0.0-3e9c8e8b",
"Solidity-RLP": "github:hamdiallam/Solidity-RLP",
"dotenv": "16.3.1",
"ds-test": "github:dapphub/ds-test#e282159",
"forge-std": "github:foundry-rs/forge-std#v1.5.6",
"isolmate": "github:defi-wonderland/isolmate#59e1804",
Expand Down
128 changes: 97 additions & 31 deletions solidity/test/e2e/Common.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,30 @@ import {UpdateStorageMirrorGuard} from 'contracts/UpdateStorageMirrorGuard.sol';
import {GuardCallbackModule} from 'contracts/GuardCallbackModule.sol';
import {BlockHeaderOracle} from 'contracts/BlockHeaderOracle.sol';
import {NeedsUpdateGuard} from 'contracts/NeedsUpdateGuard.sol';
import {VerifierModule} from 'contracts/VerifierModule.sol';
import {StorageMirrorRootRegistry} from 'contracts/StorageMirrorRootRegistry.sol';

import {IGuardCallbackModule} from 'interfaces/IGuardCallbackModule.sol';
import {ISafe} from 'interfaces/ISafe.sol';
import {IVerifierModule} from 'interfaces/IVerifierModule.sol';
import {IStorageMirrorRootRegistry} from 'interfaces/IStorageMirrorRootRegistry.sol';
import {IBlockHeaderOracle} from 'interfaces/IBlockHeaderOracle.sol';

import {IGnosisSafeProxyFactory} from 'test/e2e/IGnosisSafeProxyFactory.sol';
import {TestConstants} from 'test/utils/TestConstants.sol';
import {ContractDeploymentAddress} from 'test/utils/ContractDeploymentAddress.sol';

// solhint-disable-next-line max-states-count
contract CommonE2EBase is DSTestPlus, TestConstants {
uint256 internal constant _FORK_BLOCK = 15_452_788;
uint256 internal constant _MAINNET_FORK_BLOCK = 18_621_047;
uint256 internal constant _OPTIMISM_FORK_BLOCK = 112_491_451;

address public deployer = makeAddr('deployer');
uint256 internal _mainnetForkId;
uint256 internal _optimismForkId;

address public deployer;
uint256 public deployerKey;
address public deployerOptimism = makeAddr('deployerOptimism');
address public proposer = makeAddr('proposer');
address public safeOwner;
uint256 public safeOwnerKey;
Expand All @@ -36,35 +47,45 @@ contract CommonE2EBase is DSTestPlus, TestConstants {
GuardCallbackModule public guardCallbackModule;
BlockHeaderOracle public oracle;
NeedsUpdateGuard public needsUpdateGuard;
VerifierModule public verifierModule;
StorageMirrorRootRegistry public storageMirrorRootRegistry;
ISafe public safe;
ISafe public nonHomeChainSafe;
IVerifierModule public verifierModule = IVerifierModule(makeAddr('verifierModule'));
IGnosisSafeProxyFactory public gnosisSafeProxyFactory = IGnosisSafeProxyFactory(GNOSIS_SAFE_PROXY_FACTORY);

function setUp() public virtual {
vm.createSelectFork(vm.rpcUrl('mainnet'), _FORK_BLOCK);
// Set up both forks
_mainnetForkId = vm.createFork(vm.rpcUrl('mainnet_e2e'), _MAINNET_FORK_BLOCK);
_optimismForkId = vm.createFork(vm.rpcUrl('optimism_e2e'), _OPTIMISM_FORK_BLOCK);
// Select mainnet fork
vm.selectFork(_mainnetForkId);

// Make address and key of safe owner
(safeOwner, safeOwnerKey) = makeAddrAndKey('safeOwner');
// Make address and key of non home chain safe owner
(nonHomeChainSafeOwner, nonHomeChainSafeOwnerKey) = makeAddrAndKey('nonHomeChainSafeOwner');
safeOwner = vm.envAddress('MAINNET_SAFE_OWNER_ADDR');
safeOwnerKey = vm.envUint('MAINNET_SAFE_OWNER_PK');

// Make address and key of deployer
deployer = vm.envAddress('MAINNET_DEPLOYER_ADDR');
deployerKey = vm.envUint('MAINNET_DEPLOYER_PK');

/// =============== HOME CHAIN ===============
vm.prank(safeOwner);
vm.broadcast(safeOwnerKey);
safe = ISafe(address(gnosisSafeProxyFactory.createProxy(GNOSIS_SAFE_SINGLETON, ''))); // safeOwner nonce 0
label(address(safe), 'SafeProxy');

address _updateStorageMirrorGuardTheoriticalAddress = ContractDeploymentAddress.addressFrom(deployer, 2);
uint256 _nonce = vm.getNonce(deployer);

address _updateStorageMirrorGuardTheoriticalAddress = ContractDeploymentAddress.addressFrom(deployer, _nonce + 2);

vm.prank(deployer);
vm.broadcast(deployer);
storageMirror = new StorageMirror(); // deployer nonce 0
label(address(storageMirror), 'StorageMirror');

vm.prank(deployer);
vm.broadcast(deployer);
guardCallbackModule = new GuardCallbackModule(address(storageMirror), _updateStorageMirrorGuardTheoriticalAddress); // deployer nonce 1
label(address(guardCallbackModule), 'GuardCallbackModule');

vm.prank(deployer);
vm.broadcast(deployer);
updateStorageMirrorGuard = new UpdateStorageMirrorGuard(guardCallbackModule); // deployer nonce 2
label(address(updateStorageMirrorGuard), 'UpdateStorageMirrorGuard');

Expand All @@ -74,11 +95,11 @@ contract CommonE2EBase is DSTestPlus, TestConstants {
// Set up owner home chain safe
address[] memory _owners = new address[](1);
_owners[0] = safeOwner;
vm.prank(safeOwner); // safeOwner nonce 1
vm.broadcast(safeOwnerKey); // safeOwner nonce 1
safe.setup(_owners, 1, address(safe), bytes(''), address(0), address(0), 0, payable(0));

// Enable guard callback module
enableModule(safe, safeOwner, safeOwnerKey, address(guardCallbackModule));
enableModule(safe, safeOwnerKey, address(guardCallbackModule));

// data to sign and send to set the guard
bytes memory _setGuardData = abi.encodeWithSelector(IGuardCallbackModule.setGuard.selector);
Expand All @@ -91,7 +112,7 @@ contract CommonE2EBase is DSTestPlus, TestConstants {
bytes memory _setGuardSignature = abi.encodePacked(_r, _s, _v);

// execute setup of guard
vm.prank(safeOwner);
vm.broadcast(safeOwnerKey);
safe.execTransaction(
address(guardCallbackModule),
0,
Expand All @@ -106,45 +127,90 @@ contract CommonE2EBase is DSTestPlus, TestConstants {
);

/// =============== NON HOME CHAIN ===============
vm.selectFork(_optimismForkId);
// Make address and key of non home chain safe owner
(nonHomeChainSafeOwner, nonHomeChainSafeOwnerKey) = makeAddrAndKey('nonHomeChainSafeOwner');

address _storageMirrorRootRegistryTheoriticalAddress = ContractDeploymentAddress.addressFrom(deployerOptimism, 2);

// Set up non home chain safe
vm.prank(nonHomeChainSafeOwner);
nonHomeChainSafe = ISafe(address(gnosisSafeProxyFactory.createProxy(GNOSIS_SAFE_SINGLETON, ''))); // nonHomeChainSafeOwner nonce 0
vm.broadcast(nonHomeChainSafeOwnerKey);
nonHomeChainSafe = ISafe(address(gnosisSafeProxyFactory.createProxy(GNOSIS_SAFE_SINGLETON_L2, ''))); // nonHomeChainSafeOwner nonce 0
label(address(nonHomeChainSafe), 'NonHomeChainSafeProxy');

// Deploy non home chain contracts
vm.prank(deployer);
oracle = new BlockHeaderOracle(); // deployer nonce 3
label(address(oracle), 'MockOracle');

// vm.prank(deployer);
// verifierModule = new VerifierModule(..); // deployer nonce 4
// label(address(verifierModule), 'VerifierModule');

vm.prank(deployer);
needsUpdateGuard = new NeedsUpdateGuard(verifierModule); // deployer nonce 5
oracle = new BlockHeaderOracle(); // deployerOptimism nonce 0
label(address(oracle), 'BlockHeaderOracle');

vm.broadcast(deployerOptimism);
verifierModule = new VerifierModule(
IStorageMirrorRootRegistry(_storageMirrorRootRegistryTheoriticalAddress), address(storageMirror)
); // deployerOptimism nonce 1
label(address(verifierModule), 'VerifierModule');

vm.broadcast(deployerOptimism);
storageMirrorRootRegistry =
new StorageMirrorRootRegistry(address(storageMirror), IVerifierModule(verifierModule), IBlockHeaderOracle(oracle)); // deployerOptimism nonce 2
label(address(storageMirrorRootRegistry), 'StorageMirrorRootRegistry');

vm.broadcast(deployerOptimism);
needsUpdateGuard = new NeedsUpdateGuard(verifierModule); // deployer nonce 3
label(address(needsUpdateGuard), 'NeedsUpdateGuard');

// set up non home chain safe
address[] memory _nonHomeChainSafeOwners = new address[](1);
_nonHomeChainSafeOwners[0] = nonHomeChainSafeOwner;
vm.prank(nonHomeChainSafeOwner); // nonHomeChainSafeOwner nonce 1

vm.broadcast(nonHomeChainSafeOwnerKey); // nonHomeChainSafeOwner nonce 1
nonHomeChainSafe.setup(
_nonHomeChainSafeOwners, 1, address(nonHomeChainSafe), bytes(''), address(0), address(0), 0, payable(0)
);

// enable verifier module
enableModule(nonHomeChainSafe, nonHomeChainSafeOwnerKey, address(verifierModule));

// data to sign and send to set the guard
_setGuardData = abi.encodeWithSelector(ISafe.setGuard.selector, address(needsUpdateGuard));
_setGuardEncodedTxData = nonHomeChainSafe.encodeTransactionData(
address(nonHomeChainSafe),
0,
_setGuardData,
Enum.Operation.Call,
0,
0,
0,
address(0),
payable(0),
nonHomeChainSafe.nonce()
);

// signature
(_v, _r, _s) = vm.sign(nonHomeChainSafeOwnerKey, keccak256(_setGuardEncodedTxData));
_setGuardSignature = abi.encodePacked(_r, _s, _v);

// set needs update guard
vm.broadcast(nonHomeChainSafeOwnerKey);
nonHomeChainSafe.execTransaction(
address(nonHomeChainSafe),
0,
_setGuardData,
Enum.Operation.Call,
0,
0,
0,
address(0),
payable(0),
_setGuardSignature
);
}

/**
* @notice Enables a module for the given safe
* @param _safe The safe that will enable the module
* @param _safeOwner The address of the owner of the safe
* @param _safeOwnerKey The private key to sign the tx
* @param _module The module address to enable
*/
function enableModule(ISafe _safe, address _safeOwner, uint256 _safeOwnerKey, address _module) public {
function enableModule(ISafe _safe, uint256 _safeOwnerKey, address _module) public {
uint256 _safeNonce = _safe.nonce();
// data to sign to enable module
bytes memory _enableModuleData = abi.encodeWithSelector(ISafe.enableModule.selector, address(_module));
Expand All @@ -157,7 +223,7 @@ contract CommonE2EBase is DSTestPlus, TestConstants {
bytes memory _enableModuleSignature = abi.encodePacked(_r, _s, _v);

// execute enable module
vm.prank(_safeOwner);
vm.broadcast(safeOwnerKey);
_safe.execTransaction(
address(_safe), 0, _enableModuleData, Enum.Operation.Call, 0, 0, 0, address(0), payable(0), _enableModuleSignature
);
Expand Down
Loading

0 comments on commit 82bb744

Please sign in to comment.