Commit f9bd0cbc authored by Mark Tyneway's avatar Mark Tyneway

contracts-bedrock: delete multisig package for upgrade

This package is no longer necessary because it was used for the
upgrade to bedrock. It contained a bunch of gnosis safe tx builder
compliant json files. It is nice to have them in the history but
we do not need to keep them around after the upgrade.
parent a1cbc77d
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { console2 } from "forge-std/console2.sol";
import { Script } from "forge-std/Script.sol";
import { StdAssertions } from "forge-std/StdAssertions.sol";
/**
* @title BedrockMigrationChecker
* @notice A script to check safety of multisig operations for Bedrock.
* The usage is as follows:
* $ forge script scripts/CheckForBedrockMigration.s.sol \
* --rpc-url $ETH_RPC_URL
*/
contract BedrockMigrationChecker is Script, StdAssertions {
struct ContractSet {
// Please keep these sorted by name.
address AddressManager;
address L1CrossDomainMessengerImpl;
address L1CrossDomainMessengerProxy;
address L1ERC721BridgeImpl;
address L1ERC721BridgeProxy;
address L1ProxyAdmin;
address L1StandardBridgeImpl;
address L1StandardBridgeProxy;
address L1UpgradeKey;
address L2OutputOracleImpl;
address L2OutputOracleProxy;
address OptimismMintableERC20FactoryImpl;
address OptimismMintableERC20FactoryProxy;
address OptimismPortalImpl;
address OptimismPortalProxy;
address PortalSender;
address SystemConfigProxy;
address SystemDictatorImpl;
address SystemDictatorProxy;
}
/**
* @notice The entrypoint function.
*/
function run() external {
string memory bedrockJsonDir = vm.envString("BEDROCK_JSON_DIR");
console2.log("BEDROCK_JSON_DIR = %s", bedrockJsonDir);
ContractSet memory contracts = getContracts(bedrockJsonDir);
checkAddressManager(contracts);
checkL1CrossDomainMessengerImpl(contracts);
checkL1CrossDomainMessengerProxy(contracts);
checkL1ERC721BridgeImpl(contracts);
checkL1ERC721BridgeProxy(contracts);
checkL1ProxyAdmin(contracts);
checkL1StandardBridgeImpl(contracts);
checkL1StandardBridgeProxy(contracts);
checkL1UpgradeKey(contracts);
checkL2OutputOracleImpl(contracts);
checkL2OutputOracleProxy(contracts);
checkOptimismMintableERC20FactoryImpl(contracts);
checkOptimismMintableERC20FactoryProxy(contracts);
checkOptimismPortalImpl(contracts);
checkOptimismPortalProxy(contracts);
checkPortalSender(contracts);
checkSystemConfigProxy(contracts);
checkSystemDictatorImpl(contracts);
checkSystemDictatorProxy(contracts);
}
function checkAddressManager(ContractSet memory contracts) internal {
console2.log("Checking AddressManager %s", contracts.AddressManager);
checkAddressIsExpected(contracts.L1UpgradeKey, contracts.AddressManager, "owner()");
}
function checkL1CrossDomainMessengerImpl(ContractSet memory contracts) internal {
console2.log("Checking L1CrossDomainMessenger %s", contracts.L1CrossDomainMessengerImpl);
checkAddressIsExpected(contracts.OptimismPortalProxy, contracts.L1CrossDomainMessengerImpl, "PORTAL()");
}
function checkL1CrossDomainMessengerProxy(ContractSet memory contracts) internal {
console2.log("Checking L1CrossDomainMessengerProxy %s", contracts.L1CrossDomainMessengerProxy);
checkAddressIsExpected(contracts.L1UpgradeKey, contracts.L1CrossDomainMessengerProxy, "owner()");
checkAddressIsExpected(contracts.AddressManager, contracts.L1CrossDomainMessengerProxy, "libAddressManager()");
}
function checkL1ERC721BridgeImpl(ContractSet memory contracts) internal {
console2.log("Checking L1ERC721Bridge %s", contracts.L1ERC721BridgeImpl);
checkAddressIsExpected(contracts.L1CrossDomainMessengerProxy, contracts.L1ERC721BridgeImpl, "messenger()");
}
function checkL1ERC721BridgeProxy(ContractSet memory contracts) internal {
console2.log("Checking L1ERC721BridgeProxy %s", contracts.L1ERC721BridgeProxy);
checkAddressIsExpected(contracts.L1UpgradeKey, contracts.L1ERC721BridgeProxy, "admin()");
checkAddressIsExpected(contracts.L1CrossDomainMessengerProxy, contracts.L1ERC721BridgeProxy, "messenger()");
}
function checkL1ProxyAdmin(ContractSet memory contracts) internal {
console2.log("Checking L1ProxyAdmin %s", contracts.L1ProxyAdmin);
checkAddressIsExpected(contracts.L1UpgradeKey, contracts.L1ProxyAdmin, "owner()");
}
function checkL1StandardBridgeImpl(ContractSet memory contracts) internal {
console2.log("Checking L1StandardBridge %s", contracts.L1StandardBridgeImpl);
checkAddressIsExpected(contracts.L1CrossDomainMessengerProxy, contracts.L1StandardBridgeImpl, "messenger()");
}
function checkL1StandardBridgeProxy(ContractSet memory contracts) internal {
console2.log("Checking L1StandardBridgeProxy %s", contracts.L1StandardBridgeProxy);
checkAddressIsExpected(contracts.L1UpgradeKey, contracts.L1StandardBridgeProxy, "getOwner()");
checkAddressIsExpected(contracts.L1CrossDomainMessengerProxy, contracts.L1StandardBridgeProxy, "messenger()");
}
function checkL1UpgradeKey(ContractSet memory contracts) internal {
console2.log("Checking L1UpgradeKeyAddress %s", contracts.L1UpgradeKey);
// No need to check anything here, so just printing the address.
}
function checkL2OutputOracleImpl(ContractSet memory contracts) internal {
console2.log("Checking L2OutputOracle %s", contracts.L2OutputOracleImpl);
checkAddressIsExpected(contracts.L1UpgradeKey, contracts.L2OutputOracleImpl, "CHALLENGER()");
// 604800 seconds = 7 days, reusing the logic in
// checkAddressIsExpected for simplicity.
checkAddressIsExpected(address(604800), contracts.L2OutputOracleImpl, "FINALIZATION_PERIOD_SECONDS()");
}
function checkL2OutputOracleProxy(ContractSet memory contracts) internal {
console2.log("Checking L2OutputOracleProxy %s", contracts.L2OutputOracleProxy);
checkAddressIsExpected(contracts.L1ProxyAdmin, contracts.L2OutputOracleProxy, "admin()");
}
function checkOptimismMintableERC20FactoryImpl(ContractSet memory contracts) internal {
console2.log("Checking OptimismMintableERC20Factory %s", contracts.OptimismMintableERC20FactoryImpl);
checkAddressIsExpected(contracts.L1StandardBridgeProxy, contracts.OptimismMintableERC20FactoryImpl, "BRIDGE()");
}
function checkOptimismMintableERC20FactoryProxy(ContractSet memory contracts) internal {
console2.log("Checking OptimismMintableERC20FactoryProxy %s", contracts.OptimismMintableERC20FactoryProxy);
checkAddressIsExpected(contracts.L1ProxyAdmin, contracts.OptimismMintableERC20FactoryProxy, "admin()");
}
function checkOptimismPortalImpl(ContractSet memory contracts) internal {
console2.log("Checking OptimismPortal %s", contracts.OptimismPortalImpl);
checkAddressIsExpected(contracts.L2OutputOracleProxy, contracts.OptimismPortalImpl, "L2_ORACLE()");
}
function checkOptimismPortalProxy(ContractSet memory contracts) internal {
console2.log("Checking OptimismPortalProxy %s", contracts.OptimismPortalProxy);
checkAddressIsExpected(contracts.L1ProxyAdmin, contracts.OptimismPortalProxy, "admin()");
}
function checkPortalSender(ContractSet memory contracts) internal {
console2.log("Checking PortalSender %s", contracts.PortalSender);
checkAddressIsExpected(contracts.OptimismPortalProxy, contracts.PortalSender, "PORTAL()");
}
function checkSystemConfigProxy(ContractSet memory contracts) internal {
console2.log("Checking SystemConfigProxy %s", contracts.SystemConfigProxy);
checkAddressIsExpected(contracts.L1ProxyAdmin, contracts.SystemConfigProxy, "admin()");
}
function checkSystemDictatorImpl(ContractSet memory contracts) internal {
console2.log("Checking SystemDictator %s", contracts.SystemDictatorImpl);
checkAddressIsExpected(address(0), contracts.SystemDictatorImpl, "owner()");
}
function checkSystemDictatorProxy(ContractSet memory contracts) internal {
console2.log("Checking SystemDictatorProxy %s", contracts.SystemDictatorProxy);
checkAddressIsExpected(contracts.SystemDictatorImpl, contracts.SystemDictatorProxy, "implementation()");
checkAddressIsExpected(contracts.L1UpgradeKey, contracts.SystemDictatorProxy, "owner()");
checkAddressIsExpected(contracts.L1UpgradeKey, contracts.SystemDictatorProxy, "admin()");
}
function checkAddressIsExpected(address expectedAddr, address contractAddr, string memory signature) internal {
address actual = getAddressFromCall(contractAddr, signature);
if (expectedAddr != actual) {
console2.log(" !! Error: %s != %s.%s, ", expectedAddr, contractAddr, signature);
console2.log(" which is %s", actual);
} else {
console2.log(" -- Success: %s == %s.%s.", expectedAddr, contractAddr, signature);
}
}
function getAddressFromCall(address contractAddr, string memory signature) internal returns (address) {
vm.prank(address(0));
(bool success, bytes memory addrBytes) = contractAddr.staticcall(abi.encodeWithSignature(signature));
if (!success) {
console2.log(" !! Error calling %s.%s", contractAddr, signature);
return address(0);
}
return abi.decode(addrBytes, (address));
}
function getContracts(string memory bedrockJsonDir) internal returns (ContractSet memory) {
return ContractSet({
AddressManager: getAddressFromJson(string.concat(bedrockJsonDir, "/AddressManager.json")),
L1CrossDomainMessengerImpl: getAddressFromJson(string.concat(bedrockJsonDir, "/L1CrossDomainMessenger.json")),
L1CrossDomainMessengerProxy: getAddressFromJson(string.concat(bedrockJsonDir, "/L1CrossDomainMessengerProxy.json")),
L1ERC721BridgeImpl: getAddressFromJson(string.concat(bedrockJsonDir, "/L1ERC721Bridge.json")),
L1ERC721BridgeProxy: getAddressFromJson(string.concat(bedrockJsonDir, "/L1ERC721BridgeProxy.json")),
L1ProxyAdmin: getAddressFromJson(string.concat(bedrockJsonDir, "/ProxyAdmin.json")),
L1StandardBridgeImpl: getAddressFromJson(string.concat(bedrockJsonDir, "/L1StandardBridge.json")),
L1StandardBridgeProxy: getAddressFromJson(string.concat(bedrockJsonDir, "/L1StandardBridgeProxy.json")),
L1UpgradeKey: vm.envAddress("L1_UPGRADE_KEY"),
L2OutputOracleImpl: getAddressFromJson(string.concat(bedrockJsonDir, "/L2OutputOracle.json")),
L2OutputOracleProxy: getAddressFromJson(string.concat(bedrockJsonDir, "/L2OutputOracleProxy.json")),
OptimismMintableERC20FactoryImpl: getAddressFromJson(string.concat(bedrockJsonDir, "/OptimismMintableERC20Factory.json")),
OptimismMintableERC20FactoryProxy: getAddressFromJson(string.concat(bedrockJsonDir, "/OptimismMintableERC20FactoryProxy.json")),
OptimismPortalImpl: getAddressFromJson(string.concat(bedrockJsonDir, "/OptimismPortal.json")),
OptimismPortalProxy: getAddressFromJson(string.concat(bedrockJsonDir, "/OptimismPortalProxy.json")),
PortalSender: getAddressFromJson(string.concat(bedrockJsonDir, "/PortalSender.json")),
SystemConfigProxy: getAddressFromJson(string.concat(bedrockJsonDir, "/SystemConfigProxy.json")),
SystemDictatorImpl: getAddressFromJson(string.concat(bedrockJsonDir, "/SystemDictator.json")),
SystemDictatorProxy: getAddressFromJson(string.concat(bedrockJsonDir, "/SystemDictatorProxy.json"))
});
}
function getAddressFromJson(string memory jsonPath) internal returns (address) {
string memory json = vm.readFile(jsonPath);
return vm.parseJsonAddress(json, ".address");
}
}
## Multisig Operation Scripts
A collection of scripts used by multisig signers to verify the
integrity of the transactions to be signed.
### Contract Verification for Bedrock Migration
[CheckForBedrockMigration.s.sol](./CheckForBedrockMigration.s.sol) is
a script used by the Bedrock migration signers before the migration,
to verify the contracts affected by the migration are always under the
control of the multisig, and security critical configurations are
correctly initialized.
Example usage:
``` bash
git clone git@github.com:ethereum-optimism/optimism.git
cd optimism/
git pull
git checkout develop
nvm use
yarn install
yarn clean
yarn build
cd packages/contracts-bedrock
export L1_UPGRADE_KEY=0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A
export BEDROCK_JSON_DIR=deployments/mainnet
forge script scripts/multisig/CheckForBedrockMigration.s.sol --rpc-url <TRUSTWORTHY_L1_RPC_URL>
```
Expected output:
``` bash
Script ran successfully.
BEDROCK_JSON_DIR = deployments/mainnet
Checking AddressManager 0xdE1FCfB0851916CA5101820A69b13a4E276bd81F
-- Success: 0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A == 0xdE1FCfB0851916CA5101820A69b13a4E276bd81F.owner().
Checking L1CrossDomainMessenger 0x2150Bc3c64cbfDDbaC9815EF615D6AB8671bfe43
-- Success: 0xbEb5Fc579115071764c7423A4f12eDde41f106Ed == 0x2150Bc3c64cbfDDbaC9815EF615D6AB8671bfe43.PORTAL().
Checking L1CrossDomainMessengerProxy 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1
-- Success: 0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A == 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1.owner().
-- Success: 0xdE1FCfB0851916CA5101820A69b13a4E276bd81F == 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1.libAddressManager().
Checking L1ERC721Bridge 0x4afDD3A48E13B305e98D9EEad67B1b5867E370DF
-- Success: 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1 == 0x4afDD3A48E13B305e98D9EEad67B1b5867E370DF.messenger().
Checking L1ERC721BridgeProxy 0x5a7749f83b81B301cAb5f48EB8516B986DAef23D
-- Success: 0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A == 0x5a7749f83b81B301cAb5f48EB8516B986DAef23D.admin().
-- Success: 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1 == 0x5a7749f83b81B301cAb5f48EB8516B986DAef23D.messenger().
Checking L1ProxyAdmin 0x543bA4AADBAb8f9025686Bd03993043599c6fB04
-- Success: 0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB == 0x543bA4AADBAb8f9025686Bd03993043599c6fB04.owner().
Checking L1StandardBridge 0xBFB731Cd36D26c2a7287716DE857E4380C73A64a
-- Success: 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1 == 0xBFB731Cd36D26c2a7287716DE857E4380C73A64a.messenger().
Checking L1StandardBridgeProxy 0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1
-- Success: 0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A == 0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1.getOwner().
-- Success: 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1 == 0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1.messenger().
Checking L1UpgradeKeyAddress 0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A
Checking L2OutputOracle 0xd2E67B6a032F0A9B1f569E63ad6C38f7342c2e00
-- Success: 0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A == 0xd2E67B6a032F0A9B1f569E63ad6C38f7342c2e00.CHALLENGER().
-- Success: 0x0000000000000000000000000000000000093A80 == 0xd2E67B6a032F0A9B1f569E63ad6C38f7342c2e00.FINALIZATION_PERIOD_SECONDS().
Checking L2OutputOracleProxy 0xdfe97868233d1aa22e815a266982f2cf17685a27
-- Success: 0x543bA4AADBAb8f9025686Bd03993043599c6fB04 == 0xdfe97868233d1aa22e815a266982f2cf17685a27.admin().
Checking OptimismMintableERC20Factory 0xaE849EFA4BcFc419593420e14707996936E365E2
-- Success: 0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1 == 0xaE849EFA4BcFc419593420e14707996936E365E2.BRIDGE().
Checking OptimismMintableERC20FactoryProxy 0x75505a97BD334E7BD3C476893285569C4136Fa0F
-- Success: 0x543bA4AADBAb8f9025686Bd03993043599c6fB04 == 0x75505a97BD334E7BD3C476893285569C4136Fa0F.admin().
Checking OptimismPortal 0x28a55488fef40005309e2DA0040DbE9D300a64AB
-- Success: 0xdfe97868233d1aa22e815a266982f2cf17685a27 == 0x28a55488fef40005309e2DA0040DbE9D300a64AB.L2_ORACLE().
Checking OptimismPortalProxy 0xbEb5Fc579115071764c7423A4f12eDde41f106Ed
-- Success: 0x543bA4AADBAb8f9025686Bd03993043599c6fB04 == 0xbEb5Fc579115071764c7423A4f12eDde41f106Ed.admin().
Checking PortalSender 0x0A893d9576b9cFD9EF78595963dc973238E78210
-- Success: 0xbEb5Fc579115071764c7423A4f12eDde41f106Ed == 0x0A893d9576b9cFD9EF78595963dc973238E78210.PORTAL().
Checking SystemConfigProxy 0x229047fed2591dbec1eF1118d64F7aF3dB9EB290
-- Success: 0x543bA4AADBAb8f9025686Bd03993043599c6fB04 == 0x229047fed2591dbec1eF1118d64F7aF3dB9EB290.admin().
Checking SystemDictator 0x09E040a72FD3492355C5aEEdbC3154075f83488a
-- Success: 0x0000000000000000000000000000000000000000 == 0x09E040a72FD3492355C5aEEdbC3154075f83488a.owner().
Checking SystemDictatorProxy 0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB
-- Success: 0x09E040a72FD3492355C5aEEdbC3154075f83488a == 0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB.implementation().
-- Success: 0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A == 0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB.owner().
-- Success: 0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A == 0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB.admin().
```
{
"version": "1.0",
"chainId": "1",
"meta": {
"name": "batch1",
"description": "ProxyAdmin.transferOwnership, AddressManager.transferOwnership, L1StandardBridgeProxy.setOwner, L1ERC721BridgeProxy.changeAdmin, and SystemDictatorProxy.phase1.",
"txBuilderVersion": "1.13.3"
},
"transactions": [
{
"to": "0x543bA4AADBAb8f9025686Bd03993043599c6fB04",
"value": "0",
"data": null,
"contractMethod": {
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"payable": false
},
"contractInputsValues": {
"newOwner": "0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB"
}
},
{
"to": "0xdE1FCfB0851916CA5101820A69b13a4E276bd81F",
"value": "0",
"data": null,
"contractMethod": {
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"payable": false
},
"contractInputsValues": {
"newOwner": "0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB"
}
},
{
"to": "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1",
"value": "0",
"data": null,
"contractMethod": {
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"name": "setOwner",
"payable": false
},
"contractInputsValues": {
"_owner": "0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB"
}
},
{
"to": "0x5a7749f83b81B301cAb5f48EB8516B986DAef23D",
"value": "0",
"data": null,
"contractMethod": {
"inputs": [
{
"internalType": "address",
"name": "_admin",
"type": "address"
}
],
"name": "changeAdmin",
"payable": false
},
"contractInputsValues": {
"_admin": "0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB"
}
},
{
"to": "0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB",
"value": "0",
"data": null,
"contractMethod": {
"inputs": [],
"name": "phase1",
"payable": false
},
"contractInputsValues": {}
}
]
}
{
"version": "1.0",
"chainId": "1",
"meta": {
"name": "batch2",
"description": "SystemDictatorProxy.updateDynamicConfig.",
"txBuilderVersion": "1.13.3"
},
"transactions": [
{
"to": "0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB",
"value": "0",
"data": null,
"contractMethod": {
"inputs": [
{
"components": [
{
"internalType": "uint256",
"name": "l2OutputOracleStartingBlockNumber",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "l2OutputOracleStartingTimestamp",
"type": "uint256"
}
],
"internalType": "struct SystemDictator.L2OutputOracleDynamicConfig",
"name": "_l2OutputOracleDynamicConfig",
"type": "tuple"
},
{
"internalType": "bool",
"name": "_optimismPortalDynamicConfig",
"type": "bool"
}
],
"name": "updateDynamicConfig",
"payable": false
},
"contractInputsValues": {
"_l2OutputOracleDynamicConfig": "[105235063, 1686068903]",
"_optimismPortalDynamicConfig": "true"
}
}
]
}
{
"version": "1.0",
"chainId": "1",
"meta": {
"name": "batch3",
"description": "SystemDictatorProxy.phase2 and OptimismPortalProxy.unpause.",
"txBuilderVersion": "1.13.3"
},
"transactions": [
{
"to": "0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB",
"value": "0",
"data": null,
"contractMethod": {
"inputs": [],
"name": "phase2",
"payable": false
},
"contractInputsValues": {}
},
{
"to": "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed",
"value": "0",
"data": null,
"contractMethod": {
"inputs": [],
"name": "unpause",
"payable": false
},
"contractInputsValues": {}
}
]
}
{
"version": "1.0",
"chainId": "1",
"meta": {
"name": "EscapeHatch",
"description": "SystemDictatorProxy.exit1 and finalize.",
"txBuilderVersion": "1.14.1"
},
"transactions": [
{
"to": "0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB",
"value": "0",
"data": null,
"contractMethod": {
"inputs": [],
"name": "exit1",
"payable": false
},
"contractInputsValues": null
},
{
"to": "0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB",
"value": "0",
"data": null,
"contractMethod": {
"inputs": [],
"name": "finalize",
"payable": false
},
"contractInputsValues": null
}
]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment