From fba4febc82a113e1d82c13b31002bebbecaea34c Mon Sep 17 00:00:00 2001 From: livingrockrises <90545960+livingrockrises@users.noreply.github.com> Date: Wed, 26 Apr 2023 09:04:10 +0400 Subject: [PATCH] refs setup --- contracts/references/CandidePaymaster.sol | 148 ++++++++ contracts/references/soul/IPriceOracle.sol | 8 + contracts/references/soul/ITokenPaymaster.sol | 37 ++ contracts/references/soul/PriceOracle.sol | 36 ++ contracts/references/soul/TokenPaymaster.sol | 338 ++++++++++++++++++ package.json | 3 +- yarn.lock | 26 +- 7 files changed, 591 insertions(+), 5 deletions(-) create mode 100644 contracts/references/CandidePaymaster.sol create mode 100644 contracts/references/soul/IPriceOracle.sol create mode 100644 contracts/references/soul/ITokenPaymaster.sol create mode 100644 contracts/references/soul/PriceOracle.sol create mode 100644 contracts/references/soul/TokenPaymaster.sol diff --git a/contracts/references/CandidePaymaster.sol b/contracts/references/CandidePaymaster.sol new file mode 100644 index 0000000..1cc6cc8 --- /dev/null +++ b/contracts/references/CandidePaymaster.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +/// @author CandideWallet Team + +import "@account-abstraction/contracts/core/BasePaymaster.sol"; +import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract CandidePaymaster is BasePaymaster { + + using ECDSA for bytes32; + using UserOperationLib for UserOperation; + using SafeERC20 for IERC20Metadata; + + enum SponsoringMode { + FULL, + GAS, + FREE + } + + struct PaymasterData { + IERC20Metadata token; + SponsoringMode mode; + uint48 validUntil; + uint256 fee; + uint256 exchangeRate; + bytes signature; + } + + //calculated cost of the postOp + uint256 constant public COST_OF_POST = 45000; + mapping(IERC20Metadata => uint256) public balances; + // + + event UserOperationSponsored(address indexed sender, address indexed token, uint256 cost); + + constructor(IEntryPoint _entryPoint, address _owner) BasePaymaster(_entryPoint) { + _transferOwnership(_owner); + } + + /** + * withdraw tokens. + * @param token the token deposit to withdraw + * @param target address to send to + * @param amount amount to withdraw + */ + function withdrawTokensTo(IERC20Metadata token, address target, uint256 amount) public { + require(owner() == msg.sender, "CP00: only owner can withdraw tokens"); + balances[token] -= amount; + token.safeTransfer(target, amount); + } + + function pack(UserOperation calldata userOp) internal pure returns (bytes32) { + return keccak256(abi.encode( + userOp.sender, + userOp.nonce, + keccak256(userOp.initCode), + keccak256(userOp.callData), + userOp.callGasLimit, + userOp.verificationGasLimit, + userOp.preVerificationGas, + userOp.maxFeePerGas, + userOp.maxPriorityFeePerGas + )); + } + + /** + * return the hash we're going to sign off-chain (and validate on-chain) + * this method is called by the off-chain service, to sign the request. + * it is called on-chain from the validatePaymasterUserOp, to validate the signature. + * note that this signature covers all fields of the UserOperation, except the "paymasterAndData", + * which will carry the signature itself. + */ + function getHash(UserOperation calldata userOp, PaymasterData memory paymasterData) + public view returns (bytes32) { + return keccak256(abi.encode( + pack(userOp), + block.chainid, + address(this), + address(paymasterData.token), + paymasterData.mode, + paymasterData.validUntil, + paymasterData.fee, + paymasterData.exchangeRate + )); + } + + function parsePaymasterAndData(bytes calldata paymasterAndData) + public pure returns (PaymasterData memory) { + IERC20Metadata token = IERC20Metadata(address(bytes20(paymasterAndData[20:40]))); + SponsoringMode mode = SponsoringMode(uint8(bytes1(paymasterAndData[40:41]))); + uint48 validUntil = uint48(bytes6(paymasterAndData[41:47])); + uint256 fee = uint256(bytes32(paymasterAndData[47:79])); + uint256 exchangeRate = uint256(bytes32(paymasterAndData[79:111])); + bytes memory signature = bytes(paymasterAndData[111:]); + return PaymasterData(token, mode, validUntil, fee, exchangeRate, signature); + } + + /** + * Verify our external signer signed this request and decode paymasterData + * paymasterData contains the following: + * token address length 20 + * signature length 64 or 65 + */ + function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) + internal virtual override returns (bytes memory context, uint256 validationData){ + (userOpHash); + + PaymasterData memory paymasterData = parsePaymasterAndData(userOp.paymasterAndData); + require(paymasterData.signature.length == 64 || paymasterData.signature.length == 65, "CP01: invalid signature length in paymasterAndData"); + + bytes32 _hash = getHash(userOp, paymasterData).toEthSignedMessageHash(); + if (owner() != _hash.recover(paymasterData.signature)) { + return ("", _packValidationData(true, paymasterData.validUntil, 0)); + } + + address account = userOp.getSender(); + uint256 gasPriceUserOp = userOp.gasPrice(); + bytes memory _context = abi.encode(account, paymasterData.token, paymasterData.mode, paymasterData.fee, paymasterData.exchangeRate, gasPriceUserOp); + + return (_context, _packValidationData(false, paymasterData.validUntil, 0)); + } + + /** + * Perform the post-operation to charge the sender for the gas. + */ + function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) internal override { + + (address account, IERC20Metadata token, SponsoringMode sponsoringMode, uint256 fee, uint256 exchangeRate, uint256 gasPricePostOp) + = abi.decode(context, (address, IERC20Metadata, SponsoringMode, uint256, uint256, uint256)); + if (sponsoringMode == SponsoringMode.FREE) return; + // + uint256 actualTokenCost = ((actualGasCost + (COST_OF_POST * gasPricePostOp)) * exchangeRate) / 1e18; + if (sponsoringMode == SponsoringMode.FULL){ + actualTokenCost = actualTokenCost + fee; + } + if (mode != PostOpMode.postOpReverted) { + token.safeTransferFrom(account, address(this), actualTokenCost); + balances[token] += actualTokenCost; + emit UserOperationSponsored(account, address(token), actualTokenCost); + } + } +} \ No newline at end of file diff --git a/contracts/references/soul/IPriceOracle.sol b/contracts/references/soul/IPriceOracle.sol new file mode 100644 index 0000000..aa70f4a --- /dev/null +++ b/contracts/references/soul/IPriceOracle.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +interface IPriceOracle { + + function exchangePrice(address _token) external view returns (uint256 price, uint8 decimals); + +} \ No newline at end of file diff --git a/contracts/references/soul/ITokenPaymaster.sol b/contracts/references/soul/ITokenPaymaster.sol new file mode 100644 index 0000000..6f89040 --- /dev/null +++ b/contracts/references/soul/ITokenPaymaster.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@account-abstraction/contracts/interfaces/IPaymaster.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +interface ITokenPaymaster is IPaymaster, IERC165 { + + /** + * @dev Emitted when token is added. + */ + event TokenAdded(address token); + + /** + * @dev Emitted when token is removed. + */ + event TokenRemoved(address token); + + /** + * @dev Returns the supported entrypoint. + */ + function entryPoint() external view returns (address); + + + /** + * @dev Returns true if this contract supports the given token address. + */ + function isSupportedToken(address _token) external view returns (bool); + + + /** + * @dev Returns the exchange price of the token in wei. + */ + function exchangePrice(address _token) external view returns (uint256,uint8); + + +} \ No newline at end of file diff --git a/contracts/references/soul/PriceOracle.sol b/contracts/references/soul/PriceOracle.sol new file mode 100644 index 0000000..1eb1f3a --- /dev/null +++ b/contracts/references/soul/PriceOracle.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "./IPriceOracle.sol"; +import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; + +contract PriceOracle is IPriceOracle { + /** + * @notice for security reason, the price feed is immutable + */ + AggregatorV3Interface public immutable priceFeed; + + mapping (address => bool) private supportedToken; + + constructor(AggregatorV3Interface _priceFeed) { + priceFeed = _priceFeed; + supportedToken[address(0)] = true; + } + + function exchangePrice( + address token + ) external view override returns (uint256 price, uint8 decimals) { + (token); + ( + /* uint80 roundID */, + int256 _price, + /*uint startedAt*/, + /*uint timeStamp*/, + /*uint80 answeredInRound*/ + ) = priceFeed.latestRoundData(); + // price -> uint256 + require(_price >= 0, "price is negative"); + price = uint256(_price); + decimals = priceFeed.decimals(); + } +} \ No newline at end of file diff --git a/contracts/references/soul/TokenPaymaster.sol b/contracts/references/soul/TokenPaymaster.sol new file mode 100644 index 0000000..d6b8a74 --- /dev/null +++ b/contracts/references/soul/TokenPaymaster.sol @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./ITokenPaymaster.sol"; +import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import "./IPriceOracle.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +contract TokenPaymaster is ITokenPaymaster, Ownable { + using UserOperationLib for UserOperation; + using SafeERC20 for IERC20; + + IEntryPoint public immutable _IEntryPoint; + address public immutable walletFactory; + + mapping(address => IPriceOracle) private supportedToken; + + // calculated cost of the postOp + uint256 private constant COST_OF_POST = 40000; + + // Trusted token approve gas cost + uint256 private constant SAFE_APPROVE_GAS_COST = 50000; + + + constructor(IEntryPoint _entryPoint, address _owner, address _walletFactory) { + require(address(_entryPoint) != address(0), "invalid etnrypoint addr"); + _IEntryPoint = _entryPoint; + + if (_owner != address(0)) { + _transferOwnership(_owner); + } + require(address(_walletFactory) != address(0), "invalid etnrypoint addr"); + walletFactory = _walletFactory; + } + + /** + * @dev Returns the supported entrypoint. + */ + function entryPoint() external view override returns (address) { + return address(_IEntryPoint); + } + + /** + * @dev Returns true if this contract supports the given token address. + */ + function isSupportedToken( + address _token + ) external view override returns (bool) { + return _isSupportedToken(_token); + } + + function _isSupportedToken(address _token) private view returns (bool) { + return address(supportedToken[_token]) != address(0); + } + + /** + * @dev Returns the exchange price of the token in wei. + */ + function exchangePrice( + address _token + ) external view override returns (uint256 price, uint8 decimals) { + + /* + Note the current alpha version of paymaster is using storage other than + `account storage`, bundler needs to whitelist the current paymaster. + (this means that the bundler has to take some risk itself) + */ + + (price, decimals) = supportedToken[_token].exchangePrice(_token); + price = (price * 99) / 100; // 1% conver chainlink `Deviation threshold` + } + + /** + * @dev add a token to the supported token list. + */ + function setToken( + address[] calldata _token, + address[] calldata _priceOracle + ) external onlyOwner { + require(_token.length == _priceOracle.length, "length mismatch"); + for (uint256 i = 0; i < _token.length; i++) { + address token = _token[i]; + address priceOracle = _priceOracle[i]; + require(token != address(0), "token cannot be zero address"); + address currentPriceOracle = address(supportedToken[token]); + if (priceOracle == address(0)) { + if (currentPriceOracle != address(0)) { + // remove token + delete supportedToken[token]; + emit TokenRemoved(token); + } + } else { + if (currentPriceOracle != address(0)) { + emit TokenRemoved(currentPriceOracle); + } + supportedToken[token] = IPriceOracle(priceOracle); + emit TokenAdded(token); + } + } + } + + function validatePaymasterUserOp( + UserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) external view override returns (bytes memory context, uint256 deadline) { + _requireFromEntryPoint(); + return _validatePaymasterUserOp(userOp, userOpHash, maxCost); + } + + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost + ) external override { + _requireFromEntryPoint(); + _postOp(mode, context, actualGasCost); + } + + function _decodeApprove( + bytes memory func + ) private pure returns (address spender, uint256 value) { + // 0x095ea7b3 approve(address,uint256) + // 0x095ea7b3 address uint256 + // ____4_____|____32___|___32__ + + require(bytes4(func) == bytes4(0x095ea7b3), "invalid approve func"); + assembly { + spender := mload(add(func, 36)) // 32 + 4 + value := mload(add(func, 68)) // 32 + 4 +32 + } + } + + function _validateConstructor( + UserOperation calldata userOp, + address token, + uint256 tokenRequiredPreFund + ) internal view { + address factory = address(bytes20(userOp.initCode)); + require(factory == walletFactory, "unknown wallet factory"); + require( + bytes4(userOp.callData) == bytes4(0x2763604f /* 0x2763604f execFromEntryPoint(address[],uint256[],bytes[]) */ ), + "invalid callData" + ); + ( + address[] memory dest, + uint256[] memory value, + bytes[] memory func + ) = abi.decode(userOp.callData[4:], (address[], uint256[], bytes[])); + require(dest.length == value.length && dest.length == func.length, "invalid callData"); + + address _destAddress = address(0); + for (uint256 i = 0; i < dest.length; i++) { + address destAddr = dest[i]; + require(_isSupportedToken(destAddr), "unsupported token"); + if (destAddr == token) { + (address spender, uint256 amount) = _decodeApprove(func[i]); + require(spender == address(this), "invalid spender"); + require(amount >= tokenRequiredPreFund, "not enough approve"); + } + require(destAddr > _destAddress, "duplicate"); + _destAddress = destAddr; + } + // callGasLimit + uint256 callGasLimit = dest.length * SAFE_APPROVE_GAS_COST; + require( + userOp.callGasLimit >= callGasLimit, + "Paymaster: gas too low for postOp" + ); + } + + function _validatePaymasterUserOp( + UserOperation calldata userOp, + bytes32 /*userOpHash*/, + uint256 requiredPreFund + ) private view returns (bytes memory context, uint256 deadline) { + require( + userOp.verificationGasLimit > 45000, + "Paymaster: gas too low for postOp" + ); + + address sender = userOp.getSender(); + + // paymasterAndData: [paymaster, token, maxCost] + (address token, uint256 maxCost) = abi.decode( + userOp.paymasterAndData[20:], + (address, uint256) + ); + IERC20 ERC20Token = IERC20(token); + + (uint256 _price, uint8 _decimals) = this.exchangePrice(token); + uint8 tokenDecimals = IERC20Metadata(token).decimals(); + + // #risk: overflow + // exchangeRate = ( _price * 10^tokenDecimals ) / 10^_decimals / 10^18 + uint256 exchangeRate = (_price * 10 ** tokenDecimals) / 10 ** _decimals; // ./10^18 + // tokenRequiredPreFund = requiredPreFund * exchangeRate / 10^18 + + uint256 costOfPost = userOp.gasPrice() * COST_OF_POST; + + uint256 tokenRequiredPreFund = ((requiredPreFund + costOfPost) * + exchangeRate) / 10 ** 18; + + require(tokenRequiredPreFund <= maxCost, "Paymaster: maxCost too low"); + + if (userOp.initCode.length != 0) { + _validateConstructor(userOp, token, tokenRequiredPreFund); + } else { + require( + ERC20Token.allowance(sender, address(this)) >= + tokenRequiredPreFund, + "Paymaster: not enough allowance" + ); + } + + require( + ERC20Token.balanceOf(sender) >= tokenRequiredPreFund, + "Paymaster: not enough balance" + ); + + return (abi.encode(sender, token, costOfPost, exchangeRate), 0); + } + + /** + * post-operation handler. + * (verified to be called only through the entryPoint) + * @dev if subclass returns a non-empty context from validatePaymasterUserOp, it must also implement this method. + * @param mode enum with the following options: + * opSucceeded - user operation succeeded. + * opReverted - user op reverted. still has to pay for gas. + * postOpReverted - user op succeeded, but caused postOp (in mode=opSucceeded) to revert. + * Now this is the 2nd call, after user's op was deliberately reverted. + * @param context - the context value returned by validatePaymasterUserOp + * @param actualGasCost - actual gas used so far (without this postOp call). + */ + function _postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost + ) private { + (mode); + ( + address sender, + address payable token, + uint256 costOfPost, + uint256 exchangeRate + ) = abi.decode(context, (address, address, uint256, uint256)); + uint256 tokenRequiredFund = ((actualGasCost + costOfPost) * + exchangeRate) / 10 ** 18; + IERC20(token).safeTransferFrom(sender, address(this), tokenRequiredFund); + } + + /** + * add a deposit for this paymaster, used for paying for transaction fees + */ + function deposit() public payable { + _IEntryPoint.depositTo{value: msg.value}(address(this)); + } + + /** + * withdraw value from the deposit + * @param withdrawAddress target to send to + * @param amount to withdraw + */ + function withdrawTo( + address payable withdrawAddress, + uint256 amount + ) public onlyOwner { + _IEntryPoint.withdrawTo(withdrawAddress, amount); + } + + /** + * add stake for this paymaster. + * This method can also carry eth value to add to the current stake. + * @param unstakeDelaySec - the unstake delay for this paymaster. Can only be increased. + */ + function addStake(uint32 unstakeDelaySec) external payable onlyOwner { + _IEntryPoint.addStake{value: msg.value}(unstakeDelaySec); + } + + /** + * return current paymaster's deposit on the entryPoint. + */ + function getDeposit() public view returns (uint256) { + return _IEntryPoint.balanceOf(address(this)); + } + + /** + * unlock the stake, in order to withdraw it. + * The paymaster can't serve requests once unlocked, until it calls addStake again + */ + function unlockStake() external onlyOwner { + _IEntryPoint.unlockStake(); + } + + /** + * withdraw the entire paymaster's stake. + * stake must be unlocked first (and then wait for the unstakeDelay to be over) + * @param withdrawAddress the address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external onlyOwner { + _IEntryPoint.withdrawStake(withdrawAddress); + } + + /// validate the call is made from a valid entrypoint + function _requireFromEntryPoint() private view { + require(msg.sender == address(_IEntryPoint)); + } + + function _withdrawToken(address token, address to, uint256 amount) private { + IERC20(token).transfer(to, amount); + } + + // withdraw token from this contract + function withdrawToken(address token, address to, uint256 amount) external onlyOwner { + _withdrawToken(token, to, amount); + } + + // withdraw token from this contract + function withdrawToken(address[] calldata token, address to, uint256[] calldata amount) external onlyOwner { + require(token.length == amount.length, "length mismatch"); + for (uint256 i = 0; i < token.length; i++) { + _withdrawToken(token[i], to, amount[i]); + } + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface( + bytes4 interfaceId + ) public pure override(IERC165) returns (bool) { + return interfaceId == type(ITokenPaymaster).interfaceId; + } +} \ No newline at end of file diff --git a/package.json b/package.json index fd76d10..9d6b7a5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ }, "homepage": "https://github.com/bcnmy/biconomy-paymasters#readme", "devDependencies": { + "@chainlink/contracts": "^0.6.0", "@nomicfoundation/hardhat-chai-matchers": "^1.0.6", "@nomicfoundation/hardhat-network-helpers": "^1.0.8", "@nomicfoundation/hardhat-toolbox": "^2.0.2", @@ -41,8 +42,8 @@ "typescript": "^5.0.4" }, "dependencies": { + "@chainlink/contracts": "^0.6.0", "@account-abstraction/contracts": "^0.6.0", - "@chainlink/contracts": "^0.4.1", "@ethersproject/abstract-signer": "^5.6.2", "@ethersproject/constants": "^5.6.1", "@openzeppelin/contracts": "4.8.1", diff --git a/yarn.lock b/yarn.lock index aef86c0..8e71e69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -90,12 +90,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" -"@chainlink/contracts@^0.4.1": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@chainlink/contracts/-/contracts-0.4.2.tgz#2928a35e8da94664b8ffeb8f5a54b1a3f14d5b3f" - integrity sha512-wVI/KZ9nIH0iqoebVxYrZfNVWO23vwds1UrHdbF+S0JwyixtT+54xYGlot723jCrAeBeQHsDRQXnEhhbUEHpgQ== +"@chainlink/contracts@^0.6.0": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@chainlink/contracts/-/contracts-0.6.1.tgz#8842b57e755793cbdbcbc45277fb5d179c993e19" + integrity sha512-EuwijGexttw0UjfrW+HygwhQIrGAbqpf1ue28R55HhWMHBzphEH0PhWm8DQmFfj5OZNy8Io66N4L0nStkZ3QKQ== dependencies: "@eth-optimism/contracts" "^0.5.21" + "@openzeppelin/contracts" "~4.3.3" + "@openzeppelin/contracts-upgradeable" "^4.7.3" + "@openzeppelin/contracts-v0.7" "npm:@openzeppelin/contracts@v3.4.2" "@chainsafe/as-sha256@^0.3.1": version "0.3.1" @@ -882,11 +885,26 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.8.1.tgz#363f7dd08f25f8f77e16d374350c3d6b43340a7a" integrity sha512-1wTv+20lNiC0R07jyIAbHU7TNHKRwGiTGRfiNnA8jOWjKT98g5OgLpYWOi40Vgpk8SPLA9EvfJAbAeIyVn+7Bw== +"@openzeppelin/contracts-upgradeable@^4.7.3": + version "4.8.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.8.3.tgz#6b076a7b751811b90fe3a172a7faeaa603e13a3f" + integrity sha512-SXDRl7HKpl2WDoJpn7CK/M9U4Z8gNXDHHChAKh0Iz+Wew3wu6CmFYBeie3je8V0GSXZAIYYwUktSrnW/kwVPtg== + +"@openzeppelin/contracts-v0.7@npm:@openzeppelin/contracts@v3.4.2": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2.tgz#d81f786fda2871d1eb8a8c5a73e455753ba53527" + integrity sha512-z0zMCjyhhp4y7XKAcDAi3Vgms4T2PstwBdahiO0+9NaGICQKjynK3wduSRplTgk4LXmoO1yfDGO5RbjKYxtuxA== + "@openzeppelin/contracts@4.8.1": version "4.8.1" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.1.tgz#709cfc4bbb3ca9f4460d60101f15dac6b7a2d5e4" integrity sha512-xQ6eUZl+RDyb/FiZe1h+U7qr/f4p/SrTSQcTPH2bjur3C5DbuW/zFgCU/b1P/xcIaEqJep+9ju4xDRi3rmChdQ== +"@openzeppelin/contracts@~4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.3.3.tgz#ff6ee919fc2a1abaf72b22814bfb72ed129ec137" + integrity sha512-tDBopO1c98Yk7Cv/PZlHqrvtVjlgK5R4J6jxLwoO7qxK4xqOiZG+zSkIvGFpPZ0ikc3QOED3plgdqjgNTnBc7g== + "@scure/base@~1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"