Skip to content

Commit

Permalink
Emit Event for initializer and saltNonce data (#849)
Browse files Browse the repository at this point in the history
As part of improving the capabilities for indexing safe creation data, a
new set of functions is introduced which emits an extra event named
`ProxyCreationL2` which also emits the `initializer` and `saltNonce`.
The previous function remains the same for backward compatibility.
  • Loading branch information
remedcu authored Oct 28, 2024
1 parent 6fde75d commit b0514b8
Show file tree
Hide file tree
Showing 2 changed files with 255 additions and 0 deletions.
33 changes: 33 additions & 0 deletions contracts/proxies/SafeProxyFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {SafeProxy} from "./SafeProxy.sol";
*/
contract SafeProxyFactory {
event ProxyCreation(SafeProxy indexed proxy, address singleton);
event ProxyCreationL2(SafeProxy indexed proxy, address singleton, bytes initializer, uint256 saltNonce);

/// @dev Allows to retrieve the creation code used for the Proxy deployment. With this it is easily possible to calculate predicted address.
function proxyCreationCode() public pure returns (bytes memory) {
Expand Down Expand Up @@ -60,6 +61,18 @@ contract SafeProxyFactory {
emit ProxyCreation(proxy, _singleton);
}

/**
* @notice Deploys a new proxy with `_singleton` singleton and `saltNonce` salt. Optionally executes an initializer call to a new proxy.
* @dev Emits an extra event to allow tracking of `initializer` and `saltNonce`.
* @param _singleton Address of singleton contract. Must be deployed at the time of execution.
* @param initializer Payload for a message call to be sent to a new proxy contract.
* @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract.
*/
function createProxyWithNonceL2(address _singleton, bytes memory initializer, uint256 saltNonce) public returns (SafeProxy proxy) {
proxy = createProxyWithNonce(_singleton, initializer, saltNonce);
emit ProxyCreationL2(proxy, _singleton, initializer, saltNonce);
}

/**
* @notice Deploys a new chain-specific proxy with `_singleton` singleton and `saltNonce` salt. Optionally executes an initializer call to a new proxy.
* @dev Allows to create a new proxy contract that should exist only on 1 network (e.g. specific governance or admin accounts)
Expand Down Expand Up @@ -98,6 +111,26 @@ contract SafeProxyFactory {
if (address(callback) != address(0)) callback.proxyCreated(proxy, _singleton, initializer, saltNonce);
}

/**
* @notice Deploy a new proxy with `_singleton` singleton and `saltNonce` salt.
* Optionally executes an initializer call to a new proxy and calls a specified callback address `callback`.
* @dev Emits an extra event to allow tracking of `initializer` and `saltNonce`.
* @param _singleton Address of singleton contract. Must be deployed at the time of execution.
* @param initializer Payload for a message call to be sent to a new proxy contract.
* @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract.
* @param callback Callback that will be invoked after the new proxy contract has been successfully deployed and initialized.
*/
function createProxyWithCallbackL2(
address _singleton,
bytes memory initializer,
uint256 saltNonce,
IProxyCreationCallback callback
) public returns (SafeProxy proxy) {
uint256 saltNonceWithCallback = uint256(keccak256(abi.encodePacked(saltNonce, callback)));
proxy = createProxyWithNonceL2(_singleton, initializer, saltNonceWithCallback);
if (address(callback) != address(0)) callback.proxyCreated(proxy, _singleton, initializer, saltNonce);
}

/**
* @notice Returns true if `account` is a contract.
* @dev This function will return false if invoked during the constructor of a contract,
Expand Down
222 changes: 222 additions & 0 deletions test/factory/ProxyFactory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,93 @@ describe("ProxyFactory", () => {
});
});

describe("createProxyWithNonceL2", () => {
const saltNonce = 42;

it("should revert if singleton address is not a contract", async () => {
const { factory } = await setupTests();
const randomAddress = ethers.getAddress(ethers.hexlify(ethers.randomBytes(20)));
await expect(factory.createProxyWithNonceL2(randomAddress, "0x", saltNonce)).to.be.revertedWith(
"Singleton contract not deployed",
);
});

it("should revert with invalid initializer", async () => {
const { factory, singleton } = await setupTests();
const singletonAddress = await singleton.getAddress();
await expect(factory.createProxyWithNonceL2(singletonAddress, "0x42baddad", saltNonce)).to.be.revertedWithoutReason();
});

it("should emit event without initializing", async () => {
const { factory, singleton } = await setupTests();
const singletonAddress = await singleton.getAddress();
const initCode = "0x";
const proxyAddress = await calculateProxyAddress(factory, singletonAddress, initCode, saltNonce, hre.network.zksync);
await expect(factory.createProxyWithNonceL2(singletonAddress, initCode, saltNonce))
.to.emit(factory, "ProxyCreation")
.withArgs(proxyAddress, singletonAddress)
.to.emit(factory, "ProxyCreationL2")
.withArgs(proxyAddress, singletonAddress, initCode, saltNonce);
const proxy = singleton.attach(proxyAddress) as Contract;

expect(await proxy.creator()).to.be.eq(AddressZero);
expect(await proxy.isInitialized()).to.be.eq(false);
expect(await proxy.masterCopy()).to.be.eq(singletonAddress);
expect(await singleton.masterCopy()).to.be.eq(AddressZero);
expect(await hre.ethers.provider.getCode(proxyAddress)).to.be.eq(await getSafeProxyRuntimeCode());
});

it("should emit event with initializing", async () => {
const { factory, singleton } = await setupTests();
const singletonAddress = await singleton.getAddress();
const factoryAddress = await factory.getAddress();

const initCode = singleton.interface.encodeFunctionData("init", []);
const proxyAddress = await calculateProxyAddress(factory, singletonAddress, initCode, saltNonce, hre.network.zksync);
await expect(factory.createProxyWithNonceL2(singletonAddress, initCode, saltNonce))
.to.emit(factory, "ProxyCreation")
.withArgs(proxyAddress, singletonAddress)
.to.emit(factory, "ProxyCreationL2")
.withArgs(proxyAddress, singletonAddress, initCode, saltNonce);
const proxy = singleton.attach(proxyAddress) as Contract;
expect(await proxy.creator()).to.be.eq(factoryAddress);
expect(await proxy.isInitialized()).to.be.eq(true);
expect(await proxy.masterCopy()).to.be.eq(singletonAddress);
expect(await singleton.masterCopy()).to.be.eq(AddressZero);
expect(await hre.ethers.provider.getCode(proxyAddress)).to.be.eq(await getSafeProxyRuntimeCode());
});

it("should not be able to deploy same proxy twice", async () => {
const { factory, singleton } = await setupTests();
const singletonAddress = await singleton.getAddress();

const initCode = singleton.interface.encodeFunctionData("init", []);
const proxyAddress = await calculateProxyAddress(factory, singletonAddress, initCode, saltNonce, hre.network.zksync);
await expect(factory.createProxyWithNonceL2(singletonAddress, initCode, saltNonce))
.to.emit(factory, "ProxyCreation")
.withArgs(proxyAddress, singletonAddress)
.to.emit(factory, "ProxyCreationL2")
.withArgs(proxyAddress, singletonAddress, initCode, saltNonce);
await expect(factory.createProxyWithNonceL2(singletonAddress, initCode, saltNonce)).to.be.revertedWith("Create2 call failed");
});
});

describe("createProxyWithNonce & createProxyWithNonceL2", () => {
const saltNonce = 42;

it("should result in same proxy", async () => {
const { factory, singleton } = await setupTests();
const singletonAddress = await singleton.getAddress();

const initCode = singleton.interface.encodeFunctionData("init", []);
const proxyAddress = await calculateProxyAddress(factory, singletonAddress, initCode, saltNonce, hre.network.zksync);
const proxyAddressOnchain = await factory.createProxyWithNonce.staticCall(singletonAddress, initCode, saltNonce);
const proxyAddressOnchainL2 = await factory.createProxyWithNonceL2.staticCall(singletonAddress, initCode, saltNonce);
expect(proxyAddress).to.be.eq(proxyAddressOnchain);
expect(proxyAddress).to.be.eq(proxyAddressOnchainL2);
});
});

describe("createChainSpecificProxyWithNonce", () => {
const saltNonce = 42;

Expand Down Expand Up @@ -268,4 +355,139 @@ describe("ProxyFactory", () => {
expect(await hre.ethers.provider.getCode(proxyAddress)).to.be.eq(await getSafeProxyRuntimeCode());
});
});

describe("createProxyWithCallbackL2", () => {
const saltNonce = 42;

it("check callback is invoked", async () => {
const { factory, mock, singleton } = await setupTests();
const mockAddress = await mock.getAddress();
const singletonAddress = await singleton.getAddress();
const factoryAddress = await factory.getAddress();

const callback = await hre.ethers.getContractAt("IProxyCreationCallback", mockAddress);
const initCode = singleton.interface.encodeFunctionData("init", []);

const proxyAddress = await calculateProxyAddressWithCallback(
factory,
singletonAddress,
initCode,
saltNonce,
mockAddress,
hre.network.zksync,
);
await expect(factory.createProxyWithCallbackL2(singletonAddress, initCode, saltNonce, mockAddress))
.to.emit(factory, "ProxyCreation")
.withArgs(proxyAddress, singletonAddress)
.to.emit(factory, "ProxyCreationL2")
.withArgs(
proxyAddress,
singletonAddress,
initCode,
ethers.solidityPackedKeccak256(["uint256", "address"], [saltNonce, mockAddress]),
);

expect(await mock.invocationCount()).to.be.deep.equal(1n);

const callbackData = callback.interface.encodeFunctionData("proxyCreated", [proxyAddress, factoryAddress, initCode, saltNonce]);
expect(await mock.invocationCountForMethod(callbackData)).to.eq(1n);
});

it("check callback error cancels deployment", async () => {
const { factory, mock, singleton } = await setupTests();
const singletonAddress = await singleton.getAddress();
const mockAddress = await mock.getAddress();
const initCode = "0x";
await mock.givenAnyRevert();
await expect(
factory.createProxyWithCallbackL2(singletonAddress, initCode, saltNonce, mockAddress),
"Should fail if callback fails",
).to.be.reverted;

await mock.reset();
// Should be successfull now
const proxyAddress = await calculateProxyAddressWithCallback(
factory,
singletonAddress,
initCode,
saltNonce,
mockAddress,
hre.network.zksync,
);
await expect(factory.createProxyWithCallbackL2(singletonAddress, initCode, saltNonce, mockAddress))
.to.emit(factory, "ProxyCreation")
.withArgs(proxyAddress, singletonAddress)
.to.emit(factory, "ProxyCreationL2")
.withArgs(
proxyAddress,
singletonAddress,
initCode,
ethers.solidityPackedKeccak256(["uint256", "address"], [saltNonce, mockAddress]),
);
});

it("should work without callback", async () => {
const { factory, singleton } = await setupTests();
const singletonAddress = await singleton.getAddress();
const initCode = "0x";
const proxyAddress = await calculateProxyAddressWithCallback(
factory,
singletonAddress,
initCode,
saltNonce,
AddressZero,
hre.network.zksync,
);
await expect(factory.createProxyWithCallbackL2(singletonAddress, initCode, saltNonce, AddressZero))
.to.emit(factory, "ProxyCreation")
.withArgs(proxyAddress, singletonAddress)
.to.emit(factory, "ProxyCreationL2")
.withArgs(
proxyAddress,
singletonAddress,
initCode,
ethers.solidityPackedKeccak256(["uint256", "address"], [saltNonce, AddressZero]),
);
const proxy = singleton.attach(proxyAddress) as Contract;
expect(await proxy.creator()).to.be.eq(AddressZero);
expect(await proxy.isInitialized()).to.be.eq(false);
expect(await proxy.masterCopy()).to.be.eq(singletonAddress);
expect(await singleton.masterCopy()).to.be.eq(AddressZero);
expect(await hre.ethers.provider.getCode(proxyAddress)).to.be.eq(await getSafeProxyRuntimeCode());
});
});

describe("createProxyWithCallback & createProxyWithCallbackL2", () => {
const saltNonce = 42;

it("should result in same proxy", async () => {
const { factory, mock, singleton } = await setupTests();
const mockAddress = await mock.getAddress();
const singletonAddress = await singleton.getAddress();

const initCode = singleton.interface.encodeFunctionData("init", []);
const proxyAddress = await calculateProxyAddressWithCallback(
factory,
singletonAddress,
initCode,
saltNonce,
mockAddress,
hre.network.zksync,
);
const proxyAddressOnchain = await factory.createProxyWithCallback.staticCall(
singletonAddress,
initCode,
saltNonce,
mockAddress,
);
const proxyAddressOnchainL2 = await factory.createProxyWithCallbackL2.staticCall(
singletonAddress,
initCode,
saltNonce,
mockAddress,
);
expect(proxyAddress).to.be.eq(proxyAddressOnchain);
expect(proxyAddress).to.be.eq(proxyAddressOnchainL2);
});
});
});

0 comments on commit b0514b8

Please sign in to comment.