diff --git a/packages/evm/.env.sample b/packages/evm/.env.sample
index 9ec3628..aed0018 100644
--- a/packages/evm/.env.sample
+++ b/packages/evm/.env.sample
@@ -3,5 +3,7 @@ AXIA=0x3F4C47E37A94caeE31d0B585f54F3fFA1f2294C9
SOLVER=0xE0D76433Edd9f5df370561bd0AF231E72c83Cd3a
VALIDATOR=0xc76B16fA2Fa75D93e08099DC16413D9a083404A1
+SETTLER_PROXY=
+
ETHERSCAN_KEY=
DEPLOYER_PRIVATE_KEY=
diff --git a/packages/evm/contracts/Settler.sol b/packages/evm/contracts/Settler.sol
index b56b579..8a30061 100644
--- a/packages/evm/contracts/Settler.sol
+++ b/packages/evm/contracts/Settler.sol
@@ -14,16 +14,15 @@
pragma solidity ^0.8.20;
-import '@openzeppelin/contracts/access/Ownable.sol';
+import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
+import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';
+import '@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol';
+import '@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
-import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';
-import '@openzeppelin/contracts/utils/cryptography/EIP712.sol';
-import '@openzeppelin/contracts/utils/introspection/ERC165Checker.sol';
import './Intents.sol';
-import './dynamic-calls/DynamicCallEncoder.sol';
import './interfaces/IController.sol';
import './interfaces/IDynamicCallEncoder.sol';
import './interfaces/IOperationsValidator.sol';
@@ -38,7 +37,7 @@ import './smart-accounts/SmartAccountsHandlerHelpers.sol';
* @title Settler
* @dev Contract that provides the appropriate context for solvers to execute proposals that fulfill user intents
*/
-contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
+contract Settler is ISettler, Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable, EIP712Upgradeable {
using SafeERC20 for IERC20;
using IntentsHelpers for Intent;
using IntentsHelpers for Proposal;
@@ -46,8 +45,7 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
using SmartAccountsHandlerHelpers for address;
// Mimic controller reference
- // solhint-disable-next-line immutable-vars-naming
- address public immutable override controller;
+ address public override controller;
// Smart accounts handler reference
address public override smartAccountsHandler;
@@ -74,14 +72,26 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
}
/**
- * @dev Creates a new Settler contract
+ * @dev Disables initializers to prevent implementation contract from being initialized directly
+ */
+ constructor() {
+ _disableInitializers();
+ }
+
+ /**
+ * @dev Initializes a new Settler contract
* @param _controller Address of the Settler controller
* @param _owner Address that will own the contract
+ * @param _dynamicCallEncoder Address of the dynamic call encoder
*/
- constructor(address _controller, address _owner) Ownable(_owner) EIP712('Mimic Protocol Settler', '1') {
+ function initialize(address _controller, address _owner, address _dynamicCallEncoder) external initializer {
+ __Ownable_init(_owner);
+ __ReentrancyGuard_init();
+ __EIP712_init('Mimic Protocol Settler', '1');
+
controller = _controller;
smartAccountsHandler = address(new SmartAccountsHandler());
- dynamicCallEncoder = address(new DynamicCallEncoder());
+ _setDynamicCallEncoder(_dynamicCallEncoder);
}
/**
@@ -299,7 +309,7 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
* @dev Validates and executes a proposal to fulfill a transfer operation
* @param intent Intent that contains transfer operation to be fulfilled
* @param proposal Transfer proposal to be executed
- * @param index Position where the trasnfer proposal data and operation are located
+ * @param index Position where the transfer proposal data and operation are located
*/
function _executeTransfer(Intent memory intent, Proposal memory proposal, uint256 index)
internal
diff --git a/packages/evm/contracts/proxy/Proxy.sol b/packages/evm/contracts/proxy/Proxy.sol
new file mode 100644
index 0000000..7652c70
--- /dev/null
+++ b/packages/evm/contracts/proxy/Proxy.sol
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.8.20;
+
+import '@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol';
+
+contract Proxy is TransparentUpgradeableProxy {
+ constructor(address implementation, address initialOwner, bytes memory data)
+ TransparentUpgradeableProxy(implementation, initialOwner, data)
+ {}
+}
diff --git a/packages/evm/contracts/test/SettlerV2Mock.sol b/packages/evm/contracts/test/SettlerV2Mock.sol
new file mode 100644
index 0000000..9f48710
--- /dev/null
+++ b/packages/evm/contracts/test/SettlerV2Mock.sol
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.8.20;
+
+import '../Settler.sol';
+
+contract SettlerV2Mock is Settler {
+ function someNewFunction() external pure returns (string memory) {
+ return 'Some new function';
+ }
+}
diff --git a/packages/evm/hardhat.config.ts b/packages/evm/hardhat.config.ts
index 91e4e82..85ab355 100644
--- a/packages/evm/hardhat.config.ts
+++ b/packages/evm/hardhat.config.ts
@@ -8,6 +8,7 @@ dotenv.config()
const config: HardhatUserConfig = {
plugins: [hardhatVerify, hardhatToolboxMochaEthersPlugin],
solidity: {
+ dependenciesToCompile: ['@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol'],
profiles: {
default: {
version: '0.8.28',
diff --git a/packages/evm/package.json b/packages/evm/package.json
index 5fa448d..fa9e172 100644
--- a/packages/evm/package.json
+++ b/packages/evm/package.json
@@ -14,7 +14,8 @@
"test": "hardhat test"
},
"dependencies": {
- "@openzeppelin/contracts": "5.3.0"
+ "@openzeppelin/contracts": "5.3.0",
+ "@openzeppelin/contracts-upgradeable": "5.3.0"
},
"devDependencies": {
"@mimicprotocol/sdk": "~0.1.0",
diff --git a/packages/evm/scripts/deploy-contracts.ts b/packages/evm/scripts/deploy-contracts.ts
index 438a5a2..52043ea 100644
--- a/packages/evm/scripts/deploy-contracts.ts
+++ b/packages/evm/scripts/deploy-contracts.ts
@@ -1,4 +1,8 @@
+import { Interface } from 'ethers'
+
import ControllerArtifact from '../artifacts/contracts/Controller.sol/Controller.json'
+import DynamicCallEncoderArtifact from '../artifacts/contracts/dynamic-calls/DynamicCallEncoder.sol/DynamicCallEncoder.json'
+import ProxyArtifact from '../artifacts/contracts/proxy/Proxy.sol/Proxy.json'
import SettlerArtifact from '../artifacts/contracts/Settler.sol/Settler.json'
import SmartAccount7702 from '../artifacts/contracts/smart-accounts/SmartAccount7702.sol/SmartAccount7702.json'
import MimicHelperArtifact from '../artifacts/contracts/utils/MimicHelper.sol/MimicHelper.json'
@@ -15,8 +19,18 @@ async function main(): Promise {
const controllerArgs = [ADMIN, [SOLVER], [], [AXIA], [VALIDATOR], MIN_VALIDATORS]
const controller = await deployCreate3(ControllerArtifact, controllerArgs, '0x17')
- const settler = await deployCreate3(SettlerArtifact, [controller.target, ADMIN], '0x18')
- await deployCreate3(SmartAccount7702, [settler.target], '0x19')
+
+ const dynamicCallEncoder = await deployCreate3(DynamicCallEncoderArtifact, [], '0x20')
+ const settlerImplementation = await deployCreate3(SettlerArtifact, [], '0x1801')
+
+ const initializeData = new Interface(SettlerArtifact.abi).encodeFunctionData('initialize', [
+ controller.target,
+ ADMIN,
+ dynamicCallEncoder.target,
+ ])
+ const settlerProxy = await deployCreate3(ProxyArtifact, [settlerImplementation.target, ADMIN, initializeData], '0x18')
+
+ await deployCreate3(SmartAccount7702, [settlerProxy.target], '0x19')
await deployCreate3(MimicHelperArtifact, [], '0x42')
}
diff --git a/packages/evm/scripts/upgrade-settler.ts b/packages/evm/scripts/upgrade-settler.ts
new file mode 100644
index 0000000..32970ba
--- /dev/null
+++ b/packages/evm/scripts/upgrade-settler.ts
@@ -0,0 +1,36 @@
+import { HardhatEthers, HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'
+import { Contract, getAddress } from 'ethers'
+import { network } from 'hardhat'
+
+import ProxyAdminArtifact from '../artifacts/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol/ProxyAdmin.json'
+import SettlerArtifact from '../artifacts/contracts/Settler.sol/Settler.json'
+import { deployCreate3 } from './deploy-create3'
+
+const ERC1967_ADMIN_SLOT = '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103'
+
+async function main(): Promise {
+ const { ethers } = await network.connect()
+ const [signer] = await ethers.getSigners()
+
+ if (!process.env.SETTLER_PROXY) throw Error('SETTLER_PROXY env variable not provided')
+ const proxy = getAddress(process.env.SETTLER_PROXY)
+
+ const proxyAdmin = await getProxyAdmin(ethers, proxy, signer)
+ const proxyAdminOwner = await proxyAdmin.owner()
+ if (proxyAdminOwner !== signer.address) {
+ throw Error(`Signer ${signer.address} is not the ProxyAdmin owner ${proxyAdminOwner}`)
+ }
+
+ const implementation = await deployCreate3(SettlerArtifact, [], '0x1802')
+ const tx = await proxyAdmin.upgradeAndCall(proxy, implementation.target, '0x')
+ await tx.wait()
+ console.log(`✅ Settler ${proxy} upgraded in tx ${tx.hash}`)
+}
+
+async function getProxyAdmin(ethers: HardhatEthers, proxy: string, signer: HardhatEthersSigner): Promise {
+ const rawAdmin = await ethers.provider.getStorage(proxy, ERC1967_ADMIN_SLOT)
+ const adminAddress = getAddress(`0x${rawAdmin.slice(-40)}`)
+ return ethers.getContractAt(ProxyAdminArtifact.abi, adminAddress, signer)
+}
+
+main().catch(console.error)
diff --git a/packages/evm/test/Settler.test.ts b/packages/evm/test/Settler.test.ts
index 3a26fc8..76e914a 100644
--- a/packages/evm/test/Settler.test.ts
+++ b/packages/evm/test/Settler.test.ts
@@ -31,6 +31,7 @@ import {
TransferExecutorMock,
} from '../types/ethers-contracts/index.js'
import itBehavesLikeOwnable from './behaviors/Ownable.behavior'
+import itBehavesLikeUpgradeable from './behaviors/Upgradeable.behavior'
import {
Account,
CallOperation,
@@ -52,6 +53,7 @@ import {
createTransferOperation,
createTransferProposal,
currentTimestamp,
+ deployProxy,
DynamicCallOperation,
hashIntent,
hashProposal,
@@ -75,15 +77,20 @@ const { ethers } = await network.connect()
/* eslint-disable @typescript-eslint/no-non-null-assertion */
describe('Settler', () => {
- let settler: Settler, controller: Controller
- let user: HardhatEthersSigner, other: HardhatEthersSigner
- let admin: HardhatEthersSigner, owner: HardhatEthersSigner, solver: HardhatEthersSigner
+ let settler: Settler, controller: Controller, dynamicCallEncoder: DynamicCallEncoder
+ let user: HardhatEthersSigner, other: HardhatEthersSigner, solver: HardhatEthersSigner
+ let admin: HardhatEthersSigner, owner: HardhatEthersSigner, proxyOwner: HardhatEthersSigner
beforeEach('deploy settler', async () => {
// eslint-disable-next-line prettier/prettier
- [, admin, owner, user, other, solver] = await ethers.getSigners()
+ [, admin, owner, user, other, solver, proxyOwner] = await ethers.getSigners()
controller = await ethers.deployContract('Controller', [admin, [], [], [], [], 0])
- settler = await ethers.deployContract('Settler', [controller, owner])
+ dynamicCallEncoder = await ethers.deployContract('DynamicCallEncoder', [])
+ settler = await deployProxy(ethers, 'Settler', proxyOwner, [
+ controller.target,
+ owner.address,
+ dynamicCallEncoder.target,
+ ])
})
const balanceOf = (token: TokenMock | string, account: Account) => {
@@ -107,10 +114,33 @@ describe('Settler', () => {
})
it('has a dynamic call decoder', async () => {
- expect(await settler.dynamicCallEncoder()).to.not.be.equal(ZERO_ADDRESS)
+ expect(await settler.dynamicCallEncoder()).to.be.equal(dynamicCallEncoder)
})
})
+ describe('upgradeable', () => {
+ beforeEach('set upgradeable context', function () {
+ this.ethers = ethers
+ this.proxy = settler
+ this.proxyOwner = proxyOwner
+ this.other = other
+ this.implementationNameV1 = 'Settler'
+ this.implementationNameV2 = 'SettlerV2Mock'
+ this.initializeArgs = [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS]
+ this.assertUpgrade = async (proxy: Settler) => {
+ const upgraded = await ethers.getContractAt('SettlerV2Mock', proxy)
+ expect(await upgraded.someNewFunction()).to.be.equal('Some new function')
+ expect(await upgraded.owner()).to.be.equal(owner)
+ expect(await upgraded.controller()).to.be.equal(controller)
+ expect(await upgraded.operationsValidator()).to.be.equal(ZERO_ADDRESS)
+ expect(await upgraded.smartAccountsHandler()).to.not.be.equal(ZERO_ADDRESS)
+ expect(await upgraded.dynamicCallEncoder()).to.be.equal(dynamicCallEncoder)
+ }
+ })
+
+ itBehavesLikeUpgradeable()
+ })
+
describe('ownable', () => {
beforeEach('set instance', function () {
this.owner = owner
@@ -3081,7 +3111,6 @@ describe('Settler', () => {
let target: Account
let feeToken: TokenMock
let proposal: Proposal
- let dynamicCallEncoder: DynamicCallEncoder
const arg0 = randomEvmAddress()
const arg1 = randomNumber(2)
@@ -3136,10 +3165,6 @@ describe('Settler', () => {
proposal = createDynamicCallProposal({ fees: [feeAmount] })
})
- beforeEach('set dynamic call encoder', async () => {
- dynamicCallEncoder = await ethers.deployContract('DynamicCallEncoder', [])
- })
-
it('executes the intent', async () => {
const preUserBalance = await balanceOf(feeToken, user)
const preSolverBalance = await balanceOf(feeToken, solver)
diff --git a/packages/evm/test/behaviors/Upgradeable.behavior.ts b/packages/evm/test/behaviors/Upgradeable.behavior.ts
new file mode 100644
index 0000000..def8382
--- /dev/null
+++ b/packages/evm/test/behaviors/Upgradeable.behavior.ts
@@ -0,0 +1,62 @@
+import { HardhatEthers } from '@nomicfoundation/hardhat-ethers/types'
+import { expect } from 'chai'
+import { Contract, getAddress } from 'ethers'
+
+/* eslint-disable no-secrets/no-secrets */
+
+const ERC1967_ADMIN_SLOT = '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103'
+
+export default function itBehavesLikeUpgradeable(): void {
+ describe('initialize', () => {
+ it('locks the implementation initializer', async function () {
+ const implementation = await this.ethers.deployContract(this.implementationNameV1)
+
+ await expect(implementation.initialize(...this.initializeArgs)).to.be.revertedWithCustomError(
+ implementation,
+ 'InvalidInitialization'
+ )
+ })
+
+ it('cannot be initialized twice', async function () {
+ await expect(this.proxy.initialize(...this.initializeArgs)).to.be.revertedWithCustomError(
+ this.proxy,
+ 'InvalidInitialization'
+ )
+ })
+ })
+
+ describe('upgradeAndCall', () => {
+ context('when the sender is the owner', () => {
+ it('upgrades the implementation', async function () {
+ const proxyAdmin = await getProxyAdmin(this.ethers, this.proxy)
+ const newImplementation = await this.ethers.deployContract(this.implementationNameV2)
+
+ await proxyAdmin.connect(this.proxyOwner).upgradeAndCall(this.proxy, newImplementation, '0x')
+ await this.assertUpgrade(this.proxy)
+ })
+ })
+
+ context('when the sender is not the owner', () => {
+ it('reverts', async function () {
+ const proxyAdmin = await getProxyAdmin(this.ethers, this.proxy)
+ const newImplementation = await this.ethers.deployContract(this.implementationNameV2)
+
+ await expect(
+ proxyAdmin.connect(this.other).upgradeAndCall(this.proxy, newImplementation, '0x')
+ ).to.be.revertedWithCustomError(proxyAdmin, 'OwnableUnauthorizedAccount')
+ })
+ })
+ })
+}
+
+async function getProxyAdmin(ethers: HardhatEthers, proxy: Contract): Promise {
+ const rawAdmin = await ethers.provider.getStorage(proxy.target as string, ERC1967_ADMIN_SLOT)
+ return new Contract(
+ getAddress(`0x${rawAdmin.slice(-40)}`),
+ [
+ 'error OwnableUnauthorizedAccount(address account)',
+ 'function upgradeAndCall(address proxy, address implementation, bytes data) payable',
+ ],
+ ethers.provider
+ )
+}
diff --git a/packages/evm/test/helpers/index.ts b/packages/evm/test/helpers/index.ts
index 16160be..51366da 100644
--- a/packages/evm/test/helpers/index.ts
+++ b/packages/evm/test/helpers/index.ts
@@ -3,5 +3,6 @@ export * from './arrays'
export * from './dynamic-calls.js'
export * from './intents'
export * from './proposal'
+export * from './proxy'
export * from './safeguards'
export * from './time'
diff --git a/packages/evm/test/helpers/proxy.ts b/packages/evm/test/helpers/proxy.ts
new file mode 100644
index 0000000..ac13d20
--- /dev/null
+++ b/packages/evm/test/helpers/proxy.ts
@@ -0,0 +1,16 @@
+import { HardhatEthers } from '@nomicfoundation/hardhat-ethers/types'
+import { Contract } from 'ethers'
+
+import { Account, toAddress } from './addresses.js'
+
+export async function deployProxy(
+ ethers: HardhatEthers,
+ implementationName: string,
+ initialOwner: Account,
+ initializeArgs: unknown[]
+): Promise {
+ const implementation = await ethers.deployContract(implementationName)
+ const initializeData = implementation.interface.encodeFunctionData('initialize', initializeArgs)
+ const proxy = await ethers.deployContract('Proxy', [implementation, toAddress(initialOwner), initializeData])
+ return ethers.getContractAt(implementationName, proxy.target) as Promise
+}
diff --git a/packages/evm/test/smart-accounts/SmartAccount7702.test.ts b/packages/evm/test/smart-accounts/SmartAccount7702.test.ts
index 29a4a60..2809623 100644
--- a/packages/evm/test/smart-accounts/SmartAccount7702.test.ts
+++ b/packages/evm/test/smart-accounts/SmartAccount7702.test.ts
@@ -5,7 +5,7 @@ import { Authorization } from 'ethers'
import { network } from 'hardhat'
import { CallMock, Controller, Settler, SmartAccount7702, TokenMock } from '../../types/ethers-contracts/index.js'
-import { Account, toAddress } from '../helpers'
+import { Account, deployProxy, toAddress } from '../helpers'
import { createCallIntent, createTransferIntent } from '../helpers/intents'
import { createCallProposal, createTransferProposal, signProposal } from '../helpers/proposal'
@@ -22,7 +22,11 @@ describe('SmartAccount7702', () => {
// eslint-disable-next-line prettier/prettier
[, admin, user, solver] = await ethers.getSigners()
controller = await ethers.deployContract('Controller', [admin, [solver], [], [admin], [], 0])
- settler = await ethers.deployContract('Settler', [controller, admin])
+ settler = await deployProxy(ethers, 'Settler', admin, [
+ controller.target,
+ admin.address,
+ randomEvmAddress(),
+ ])
smartAccount = await ethers.deployContract('SmartAccount7702', [settler])
})
diff --git a/yarn.lock b/yarn.lock
index e57e304..b55b7a7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -482,10 +482,10 @@
wrap-ansi "^8.1.0"
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
-"@mimicprotocol/sdk@0.0.2-rc.1":
- version "0.0.2-rc.1"
- resolved "https://registry.yarnpkg.com/@mimicprotocol/sdk/-/sdk-0.0.2-rc.1.tgz#2d8f69a006709c15631ae60e9ec437be9ec178ab"
- integrity sha512-mGuBKp4JCdJh0b/awozp1qUULYdbRmsXdmVuwbT1ZZfVnZmlSpANqkiA2PGPUB8VEdQ9NvhgWhZcFZJzHy2exQ==
+"@mimicprotocol/sdk@~0.1.0":
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/@mimicprotocol/sdk/-/sdk-0.1.0.tgz#a0bb3661cd0129bac253cba6c79dea7b6026b44e"
+ integrity sha512-wJqjsZC9qQOKi5j7rOutCSHg9aV1/Sg8+nMaLt0IX/z5P1Dk2BBFb8OJc1q1MFW9PZOAJSaFhtZlRxKkzhoopA==
dependencies:
"@coral-xyz/anchor" "0.32.1"
"@solana/web3.js" "^1.98.4"
@@ -780,6 +780,11 @@
"@nomicfoundation/solidity-analyzer-linux-x64-musl" "0.1.2"
"@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.2"
+"@openzeppelin/contracts-upgradeable@5.3.0":
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.3.0.tgz#79dba09ab0b4bb49f21544ea738b9de016b0ceea"
+ integrity sha512-yVzSSyTMWO6rapGI5tuqkcLpcGGXA0UA1vScyV5EhE5yw8By3Ewex9rDUw8lfVw0iTkvR/egjfcW5vpk03lqZg==
+
"@openzeppelin/contracts@5.3.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.3.0.tgz#0a90ce16f5c855e3c8239691f1722cd4999ae741"