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

Upgradable Authenticator #351

Merged
merged 20 commits into from
Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from 19 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
43 changes: 33 additions & 10 deletions src/contracts/GPv2AllowListAuthentication.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,51 @@ pragma solidity ^0.7.6;

import "@gnosis.pm/util-contracts/contracts/StorageAccessible.sol";
import "@openzeppelin/contracts/utils/EnumerableSet.sol";
import "@openzeppelin/contracts/proxy/Proxy.sol";
import "./interfaces/GPv2Authentication.sol";
import "./ownership/CustomInitiallyOwnable.sol";

/// @title Gnosis Protocol v2 Access Control Contract
/// @author Gnosis Developers
contract GPv2AllowListAuthentication is
CustomInitiallyOwnable,
GPv2Authentication,
StorageAccessible
{
contract GPv2AllowListAuthentication is GPv2Authentication, StorageAccessible {
using EnumerableSet for EnumerableSet.AddressSet;

address public manager;

EnumerableSet.AddressSet private solvers;

// solhint-disable-next-line no-empty-blocks
constructor(address initialOwner) CustomInitiallyOwnable(initialOwner) {}
function initializeManager(address manager_) external {
require(manager == address(0), "GPv2: already initialized");
manager = manager_;
}

modifier onlyProxyAdmin() {
// Slot taken from https://eips.ethereum.org/EIPS/eip-1967#specification
// obtained as bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)
bytes32 slot =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably be a constant.

address proxyAdmin;
// solhint-disable-next-line no-inline-assembly
assembly {
proxyAdmin := sload(slot)
}
require(msg.sender == proxyAdmin, "GPv2: caller not proxyAdmin");
_;
}

modifier onlyManager() {
require(manager == msg.sender, "GPv2: caller not manager");
_;
}

function updateManager(address newManager) external onlyProxyAdmin {
manager = newManager;
Copy link
Contributor

Choose a reason for hiding this comment

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

If the manager gets set to address(0), then anyone can call initializeManager and take ownership of the contract now.

Maybe instead of adding the ability of changing the manager in this PR, we do it as a separate PR. What do you think @fedgiac ?

Copy link
Contributor

Choose a reason for hiding this comment

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

My understanding is that the current changes on the owner are work in progress.
I don't mind if it's done here or done in another PR, but I think that making provision for changing the manager is necessary before releasing.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that making provision for changing the manager is necessary before releasing.

Selbstverständlich

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted the onlyProxyAdmin

}

function addSolver(address solver) external onlyOwner {
function addSolver(address solver) external onlyManager {
solvers.add(solver);
}

function removeSolver(address solver) external onlyOwner {
function removeSolver(address solver) external onlyManager {
solvers.remove(solver);
}

Expand Down
21 changes: 0 additions & 21 deletions src/contracts/ownership/CustomInitiallyOwnable.sol

This file was deleted.

10 changes: 10 additions & 0 deletions src/contracts/test/GPv2AllowListAuthenticationV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
pragma solidity ^0.7.6;

import "../GPv2AllowListAuthentication.sol";

contract GPv2AllowListAuthenticationV2 is GPv2AllowListAuthentication {
function newMethod() external pure returns (uint256) {
return 1337;
}
}
7 changes: 5 additions & 2 deletions src/deploy/001_authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ const deployAuthenticator: DeployFunction = async function ({
const { deploy } = deployments;

const { authenticator } = CONTRACT_NAMES;

await deploy(authenticator, {
from: deployer,
gasLimit: 2000000,
args: [owner],
deterministicDeployment: SALT,
log: true,
proxy: {
owner,
methodName: "initializeManager",
},
args: [owner],
});
};

Expand Down
2 changes: 1 addition & 1 deletion src/ts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type ContractName = typeof CONTRACT_NAMES[keyof typeof CONTRACT_NAMES];
export type DeploymentArguments<
T extends ContractName
> = T extends typeof CONTRACT_NAMES.authenticator
? [string]
? never
bh2smith marked this conversation as resolved.
Show resolved Hide resolved
: T extends typeof CONTRACT_NAMES.settlement
? [string]
: unknown[];
Expand Down
1 change: 1 addition & 0 deletions src/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ export * from "./settlement";
export * from "./reader";
export * from "./deploy";
export * from "./sign";
export * from "./proxy";
export * from "./types/ethers";
45 changes: 45 additions & 0 deletions src/ts/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// defined in https://eips.ethereum.org/EIPS/eip-1967

import { BigNumber } from "ethers";
import { ethers } from "hardhat";

// The proxy contract used by hardhat-deploy implements EIP-1967 (Standard Proxy
// Storage Slot). See <https://eips.ethereum.org/EIPS/eip-1967>.
function slot(string: string) {
return ethers.utils.defaultAbiCoder.encode(
["bytes32"],
[BigNumber.from(ethers.utils.id(string)).sub(1)],
);
}
const IMPLEMENTATION_STORAGE_SLOT = slot("eip1967.proxy.implementation");
const OWNER_STORAGE_SLOT = slot("eip1967.proxy.admin");

/**
* Returns the address of the implementation of an EIP-1967-compatible proxy
* from its address.
*
* @param proxy Address of the proxy contract.
* @returns The address of the contract storing the proxy implementation.
*/
export async function implementationAddress(proxy: string): Promise<string> {
const [implementation] = await ethers.utils.defaultAbiCoder.decode(
["address"],
await ethers.provider.getStorageAt(proxy, IMPLEMENTATION_STORAGE_SLOT),
);
return implementation;
}

/**
* Returns the address of the implementation of an EIP-1967-compatible proxy
* from its address.
*
* @param proxy Address of the proxy contract.
* @returns The address of the administrator of the proxy.
*/
export async function ownerAddress(proxy: string): Promise<string> {
bh2smith marked this conversation as resolved.
Show resolved Hide resolved
const [owner] = await ethers.utils.defaultAbiCoder.decode(
["address"],
await ethers.provider.getStorageAt(proxy, OWNER_STORAGE_SLOT),
);
return owner;
}
3 changes: 2 additions & 1 deletion test/AllowListStorageReader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ describe("AllowListStorageReader", () => {
);

reader = await AllowListStorageReader.deploy();
authenticator = await GPv2AllowListAuthentication.deploy(owner.address);
authenticator = await GPv2AllowListAuthentication.deploy();
await authenticator.initializeManager(owner.address);
allowListReader = new AllowListReader(authenticator, reader);
});

Expand Down
27 changes: 20 additions & 7 deletions test/GPv2AllowListAuthenticator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,28 @@ describe("GPv2AllowListAuthentication", () => {
deployer,
);

authenticator = await GPv2AllowListAuthentication.deploy(owner.address);
authenticator = await GPv2AllowListAuthentication.deploy();
await authenticator.initializeManager(owner.address);
});

describe("constructor", () => {
it("should set the owner", async () => {
expect(await authenticator.owner()).to.equal(owner.address);
it("should initialize the manager", async () => {
expect(await authenticator.manager()).to.equal(owner.address);
});
it("deployer is not the owner", async () => {
expect(await authenticator.owner()).not.to.be.equal(deployer.address);

it("ensures initializeManager is idempotent", async () => {
await expect(
authenticator.initializeManager(nonOwner.address),
).to.revertedWith("GPv2: already initialized");
bh2smith marked this conversation as resolved.
Show resolved Hide resolved

// Also reverts when called by owner.
await expect(
authenticator.connect(owner).initializeManager(nonOwner.address),
).to.revertedWith("GPv2: already initialized");
});

it("deployer is not the manager", async () => {
expect(await authenticator.manager()).not.to.be.equal(deployer.address);
});
});

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

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

bh2smith marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
3 changes: 2 additions & 1 deletion test/GPv2Settlement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ describe("GPv2Settlement", () => {
"GPv2AllowListAuthentication",
deployer,
);
authenticator = await GPv2AllowListAuthentication.deploy(owner.address);
authenticator = await GPv2AllowListAuthentication.deploy();
await authenticator.initializeManager(owner.address);

const GPv2Settlement = await ethers.getContractFactory(
"GPv2SettlementTestInterface",
Expand Down
11 changes: 6 additions & 5 deletions test/e2e/deployment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ContractName,
DeploymentArguments,
deterministicDeploymentAddress,
implementationAddress,
} from "../../src/ts";
import { builtAndDeployedMetadataCoincide } from "../bytecode";

Expand Down Expand Up @@ -45,7 +46,7 @@ describe("E2E: Deployment", () => {
it("authenticator", async () => {
expect(
await builtAndDeployedMetadataCoincide(
authenticator.address,
await implementationAddress(authenticator.address),
"GPv2AllowListAuthentication",
),
).to.be.true;
Expand All @@ -72,9 +73,9 @@ describe("E2E: Deployment", () => {

describe("deterministic addresses", () => {
it("authenticator", async () => {
expect(
await contractAddress("GPv2AllowListAuthentication", owner.address),
).to.equal(authenticator.address);
expect(await contractAddress("GPv2AllowListAuthentication")).to.equal(
await implementationAddress(authenticator.address),
);
});

bh2smith marked this conversation as resolved.
Show resolved Hide resolved
it("settlement", async () => {
Expand All @@ -86,7 +87,7 @@ describe("E2E: Deployment", () => {

describe("ownership", () => {
it("authenticator has dedicated owner", async () => {
expect(await authenticator.owner()).to.equal(owner.address);
expect(await authenticator.manager()).to.equal(owner.address);
});
});
});
72 changes: 72 additions & 0 deletions test/e2e/upgradeAuthenticator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { expect } from "chai";
import { Contract, Wallet } from "ethers";
import { deployments, ethers } from "hardhat";

import { deployTestContracts } from "./fixture";

describe("Upgrade Authenticator", () => {
let authenticator: Contract;
let deployer: Wallet;
let owner: Wallet;
let solver: Wallet;

beforeEach(async () => {
({
authenticator,
deployer,
owner,
wallets: [solver],
} = await deployTestContracts());
});

it("should upgrade authenticator", async () => {
const GPv2AllowListAuthenticationV2 = await ethers.getContractFactory(
"GPv2AllowListAuthenticationV2",
deployer,
);
// Note that, before the upgrade this is actually the old instance
const authenticatorV2 = GPv2AllowListAuthenticationV2.attach(
authenticator.address,
);
// This method doesn't exist before upgrade
await expect(authenticatorV2.newMethod()).to.be.reverted;

await upgrade(
"GPv2AllowListAuthentication",
"GPv2AllowListAuthenticationV2",
);
// This method should exist on after upgrade
expect(await authenticatorV2.newMethod()).to.equal(1337);
});

it("should preserve storage", async () => {
await authenticator.connect(owner).addSolver(solver.address);

// Upgrade after storage is set.
await upgrade(
"GPv2AllowListAuthentication",
"GPv2AllowListAuthenticationV2",
);

const GPv2AllowListAuthenticationV2 = await ethers.getContractFactory(
"GPv2AllowListAuthenticationV2",
deployer,
);
const authenticatorV2 = GPv2AllowListAuthenticationV2.attach(
authenticator.address,
);
// Both, the listed solvers and original manager are still set
expect(await authenticatorV2.isSolver(solver.address)).to.equal(true);
expect(await authenticatorV2.manager()).to.equal(owner.address);
});

fedgiac marked this conversation as resolved.
Show resolved Hide resolved
async function upgrade(contractName: string, newContractName: string) {
// Note that deterministic deployment and gasLimit are not needed/used here as deployment args.
await deployments.deploy(contractName, {
contract: newContractName,
// From differs from initial deployment here since the proxy owner is the Authenticator manager.
from: owner.address,
bh2smith marked this conversation as resolved.
Show resolved Hide resolved
proxy: true,
});
}
});