Skip to content

Commit cb993e9

Browse files
pano-skylakisPano Skylakis
andauthored
Add AccessControl, Pausable to GemGame contract (#200)
* Feat: add time-based constraint to claims * Refactor: add access control, add ability to pause * Feat: add earnGem call after deployment test * Fix: remove account mapping --------- Co-authored-by: Pano Skylakis <[email protected]>
1 parent ea127ca commit cb993e9

File tree

4 files changed

+181
-19
lines changed

4 files changed

+181
-19
lines changed

contracts/games/gems/GemGame.sol

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,63 @@
11
// Copyright (c) Immutable Pty Ltd 2018 - 2024
22
// SPDX-License-Identifier: Apache 2
3+
// solhint-disable not-rely-on-time
4+
35
pragma solidity ^0.8.19;
46

7+
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
8+
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
9+
10+
error Unauthorized();
11+
error ContractPaused();
12+
513
/**
614
* @title GemGame - A simple contract that emits an event for the purpose of indexing off-chain
715
* @author Immutable
816
* @dev The GemGame contract is not designed to be upgradeable or extended
917
*/
10-
contract GemGame {
18+
contract GemGame is AccessControl, Pausable {
1119
/// @notice Indicates that an account has earned a gem
1220
event GemEarned(address indexed account, uint256 timestamp);
1321

22+
/// @notice Role to allow pausing the contract
23+
bytes32 private constant _PAUSE = keccak256("PAUSE");
24+
25+
/// @notice Role to allow unpausing the contract
26+
bytes32 private constant _UNPAUSE = keccak256("UNPAUSE");
27+
28+
/**
29+
* @notice Sets the DEFAULT_ADMIN, PAUSE and UNPAUSE roles
30+
* @param _admin The address for the admin role
31+
* @param _pauser The address for the pauser role
32+
* @param _unpauser The address for the unpauser role
33+
*/
34+
constructor(address _admin, address _pauser, address _unpauser) {
35+
_grantRole(DEFAULT_ADMIN_ROLE, _admin);
36+
_grantRole(_PAUSE, _pauser);
37+
_grantRole(_UNPAUSE, _unpauser);
38+
}
39+
40+
/**
41+
* @notice Pauses the contract
42+
*/
43+
function pause() external {
44+
if (!hasRole(_PAUSE, msg.sender)) revert Unauthorized();
45+
_pause();
46+
}
47+
48+
/**
49+
* @notice Unpauses the contract
50+
*/
51+
function unpause() external {
52+
if (!hasRole(_UNPAUSE, msg.sender)) revert Unauthorized();
53+
_unpause();
54+
}
55+
1456
/**
1557
* @notice Function that emits a `GemEarned` event
1658
*/
1759
function earnGem() external {
18-
// solhint-disable-next-line not-rely-on-time
60+
if (paused()) revert ContractPaused();
1961
emit GemEarned(msg.sender, block.timestamp);
2062
}
2163
}

contracts/games/gems/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,17 @@ Contract threat models and audits:
1515

1616
| Description | Date |Version Audited | Link to Report |
1717
|---------------------------|------------------|-----------------|----------------|
18-
| Not audited and no threat model | - | - | - |
18+
| Not audited and no threat model | - | - | - |
19+
20+
21+
**Deploy and verify using CREATE3 factory contract:**
22+
23+
This repo includes a script for deploying via a CREATE3 factory contract. The script is defined as a test contract as per the examples [here](https://book.getfoundry.sh/reference/forge/forge-script#examples) and can be found in `./script/games/gems/DeployGemGame.sol`.
24+
25+
See the `.env.example` for required environment variables.
26+
27+
```sh
28+
forge script script/games/gems/DeployGemGame.sol --tc DeployGemGame --sig "deploy()" -vvv --rpc-url {rpc-url} --broadcast --verifier-url https://explorer.immutable.com/api --verifier blockscout --verify --gas-price 10gwei
29+
```
30+
31+
Optionally, you can also specify `--ledger` or `--trezor` for hardware deployments. See docs [here](https://book.getfoundry.sh/reference/forge/forge-script#wallet-options---hardware-wallet).

script/games/gems/DeployGemGame.sol

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,33 +26,94 @@ interface IDeployer {
2626

2727
struct DeploymentArgs {
2828
address signer;
29-
address deployer;
29+
address factory;
3030
string salt;
3131
}
3232

33+
struct GemGameContractArgs {
34+
address defaultAdmin;
35+
address pauser;
36+
address unpauser;
37+
}
38+
3339
contract DeployGemGame is Test {
40+
event GemEarned(address indexed account, uint256 timestamp);
41+
42+
function testDeploy() external {
43+
/// @dev Fork the Immutable zkEVM testnet for this test
44+
string memory rpcURL = "https://rpc.testnet.immutable.com";
45+
vm.createSelectFork(rpcURL);
46+
47+
/// @dev These are Immutable zkEVM testnet values where necessary
48+
DeploymentArgs memory deploymentArgs = DeploymentArgs({
49+
signer: 0xdDA0d9448Ebe3eA43aFecE5Fa6401F5795c19333,
50+
factory: 0x37a59A845Bb6eD2034098af8738fbFFB9D589610,
51+
salt: "salty"
52+
});
53+
54+
GemGameContractArgs memory gemGameContractArgs = GemGameContractArgs({
55+
pauser: makeAddr("pause"),
56+
unpauser: makeAddr("unpause"),
57+
defaultAdmin: makeAddr("admin")
58+
});
59+
60+
// Run deployment against forked testnet
61+
GemGame deployedGemGameContract = _deploy(deploymentArgs, gemGameContractArgs);
62+
63+
assertEq(true, deployedGemGameContract.hasRole(keccak256("PAUSE"), gemGameContractArgs.pauser));
64+
assertEq(true, deployedGemGameContract.hasRole(keccak256("UNPAUSE"), gemGameContractArgs.unpauser));
65+
assertEq(
66+
true,
67+
deployedGemGameContract.hasRole(
68+
deployedGemGameContract.DEFAULT_ADMIN_ROLE(), gemGameContractArgs.defaultAdmin
69+
)
70+
);
71+
72+
// The DEFAULT_ADMIN_ROLE should be revoked from the deployer account
73+
assertEq(
74+
false, deployedGemGameContract.hasRole(deployedGemGameContract.DEFAULT_ADMIN_ROLE(), deploymentArgs.signer)
75+
);
76+
77+
// Earn a gem
78+
vm.expectEmit(true, true, false, false);
79+
emit GemEarned(address(this), block.timestamp);
80+
deployedGemGameContract.earnGem();
81+
}
82+
3483
function deploy() external {
3584
address signer = vm.envAddress("DEPLOYER_ADDRESS");
36-
address deployer = vm.envAddress("OWNABLE_CREATE3_FACTORY_ADDRESS");
85+
address factory = vm.envAddress("OWNABLE_CREATE3_FACTORY_ADDRESS");
86+
address defaultAdmin = vm.envAddress("DEFAULT_ADMIN");
87+
address pauser = vm.envAddress("PAUSER");
88+
address unpauser = vm.envAddress("UNPAUSER");
3789
string memory salt = vm.envString("GEM_GAME_SALT");
3890

39-
DeploymentArgs memory deploymentArgs = DeploymentArgs({signer: signer, deployer: deployer, salt: salt});
91+
DeploymentArgs memory deploymentArgs = DeploymentArgs({signer: signer, factory: factory, salt: salt});
92+
93+
GemGameContractArgs memory gemGameContractArgs =
94+
GemGameContractArgs({defaultAdmin: defaultAdmin, pauser: pauser, unpauser: unpauser});
4095

41-
_deploy(deploymentArgs);
96+
_deploy(deploymentArgs, gemGameContractArgs);
4297
}
4398

44-
function _deploy(DeploymentArgs memory args) internal returns (GemGame gemGameContract) {
45-
IDeployer ownableCreate3 = IDeployer(args.deployer);
99+
function _deploy(DeploymentArgs memory deploymentArgs, GemGameContractArgs memory gemGameContractArgs)
100+
internal
101+
returns (GemGame gemGameContract)
102+
{
103+
IDeployer ownableCreate3 = IDeployer(deploymentArgs.factory);
46104

47105
// Create deployment bytecode and encode constructor args
48-
bytes memory bytecode = abi.encodePacked(type(GemGame).creationCode);
106+
bytes memory deploymentBytecode = abi.encodePacked(
107+
type(GemGame).creationCode,
108+
abi.encode(gemGameContractArgs.defaultAdmin, gemGameContractArgs.pauser, gemGameContractArgs.unpauser)
109+
);
49110

50-
bytes32 saltBytes = keccak256(abi.encode(args.salt));
111+
bytes32 saltBytes = keccak256(abi.encode(deploymentArgs.salt));
51112

52113
/// @dev Deploy the contract via the Ownable CREATE3 factory
53-
vm.startBroadcast(args.signer);
114+
vm.startBroadcast(deploymentArgs.signer);
54115

55-
address gemGameContractAddress = ownableCreate3.deploy(bytecode, saltBytes);
116+
address gemGameContractAddress = ownableCreate3.deploy(deploymentBytecode, saltBytes);
56117
gemGameContract = GemGame(gemGameContractAddress);
57118

58119
vm.stopBroadcast();

test/games/gems/GemGame.t.sol

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,68 @@
11
// Copyright Immutable Pty Ltd 2018 - 2024
22
// SPDX-License-Identifier: Apache 2.0
3-
pragma solidity ^0.8.20;
3+
// solhint-disable not-rely-on-time
4+
5+
pragma solidity ^0.8.19;
46

57
import "forge-std/Test.sol";
6-
import {GemGame} from "../../../contracts/games/gems/GemGame.sol";
8+
import {GemGame, Unauthorized, ContractPaused} from "../../../contracts/games/gems/GemGame.sol";
79

810
contract GemGameTest is Test {
911
event GemEarned(address indexed account, uint256 timestamp);
1012

11-
GemGame gemGame;
13+
GemGame private _gemGame;
1214

1315
function setUp() public {
14-
gemGame = new GemGame();
16+
_gemGame = new GemGame(address(this), address(this), address(this));
1517
}
1618

17-
function testEarnGem_EmitsGemEarnedEvent() public {
19+
function testEarnGemEmitsGemEarnedEvent() public {
1820
vm.expectEmit(true, true, false, false);
1921
emit GemEarned(address(this), block.timestamp);
20-
gemGame.earnGem();
22+
_gemGame.earnGem();
23+
}
24+
25+
function testEarnGemContractPausedReverts() public {
26+
// pause the contract
27+
_gemGame.pause();
28+
29+
// attempt to earn a gem
30+
vm.expectRevert(ContractPaused.selector);
31+
_gemGame.earnGem();
32+
}
33+
34+
function testPausePausesContract() public {
35+
// pause the contract
36+
_gemGame.pause();
37+
38+
assertEq(_gemGame.paused(), true, "GemGame should be paused");
39+
}
40+
41+
function testPauseWithoutPauseRoleReverts() public {
42+
// revoke the pause role
43+
_gemGame.revokeRole(keccak256("PAUSE"), address(this));
44+
45+
// attempt to pause
46+
vm.expectRevert(Unauthorized.selector);
47+
_gemGame.pause();
48+
}
49+
50+
function testUnpauseUnpausesContract() public {
51+
// pause the contract
52+
_gemGame.pause();
53+
54+
// unpause the contract
55+
_gemGame.unpause();
56+
57+
assertEq(_gemGame.paused(), false, "GemGame should be unpaused");
58+
}
59+
60+
function testUnpauseWithoutPauseRoleReverts() public {
61+
// revoke the unpause role
62+
_gemGame.revokeRole(keccak256("UNPAUSE"), address(this));
63+
64+
// attempt to unpause
65+
vm.expectRevert(Unauthorized.selector);
66+
_gemGame.unpause();
2167
}
2268
}

0 commit comments

Comments
 (0)