Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use owner as CREATE2 salt #130

Merged
merged 1 commit into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,11 @@ Solidity compiler and new EVM OPCODES, as well as the introduction of more user-

Enter PRBProxy, the modern successor to DSProxy; a "DSProxy 2.0", if you will. It improves upon DSProxy in several ways:

1. PRBProxy is deployed with [CREATE2][eip-1014], which allows clients to know the proxy contract's address in advance.
2. `CREATE2` seeds are generated in a way that eliminates the risk of front-running.
3. The proxy owner cannot be changed during the `DELEGATECALL` operation.
1. PRBProxy is deployed with [CREATE2][eip-1014], which allows clients to pre-compute the proxy contract's address.
2. The `CREATE2` salts are generated in a way that eliminates the risk of front-running.
3. The proxy owner is immutable, and so it cannot be changed during any `DELEGATECALL`.
4. PRBProxy uses high-level Solidity code that is easier to comprehend and less prone to errors.
5. A minimum gas reserve is stored in the proxy to prevent it from becoming unusable if future EVM opcode gas costs change.
6. PRBProxy offers more features than DSProxy.
5. PRBProxy offers more features than DSProxy.

Using CREATE2 eliminates the risk of a [chain reorg](https://en.bitcoin.it/wiki/Chain_Reorganization) overriding the proxy contract owner, making
PRBProxy a more secure alternative to DSProxy. With DSProxy, users must wait for several blocks to be mined before assuming the contract is secure.
Expand Down
63 changes: 12 additions & 51 deletions src/PRBProxyRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ contract PRBProxyRegistry is IPRBProxyRegistry {
/// @inheritdoc IPRBProxyRegistry
ConstructorParams public override constructorParams;

/// @inheritdoc IPRBProxyRegistry
mapping(address origin => bytes32 seed) public override nextSeeds;

/*//////////////////////////////////////////////////////////////////////////
INTERNAL STORAGE
//////////////////////////////////////////////////////////////////////////*/
Comment on lines 41 to 46

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The nextSeeds mapping has been removed from the contract. This change eliminates the need for the mapping(address origin => bytes32 seed) declaration and the associated code that uses it. The removal of this mapping simplifies the contract logic and reduces storage usage.

Expand Down Expand Up @@ -139,39 +136,20 @@ contract PRBProxyRegistry is IPRBProxyRegistry {
noProxy(msg.sender)
returns (IPRBProxy proxy)
{
// Load the next seed.
bytes32 seed = nextSeeds[tx.origin];

// Prevent front-running the salt by hashing the concatenation of "tx.origin" and the user-provided seed.
bytes32 salt = keccak256(abi.encode(tx.origin, seed));

// Set the constructor params.
// Use the address of the owner as the CREATE2 salt.
address owner = msg.sender;
constructorParams = ConstructorParams({ owner: owner, target: target, data: data });
bytes32 salt = bytes32(abi.encodePacked(owner));

// Deploy the proxy with CREATE2.
// Deploy the proxy with CREATE2, and execute the delegate call in the constructor.
constructorParams = ConstructorParams({ owner: owner, target: target, data: data });
proxy = new PRBProxy{ salt: salt }();
delete constructorParams;

// Associate the the owner with the proxy in the mapping.
// Associate the owner and the proxy.
_proxies[owner] = proxy;

// Increment the seed.
// Using unchecked arithmetic here because this cannot realistically overflow, ever.
unchecked {
nextSeeds[tx.origin] = bytes32(uint256(seed) + 1);
}

// Log the proxy via en event.
// forgefmt: disable-next-line
emit DeployProxy({
origin: tx.origin,
operator: msg.sender,
owner: owner,
seed: seed,
salt: salt,
proxy: proxy
});
// Log the creation of the proxy.
emit DeployProxy({ operator: msg.sender, owner: owner, proxy: proxy });
}
Comment on lines 136 to 153

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The function _deploy has been modified to use the owner's address as the CREATE2 salt instead of using a combination of tx.origin and a seed. This change improves security by preventing front-running attacks. Additionally, the constructorParams struct is updated to include the owner, target, and data values. The constructor parameters are set before deploying the proxy contract with CREATE2. After deployment, the constructor parameters are deleted, and the owner is associated with the proxy in the _proxies mapping. Finally, an event is emitted to log the creation of the proxy.


/// @inheritdoc IPRBProxyRegistry
Expand Down Expand Up @@ -249,11 +227,8 @@ contract PRBProxyRegistry is IPRBProxyRegistry {

/// @dev See the documentation for the user-facing functions that call this internal function.
function _deploy(address owner) internal returns (IPRBProxy proxy) {
// Load the next seed.
bytes32 seed = nextSeeds[tx.origin];

// Prevent front-running the salt by hashing the concatenation of "tx.origin" and the user-provided seed.
bytes32 salt = keccak256(abi.encode(tx.origin, seed));
// Use the address of the owner as the CREATE2 salt.
bytes32 salt = bytes32(abi.encodePacked(owner));

// Set the owner and empty out the target and the data to prevent reentrancy.
constructorParams = ConstructorParams({ owner: owner, target: address(0), data: "" });
Expand All @@ -262,24 +237,10 @@ contract PRBProxyRegistry is IPRBProxyRegistry {
proxy = new PRBProxy{ salt: salt }();
delete constructorParams;

// Associate the the owner with the proxy in the mapping.
// Associate the owner and the proxy.
_proxies[owner] = proxy;

// Increment the seed.
// We're using unchecked arithmetic here because this cannot realistically overflow, ever.
unchecked {
nextSeeds[tx.origin] = bytes32(uint256(seed) + 1);
}

// Log the proxy via en event.
// forgefmt: disable-next-line
emit DeployProxy({
origin: tx.origin,
operator: msg.sender,
owner: owner,
seed: seed,
salt: salt,
proxy: proxy
});
// Log the creation of the proxy.
emit DeployProxy({ operator: msg.sender, owner: owner, proxy: proxy });
}
}
Comment on lines 237 to 246

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

Note: This review was outside of the patch, so it was mapped to the patch with the greatest overlap. Original lines [227-245]

The same changes made in the previous section (lines 136-153) have been applied to the _deploy function in this section as well. The function now uses the owner's address as the CREATE2 salt, sets the constructor parameters, deploys the proxy contract, associates the owner with the proxy, and emits an event to log the creation of the proxy.

No substantive issues were detected in the provided code changes.

27 changes: 8 additions & 19 deletions src/interfaces/IPRBProxyRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,7 @@ interface IPRBProxyRegistry {
//////////////////////////////////////////////////////////////////////////*/

/// @notice Emitted when a new proxy is deployed.
event DeployProxy(
address indexed origin,
address indexed operator,
address indexed owner,
bytes32 seed,
bytes32 salt,
IPRBProxy proxy
);
event DeployProxy(address indexed operator, address indexed owner, IPRBProxy proxy);

/// @notice Emitted when a plugin is installed.
event InstallPlugin(address indexed owner, IPRBProxy indexed proxy, IPRBProxyPlugin indexed plugin);
Comment on lines 32 to 38

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The event signatures in IPRBProxyRegistry.sol have been modified. The DeployProxy event no longer includes the origin, seed, and salt parameters, and the InstallPlugin event remains unchanged. Please ensure that these changes are reflected in the event emissions throughout the codebase.

Expand Down Expand Up @@ -122,15 +115,11 @@ interface IPRBProxyRegistry {
/// @param owner The user address to make the query for.
function getProxy(address owner) external view returns (IPRBProxy proxy);

Comment on lines 115 to 117

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The nextSeeds function has been removed from the interface. Please remove this function from the implementation as well.

/// @notice The seed that will be used to deploy the next proxy for the provided origin.
/// @param origin The externally owned account (EOA) that is part of the CREATE2 salt.
function nextSeeds(address origin) external view returns (bytes32 seed);

/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Deploys a new proxy with CREATE2 by setting the caller as the owner.
/// @notice Deploys a new proxy with CREATE2, using the caller as the owner.
///
/// @dev Emits a {DeployProxy} event.
///
Expand All @@ -140,8 +129,9 @@ interface IPRBProxyRegistry {
/// @return proxy The address of the newly deployed proxy contract.
function deploy() external returns (IPRBProxy proxy);

/// @notice Deploys a new proxy via CREATE2 by setting the caller as the owner, and delegate calls to the provided
/// target contract by forwarding the data. It returns the data it gets back, and bubbles up any potential revert.
/// @notice Deploys a new proxy via CREATE2, using the caller as the owner. It delegate calls to the provided
/// target contract by forwarding the data. Then, it returns the data it gets back, and bubbles up any potential
/// revert.
///
/// @dev Emits a {DeployProxy} and an {Execute} event.
///
Comment on lines 129 to 137

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The deploy function has been updated to include additional functionality. Please review the changes and ensure that the new implementation is correct.

Expand Down Expand Up @@ -170,9 +160,8 @@ interface IPRBProxyRegistry {
/// @dev Emits an {InstallPlugin} event.
///
/// Notes:
/// - Installing a plugin is a potentially dangerous operation, because anyone will be able to run the plugin.
/// - Plugin methods that have the same selector as {PRBProxy.execute} will be installed, but they will never be run
/// by the proxy.
/// - Installing a plugin is a potentially dangerous operation, because anyone can run the plugin.
/// - Plugin methods that have the same selector as {PRBProxy.execute} can be installed, but they can never be run.
///
/// Requirements:
/// - The caller must have a proxy.
Comment on lines 160 to 167

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The comments for the installPlugin function have been updated. Please review the changes and ensure that the comments accurately reflect the behavior of the function.

Expand All @@ -193,7 +182,7 @@ interface IPRBProxyRegistry {
/// Requirements:
/// - The caller must have a proxy.
///
/// @param envoy The address of the envoy account.
/// @param envoy The address of the account given permission to call the target contract.
/// @param target The address of the target contract.
/// @param permission The boolean permission to set.
function setPermission(address envoy, address target, bool permission) external;
Comment on lines 182 to 188

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The comments for the setPermission function have been updated. Please review the changes and ensure that the comments accurately reflect the behavior of the function.

Suggested change
/// Requirements:
/// - The caller must have a proxy.
///
/// @param envoy The address of the envoy account.
/// @param envoy The address of the account given permission to call the target contract.
/// @param target The address of the target contract.
/// @param permission The boolean permission to set.
function setPermission(address envoy, address target, bool permission) external;
//////////////////////////////////////////////////////////////////////////*/
/// @notice Emitted when a new proxy is deployed.
event DeployProxy(address indexed operator, address indexed owner, IPRBProxy proxy);
/// @notice Emitted when a plugin is installed.
event InstallPlugin(address indexed owner, IPRBProxy indexed proxy, IPRBProxyPlugin indexed plugin);
Suggested change
/// Requirements:
/// - The caller must have a proxy.
///
/// @param envoy The address of the envoy account.
/// @param envoy The address of the account given permission to call the target contract.
/// @param target The address of the target contract.
/// @param permission The boolean permission to set.
function setPermission(address envoy, address target, bool permission) external;
/// @param owner The user address to make the query for.
function getProxy(address owner) external view returns (IPRBProxy proxy);
/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
/// @notice Deploys a new proxy with CREATE2, using the caller as the owner.
///
/// @dev Emits a {DeployProxy} event.
///
Suggested change
/// Requirements:
/// - The caller must have a proxy.
///
/// @param envoy The address of the envoy account.
/// @param envoy The address of the account given permission to call the target contract.
/// @param target The address of the target contract.
/// @param permission The boolean permission to set.
function setPermission(address envoy, address target, bool permission) external;
/// @return proxy The address of the newly deployed proxy contract.
function deploy() external returns (IPRBProxy proxy);
/// @notice Deploys a new proxy via CREATE2, using the caller as the owner. It delegate calls to the provided
/// target contract by forwarding the data. Then, it returns the data it gets back, and bubbles up any potential
/// revert.
///
/// @dev Emits a {DeployProxy} and an {Execute} event.
///
Suggested change
/// Requirements:
/// - The caller must have a proxy.
///
/// @param envoy The address of the envoy account.
/// @param envoy The address of the account given permission to call the target contract.
/// @param target The address of the target contract.
/// @param permission The boolean permission to set.
function setPermission(address envoy, address target, bool permission) external;
/// @dev Emits an {InstallPlugin} event.
///
/// Notes:
/// - Installing a plugin is a potentially dangerous operation, because anyone can run the plugin.
/// - Plugin methods that have the same selector as {PRBProxy.execute} can be installed, but they can never be run.
///
/// Requirements:
/// - The caller must have a proxy.
Suggested change
/// Requirements:
/// - The caller must have a proxy.
///
/// @param envoy The address of the envoy account.
/// @param envoy The address of the account given permission to call the target contract.
/// @param target The address of the target contract.
/// @param permission The boolean permission to set.
function setPermission(address envoy, address target, bool permission) external;
/// Requirements:
/// - The caller must have a proxy.
///
/// @param envoy The address of the account given permission to call the target contract.
/// @param target The address of the target contract.
/// @param permission The boolean permission to set.
function setPermission(address envoy, address target, bool permission) external;

Expand Down
15 changes: 3 additions & 12 deletions test/Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,6 @@ abstract contract Base_Test is Assertions, Events, StdCheats, StdUtils {
address payable eve;
}

/*//////////////////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////////////////*/

uint256 internal constant DEFAULT_MIN_GAS_RESERVE = 5000;
bytes32 internal constant SEED_ONE = bytes32(uint256(0x01));
bytes32 internal constant SEED_TWO = bytes32(uint256(0x02));
bytes32 internal constant SEED_ZERO = bytes32(uint256(0x00));

/*//////////////////////////////////////////////////////////////////////////
VARIABLES
//////////////////////////////////////////////////////////////////////////*/
Comment on lines 60 to 65

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The constant declarations DEFAULT_MIN_GAS_RESERVE, SEED_ONE, SEED_TWO, and SEED_ZERO have been removed. Please ensure that these constants are no longer used in the codebase and update any references accordingly.

Expand Down Expand Up @@ -123,16 +114,16 @@ abstract contract Base_Test is Assertions, Events, StdCheats, StdUtils {
deployRegistryConditionally();

// Make Alice both the caller and the origin.
vm.startPrank({ msgSender: users.alice, txOrigin: users.alice });
vm.startPrank({ msgSender: users.alice });
}

/*//////////////////////////////////////////////////////////////////////////
HELPERS
//////////////////////////////////////////////////////////////////////////*/

/// @dev Computes the proxy address without deploying it.
function computeProxyAddress(address origin, bytes32 seed) internal returns (address) {
bytes32 salt = keccak256(abi.encode(origin, seed));
function computeProxyAddress(address owner) internal returns (address) {
bytes32 salt = bytes32(abi.encodePacked(owner));
bytes32 creationBytecodeHash = keccak256(getProxyBytecode());
// Use the Create2 utility from Forge Std.
return computeCreate2Address({ salt: salt, initcodeHash: creationBytecodeHash, deployer: address(registry) });
Comment on lines 114 to 129

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The function computeProxyAddress has been modified. The parameter origin has been removed, and the parameter owner has been added. The computation of the salt has also changed from keccak256(abi.encode(origin, seed)) to bytes32(abi.encodePacked(owner)). Please verify that this change is intentional and update any references to origin accordingly.

Expand Down
68 changes: 14 additions & 54 deletions test/registry/deploy-and-execute/deployAndExecute.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,82 +32,42 @@ contract DeployAndExecute_Test is Registry_Test {
_;
}

function testFuzz_DeployAndExecute_ProxyAddress(address origin, address owner) external whenOwnerDoesNotHaveProxy {
changePrank({ txOrigin: origin, msgSender: owner });
function testFuzz_DeployAndExecute_ProxyAddress(address owner) external whenOwnerDoesNotHaveProxy {
changePrank({ msgSender: owner });
IPRBProxy actualProxy = registry.deployAndExecute(target, data);
address expectedProxy = computeProxyAddress(origin, SEED_ZERO);
address expectedProxy = computeProxyAddress(owner);
assertEq(address(actualProxy), expectedProxy, "deployed proxy address mismatch");
}
Comment on lines +35 to 40

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The function testFuzz_DeployAndExecute_ProxyAddress has been modified to accept an owner parameter instead of the origin and owner parameters. The changePrank function is called with { msgSender: owner } instead of { txOrigin: origin, msgSender: owner }. This change simplifies the function signature and removes the need for the origin parameter. No further issues are detected in this range.


function testFuzz_DeployAndExecute_ProxyOwner(address origin, address owner) external whenOwnerDoesNotHaveProxy {
changePrank({ txOrigin: origin, msgSender: owner });
function testFuzz_DeployAndExecute_ProxyOwner(address owner) external whenOwnerDoesNotHaveProxy {
changePrank({ msgSender: owner });
IPRBProxy proxy = registry.deployAndExecute(target, data);
address actualOwner = proxy.owner();
address expectedOwner = owner;
assertEq(actualOwner, expectedOwner, "proxy owner mismatch");
Comment on lines +42 to 47

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The function testFuzz_DeployAndExecute_ProxyOwner has been modified to accept an owner parameter instead of the origin and owner parameters. The changePrank function is called with { msgSender: owner } instead of { txOrigin: origin, msgSender: owner }. This change simplifies the function signature and removes the need for the origin parameter. No further issues are detected in this range.

}

function testFuzz_DeployAndExecute_UpdateNextSeeds(
address origin,
address owner
)
external
whenOwnerDoesNotHaveProxy
{
changePrank({ txOrigin: origin, msgSender: owner });
registry.deployAndExecute(target, data);

bytes32 actualNextSeed = registry.nextSeeds(origin);
bytes32 expectedNextSeed = SEED_ONE;
assertEq(actualNextSeed, expectedNextSeed, "next seed mismatch");
}

function testFuzz_DeployAndExecute_UpdateProxies(
address origin,
address owner
)
external
whenOwnerDoesNotHaveProxy
{
changePrank({ txOrigin: origin, msgSender: owner });
function testFuzz_DeployAndExecute_UpdateProxies(address owner) external whenOwnerDoesNotHaveProxy {
changePrank({ msgSender: owner });
registry.deployAndExecute(target, data);

address actualProxyAddress = address(registry.getProxy(owner));
address expectedProxyAddress = computeProxyAddress(origin, SEED_ZERO);
address expectedProxyAddress = computeProxyAddress(owner);
assertEq(actualProxyAddress, expectedProxyAddress, "proxy address mismatch");
}
Comment on lines +50 to 57

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The function testFuzz_DeployAndExecute_UpdateProxies has been modified to accept an owner parameter instead of the origin and owner parameters. The changePrank function is called with { msgSender: owner } instead of { txOrigin: origin, msgSender: owner }. This change simplifies the function signature and removes the need for the origin parameter. No further issues are detected in this range.


function testFuzz_DeployAndExecute_Event_DeployProxy(
address origin,
address owner
)
external
whenOwnerDoesNotHaveProxy
{
changePrank({ txOrigin: origin, msgSender: owner });
function testFuzz_DeployAndExecute_Event_DeployProxy(address owner) external whenOwnerDoesNotHaveProxy {
changePrank({ msgSender: owner });

vm.expectEmit({ emitter: address(registry) });
emit DeployProxy({
origin: origin,
operator: owner,
owner: owner,
seed: SEED_ZERO,
salt: keccak256(abi.encode(origin, SEED_ZERO)),
proxy: IPRBProxy(computeProxyAddress(origin, SEED_ZERO))
});
emit DeployProxy({ operator: owner, owner: owner, proxy: IPRBProxy(computeProxyAddress(owner)) });
registry.deployAndExecute(target, data);
}
Comment on lines +59 to 65

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The function testFuzz_DeployAndExecute_Event_DeployProxy has been modified to accept an owner parameter instead of the origin and owner parameters. The changePrank function is called with { msgSender: owner } instead of { txOrigin: origin, msgSender: owner }. This change simplifies the function signature and removes the need for the origin parameter. No further issues are detected in this range.


function testFuzz_DeployAndExecute_Event_Execute(
address origin,
address owner
)
external
whenOwnerDoesNotHaveProxy
{
changePrank({ txOrigin: origin, msgSender: owner });
function testFuzz_DeployAndExecute_Event_Execute(address owner) external whenOwnerDoesNotHaveProxy {
changePrank({ msgSender: owner });

vm.expectEmit({ emitter: computeProxyAddress({ origin: origin, seed: SEED_ZERO }) });
vm.expectEmit({ emitter: computeProxyAddress(owner) });
emit Execute({ target: address(targets.echo), data: data, response: abi.encode(input) });
registry.deployAndExecute(target, data);
}
Expand Down
1 change: 0 additions & 1 deletion test/registry/deploy-and-execute/deployAndExecute.tree
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ deployAndExecute.t.sol
└── when the owner does not have a proxy
├── it should generate the correct address
├── it should initialize the owner
├── it should update the next seeds mapping
├── it should update the proxies mapping
Comment on lines 4 to 7

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 OpenAI

The comment chains indicate that the nextSeeds mapping has been removed and replaced with the proxies mapping. However, the code in the new hunk still references the old next seeds mapping. Please update the code to use the correct proxies mapping instead.

---new_hunk---

4:  └── when the owner does not have a proxy
5:     ├── it should generate the correct address
6:     ├── it should initialize the owner
7:     ├── it should update the proxies mapping
8:     ├── it should delegate call to the target contract
9:     ├── it should emit a {DeployProxy} event

---old_hunk---

 └── when the owner does not have a proxy
    ├── it should generate the correct address
    ├── it should initialize the owner
   ├── it should update the next seeds mapping
    ├── it should update the proxies mapping
    ├── it should delegate call to the target contract
    ├── it should emit a {DeployProxy} event

---comment_chains---

// Comment chain 1
// Removed nextSeeds mapping

// Comment chain 2
// Replaced nextSeeds with proxies

---end_change_section---

├── it should delegate call to the target contract
├── it should emit a {DeployProxy} event
Expand Down
Loading