Skip to content

Commit

Permalink
set solc version of VRF V2 Plus related contracts to 0.8.19 (#12479)
Browse files Browse the repository at this point in the history
* set solc version of VRF V2 Plus related contracts to 0.8.19

* bump solc version in hardhat config for v2.5 coordinator

* fix failing tests due to gas change

* auto detect solc version for vrf contracts

* upgrade mercury registry

* add missing file

* make generate
  • Loading branch information
jinhoonbang committed Mar 20, 2024
1 parent e74aeab commit 93762cc
Show file tree
Hide file tree
Showing 54 changed files with 310 additions and 141 deletions.
5 changes: 5 additions & 0 deletions .changeset/stale-sloths-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

update solc version for vrf v2.5 coordinators
5 changes: 5 additions & 0 deletions contracts/.changeset/forty-ways-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chainlink/contracts": patch
---

upgrade solc version to 0.8.19 for vrf v2.5 contracts
3 changes: 0 additions & 3 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,14 @@ gas_price = 3_000_000_000 # 3 gwei
optimizer_runs = 1000
src = 'src/v0.8/vrf'
test = 'test/v0.8/foundry/vrf' # skips tests for no VRF foundry tests
solc_version = '0.8.6'

[profile.vrfv2plus_coordinator]
optimizer_runs = 50
src = 'src/v0.8/vrf'
solc_version = '0.8.6'

[profile.vrfv2plus]
optimizer_runs = 1_000_000
src = 'src/v0.8/vrf'
solc_version = '0.8.6'

[profile.automation]
optimizer_runs = 10000
Expand Down
2 changes: 1 addition & 1 deletion contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ let config = {
},
},
'src/v0.8/vrf/dev/VRFCoordinatorV2_5.sol': {
version: '0.8.6',
version: '0.8.19',
settings: {
optimizer: {
enabled: true,
Expand Down
53 changes: 30 additions & 23 deletions contracts/scripts/native_solc_compile_all_vrf
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ compileContract vrf/testhelpers/VRFV2RevertingExample.sol
compileContract vrf/testhelpers/VRFV2ProxyAdmin.sol
compileContract vrf/testhelpers/VRFV2TransparentUpgradeableProxy.sol
compileContract vrf/testhelpers/VRFConsumerV2UpgradeableExample.sol
compileContract vrf/BatchBlockhashStore.sol
compileContract vrf/BatchVRFCoordinatorV2.sol
compileContract vrf/testhelpers/VRFCoordinatorV2TestHelper.sol
compileContractAltOpts vrf/VRFCoordinatorV2.sol 10000
Expand All @@ -63,28 +62,6 @@ compileContract vrf/VRFOwner.sol
compileContract vrf/dev/VRFSubscriptionBalanceMonitor.sol
compileContract vrf/KeepersVRFConsumer.sol

# VRF V2Plus
compileContract vrf/dev/interfaces/IVRFCoordinatorV2PlusInternal.sol
compileContract vrf/dev/testhelpers/VRFV2PlusConsumerExample.sol
compileContractAltOpts vrf/dev/VRFCoordinatorV2_5.sol 50
compileContract vrf/dev/BatchVRFCoordinatorV2Plus.sol
compileContract vrf/dev/VRFV2PlusWrapper.sol
compileContract vrf/dev/testhelpers/VRFConsumerV2PlusUpgradeableExample.sol
compileContract vrf/dev/testhelpers/VRFMaliciousConsumerV2Plus.sol
compileContract vrf/dev/testhelpers/VRFV2PlusExternalSubOwnerExample.sol
compileContract vrf/dev/testhelpers/VRFV2PlusSingleConsumerExample.sol
compileContract vrf/dev/testhelpers/VRFV2PlusWrapperConsumerExample.sol
compileContract vrf/dev/testhelpers/VRFV2PlusRevertingExample.sol
compileContract vrf/dev/testhelpers/VRFConsumerV2PlusUpgradeableExample.sol
compileContract vrf/dev/testhelpers/VRFV2PlusMaliciousMigrator.sol
compileContract vrf/dev/libraries/VRFV2PlusClient.sol
compileContract vrf/dev/testhelpers/VRFCoordinatorV2Plus_V2Example.sol
compileContract vrf/dev/BlockhashStore.sol
compileContract vrf/dev/TrustedBlockhashStore.sol
compileContract vrf/dev/testhelpers/VRFV2PlusLoadTestWithMetrics.sol
compileContractAltOpts vrf/dev/testhelpers/VRFCoordinatorV2PlusUpgradedVersion.sol 5
compileContract vrf/dev/testhelpers/VRFV2PlusWrapperLoadTestConsumer.sol

# VRF V2 Wrapper
compileContract vrf/VRFV2Wrapper.sol
compileContract vrf/interfaces/VRFV2WrapperInterface.sol
Expand All @@ -108,3 +85,33 @@ compileContract vrf/testhelpers/VRFMockETHLINKAggregator.sol
compileContract vrf/interfaces/IAuthorizedReceiver.sol
compileContract vrf/interfaces/VRFCoordinatorV2Interface.sol
compileContract vrf/interfaces/VRFV2WrapperInterface.sol

SOLC_VERSION="0.8.19"

solc-select install $SOLC_VERSION
solc-select use $SOLC_VERSION
export SOLC_VERSION=$SOLC_VERSION

# v0.8.19
# VRF V2 Plus
compileContract vrf/dev/interfaces/IVRFCoordinatorV2PlusInternal.sol
compileContract vrf/dev/testhelpers/VRFV2PlusConsumerExample.sol
compileContractAltOpts vrf/dev/VRFCoordinatorV2_5.sol 50
compileContract vrf/dev/BatchVRFCoordinatorV2Plus.sol
compileContract vrf/dev/VRFV2PlusWrapper.sol
compileContract vrf/dev/testhelpers/VRFConsumerV2PlusUpgradeableExample.sol
compileContract vrf/dev/testhelpers/VRFMaliciousConsumerV2Plus.sol
compileContract vrf/dev/testhelpers/VRFV2PlusExternalSubOwnerExample.sol
compileContract vrf/dev/testhelpers/VRFV2PlusSingleConsumerExample.sol
compileContract vrf/dev/testhelpers/VRFV2PlusWrapperConsumerExample.sol
compileContract vrf/dev/testhelpers/VRFV2PlusRevertingExample.sol
compileContract vrf/dev/testhelpers/VRFConsumerV2PlusUpgradeableExample.sol
compileContract vrf/dev/testhelpers/VRFV2PlusMaliciousMigrator.sol
compileContract vrf/dev/libraries/VRFV2PlusClient.sol
compileContract vrf/dev/testhelpers/VRFCoordinatorV2Plus_V2Example.sol
compileContract vrf/dev/TrustedBlockhashStore.sol
compileContract vrf/dev/testhelpers/VRFV2PlusLoadTestWithMetrics.sol
compileContractAltOpts vrf/dev/testhelpers/VRFCoordinatorV2PlusUpgradedVersion.sol 5
compileContract vrf/dev/testhelpers/VRFV2PlusWrapperLoadTestConsumer.sol
compileContract vrf/BatchBlockhashStore.sol
compileContract vrf/dev/BlockhashStore.sol
12 changes: 3 additions & 9 deletions contracts/scripts/native_solc_compile_all_vrfv2plus
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ echo " ┌───────────────────────
echo " │ Compiling VRF contracts... │"
echo " └──────────────────────────────────────────────┘"

SOLC_VERSION="0.8.6"
SOLC_VERSION="0.8.19"
OPTIMIZE_RUNS=1000000

SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
Expand Down Expand Up @@ -37,13 +37,6 @@ compileContractAltOpts () {
"$ROOT"/contracts/src/v0.8/"$1"
}

# VRF
compileContract vrf/VRFRequestIDBase.sol
compileContract vrf/VRFConsumerBase.sol
compileContract vrf/testhelpers/VRFConsumer.sol
compileContract vrf/testhelpers/VRFRequestIDBaseTestHelper.sol
compileContract vrf/mocks/VRFCoordinatorMock.sol

# VRF V2Plus
compileContract vrf/dev/interfaces/IVRFCoordinatorV2PlusInternal.sol
compileContract vrf/dev/testhelpers/VRFV2PlusConsumerExample.sol
Expand All @@ -60,10 +53,11 @@ compileContract vrf/dev/testhelpers/VRFConsumerV2PlusUpgradeableExample.sol
compileContract vrf/dev/testhelpers/VRFV2PlusMaliciousMigrator.sol
compileContract vrf/dev/libraries/VRFV2PlusClient.sol
compileContract vrf/dev/testhelpers/VRFCoordinatorV2Plus_V2Example.sol
compileContract vrf/dev/BlockhashStore.sol
compileContract vrf/dev/TrustedBlockhashStore.sol
compileContract vrf/dev/testhelpers/VRFV2PlusLoadTestWithMetrics.sol
compileContractAltOpts vrf/dev/testhelpers/VRFCoordinatorV2PlusUpgradedVersion.sol 5
compileContract vrf/dev/testhelpers/VRFV2PlusWrapperLoadTestConsumer.sol
compileContract vrf/testhelpers/VRFMockETHLINKAggregator.sol
compileContract vrf/dev/testhelpers/VRFV2PlusLoadTestWithMetrics.sol
compileContract vrf/BatchBlockhashStore.sol
compileContract vrf/dev/BlockhashStore.sol
4 changes: 2 additions & 2 deletions contracts/src/v0.8/ChainSpecificUtil.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
pragma solidity ^0.8.9;

import {ArbSys} from "./vendor/@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol";
import {ArbGasInfo} from "./vendor/@arbitrum/nitro-contracts/src/precompiles/ArbGasInfo.sol";
import {OVM_GasPriceOracle} from "./vendor/@eth-optimism/contracts/v0.8.6/contracts/L2/predeploys/OVM_GasPriceOracle.sol";
import {OVM_GasPriceOracle} from "./vendor/@eth-optimism/contracts/v0.8.9/contracts/L2/predeploys/OVM_GasPriceOracle.sol";

/// @dev A library that abstracts out opcodes that behave differently across chains.
/// @dev The methods below return values that are pertinent to the given chain.
Expand Down
161 changes: 161 additions & 0 deletions contracts/src/v0.8/ChainSpecificUtil_v0_8_6.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.6;

import {ArbSys} from "./vendor/@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol";
import {ArbGasInfo} from "./vendor/@arbitrum/nitro-contracts/src/precompiles/ArbGasInfo.sol";
import {OVM_GasPriceOracle} from "./vendor/@eth-optimism/contracts/v0.8.6/contracts/L2/predeploys/OVM_GasPriceOracle.sol";

/// @dev A library that abstracts out opcodes that behave differently across chains.
/// @dev The methods below return values that are pertinent to the given chain.
/// @dev For instance, ChainSpecificUtil.getBlockNumber() returns L2 block number in L2 chains
library ChainSpecificUtil {
// ------------ Start Arbitrum Constants ------------

/// @dev ARBSYS_ADDR is the address of the ArbSys precompile on Arbitrum.
/// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbSys.sol#L10
address private constant ARBSYS_ADDR = address(0x0000000000000000000000000000000000000064);
ArbSys private constant ARBSYS = ArbSys(ARBSYS_ADDR);

/// @dev ARBGAS_ADDR is the address of the ArbGasInfo precompile on Arbitrum.
/// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbGasInfo.sol#L10
address private constant ARBGAS_ADDR = address(0x000000000000000000000000000000000000006C);
ArbGasInfo private constant ARBGAS = ArbGasInfo(ARBGAS_ADDR);

uint256 private constant ARB_MAINNET_CHAIN_ID = 42161;
uint256 private constant ARB_GOERLI_TESTNET_CHAIN_ID = 421613;
uint256 private constant ARB_SEPOLIA_TESTNET_CHAIN_ID = 421614;

// ------------ End Arbitrum Constants ------------

// ------------ Start Optimism Constants ------------
/// @dev L1_FEE_DATA_PADDING includes 35 bytes for L1 data padding for Optimism
bytes internal constant L1_FEE_DATA_PADDING =
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
/// @dev OVM_GASPRICEORACLE_ADDR is the address of the OVM_GasPriceOracle precompile on Optimism.
/// @dev reference: https://community.optimism.io/docs/developers/build/transaction-fees/#estimating-the-l1-data-fee
address private constant OVM_GASPRICEORACLE_ADDR = address(0x420000000000000000000000000000000000000F);
OVM_GasPriceOracle private constant OVM_GASPRICEORACLE = OVM_GasPriceOracle(OVM_GASPRICEORACLE_ADDR);

uint256 private constant OP_MAINNET_CHAIN_ID = 10;
uint256 private constant OP_GOERLI_CHAIN_ID = 420;
uint256 private constant OP_SEPOLIA_CHAIN_ID = 11155420;

/// @dev Base is a OP stack based rollup and follows the same L1 pricing logic as Optimism.
uint256 private constant BASE_MAINNET_CHAIN_ID = 8453;
uint256 private constant BASE_GOERLI_CHAIN_ID = 84531;

// ------------ End Optimism Constants ------------

/**
* @notice Returns the blockhash for the given blockNumber.
* @notice If the blockNumber is more than 256 blocks in the past, returns the empty string.
* @notice When on a known Arbitrum chain, it uses ArbSys.arbBlockHash to get the blockhash.
* @notice Otherwise, it uses the blockhash opcode.
* @notice Note that the blockhash opcode will return the L2 blockhash on Optimism.
*/
function _getBlockhash(uint64 blockNumber) internal view returns (bytes32) {
uint256 chainid = block.chainid;
if (_isArbitrumChainId(chainid)) {
if ((_getBlockNumber() - blockNumber) > 256 || blockNumber >= _getBlockNumber()) {
return "";
}
return ARBSYS.arbBlockHash(blockNumber);
}
return blockhash(blockNumber);
}

/**
* @notice Returns the block number of the current block.
* @notice When on a known Arbitrum chain, it uses ArbSys.arbBlockNumber to get the block number.
* @notice Otherwise, it uses the block.number opcode.
* @notice Note that the block.number opcode will return the L2 block number on Optimism.
*/
function _getBlockNumber() internal view returns (uint256) {
uint256 chainid = block.chainid;
if (_isArbitrumChainId(chainid)) {
return ARBSYS.arbBlockNumber();
}
return block.number;
}

/**
* @notice Returns the L1 fees that will be paid for the current transaction, given any calldata
* @notice for the current transaction.
* @notice When on a known Arbitrum chain, it uses ArbGas.getCurrentTxL1GasFees to get the fees.
* @notice On Arbitrum, the provided calldata is not used to calculate the fees.
* @notice On Optimism, the provided calldata is passed to the OVM_GasPriceOracle predeploy
* @notice and getL1Fee is called to get the fees.
*/
function _getCurrentTxL1GasFees(bytes memory txCallData) internal view returns (uint256) {
uint256 chainid = block.chainid;
if (_isArbitrumChainId(chainid)) {
return ARBGAS.getCurrentTxL1GasFees();
} else if (_isOptimismChainId(chainid)) {
return OVM_GASPRICEORACLE.getL1Fee(bytes.concat(txCallData, L1_FEE_DATA_PADDING));
}
return 0;
}

/**
* @notice Returns the gas cost in wei of calldataSizeBytes of calldata being posted
* @notice to L1.
*/
function _getL1CalldataGasCost(uint256 calldataSizeBytes) internal view returns (uint256) {
uint256 chainid = block.chainid;
if (_isArbitrumChainId(chainid)) {
(, uint256 l1PricePerByte, , , , ) = ARBGAS.getPricesInWei();
// see https://developer.arbitrum.io/devs-how-tos/how-to-estimate-gas#where-do-we-get-all-this-information-from
// for the justification behind the 140 number.
return l1PricePerByte * (calldataSizeBytes + 140);
} else if (_isOptimismChainId(chainid)) {
return _calculateOptimismL1DataFee(calldataSizeBytes);
}
return 0;
}

/**
* @notice Return true if and only if the provided chain ID is an Arbitrum chain ID.
*/
function _isArbitrumChainId(uint256 chainId) internal pure returns (bool) {
return
chainId == ARB_MAINNET_CHAIN_ID ||
chainId == ARB_GOERLI_TESTNET_CHAIN_ID ||
chainId == ARB_SEPOLIA_TESTNET_CHAIN_ID;
}

/**
* @notice Return true if and only if the provided chain ID is an Optimism chain ID.
* @notice Note that optimism chain id's are also OP stack chain id's.
*/
function _isOptimismChainId(uint256 chainId) internal pure returns (bool) {
return
chainId == OP_MAINNET_CHAIN_ID ||
chainId == OP_GOERLI_CHAIN_ID ||
chainId == OP_SEPOLIA_CHAIN_ID ||
chainId == BASE_MAINNET_CHAIN_ID ||
chainId == BASE_GOERLI_CHAIN_ID;
}

function _calculateOptimismL1DataFee(uint256 calldataSizeBytes) internal view returns (uint256) {
// from: https://community.optimism.io/docs/developers/build/transaction-fees/#the-l1-data-fee
// l1_data_fee = l1_gas_price * (tx_data_gas + fixed_overhead) * dynamic_overhead
// tx_data_gas = count_zero_bytes(tx_data) * 4 + count_non_zero_bytes(tx_data) * 16
// note we conservatively assume all non-zero bytes.
uint256 l1BaseFeeWei = OVM_GASPRICEORACLE.l1BaseFee();
uint256 numZeroBytes = 0;
uint256 numNonzeroBytes = calldataSizeBytes - numZeroBytes;
uint256 txDataGas = numZeroBytes * 4 + numNonzeroBytes * 16;
uint256 fixedOverhead = OVM_GASPRICEORACLE.overhead();

// The scalar is some value like 0.684, but is represented as
// that times 10 ^ number of scalar decimals.
// e.g scalar = 0.684 * 10^6
// The divisor is used to divide that and have a net result of the true scalar.
uint256 scalar = OVM_GASPRICEORACLE.scalar();
uint256 scalarDecimals = OVM_GASPRICEORACLE.decimals();
uint256 divisor = 10 ** scalarDecimals;

uint256 l1DataFee = (l1BaseFeeWei * (txDataGas + fixedOverhead) * scalar) / divisor;
return l1DataFee;
}
}
2 changes: 1 addition & 1 deletion contracts/src/v0.8/automation/dev/MercuryRegistry.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pragma solidity 0.8.6;
pragma solidity 0.8.19;

import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol";
import {AutomationCompatibleInterface} from "../interfaces/AutomationCompatibleInterface.sol";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pragma solidity 0.8.6;
pragma solidity 0.8.19;

import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol";
import {AutomationCompatibleInterface} from "../interfaces/AutomationCompatibleInterface.sol";
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/v0.8/vrf/BatchBlockhashStore.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
// solhint-disable-next-line one-contract-per-file
pragma solidity 0.8.6;
pragma solidity 0.8.19;

import {ChainSpecificUtil} from "../ChainSpecificUtil.sol";

Expand Down
3 changes: 1 addition & 2 deletions contracts/src/v0.8/vrf/VRFCoordinatorV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import {IERC677Receiver} from "../shared/interfaces/IERC677Receiver.sol";
import {VRF} from "./VRF.sol";
import {ConfirmedOwner} from "../shared/access/ConfirmedOwner.sol";
import {VRFConsumerBaseV2} from "./VRFConsumerBaseV2.sol";
import {ChainSpecificUtil} from "../ChainSpecificUtil.sol";

import {ChainSpecificUtil} from "../ChainSpecificUtil_v0_8_6.sol";
contract VRFCoordinatorV2 is VRF, ConfirmedOwner, TypeAndVersionInterface, VRFCoordinatorV2Interface, IERC677Receiver {
// solhint-disable-next-line chainlink-solidity/prefix-immutable-variables-with-i
LinkTokenInterface public immutable LINK;
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/v0.8/vrf/VRFTypes.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.6;
pragma solidity ^0.8.6;

/**
* @title VRFTypes
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/v0.8/vrf/VRFV2Wrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {AggregatorV3Interface} from "../shared/interfaces/AggregatorV3Interface.
import {VRFCoordinatorV2Interface} from "./interfaces/VRFCoordinatorV2Interface.sol";
import {VRFV2WrapperInterface} from "./interfaces/VRFV2WrapperInterface.sol";
import {VRFV2WrapperConsumerBase} from "./VRFV2WrapperConsumerBase.sol";
import {ChainSpecificUtil} from "../ChainSpecificUtil.sol";
import {ChainSpecificUtil} from "../ChainSpecificUtil_v0_8_6.sol";

/**
* @notice A wrapper for VRFCoordinatorV2 that provides an interface better suited to one-off
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/v0.8/vrf/dev/BatchVRFCoordinatorV2Plus.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
// solhint-disable-next-line one-contract-per-file
pragma solidity 0.8.6;
pragma solidity 0.8.19;

import {VRFTypes} from "../VRFTypes.sol";

Expand Down
2 changes: 1 addition & 1 deletion contracts/src/v0.8/vrf/dev/BlockhashStore.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.6;
pragma solidity 0.8.19;

import {ChainSpecificUtil} from "../../ChainSpecificUtil.sol";

Expand Down
2 changes: 1 addition & 1 deletion contracts/src/v0.8/vrf/dev/SubscriptionAPI.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma solidity 0.8.19;

import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol";
import {LinkTokenInterface} from "../../shared/interfaces/LinkTokenInterface.sol";
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/v0.8/vrf/dev/TrustedBlockhashStore.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.6;
pragma solidity 0.8.19;

import {ChainSpecificUtil} from "../../ChainSpecificUtil.sol";
import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol";
Expand Down
Loading

0 comments on commit 93762cc

Please sign in to comment.