Commit d699c21e authored by Mark Tyneway's avatar Mark Tyneway

contracts-bedrock: delete deploy config type

The deploy config type in typescript is no longer required. The
Go version should now be considered the canonical version. This is
prep work for creating validation scripts in `op-chain-ops` for
deploy config JSON files that are added to the repository to
ensure that they have sane values, are not missing any values
and do not have any extra values.

This will make it easier to add or remove values over time,
reducing the number of implementations from 3 to 2 as there
is also a solidity version. The solidity version currently
has additional config that is not in the Go version, a follow
up PR will add those values to the Go version.

Also remove `ethers` from `contracts-bedrock` as a dep as
it is no longer needed.
parent 6762d023
......@@ -27,25 +27,52 @@ var (
ErrInvalidImmutablesConfig = errors.New("invalid immutables config")
)
// DeployConfig represents the deployment configuration for Optimism
// DeployConfig represents the deployment configuration for an OP Stack chain.
type DeployConfig struct {
// L1StartingBlockTag is used to fill in the storage of the L1Block info predeploy. The rollup
// config script uses this to fill the L1 genesis info for the rollup. The Output oracle deploy
// script may use it if the L2 starting timestamp is nil, assuming the L2 genesis is set up
// with this.
L1StartingBlockTag *MarshalableRPCBlockNumberOrHash `json:"l1StartingBlockTag"`
L1ChainID uint64 `json:"l1ChainID"`
L2ChainID uint64 `json:"l2ChainID"`
L2BlockTime uint64 `json:"l2BlockTime"`
FinalizationPeriodSeconds uint64 `json:"finalizationPeriodSeconds"`
MaxSequencerDrift uint64 `json:"maxSequencerDrift"`
SequencerWindowSize uint64 `json:"sequencerWindowSize"`
ChannelTimeout uint64 `json:"channelTimeout"`
P2PSequencerAddress common.Address `json:"p2pSequencerAddress"`
BatchInboxAddress common.Address `json:"batchInboxAddress"`
BatchSenderAddress common.Address `json:"batchSenderAddress"`
L2OutputOracleSubmissionInterval uint64 `json:"l2OutputOracleSubmissionInterval"`
L2OutputOracleStartingTimestamp int `json:"l2OutputOracleStartingTimestamp"`
L2OutputOracleProposer common.Address `json:"l2OutputOracleProposer"`
L2OutputOracleChallenger common.Address `json:"l2OutputOracleChallenger"`
// L1ChainID is the chain ID of the L1 chain.
L1ChainID uint64 `json:"l1ChainID"`
// L2ChainID is the chain ID of the L2 chain.
L2ChainID uint64 `json:"l2ChainID"`
// L2BlockTime is the number of seconds between each L2 block.
L2BlockTime uint64 `json:"l2BlockTime"`
// FinalizationPeriodSeconds represents the number of seconds before an output is considered
// finalized. This impacts the amount of time that withdrawals take to finalize and is
// generally set to 1 week.
FinalizationPeriodSeconds uint64 `json:"finalizationPeriodSeconds"`
// MaxSequencerDrift is the number of seconds after the L1 timestamp of the end of the
// sequencing window that batches must be included, otherwise L2 blocks including
// deposits are force included.
MaxSequencerDrift uint64 `json:"maxSequencerDrift"`
// SequencerWindowSize is the number of L1 blocks per sequencing window.
SequencerWindowSize uint64 `json:"sequencerWindowSize"`
// ChannelTimeout is the number of L1 blocks that a frame stays valid when included in L1.
ChannelTimeout uint64 `json:"channelTimeout"`
// P2PSequencerAddress is the address of the key the sequencer uses to sign blocks on the P2P layer.
P2PSequencerAddress common.Address `json:"p2pSequencerAddress"`
// BatchInboxAddress is the L1 account that batches are sent to.
BatchInboxAddress common.Address `json:"batchInboxAddress"`
// BatchSenderAddress represents the initial sequencer account that authorizes batches.
// Transactions sent from this account to the batch inbox address are considered valid.
BatchSenderAddress common.Address `json:"batchSenderAddress"`
// L2OutputOracleSubmissionInterval is the number of L2 blocks between outputs that are submitted
// to the L2OutputOracle contract located on L1.
L2OutputOracleSubmissionInterval uint64 `json:"l2OutputOracleSubmissionInterval"`
// L2OutputOracleStartingTimestamp is the starting timestamp for the L2OutputOracle.
// MUST be the same as the timestamp of the L2OO start block.
L2OutputOracleStartingTimestamp int `json:"l2OutputOracleStartingTimestamp"`
// L2OutputOracleStartingBlockNumber is the starting block number for the L2OutputOracle.
// Must be greater than or equal to the first Bedrock block. The first L2 output will correspond
// to this value plus the submission interval.
L2OutputOracleStartingBlockNumber uint64 `json:"l2OutputOracleStartingBlockNumber"`
// L2OutputOracleProposer is the address of the account that proposes L2 outputs.
L2OutputOracleProposer common.Address `json:"l2OutputOracleProposer"`
// L2OutputOracleChallenger is the address of the account that challenges L2 outputs.
L2OutputOracleChallenger common.Address `json:"l2OutputOracleChallenger"`
L1BlockTime uint64 `json:"l1BlockTime"`
L1GenesisBlockTimestamp hexutil.Uint64 `json:"l1GenesisBlockTimestamp"`
......@@ -69,64 +96,76 @@ type DeployConfig struct {
L2GenesisBlockParentHash common.Hash `json:"l2GenesisBlockParentHash"`
L2GenesisBlockBaseFeePerGas *hexutil.Big `json:"l2GenesisBlockBaseFeePerGas"`
// Seconds after genesis block that Regolith hard fork activates. 0 to activate at genesis. Nil to disable regolith
// L2GenesisRegolithTimeOffset is the number of seconds after genesis block that Regolith hard fork activates.
// Set it to 0 to activate at genesis. Nil to disable regolith.
L2GenesisRegolithTimeOffset *hexutil.Uint64 `json:"l2GenesisRegolithTimeOffset,omitempty"`
// Configurable extradata. Will default to []byte("BEDROCK") if left unspecified.
L2GenesisBlockExtraData []byte `json:"l2GenesisBlockExtraData"`
// Owner of the ProxyAdmin predeploy
// ProxyAdminOwner represents the owner of the ProxyAdmin predeploy on L2.
ProxyAdminOwner common.Address `json:"proxyAdminOwner"`
// Owner of the system on L1
// FinalSystemOwner is the owner of the system on L1. Any L1 contract that is ownable has
// this account set as its owner.
FinalSystemOwner common.Address `json:"finalSystemOwner"`
// GUARDIAN account in the OptimismPortal
// PortalGuardian represents the GUARDIAN account in the OptimismPortal. Has the ability to pause withdrawals.
PortalGuardian common.Address `json:"portalGuardian"`
// L1 recipient of fees accumulated in the BaseFeeVault
// BaseFeeVaultRecipient represents the recipient of fees accumulated in the BaseFeeVault.
// Can be an account on L1 or L2, depending on the BaseFeeVaultWithdrawalNetwork value.
BaseFeeVaultRecipient common.Address `json:"baseFeeVaultRecipient"`
// L1 recipient of fees accumulated in the L1FeeVault
// L1FeeVaultRecipient represents the recipient of fees accumulated in the L1FeeVault.
// Can be an account on L1 or L2, depending on the L1FeeVaultWithdrawalNetwork value.
L1FeeVaultRecipient common.Address `json:"l1FeeVaultRecipient"`
// L1 recipient of fees accumulated in the SequencerFeeVault
// SequencerFeeVaultRecipient represents the recipient of fees accumulated in the SequencerFeeVault.
// Can be an account on L1 or L2, depending on the SequencerFeeVaultWithdrawalNetwork value.
SequencerFeeVaultRecipient common.Address `json:"sequencerFeeVaultRecipient"`
// Minimum withdrawal amount for the BaseFeeVault
// BaseFeeVaultMinimumWithdrawalAmount represents the minimum withdrawal amount for the BaseFeeVault.
BaseFeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"baseFeeVaultMinimumWithdrawalAmount"`
// Minimum withdrawal amount for the L1FeeVault
// L1FeeVaultMinimumWithdrawalAmount represents the minimum withdrawal amount for the L1FeeVault.
L1FeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"l1FeeVaultMinimumWithdrawalAmount"`
// Minimum withdrawal amount for the SequencerFeeVault
// SequencerFeeVaultMinimumWithdrawalAmount represents the minimum withdrawal amount for the SequencerFeeVault.
SequencerFeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"sequencerFeeVaultMinimumWithdrawalAmount"`
// Withdrawal network for the BaseFeeVault
// BaseFeeVaultWithdrawalNetwork represents the withdrawal network for the BaseFeeVault.
BaseFeeVaultWithdrawalNetwork uint8 `json:"baseFeeVaultWithdrawalNetwork"`
// Withdrawal network for the L1FeeVault
// L1FeeVaultWithdrawalNetwork represents the withdrawal network for the L1FeeVault.
L1FeeVaultWithdrawalNetwork uint8 `json:"l1FeeVaultWithdrawalNetwork"`
// Withdrawal network for the SequencerFeeVault
// SequencerFeeVaultWithdrawalNetwork represents the withdrawal network for the SequencerFeeVault.
SequencerFeeVaultWithdrawalNetwork uint8 `json:"sequencerFeeVaultWithdrawalNetwork"`
// L1StandardBridge proxy address on L1
// L1StandardBridgeProxy represents the address of the L1StandardBridgeProxy on L1 and is used
// as part of building the L2 genesis state.
L1StandardBridgeProxy common.Address `json:"l1StandardBridgeProxy"`
// L1CrossDomainMessenger proxy address on L1
// L1CrossDomainMessengerProxy represents the address of the L1CrossDomainMessengerProxy on L1 and is used
// as part of building the L2 genesis state.
L1CrossDomainMessengerProxy common.Address `json:"l1CrossDomainMessengerProxy"`
// L1ERC721Bridge proxy address on L1
// L1ERC721Bridge represents the address of the L1ERC721Bridge on L1 and is used
// as part of building the L2 genesis state.
L1ERC721BridgeProxy common.Address `json:"l1ERC721BridgeProxy"`
// SystemConfig proxy address on L1
// SystemConfigProxy represents the address of the SystemConfigProxy on L1 and is used
// as part of the derivation pipeline.
SystemConfigProxy common.Address `json:"systemConfigProxy"`
// OptimismPortal proxy address on L1
// OptimismPortalProxy represents the address of the OptimismPortalProxy on L1 and is used
// as part of the derivation pipeline.
OptimismPortalProxy common.Address `json:"optimismPortalProxy"`
// The initial value of the gas overhead
// GasPriceOracleOverhead represents the initial value of the gas overhead in the GasPriceOracle predeploy.
GasPriceOracleOverhead uint64 `json:"gasPriceOracleOverhead"`
// The initial value of the gas scalar
// GasPriceOracleScalar represents the initial value of the gas scalar in the GasPriceOracle predeploy.
GasPriceOracleScalar uint64 `json:"gasPriceOracleScalar"`
// Whether or not include governance token predeploy
// EnableGovernance configures whether or not include governance token predeploy.
EnableGovernance bool `json:"enableGovernance"`
// The ERC20 symbol of the GovernanceToken
// GovernanceTokenSymbol represents the ERC20 symbol of the GovernanceToken.
GovernanceTokenSymbol string `json:"governanceTokenSymbol"`
// The ERC20 name of the GovernanceToken
// GovernanceTokenName represents the ERC20 name of the GovernanceToken
GovernanceTokenName string `json:"governanceTokenName"`
// The owner of the GovernanceToken
// GovernanceTokenOwner represents the owner of the GovernanceToken. Has the ability
// to mint and burn tokens.
GovernanceTokenOwner common.Address `json:"governanceTokenOwner"`
// DeploymentWaitConfirmations is the number of confirmations to wait during
// deployment. This is DEPRECATED and should be removed in a future PR.
DeploymentWaitConfirmations int `json:"deploymentWaitConfirmations"`
EIP1559Elasticity uint64 `json:"eip1559Elasticity"`
// EIP1559Elasticity is the elasticity of the EIP1559 fee market.
EIP1559Elasticity uint64 `json:"eip1559Elasticity"`
// EIP1559BaseFeeMaxChangeDenominator is the denominator of EIP1559 base fee market.
EIP1559Denominator uint64 `json:"eip1559Denominator"`
// FundDevAccounts configures whether or not to fund the dev accounts. Should only be used
// during devnet deployments.
FundDevAccounts bool `json:"fundDevAccounts"`
}
......@@ -147,6 +186,9 @@ func (d *DeployConfig) Check() error {
if d.FinalizationPeriodSeconds == 0 {
return fmt.Errorf("%w: FinalizationPeriodSeconds cannot be 0", ErrInvalidDeployConfig)
}
if d.L2OutputOracleStartingBlockNumber == 0 {
log.Warn("L2OutputOracleStartingBlockNumber is 0, should only be 0 for fresh chains")
}
if d.PortalGuardian == (common.Address{}) {
return fmt.Errorf("%w: PortalGuardian cannot be address(0)", ErrInvalidDeployConfig)
}
......
......@@ -40,8 +40,7 @@
"@openzeppelin/contracts": "4.7.3",
"@openzeppelin/contracts-upgradeable": "4.7.3",
"@rari-capital/solmate": "github:transmissions11/solmate#8f9b23f8838670afda0fd8983f2c41e8037ae6bc",
"clones-with-immutable-args": "github:Saw-mon-and-Natalie/clones-with-immutable-args#105efee1b9127ed7f6fedf139e1fc796ce8791f2",
"ethers": "^5.7.0"
"clones-with-immutable-args": "github:Saw-mon-and-Natalie/clones-with-immutable-args#105efee1b9127ed7f6fedf139e1fc796ce8791f2"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.60.1",
......
import { ethers } from 'ethers'
/**
* Core required deployment configuration.
*/
interface RequiredDeployConfig {
/**
* Number of confirmations to wait when deploying contracts.
*/
numDeployConfirmations?: number
/**
* Address that will own the entire system on L1 when the deploy is complete.
*/
finalSystemOwner?: string
/**
* Address that is deployed as the GUARDIAN in the OptimismPortal. Has the
* ability to pause withdrawals.
*/
portalGuardian: string
/**
* Address that will own the entire system on L1 during the deployment process. This address will
* not own the system after the deployment is complete, ownership will be transferred to the
* final system owner.
*/
controller?: string
/**
* The L2 genesis script uses this to fill the storage of the L1Block info predeploy. The rollup
* config script uses this to fill the L1 genesis info for the rollup. The Output oracle deploy
* script may use it if the L2 starting timestamp is undefined, assuming the L2 genesis is set up
* with this.
*/
l1StartingBlockTag: string
/**
* Chain ID for the L1 network.
*/
l1ChainID: number
/**
* Chain ID for the L2 network.
*/
l2ChainID: number
/**
* Number of seconds in between each L2 block.
*/
l2BlockTime: number
/**
* Sequencer batches may not be more than maxSequencerDrift seconds after the L1 timestamp of the
* end of the sequencing window end.
*/
maxSequencerDrift: number
/**
* Number of L1 blocks per sequencing window.
*/
sequencerWindowSize: number
/**
* Number of L1 blocks that a frame stays valid when included in L1.
*/
channelTimeout: number
/**
* Address of the key the sequencer uses to sign blocks on the P2P layer.
*/
p2pSequencerAddress: string
/**
* L1 address that batches are sent to.
*/
batchInboxAddress: string
/**
* Acceptable batch-sender address, to filter transactions going into the batchInboxAddress on L1 for data.
* Warning: this address is hardcoded now, but is intended to become governed via L1.
*/
batchSenderAddress: string
/**
* Output Oracle submission interval in L2 blocks.
*/
l2OutputOracleSubmissionInterval: number
/**
* Starting block number for the output oracle.
* Must be greater than or equal to the first Bedrock block. The first L2 output will correspond
* to this value plus the submission interval.
*/
l2OutputOracleStartingBlockNumber?: number
/**
* Starting timestamp for the output oracle.
* MUST be the same as the timestamp of the L2OO start block.
*/
l2OutputOracleStartingTimestamp?: number
/**
* Address of the L2 output oracle proposer.
*/
l2OutputOracleProposer: string
/**
* Address of the L2 output oracle challenger.
*/
l2OutputOracleChallenger: string
/**
* Whether to enable governance token predeploy.
*/
enableGovernance: boolean
/**
* ERC20 symbol used for the L2 GovernanceToken.
*/
governanceTokenSymbol: string
/**
* ERC20 name used for the L2 GovernanceToken.
*/
governanceTokenName: string
/**
* Owner of the L2 GovernanceToken. Has mint/burn capability.
*/
governanceTokenOwner: string
/**
* Output finalization period in seconds.
*/
finalizationPeriodSeconds: number
/**
* Owner of the ProxyAdmin contract.
*/
proxyAdminOwner: string
/**
* L1 or higher (e.g. L2) address which receives the base fee for the L2 network.
*/
baseFeeVaultRecipient: string
/**
* L1 or higher (e.g. L2) address which receives data fees for the L2 network.
*/
l1FeeVaultRecipient: string
/**
* L1 or higher (e.g. L2) address which receives tip fees for the L2 network.
*/
sequencerFeeVaultRecipient: string
/**
* Minimum withdrawal amount for the BaseFeeVault contract.
*/
baseFeeVaultMinimumWithdrawalAmount: string
/**
* Minimum withdrawal amount for the L1FeeVault contract.
*/
l1FeeVaultMinimumWithdrawalAmount: string
/**
* Minimum withdrawal amount for the SequencerFeeVault contract.
*/
sequencerFeeVaultMinimumWithdrawalAmount: string
/**
* The network that BaseFeeVault contract withdrawals are sent to.
*/
baseFeeVaultWithdrawalNetwork: number
/**
* The network that L1FeeVault contract withdrawals are sent to.
*/
l1FeeVaultWithdrawalNetwork: number
/**
* The network that SequencerFeeVault contract withdrawals are sent to.
*/
sequencerFeeVaultWithdrawalNetwork: number
}
/**
* Optional deployment configuration when spinning up an L1 network as part of the deployment.
*/
interface OptionalL1DeployConfig {
cliqueSignerAddress: string
l1BlockTime: number
l1GenesisBlockNonce: string
l1GenesisBlockGasLimit: string
l1GenesisBlockDifficulty: string
l1GenesisBlockMixHash: string
l1GenesisBlockCoinbase: string
l1GenesisBlockNumber: string
l1GenesisBlockGasUsed: string
l1GenesisBlockParentHash: string
l1GenesisBlockBaseFeePerGas: string
faultGameAbsolutePrestate: number
faultGameMaxDepth: number
}
/**
* Optional deployment configuration when spinning up an L2 network as part of the deployment.
*/
interface OptionalL2DeployConfig {
l2GenesisBlockNonce: string
l2GenesisBlockGasLimit: string
l2GenesisBlockDifficulty: string
l2GenesisBlockMixHash: string
l2GenesisBlockNumber: string
l2GenesisBlockGasUsed: string
l2GenesisBlockParentHash: string
l2GenesisBlockBaseFeePerGas: string
l2GenesisBlockCoinbase: string
l2GenesisRegolithTimeOffset: string
eip1559Denominator: number
eip1559Elasticity: number
gasPriceOracleOverhead: number
gasPriceOracleScalar: number
}
/**
* Full deployment configuration.
*/
export type DeployConfig = RequiredDeployConfig &
Partial<OptionalL1DeployConfig> &
Partial<OptionalL2DeployConfig>
/**
* Deployment configuration specification for the hardhat plugin.
*/
export const deployConfigSpec: {
[K in keyof DeployConfig]: {
type: 'string' | 'number' | 'boolean' | 'address'
default?: any
}
} = {
numDeployConfirmations: {
type: 'number',
default: 1,
},
finalSystemOwner: {
type: 'address',
},
portalGuardian: {
type: 'address',
},
controller: {
type: 'address',
},
l1StartingBlockTag: {
type: 'string',
},
l1ChainID: {
type: 'number',
},
l2ChainID: {
type: 'number',
},
l2BlockTime: {
type: 'number',
},
maxSequencerDrift: {
type: 'number',
},
sequencerWindowSize: {
type: 'number',
},
channelTimeout: {
type: 'number',
},
p2pSequencerAddress: {
type: 'address',
},
batchInboxAddress: {
type: 'address',
},
batchSenderAddress: {
type: 'address',
},
l2OutputOracleSubmissionInterval: {
type: 'number',
},
l2OutputOracleStartingBlockNumber: {
type: 'number',
default: 0,
},
l2OutputOracleStartingTimestamp: {
type: 'number',
},
l2OutputOracleProposer: {
type: 'address',
},
l2OutputOracleChallenger: {
type: 'address',
},
finalizationPeriodSeconds: {
type: 'number',
default: 2,
},
proxyAdminOwner: {
type: 'address',
},
baseFeeVaultRecipient: {
type: 'address',
},
l1FeeVaultRecipient: {
type: 'address',
},
sequencerFeeVaultRecipient: {
type: 'address',
},
baseFeeVaultMinimumWithdrawalAmount: {
type: 'string',
default: '0x8ac7230489e80000', // 10 ether
},
l1FeeVaultMinimumWithdrawalAmount: {
type: 'string',
default: '0x8ac7230489e80000', // 10 ether
},
sequencerFeeVaultMinimumWithdrawalAmount: {
type: 'string',
default: '0x8ac7230489e80000', // 10 ether
},
baseFeeVaultWithdrawalNetwork: {
type: 'number',
},
l1FeeVaultWithdrawalNetwork: {
type: 'number',
},
sequencerFeeVaultWithdrawalNetwork: {
type: 'number',
},
cliqueSignerAddress: {
type: 'address',
default: ethers.constants.AddressZero,
},
l1BlockTime: {
type: 'number',
default: 15,
},
l1GenesisBlockNonce: {
type: 'string', // uint64
default: '0x0',
},
l1GenesisBlockGasLimit: {
type: 'string',
default: ethers.BigNumber.from(15_000_000).toHexString(),
},
l1GenesisBlockDifficulty: {
type: 'string', // uint256
default: '0x1',
},
l1GenesisBlockMixHash: {
type: 'string', // bytes32
default: ethers.constants.HashZero,
},
l1GenesisBlockCoinbase: {
type: 'address',
default: ethers.constants.AddressZero,
},
l1GenesisBlockNumber: {
type: 'string', // uint64
default: '0x0',
},
l1GenesisBlockGasUsed: {
type: 'string', // uint64
default: '0x0',
},
l1GenesisBlockParentHash: {
type: 'string', // bytes32
default: ethers.constants.HashZero,
},
l1GenesisBlockBaseFeePerGas: {
type: 'string', // uint256
default: ethers.BigNumber.from(1000_000_000).toHexString(), // 1 gwei
},
l2GenesisBlockNonce: {
type: 'string', // uint64
default: '0x0',
},
l2GenesisBlockGasLimit: {
type: 'string',
default: ethers.BigNumber.from(15_000_000).toHexString(),
},
l2GenesisBlockDifficulty: {
type: 'string', // uint256
default: '0x1',
},
l2GenesisBlockMixHash: {
type: 'string', // bytes32
default: ethers.constants.HashZero,
},
l2GenesisBlockNumber: {
type: 'string', // uint64
default: '0x0',
},
l2GenesisBlockGasUsed: {
type: 'string', // uint64
default: '0x0',
},
l2GenesisBlockParentHash: {
type: 'string', // bytes32
default: ethers.constants.HashZero,
},
l2GenesisBlockBaseFeePerGas: {
type: 'string', // uint256
default: ethers.BigNumber.from(1000_000_000).toHexString(), // 1 gwei
},
gasPriceOracleOverhead: {
type: 'number',
default: 2100,
},
gasPriceOracleScalar: {
type: 'number',
default: 1_000_000,
},
enableGovernance: {
type: 'boolean',
default: false,
},
governanceTokenSymbol: {
type: 'string',
},
governanceTokenName: {
type: 'string',
},
governanceTokenOwner: {
type: 'string',
},
}
......@@ -264,9 +264,6 @@ importers:
clones-with-immutable-args:
specifier: github:Saw-mon-and-Natalie/clones-with-immutable-args#105efee1b9127ed7f6fedf139e1fc796ce8791f2
version: github.com/Saw-mon-and-Natalie/clones-with-immutable-args/105efee1b9127ed7f6fedf139e1fc796ce8791f2
ethers:
specifier: ^5.7.0
version: 5.7.1
devDependencies:
'@typescript-eslint/eslint-plugin':
specifier: ^5.60.1
......
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