Skip to content

Commit 66f1a65

Browse files
authored
feat: add access control (#131)
1 parent 77ecf74 commit 66f1a65

File tree

7 files changed

+165
-46
lines changed

7 files changed

+165
-46
lines changed

contracts/BaseDocumentStore.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ contract BaseDocumentStore is Initializable {
1717
event DocumentIssued(bytes32 indexed document);
1818
event DocumentRevoked(bytes32 indexed document);
1919

20-
function initialize(string memory _name) internal onlyInitializing {
20+
function __BaseDocumentStore_init(string memory _name) internal onlyInitializing {
2121
version = "2.3.0";
2222
name = _name;
2323
}

contracts/DocumentStore.sol

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,31 @@ import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
66
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
77

88
import "./BaseDocumentStore.sol";
9+
import "./base/DocumentStoreAccessControl.sol";
910

10-
contract DocumentStore is BaseDocumentStore, OwnableUpgradeable {
11+
contract DocumentStore is BaseDocumentStore, DocumentStoreAccessControl {
1112
constructor(string memory _name, address owner) {
1213
initialize(_name, owner);
1314
}
1415

1516
function initialize(string memory _name, address owner) internal initializer {
16-
require(owner != address(0), "Owner is required");
17-
super.__Ownable_init();
18-
super.transferOwnership(owner);
19-
BaseDocumentStore.initialize(_name);
17+
__DocumentStoreAccessControl_init(owner);
18+
__BaseDocumentStore_init(_name);
2019
}
2120

22-
function issue(bytes32 document) public onlyOwner onlyNotIssued(document) {
21+
function issue(bytes32 document) public onlyRole(ISSUER_ROLE) onlyNotIssued(document) {
2322
BaseDocumentStore._issue(document);
2423
}
2524

26-
function bulkIssue(bytes32[] memory documents) public onlyOwner {
25+
function bulkIssue(bytes32[] memory documents) public onlyRole(ISSUER_ROLE) {
2726
BaseDocumentStore._bulkIssue(documents);
2827
}
2928

30-
function revoke(bytes32 document) public onlyOwner onlyNotRevoked(document) returns (bool) {
29+
function revoke(bytes32 document) public onlyRole(REVOKER_ROLE) onlyNotRevoked(document) returns (bool) {
3130
return BaseDocumentStore._revoke(document);
3231
}
3332

34-
function bulkRevoke(bytes32[] memory documents) public onlyOwner {
33+
function bulkRevoke(bytes32[] memory documents) public onlyRole(REVOKER_ROLE) {
3534
return BaseDocumentStore._bulkRevoke(documents);
3635
}
3736
}

contracts/DocumentStoreWithRevokeReasons.sol

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ contract DocumentStoreWithRevokeReasons is DocumentStore {
1212

1313
constructor(string memory _name, address owner) DocumentStore(_name, owner) {}
1414

15-
function revoke(bytes32 document, uint256 reason) public onlyOwner onlyNotRevoked(document) returns (bool) {
15+
function revoke(bytes32 document, uint256 reason)
16+
public
17+
onlyRole(REVOKER_ROLE)
18+
onlyNotRevoked(document)
19+
returns (bool)
20+
{
1621
revoke(document);
1722
revokeReason[document] = reason;
1823
emit DocumentRevokedWithReason(document, reason);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
pragma solidity ^0.8.0;
4+
5+
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
6+
7+
contract DocumentStoreAccessControl is AccessControlUpgradeable {
8+
bytes32 public constant ISSUER_ROLE = keccak256("ISSUER_ROLE");
9+
bytes32 public constant REVOKER_ROLE = keccak256("REVOKER_ROLE");
10+
11+
function __DocumentStoreAccessControl_init(address owner) internal onlyInitializing {
12+
require(owner != address(0), "Owner is zero");
13+
_setupRole(DEFAULT_ADMIN_ROLE, owner);
14+
_setupRole(ISSUER_ROLE, owner);
15+
_setupRole(REVOKER_ROLE, owner);
16+
}
17+
}

src/index.test.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { providers } from "ethers";
1+
import { providers, ethers } from "ethers";
22
import { deploy, deployAndWait, connect } from "./index";
33
import { DocumentStoreCreator__factory as DocumentStoreCreatorFactory } from "./contracts";
44

@@ -7,6 +7,10 @@ const signer = provider.getSigner();
77
let account: string;
88
let documentStoreCreatorAddressOverride: string;
99

10+
const adminRole = ethers.constants.HashZero;
11+
const issuerRole = ethers.utils.id("ISSUER_ROLE");
12+
const revokerRole = ethers.utils.id("REVOKER_ROLE");
13+
1014
beforeAll(async () => {
1115
// Deploy an instance of DocumentStoreFactory on the new blockchain
1216
const factory = new DocumentStoreCreatorFactory(signer);
@@ -25,8 +29,14 @@ describe("deploy", () => {
2529
describe("deployAndWait", () => {
2630
it("deploys a new DocumentStore contract", async () => {
2731
const instance = await deployAndWait("My Store", signer, { documentStoreCreatorAddressOverride });
28-
const owner = await instance.owner();
29-
expect(owner).toBe(account);
32+
33+
const hasAdminRole = await instance.hasRole(adminRole, account);
34+
const hasIssuerRole = await instance.hasRole(issuerRole, account);
35+
const hasRevokerRole = await instance.hasRole(revokerRole, account);
36+
expect(hasAdminRole).toBe(true);
37+
expect(hasIssuerRole).toBe(true);
38+
expect(hasRevokerRole).toBe(true);
39+
3040
const name = await instance.name();
3141
expect(name).toBe("My Store");
3242
});
@@ -37,8 +47,6 @@ describe("connect", () => {
3747
const { address } = await deployAndWait("My Store", signer, { documentStoreCreatorAddressOverride });
3848
console.log(address);
3949
const instance = await connect(address, signer);
40-
const owner = await instance.owner();
41-
expect(owner).toBe(account);
4250
const name = await instance.name();
4351
expect(name).toBe("My Store");
4452
});

test/DocumentStore.js

Lines changed: 115 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { expect } = require("chai").use(require("chai-as-promised"));
1+
const { expect, assert } = require("chai").use(require("chai-as-promised"));
22
const { ethers } = require("hardhat");
33
const { get } = require("lodash");
44
const config = require("../config.js");
@@ -8,6 +8,10 @@ describe("DocumentStore", async () => {
88
let DocumentStore;
99
let DocumentStoreInstance;
1010

11+
const adminRole = ethers.constants.HashZero;
12+
const issuerRole = ethers.utils.id("ISSUER_ROLE");
13+
const revokerRole = ethers.utils.id("REVOKER_ROLE");
14+
1115
beforeEach("", async () => {
1216
Accounts = await ethers.getSigners();
1317
DocumentStore = await ethers.getContractFactory("DocumentStore");
@@ -20,10 +24,35 @@ describe("DocumentStore", async () => {
2024
const name = await DocumentStoreInstance.name();
2125
expect(name).to.be.equal(config.INSTITUTE_NAME, "Name of institute does not match");
2226
});
27+
});
28+
29+
describe("Access Control", () => {
30+
describe("Initialisation", () => {
31+
it("should revert if owner is zero address", async () => {
32+
const tx = DocumentStore.connect(Accounts[0]).deploy(config.INSTITUTE_NAME, ethers.constants.AddressZero);
33+
34+
await expect(tx).to.be.revertedWith("Owner is zero");
35+
});
36+
37+
describe("Owner Default Roles", () => {
38+
it("should have default admin role", async () => {
39+
const hasRole = await DocumentStoreInstance.hasRole(adminRole, Accounts[0].address);
40+
41+
expect(hasRole).to.be.true;
42+
});
2343

24-
it("it should have the corrent owner", async () => {
25-
const owner = await DocumentStoreInstance.owner();
26-
expect(owner).to.be.equal(Accounts[0].address);
44+
it("should have issuer role", async () => {
45+
const hasRole = await DocumentStoreInstance.hasRole(issuerRole, Accounts[0].address);
46+
47+
expect(hasRole).to.be.true;
48+
});
49+
50+
it("should have revoker role", async () => {
51+
const hasRole = await DocumentStoreInstance.hasRole(revokerRole, Accounts[0].address);
52+
53+
expect(hasRole).to.be.true;
54+
});
55+
});
2756
});
2857
});
2958

@@ -35,8 +64,9 @@ describe("DocumentStore", async () => {
3564
});
3665

3766
describe("issue", () => {
67+
const documentMerkleRoot = "0x3a267813bea8120f55a7b9ca814c34dd89f237502544d7c75dfd709a659f6330";
68+
3869
it("should be able to issue a document", async () => {
39-
const documentMerkleRoot = "0x3a267813bea8120f55a7b9ca814c34dd89f237502544d7c75dfd709a659f6330";
4070
const tx = await DocumentStoreInstance.issue(documentMerkleRoot);
4171
const receipt = await tx.wait();
4272

@@ -49,7 +79,6 @@ describe("DocumentStore", async () => {
4979
});
5080

5181
it("should not allow duplicate issues", async () => {
52-
const documentMerkleRoot = "0x3a267813bea8120f55a7b9ca814c34dd89f237502544d7c75dfd709a659f6330";
5382
await DocumentStoreInstance.issue(documentMerkleRoot);
5483

5584
// Check that reissue is rejected
@@ -59,13 +88,25 @@ describe("DocumentStore", async () => {
5988
);
6089
});
6190

62-
it("only allows the owner to issue", async () => {
63-
const nonOwner = Accounts[1];
64-
const owner = await DocumentStoreInstance.owner();
65-
expect(nonOwner).to.not.be.equal(owner);
91+
it("should revert when caller has no issuer role", async () => {
92+
const account = Accounts[1];
93+
const hasNoIssuerRole = await DocumentStoreInstance.hasRole(issuerRole, account.address);
94+
assert.isFalse(hasNoIssuerRole, "Non-Issuer Account has issuer role");
6695

67-
const documentMerkleRoot = "0x3a267813bea8120f55a7b9ca814c34dd89f237502544d7c75dfd709a659f6330";
68-
await expect(DocumentStoreInstance.connect(nonOwner).issue(documentMerkleRoot)).to.be.rejectedWith(/revert/);
96+
await expect(DocumentStoreInstance.connect(account).issue(documentMerkleRoot)).to.be.rejectedWith(
97+
/AccessControl/
98+
);
99+
});
100+
101+
it("should issue successfully when caller has issuer role", async () => {
102+
const account = Accounts[0];
103+
const hasIssuerRole = await DocumentStoreInstance.hasRole(issuerRole, account.address);
104+
assert.isTrue(hasIssuerRole, "Issuer Account has issuer role");
105+
106+
await DocumentStoreInstance.connect(account).issue(documentMerkleRoot);
107+
const issued = await DocumentStoreInstance.isIssued(documentMerkleRoot);
108+
109+
expect(issued).to.be.true;
69110
});
70111
});
71112

@@ -116,15 +157,30 @@ describe("DocumentStore", async () => {
116157
);
117158
});
118159

119-
it("only allows the owner to issue", async () => {
120-
const nonOwner = Accounts[1];
121-
const owner = await DocumentStoreInstance.owner();
122-
expect(nonOwner).to.not.be.equal(owner);
160+
it("should revert when caller has no issuer role", async () => {
161+
const nonIssuerAccount = Accounts[1];
162+
const hasNoIssuerRole = await DocumentStoreInstance.hasRole(issuerRole, nonIssuerAccount.address);
163+
assert.isFalse(hasNoIssuerRole, "Non-Issuer Account has issuer role");
123164

124165
const documentMerkleRoots = ["0x3a267813bea8120f55a7b9ca814c34dd89f237502544d7c75dfd709a659f6330"];
125166

126167
// FIXME:
127-
await expect(DocumentStoreInstance.connect(nonOwner).bulkIssue(documentMerkleRoots)).to.be.rejectedWith(/revert/);
168+
await expect(DocumentStoreInstance.connect(nonIssuerAccount).bulkIssue(documentMerkleRoots)).to.be.rejectedWith(
169+
/AccessControl/
170+
);
171+
});
172+
173+
it("should bulk issue successfully when caller has issuer role", async () => {
174+
const documentMerkleRoots = ["0x3a267813bea8120f55a7b9ca814c34dd89f237502544d7c75dfd709a659f6330"];
175+
176+
const account = Accounts[0];
177+
const hasIssuerRole = await DocumentStoreInstance.hasRole(issuerRole, account.address);
178+
assert.isTrue(hasIssuerRole, "Issuer Account has no issuer role");
179+
180+
await DocumentStoreInstance.connect(account).bulkIssue(documentMerkleRoots);
181+
const issued = await DocumentStoreInstance.isIssued(documentMerkleRoots[0]);
182+
183+
expect(issued).to.be.true;
128184
});
129185
});
130186

@@ -166,8 +222,9 @@ describe("DocumentStore", async () => {
166222
});
167223

168224
describe("revoke", () => {
225+
const documentMerkleRoot = "0x3a267813bea8120f55a7b9ca814c34dd89f237502544d7c75dfd709a659f6330";
226+
169227
it("should allow the revocation of a valid and issued document", async () => {
170-
const documentMerkleRoot = "0x3a267813bea8120f55a7b9ca814c34dd89f237502544d7c75dfd709a659f6330";
171228
const documentHash = "0x10327d7f904ee3ee0e69d592937be37a33692a78550bd100d635cdea2344e6c7";
172229

173230
await DocumentStoreInstance.issue(documentMerkleRoot);
@@ -181,7 +238,6 @@ describe("DocumentStore", async () => {
181238
});
182239

183240
it("should allow the revocation of an issued root", async () => {
184-
const documentMerkleRoot = "0x3a267813bea8120f55a7b9ca814c34dd89f237502544d7c75dfd709a659f6330";
185241
const documentHash = documentMerkleRoot;
186242

187243
await DocumentStoreInstance.issue(documentMerkleRoot);
@@ -195,7 +251,6 @@ describe("DocumentStore", async () => {
195251
});
196252

197253
it("should not allow repeated revocation of a valid and issued document", async () => {
198-
const documentMerkleRoot = "0x3a267813bea8120f55a7b9ca814c34dd89f237502544d7c75dfd709a659f6330";
199254
const documentHash = "0x10327d7f904ee3ee0e69d592937be37a33692a78550bd100d635cdea2344e6c7";
200255

201256
await DocumentStoreInstance.issue(documentMerkleRoot);
@@ -215,6 +270,27 @@ describe("DocumentStore", async () => {
215270
expect(receipt.events[0].event).to.be.equal("DocumentRevoked");
216271
expect(receipt.events[0].args.document).to.be.equal(documentHash);
217272
});
273+
274+
it("should revert when caller has no revoker role", async () => {
275+
const nonRevokerAccount = Accounts[1];
276+
const hasNoRevokerRole = await DocumentStoreInstance.hasRole(revokerRole, nonRevokerAccount.address);
277+
assert.isFalse(hasNoRevokerRole, "Non-Revoker Account has revoker role");
278+
279+
await expect(DocumentStoreInstance.connect(nonRevokerAccount).revoke(documentMerkleRoot)).to.be.rejectedWith(
280+
/AccessControl/
281+
);
282+
});
283+
284+
it("should revoke successfully when caller has issuer role", async () => {
285+
const account = Accounts[0];
286+
const hasIssuerRole = await DocumentStoreInstance.hasRole(issuerRole, account.address);
287+
assert.isTrue(hasIssuerRole, "Revoker Account has no revoker role");
288+
289+
await DocumentStoreInstance.connect(account).revoke(documentMerkleRoot);
290+
const issued = await DocumentStoreInstance.isRevoked(documentMerkleRoot);
291+
292+
expect(issued).to.be.true;
293+
});
218294
});
219295

220296
describe("bulkRevoke", () => {
@@ -266,16 +342,29 @@ describe("DocumentStore", async () => {
266342
);
267343
});
268344

269-
it("only allows the owner to revoke", async () => {
270-
const nonOwner = Accounts[1];
271-
const owner = await DocumentStoreInstance.owner();
272-
expect(nonOwner).to.not.be.equal(owner);
345+
it("should revert when caller has no revoker role", async () => {
346+
const nonRevokerAccount = Accounts[1];
347+
const hasNoRevokerRole = await DocumentStoreInstance.hasRole(revokerRole, nonRevokerAccount.address);
348+
assert.isFalse(hasNoRevokerRole, "Non-Revoker Account has revoker role");
273349

274350
const documentMerkleRoots = ["0x3a267813bea8120f55a7b9ca814c34dd89f237502544d7c75dfd709a659f6330"];
275-
await expect(DocumentStoreInstance.connect(nonOwner).bulkRevoke(documentMerkleRoots)).to.be.rejectedWith(
276-
/revert/
351+
await expect(DocumentStoreInstance.connect(nonRevokerAccount).bulkRevoke(documentMerkleRoots)).to.be.rejectedWith(
352+
/AccessControl/
277353
);
278354
});
355+
356+
it("should bulk revoke successfully when caller has issuer role", async () => {
357+
const documentMerkleRoots = ["0x3a267813bea8120f55a7b9ca814c34dd89f237502544d7c75dfd709a659f6330"];
358+
359+
const account = Accounts[0];
360+
const hasIssuerRole = await DocumentStoreInstance.hasRole(issuerRole, account.address);
361+
assert.isTrue(hasIssuerRole, "Revoker Account has no revoker role");
362+
363+
await DocumentStoreInstance.connect(account).bulkRevoke(documentMerkleRoots);
364+
const issued = await DocumentStoreInstance.isRevoked(documentMerkleRoots[0]);
365+
366+
expect(issued).to.be.true;
367+
});
279368
});
280369

281370
describe("isRevoked", () => {

test/DocumentStoreCreator.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,17 @@ describe("DocumentStoreCreator", async () => {
2424
// Test for events emitted by factory
2525
const tx = await DocumentStoreCreatorInstance.deploy(config.INSTITUTE_NAME);
2626
const receipt = await tx.wait();
27-
expect(receipt.events[2].args.creator).to.be.equal(
27+
expect(receipt.events[3].args.creator).to.be.equal(
2828
Accounts[0].address,
2929
"Emitted contract creator does not match"
3030
);
3131
// Test correctness of deployed DocumentStore
32-
const deployedDocumentStore = await DocumentStore.attach(receipt.events[2].args.instance);
32+
const deployedDocumentStore = await DocumentStore.attach(receipt.events[3].args.instance);
3333
const name = await deployedDocumentStore.name();
3434
expect(name).to.be.equal(config.INSTITUTE_NAME, "Name of institute does not match");
35-
const owner = await deployedDocumentStore.owner();
36-
expect(owner).to.be.equal(Accounts[0].address, "Owner of deployed contract does not match creator");
35+
36+
const hasAdminRole = await deployedDocumentStore.hasRole(ethers.constants.HashZero, Accounts[0].address);
37+
expect(hasAdminRole).to.be.true;
3738
});
3839
});
3940
});

0 commit comments

Comments
 (0)