From 30da23ca0caf77430e4cf1fe284eeebe405e0876 Mon Sep 17 00:00:00 2001 From: Ankur Dubey Date: Mon, 4 Dec 2023 11:55:30 +0530 Subject: [PATCH] fix: dependencies and wallet utils --- .vscode/settings.json | 3 + .../modules/ISessionKeyManagerModule.sol | 6 + ...l => ISessionKeyManagerModuleStateful.sol} | 2 +- .../ISessionKeyManagerModuleStateless.sol | 33 ---- .../modules/SessionKeyManagerModule.sol | 34 ++-- .../modules/SessionKeyManagerStateful.sol | 37 ++-- .../modules/SessionKeyMangerModuleHybrid.sol | 9 +- .../SessionKeyMangerModuleStateless.sol | 166 ------------------ package.json | 5 + walletUtils.js | 4 +- yarn.lock | 53 ++++++ 11 files changed, 105 insertions(+), 247 deletions(-) create mode 100644 .vscode/settings.json rename contracts/smart-account/interfaces/modules/{ISessionKeyManagerModuleStatefull.sol => ISessionKeyManagerModuleStateful.sol} (95%) delete mode 100644 contracts/smart-account/interfaces/modules/ISessionKeyManagerModuleStateless.sol delete mode 100644 contracts/smart-account/modules/SessionKeyMangerModuleStateless.sol diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..bee85e7c4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "solidity.compileUsingRemoteVersion": "v0.8.21+commit.d9974bed" +} diff --git a/contracts/smart-account/interfaces/modules/ISessionKeyManagerModule.sol b/contracts/smart-account/interfaces/modules/ISessionKeyManagerModule.sol index c1bf4bc29..44a1efcdc 100644 --- a/contracts/smart-account/interfaces/modules/ISessionKeyManagerModule.sol +++ b/contracts/smart-account/interfaces/modules/ISessionKeyManagerModule.sol @@ -17,6 +17,12 @@ interface ISessionKeyManagerModule { bytes32 merkleRoot; } + /** + * @dev Emitted when the merkle root is updated for the Smart Account + * It happens when there's a need to add\remove\replace session (leaves) in the Merkle Tree + */ + event MerkleRootUpdated(address smartAccount, bytes32 newRoot); + /** * @dev validates that Session Key + parameters are enabled * by being included into the merkle tree diff --git a/contracts/smart-account/interfaces/modules/ISessionKeyManagerModuleStatefull.sol b/contracts/smart-account/interfaces/modules/ISessionKeyManagerModuleStateful.sol similarity index 95% rename from contracts/smart-account/interfaces/modules/ISessionKeyManagerModuleStatefull.sol rename to contracts/smart-account/interfaces/modules/ISessionKeyManagerModuleStateful.sol index 35208e884..71e0c43b5 100644 --- a/contracts/smart-account/interfaces/modules/ISessionKeyManagerModuleStatefull.sol +++ b/contracts/smart-account/interfaces/modules/ISessionKeyManagerModuleStateful.sol @@ -7,7 +7,7 @@ pragma solidity ^0.8.20; * @author Ankur Dubey - * @author Fil Makarov - */ -interface ISessionKeyManagerModuleStatefull { +interface ISessionKeyManagerModuleStateful { struct SessionData { uint48 validUntil; uint48 validAfter; diff --git a/contracts/smart-account/interfaces/modules/ISessionKeyManagerModuleStateless.sol b/contracts/smart-account/interfaces/modules/ISessionKeyManagerModuleStateless.sol deleted file mode 100644 index 1875cff09..000000000 --- a/contracts/smart-account/interfaces/modules/ISessionKeyManagerModuleStateless.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/** - * @title Session Key Manager module for Biconomy Modular Smart Accounts. - * @dev TODO - * @author Ankur Dubey - - * @author Fil Makarov - - */ -interface ISessionKeyManagerModuleStateless { - /** - * @dev validates that Session Key + parameters are enabled - * by being included into the merkle tree - * @param userOpSender smartAccount for which session key is being validated - * @param validUntil timestamp when the session key expires - * @param validAfter timestamp when the session key becomes valid - * @param sessionKeyIndex index of the session key being used - * @param sessionValidationModule address of the Session Validation Module - * @param sessionKeyData session parameters (limitations/permissions) - * @param sessionEnableSignature eip1271 signature which enables the session key - * @dev if doesn't revert, session key is considered valid - */ - function validateSessionKey( - address userOpSender, - uint48 validUntil, - uint48 validAfter, - uint256 sessionKeyIndex, - address sessionValidationModule, - bytes calldata sessionKeyData, - bytes calldata sessionEnableData, - bytes calldata sessionEnableSignature - ) external; -} diff --git a/contracts/smart-account/modules/SessionKeyManagerModule.sol b/contracts/smart-account/modules/SessionKeyManagerModule.sol index 023d75b9b..8dade0515 100644 --- a/contracts/smart-account/modules/SessionKeyManagerModule.sol +++ b/contracts/smart-account/modules/SessionKeyManagerModule.sol @@ -9,7 +9,6 @@ import {ISessionValidationModule} from "../interfaces/modules/ISessionValidation import {ISessionKeyManagerModule} from "../interfaces/modules/ISessionKeyManagerModule.sol"; import {IAuthorizationModule} from "../interfaces/IAuthorizationModule.sol"; import {ISignatureValidator} from "../interfaces/ISignatureValidator.sol"; -import "hardhat/console.sol"; /** * @title Session Key Manager module for Biconomy Modular Smart Accounts. @@ -36,16 +35,14 @@ contract SessionKeyManager is /// @inheritdoc ISessionKeyManagerModule function setMerkleRoot(bytes32 _merkleRoot) external override { _userSessions[msg.sender].merkleRoot = _merkleRoot; - // TODO:should we add an event here? which emits the new merkle root + emit MerkleRootUpdated(msg.sender, _merkleRoot); } /// @inheritdoc IAuthorizationModule function validateUserOp( UserOperation calldata userOp, bytes32 userOpHash - ) external virtual returns (uint256 rv) { - uint256 gas = gasleft(); - + ) external virtual returns (uint256) { (bytes memory moduleSignature, ) = abi.decode( userOp.signature, (bytes, address) @@ -71,20 +68,19 @@ contract SessionKeyManager is merkleProof ); - rv = _packValidationData( - //_packValidationData expects true if sig validation has failed, false otherwise - !ISessionValidationModule(sessionValidationModule) - .validateSessionUserOp( - userOp, - userOpHash, - sessionKeyData, - sessionKeySignature - ), - validUntil, - validAfter - ); - - console.log("Merkle Tree Validation Gas: ", gas - gasleft()); + return + _packValidationData( + //_packValidationData expects true if sig validation has failed, false otherwise + !ISessionValidationModule(sessionValidationModule) + .validateSessionUserOp( + userOp, + userOpHash, + sessionKeyData, + sessionKeySignature + ), + validUntil, + validAfter + ); } /// @inheritdoc ISessionKeyManagerModule diff --git a/contracts/smart-account/modules/SessionKeyManagerStateful.sol b/contracts/smart-account/modules/SessionKeyManagerStateful.sol index 524de8461..42f7d8b3b 100644 --- a/contracts/smart-account/modules/SessionKeyManagerStateful.sol +++ b/contracts/smart-account/modules/SessionKeyManagerStateful.sol @@ -5,10 +5,9 @@ import {BaseAuthorizationModule} from "./BaseAuthorizationModule.sol"; import {_packValidationData} from "@account-abstraction/contracts/core/Helpers.sol"; import {UserOperation} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; import {ISessionValidationModule} from "../interfaces/modules/ISessionValidationModule.sol"; -import {ISessionKeyManagerModuleStatefull} from "../interfaces/modules/ISessionKeyManagerModuleStatefull.sol"; +import {ISessionKeyManagerModuleStateful} from "../interfaces/modules/ISessionKeyManagerModuleStateful.sol"; import {IAuthorizationModule} from "../interfaces/IAuthorizationModule.sol"; import {ISignatureValidator} from "../interfaces/ISignatureValidator.sol"; -import "hardhat/console.sol"; /** * @title Session Key Manager module for Biconomy Modular Smart Accounts. @@ -17,9 +16,9 @@ import "hardhat/console.sol"; * @author Fil Makarov - */ -contract SessionKeyManagerStatefull is +contract SessionKeyManagerStateful is BaseAuthorizationModule, - ISessionKeyManagerModuleStatefull + ISessionKeyManagerModuleStateful { // Inverting the order of the mapping seems to make it non-compliant with the bundlers mapping(bytes32 sessionDataDigest => mapping(address sa => SessionData data)) @@ -30,8 +29,6 @@ contract SessionKeyManagerStatefull is UserOperation calldata userOp, bytes32 userOpHash ) external virtual returns (uint256 rv) { - uint256 gas = gasleft(); - (bytes memory moduleSignature, ) = abi.decode( userOp.signature, (bytes, address) @@ -58,23 +55,9 @@ contract SessionKeyManagerStatefull is sessionData.validUntil, sessionData.validAfter ); - - console.log("Statefull Validation Gas: ", gas - gasleft()); - } - - /// @inheritdoc ISessionKeyManagerModuleStatefull - function validateSessionKey( - address smartAccount, - bytes32 sessionKeyDataDigest - ) public virtual override { - require( - enabledSessions[sessionKeyDataDigest][smartAccount] - .sessionValidationModule != address(0), - "SessionKeyManager: session key is not enabled" - ); } - /// @inheritdoc ISessionKeyManagerModuleStatefull + /// @inheritdoc ISessionKeyManagerModuleStateful function enableSessionKey( SessionData calldata sessionData ) external override { @@ -89,6 +72,18 @@ contract SessionKeyManagerStatefull is enabledSessions[sessionDataDigest][msg.sender] = sessionData; } + /// @inheritdoc ISessionKeyManagerModuleStateful + function validateSessionKey( + address smartAccount, + bytes32 sessionKeyDataDigest + ) public virtual override { + require( + enabledSessions[sessionKeyDataDigest][smartAccount] + .sessionValidationModule != address(0), + "SKM: Session Key is not enabled" + ); + } + /// @inheritdoc ISignatureValidator function isValidSignature( bytes32 _dataHash, diff --git a/contracts/smart-account/modules/SessionKeyMangerModuleHybrid.sol b/contracts/smart-account/modules/SessionKeyMangerModuleHybrid.sol index c7b058eae..359338dad 100644 --- a/contracts/smart-account/modules/SessionKeyMangerModuleHybrid.sol +++ b/contracts/smart-account/modules/SessionKeyMangerModuleHybrid.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +/* solhint-disable function-max-lines */ + import {BaseAuthorizationModule} from "./BaseAuthorizationModule.sol"; import {_packValidationData} from "@account-abstraction/contracts/core/Helpers.sol"; import {UserOperation} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; @@ -8,7 +10,6 @@ import {ISessionValidationModule} from "../interfaces/modules/ISessionValidation import {ISessionKeyManagerModuleHybrid} from "../interfaces/modules/ISessionKeyManagerModuleHybrid.sol"; import {IAuthorizationModule} from "../interfaces/IAuthorizationModule.sol"; import {ISignatureValidator, EIP1271_MAGIC_VALUE} from "../interfaces/ISignatureValidator.sol"; -import "hardhat/console.sol"; /** * @title Session Key Manager module for Biconomy Modular Smart Accounts. @@ -29,8 +30,6 @@ contract SessionKeyManagerHybrid is UserOperation calldata userOp, bytes32 userOpHash ) external virtual returns (uint256 rv) { - uint256 gas = gasleft(); - (bytes memory moduleSignature, ) = abi.decode( userOp.signature, (bytes, address) @@ -116,8 +115,6 @@ contract SessionKeyManagerHybrid is sessionData.validAfter ); } - - console.log("Hybrid Validation Gas: ", gas - gasleft()); } /// @inheritdoc ISessionKeyManagerModuleHybrid @@ -217,7 +214,7 @@ contract SessionKeyManagerHybrid is require( enabledSessions[sessionKeyDataDigest][smartAccount] .sessionValidationModule != address(0), - "SessionKeyManager: session key is not enabled" + "SKM: Session key is not enabled" ); } diff --git a/contracts/smart-account/modules/SessionKeyMangerModuleStateless.sol b/contracts/smart-account/modules/SessionKeyMangerModuleStateless.sol deleted file mode 100644 index a1df706c8..000000000 --- a/contracts/smart-account/modules/SessionKeyMangerModuleStateless.sol +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {BaseAuthorizationModule} from "./BaseAuthorizationModule.sol"; -import {_packValidationData} from "@account-abstraction/contracts/core/Helpers.sol"; -import {UserOperation} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; -import {ISessionValidationModule} from "../interfaces/modules/ISessionValidationModule.sol"; -import {ISessionKeyManagerModuleStateless} from "../interfaces/modules/ISessionKeyManagerModuleStateless.sol"; -import {IAuthorizationModule} from "../interfaces/IAuthorizationModule.sol"; -import {ISignatureValidator, EIP1271_MAGIC_VALUE} from "../interfaces/ISignatureValidator.sol"; -import "hardhat/console.sol"; - -/** - * @title Session Key Manager module for Biconomy Modular Smart Accounts. - * @dev TODO - * @author Ankur Dubey - - * @author Fil Makarov - - */ - -contract SessionKeyManagerStateless is - BaseAuthorizationModule, - ISessionKeyManagerModuleStateless -{ - /// @inheritdoc IAuthorizationModule - function validateUserOp( - UserOperation calldata userOp, - bytes32 userOpHash - ) external virtual returns (uint256 rv) { - uint256 gas = gasleft(); - - (bytes memory moduleSignature, ) = abi.decode( - userOp.signature, - (bytes, address) - ); - ( - uint48 validUntil, - uint48 validAfter, - uint256 sessionKeyIndex, - address sessionValidationModule, - bytes memory sessionKeyData, - bytes memory sessionEnableData, - bytes memory sessionEnableSignature, - bytes memory sessionKeySignature - ) = abi.decode( - moduleSignature, - (uint48, uint48, uint256, address, bytes, bytes, bytes, bytes) - ); - - validateSessionKey( - userOp.sender, - validUntil, - validAfter, - sessionKeyIndex, - sessionValidationModule, - sessionKeyData, - sessionEnableData, - sessionEnableSignature - ); - - rv = _packValidationData( - //_packValidationData expects true if sig validation has failed, false otherwise - !ISessionValidationModule(sessionValidationModule) - .validateSessionUserOp( - userOp, - userOpHash, - sessionKeyData, - sessionKeySignature - ), - validUntil, - validAfter - ); - - console.log("Stateless Validation Gas: ", gas - gasleft()); - } - - /// @inheritdoc ISessionKeyManagerModuleStateless - function validateSessionKey( - address smartAccount, - uint48 validUntil, - uint48 validAfter, - uint256 sessionKeyIndex, - address sessionValidationModule, - bytes memory sessionKeyData, - bytes memory sessionEnableData, - bytes memory sessionEnableSignature - ) public virtual override { - // Verify the signature on the session enable data - bytes32 sessionEnableDataDigest = keccak256(sessionEnableData); - if ( - ISignatureValidator(smartAccount).isValidSignature( - sessionEnableDataDigest, - sessionEnableSignature - ) != EIP1271_MAGIC_VALUE - ) { - revert("SessionNotApproved"); - } - - /* - * Session Enable Data Layout - * Offset (in bytes) | Length (in bytes) | Contents - * 0x0 | 0x1 | No of session keys enabled - * 0x1 | 0x8 x count | Chain IDs - * 0x1 + 0x8 x count | 0x20 x count | Session Data Hash - */ - uint8 enabledKeysCount; - uint64 sessionChainId; - bytes32 sessionDigest; - - assembly ("memory-safe") { - let offset := add(sessionEnableData, 0x20) - - enabledKeysCount := shr(248, mload(offset)) - - sessionChainId := shr( - 192, - mload(add(add(offset, 0x1), mul(0x8, sessionKeyIndex))) - ) - - sessionDigest := mload( - add( - add(add(offset, 0x1), mul(0x8, enabledKeysCount)), - mul(0x20, sessionKeyIndex) - ) - ) - } - - if (sessionKeyIndex >= enabledKeysCount) { - revert("SessionKeyIndexInvalid"); - } - - if (sessionChainId != block.chainid) { - revert("SessionChainIdMismatch"); - } - - bytes32 computedDigest = keccak256( - abi.encodePacked( - validUntil, - validAfter, - sessionValidationModule, - sessionKeyData - ) - ); - - if (sessionDigest != computedDigest) { - revert("SessionKeyDataHashMismatch"); - } - } - - /// @inheritdoc ISignatureValidator - function isValidSignature( - bytes32 _dataHash, - bytes memory _signature - ) public pure override returns (bytes4) { - (_dataHash, _signature); - return 0xffffffff; // do not support it here - } - - /// @inheritdoc ISignatureValidator - function isValidSignatureUnsafe( - bytes32 _dataHash, - bytes memory _signature - ) public pure override returns (bytes4) { - (_dataHash, _signature); - return 0xffffffff; // do not support it here - } -} diff --git a/package.json b/package.json index dbe9f4769..4bd4232bb 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,17 @@ "license": "MIT", "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "^1.0.6", + "@nomicfoundation/hardhat-ethers": "^3.0.0", "@nomicfoundation/hardhat-foundry": "^1.1.1", + "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomicfoundation/hardhat-toolbox": "^4.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.0", "@nomiclabs/hardhat-ethers": "^2.2.3", "@nomiclabs/hardhat-etherscan": "^3.1.7", "@nomiclabs/hardhat-waffle": "^2.0.1", "@openzeppelin/contracts": "^4.9.3", "@typechain/ethers-v5": "^10.2.0", + "@typechain/ethers-v6": "^0.5.0", "@typechain/hardhat": "^6.1.5", "@types/chai": "^4.3.4", "@types/mocha": "^10.0.1", diff --git a/walletUtils.js b/walletUtils.js index 06313aa62..6266bff74 100644 --- a/walletUtils.js +++ b/walletUtils.js @@ -2,7 +2,9 @@ const ethers = require("ethers"); const fs = require("fs"); -const mnemonic = fs.readFileSync(".secret").toString().trim(); +const mnemonic = fs.existsSync(".secret") + ? fs.readFileSync(".secret").toString().trim() + : "test test test test test test test test test test test junk"; const makeKeyList = ( num = 5, diff --git a/yarn.lock b/yarn.lock index af6bd6831..f63030572 100644 --- a/yarn.lock +++ b/yarn.lock @@ -897,6 +897,14 @@ deep-eql "^4.0.1" ordinal "^1.0.3" +"@nomicfoundation/hardhat-ethers@^3.0.0": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.0.5.tgz#0422c2123dec7c42e7fb2be8e1691f1d9708db56" + integrity sha512-RNFe8OtbZK6Ila9kIlHp0+S80/0Bu/3p41HUpaRIoHLm6X3WekTd83vob3rE54Duufu1edCiBDxspBzi2rxHHw== + dependencies: + debug "^4.1.1" + lodash.isequal "^4.5.0" + "@nomicfoundation/hardhat-foundry@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-foundry/-/hardhat-foundry-1.1.1.tgz#db72b1f33f9cfaecc27e67f69ad436f8710162d6" @@ -904,6 +912,33 @@ dependencies: chalk "^2.4.2" +"@nomicfoundation/hardhat-network-helpers@^1.0.0": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.9.tgz#767449e8a2acda79306ac84626117583d95d25aa" + integrity sha512-OXWCv0cHpwLUO2u7bFxBna6dQtCC2Gg/aN/KtJLO7gmuuA28vgmVKYFRCDUqrbjujzgfwQ2aKyZ9Y3vSmDqS7Q== + dependencies: + ethereumjs-util "^7.1.4" + +"@nomicfoundation/hardhat-toolbox@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-toolbox/-/hardhat-toolbox-4.0.0.tgz#eb1f619218dd1414fa161dfec92d3e5e53a2f407" + integrity sha512-jhcWHp0aHaL0aDYj8IJl80v4SZXWMS1A2XxXa1CA6pBiFfJKuZinCkO6wb+POAt0LIfXB3gA3AgdcOccrcwBwA== + +"@nomicfoundation/hardhat-verify@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.0.1.tgz#1b9d707516f8e5db4e1d6bd679acbfd71e567928" + integrity sha512-TuJrhW5p9x92wDRiRhNkGQ/wzRmOkfCLkoRg8+IRxyeLigOALbayQEmkNiGWR03vGlxZS4znXhKI7y97JwZ6Og== + dependencies: + "@ethersproject/abi" "^5.1.2" + "@ethersproject/address" "^5.0.2" + cbor "^8.1.0" + chalk "^2.4.2" + debug "^4.1.1" + lodash.clonedeep "^4.5.0" + semver "^6.3.0" + table "^6.8.0" + undici "^5.14.0" + "@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.1.tgz#4c858096b1c17fe58a474fe81b46815f93645c15" @@ -1201,6 +1236,14 @@ lodash "^4.17.15" ts-essentials "^7.0.1" +"@typechain/ethers-v6@^0.5.0": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@typechain/ethers-v6/-/ethers-v6-0.5.1.tgz#42fe214a19a8b687086c93189b301e2b878797ea" + integrity sha512-F+GklO8jBWlsaVV+9oHaPh5NJdd6rAKN4tklGfInX1Q7h0xPgVLP39Jl3eCulPB5qexI71ZFHwbljx4ZXNfouA== + dependencies: + lodash "^4.17.15" + ts-essentials "^7.0.1" + "@typechain/hardhat@^6.1.5": version "6.1.6" resolved "https://registry.yarnpkg.com/@typechain/hardhat/-/hardhat-6.1.6.tgz#1a749eb35e5054c80df531cf440819cb347c62ea" @@ -4420,6 +4463,16 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"