Skip to content
This repository was archived by the owner on Apr 27, 2022. It is now read-only.

Commit 9b5796c

Browse files
authored
Upgradable Authenticator (#351)
Closes #167 This PR introduces minor changes to the `AllowListAuthenticator` making it upgradable and includes tests verifying upgradability. In particular: - removing `customInitiallyOwnable` as it is no longer used: Not yet sure how this will affect the `deployments` directory which still includes it. - Updating authenticator deployment script to deploy as proxy. - rename authenticator `owner` to `manager` because of name collision with proxy owner - introduce proxy.ts with helper methods to fetch implementation and owner address. - Include two unit tests: one verifying that upgrade is possible and the other to ensure preservation of storage. Note that the unit tests involving Authenticator's non-upgrade related functionality do not use the proxy deployment as specified in the migration scripts, so that manager must be set immediately after deployment. We had originally planned/hoped to use the `hardhat-upgrades` plugin and opened the following two PRs to `openzeppelin-upgrades`, but this plugin turned out to be unnecessary. - Custom Deployments: OpenZeppelin/openzeppelin-upgrades#273 - Manual Safety Override: OpenZeppelin/openzeppelin-upgrades#280 ### Test Plan Old + new unit and e2e tests.
1 parent 2598df9 commit 9b5796c

12 files changed

+178
-48
lines changed

src/contracts/GPv2AllowListAuthentication.sol

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,31 @@ pragma solidity ^0.7.6;
44
import "@gnosis.pm/util-contracts/contracts/StorageAccessible.sol";
55
import "@openzeppelin/contracts/utils/EnumerableSet.sol";
66
import "./interfaces/GPv2Authentication.sol";
7-
import "./ownership/CustomInitiallyOwnable.sol";
87

98
/// @title Gnosis Protocol v2 Access Control Contract
109
/// @author Gnosis Developers
11-
contract GPv2AllowListAuthentication is
12-
CustomInitiallyOwnable,
13-
GPv2Authentication,
14-
StorageAccessible
15-
{
10+
contract GPv2AllowListAuthentication is GPv2Authentication, StorageAccessible {
1611
using EnumerableSet for EnumerableSet.AddressSet;
1712

13+
address public manager;
14+
1815
EnumerableSet.AddressSet private solvers;
1916

20-
// solhint-disable-next-line no-empty-blocks
21-
constructor(address initialOwner) CustomInitiallyOwnable(initialOwner) {}
17+
function initializeManager(address manager_) external {
18+
require(manager == address(0), "GPv2: already initialized");
19+
manager = manager_;
20+
}
21+
22+
modifier onlyManager() {
23+
require(manager == msg.sender, "GPv2: caller not manager");
24+
_;
25+
}
2226

23-
function addSolver(address solver) external onlyOwner {
27+
function addSolver(address solver) external onlyManager {
2428
solvers.add(solver);
2529
}
2630

27-
function removeSolver(address solver) external onlyOwner {
31+
function removeSolver(address solver) external onlyManager {
2832
solvers.remove(solver);
2933
}
3034

src/contracts/ownership/CustomInitiallyOwnable.sol

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// SPDX-License-Identifier: LGPL-3.0-or-later
2+
pragma solidity ^0.7.6;
3+
4+
import "../GPv2AllowListAuthentication.sol";
5+
6+
contract GPv2AllowListAuthenticationV2 is GPv2AllowListAuthentication {
7+
function newMethod() external pure returns (uint256) {
8+
return 1337;
9+
}
10+
}

src/deploy/001_authenticator.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ const deployAuthenticator: DeployFunction = async function ({
1111
const { deploy } = deployments;
1212

1313
const { authenticator } = CONTRACT_NAMES;
14-
1514
await deploy(authenticator, {
1615
from: deployer,
1716
gasLimit: 2000000,
18-
args: [owner],
1917
deterministicDeployment: SALT,
2018
log: true,
19+
proxy: {
20+
owner,
21+
methodName: "initializeManager",
22+
},
23+
args: [owner],
2124
});
2225
};
2326

src/ts/deploy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export type ContractName = typeof CONTRACT_NAMES[keyof typeof CONTRACT_NAMES];
3434
export type DeploymentArguments<
3535
T extends ContractName
3636
> = T extends typeof CONTRACT_NAMES.authenticator
37-
? [string]
37+
? never
3838
: T extends typeof CONTRACT_NAMES.settlement
3939
? [string]
4040
: unknown[];

src/ts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ export * from "./settlement";
2525
export * from "./reader";
2626
export * from "./deploy";
2727
export * from "./sign";
28+
export * from "./proxy";
2829
export * from "./types/ethers";

src/ts/proxy.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// defined in https://eips.ethereum.org/EIPS/eip-1967
2+
3+
import { BigNumber } from "ethers";
4+
import { ethers } from "hardhat";
5+
6+
// The proxy contract used by hardhat-deploy implements EIP-1967 (Standard Proxy
7+
// Storage Slot). See <https://eips.ethereum.org/EIPS/eip-1967>.
8+
function slot(string: string) {
9+
return ethers.utils.defaultAbiCoder.encode(
10+
["bytes32"],
11+
[BigNumber.from(ethers.utils.id(string)).sub(1)],
12+
);
13+
}
14+
const IMPLEMENTATION_STORAGE_SLOT = slot("eip1967.proxy.implementation");
15+
const OWNER_STORAGE_SLOT = slot("eip1967.proxy.admin");
16+
17+
/**
18+
* Returns the address of the implementation of an EIP-1967-compatible proxy
19+
* from its address.
20+
*
21+
* @param proxy Address of the proxy contract.
22+
* @returns The address of the contract storing the proxy implementation.
23+
*/
24+
export async function implementationAddress(proxy: string): Promise<string> {
25+
const [implementation] = await ethers.utils.defaultAbiCoder.decode(
26+
["address"],
27+
await ethers.provider.getStorageAt(proxy, IMPLEMENTATION_STORAGE_SLOT),
28+
);
29+
return implementation;
30+
}
31+
32+
/**
33+
* Returns the address of the implementation of an EIP-1967-compatible proxy
34+
* from its address.
35+
*
36+
* @param proxy Address of the proxy contract.
37+
* @returns The address of the administrator of the proxy.
38+
*/
39+
export async function ownerAddress(proxy: string): Promise<string> {
40+
const [owner] = await ethers.utils.defaultAbiCoder.decode(
41+
["address"],
42+
await ethers.provider.getStorageAt(proxy, OWNER_STORAGE_SLOT),
43+
);
44+
return owner;
45+
}

test/AllowListStorageReader.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ describe("AllowListStorageReader", () => {
2121
);
2222

2323
reader = await AllowListStorageReader.deploy();
24-
authenticator = await GPv2AllowListAuthentication.deploy(owner.address);
24+
authenticator = await GPv2AllowListAuthentication.deploy();
25+
await authenticator.initializeManager(owner.address);
2526
allowListReader = new AllowListReader(authenticator, reader);
2627
});
2728

test/GPv2AllowListAuthenticator.test.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,28 @@ describe("GPv2AllowListAuthentication", () => {
1212
deployer,
1313
);
1414

15-
authenticator = await GPv2AllowListAuthentication.deploy(owner.address);
15+
authenticator = await GPv2AllowListAuthentication.deploy();
16+
await authenticator.initializeManager(owner.address);
1617
});
1718

1819
describe("constructor", () => {
19-
it("should set the owner", async () => {
20-
expect(await authenticator.owner()).to.equal(owner.address);
20+
it("should initialize the manager", async () => {
21+
expect(await authenticator.manager()).to.equal(owner.address);
2122
});
22-
it("deployer is not the owner", async () => {
23-
expect(await authenticator.owner()).not.to.be.equal(deployer.address);
23+
24+
it("ensures initializeManager is idempotent", async () => {
25+
await expect(
26+
authenticator.initializeManager(nonOwner.address),
27+
).to.revertedWith("GPv2: already initialized");
28+
29+
// Also reverts when called by owner.
30+
await expect(
31+
authenticator.connect(owner).initializeManager(nonOwner.address),
32+
).to.revertedWith("GPv2: already initialized");
33+
});
34+
35+
it("deployer is not the manager", async () => {
36+
expect(await authenticator.manager()).not.to.be.equal(deployer.address);
2437
});
2538
});
2639

@@ -33,7 +46,7 @@ describe("GPv2AllowListAuthentication", () => {
3346
it("should not allow non-owner to add solver", async () => {
3447
await expect(
3548
authenticator.connect(nonOwner).addSolver(solver.address),
36-
).to.be.revertedWith("caller is not the owner");
49+
).to.be.revertedWith("GPv2: caller not manager");
3750
});
3851
});
3952

@@ -46,7 +59,7 @@ describe("GPv2AllowListAuthentication", () => {
4659
it("should not allow non-owner to remove solver", async () => {
4760
await expect(
4861
authenticator.connect(nonOwner).removeSolver(solver.address),
49-
).to.be.revertedWith("caller is not the owner");
62+
).to.be.revertedWith("GPv2: caller not manager");
5063
});
5164
});
5265

test/GPv2Settlement.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ describe("GPv2Settlement", () => {
4040
"GPv2AllowListAuthentication",
4141
deployer,
4242
);
43-
authenticator = await GPv2AllowListAuthentication.deploy(owner.address);
43+
authenticator = await GPv2AllowListAuthentication.deploy();
44+
await authenticator.initializeManager(owner.address);
4445

4546
const GPv2Settlement = await ethers.getContractFactory(
4647
"GPv2SettlementTestInterface",

0 commit comments

Comments
 (0)