Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
49 changes: 47 additions & 2 deletions contracts/games/gems/GemGame.sol
Original file line number Diff line number Diff line change
@@ -1,21 +1,66 @@
// Copyright (c) Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: Apache 2
// solhint-disable not-rely-on-time

Choose a reason for hiding this comment

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

we don't need this anymore

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

Ah!


pragma solidity ^0.8.19;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";

error Unauthorized();
error ContractPaused();

/**
* @title GemGame - A simple contract that emits an event for the purpose of indexing off-chain
* @author Immutable
* @dev The GemGame contract is not designed to be upgradeable or extended
*/
contract GemGame {
contract GemGame is AccessControl, Pausable {
/// @notice Indicates that an account has earned a gem
event GemEarned(address indexed account, uint256 timestamp);

/// @notice Mapping of the last time an account earned a gem
mapping(address account => uint256 lastEarned) public accountLastEarned;

/// @notice Role to allow pausing the contract
bytes32 private constant _PAUSE = keccak256("PAUSE");

/// @notice Role to allow unpausing the contract
bytes32 private constant _UNPAUSE = keccak256("UNPAUSE");

/**
* @notice Sets the DEFAULT_ADMIN, PAUSE and UNPAUSE roles
* @param _admin The address for the admin role
* @param _pauser The address for the pauser role
* @param _unpauser The address for the unpauser role
*/
constructor(address _admin, address _pauser, address _unpauser) {
_grantRole(DEFAULT_ADMIN_ROLE, _admin);
_grantRole(_PAUSE, _pauser);
_grantRole(_UNPAUSE, _unpauser);
}

/**
* @notice Pauses the contract
*/
function pause() external {
if (!hasRole(_PAUSE, msg.sender)) revert Unauthorized();
_pause();
}

/**
* @notice Unpauses the contract
*/
function unpause() external {
if (!hasRole(_UNPAUSE, msg.sender)) revert Unauthorized();
_unpause();
}

/**
* @notice Function that emits a `GemEarned` event
*/
function earnGem() external {
// solhint-disable-next-line not-rely-on-time
if (paused()) revert ContractPaused();
emit GemEarned(msg.sender, block.timestamp);
}
}
15 changes: 14 additions & 1 deletion contracts/games/gems/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,17 @@ Contract threat models and audits:

| Description | Date |Version Audited | Link to Report |
|---------------------------|------------------|-----------------|----------------|
| Not audited and no threat model | - | - | - |
| Not audited and no threat model | - | - | - |


**Deploy and verify using CREATE3 factory contract:**

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`.

See the `.env.example` for required environment variables.

```sh
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
```

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).
81 changes: 71 additions & 10 deletions script/games/gems/DeployGemGame.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,94 @@ interface IDeployer {

struct DeploymentArgs {
address signer;
address deployer;
address factory;
string salt;
}

struct GemGameContractArgs {
address defaultAdmin;
address pauser;
address unpauser;
}

contract DeployGemGame is Test {
event GemEarned(address indexed account, uint256 timestamp);

function testDeploy() external {
/// @dev Fork the Immutable zkEVM testnet for this test
string memory rpcURL = "https://rpc.testnet.immutable.com";
vm.createSelectFork(rpcURL);

/// @dev These are Immutable zkEVM testnet values where necessary
DeploymentArgs memory deploymentArgs = DeploymentArgs({
signer: 0xdDA0d9448Ebe3eA43aFecE5Fa6401F5795c19333,
factory: 0x37a59A845Bb6eD2034098af8738fbFFB9D589610,
salt: "salty"
});

GemGameContractArgs memory gemGameContractArgs = GemGameContractArgs({
pauser: makeAddr("pause"),
unpauser: makeAddr("unpause"),
defaultAdmin: makeAddr("admin")
});

// Run deployment against forked testnet
GemGame deployedGemGameContract = _deploy(deploymentArgs, gemGameContractArgs);

assertEq(true, deployedGemGameContract.hasRole(keccak256("PAUSE"), gemGameContractArgs.pauser));
assertEq(true, deployedGemGameContract.hasRole(keccak256("UNPAUSE"), gemGameContractArgs.unpauser));
assertEq(
true,
deployedGemGameContract.hasRole(
deployedGemGameContract.DEFAULT_ADMIN_ROLE(), gemGameContractArgs.defaultAdmin
)
);

// The DEFAULT_ADMIN_ROLE should be revoked from the deployer account
assertEq(
false, deployedGemGameContract.hasRole(deployedGemGameContract.DEFAULT_ADMIN_ROLE(), deploymentArgs.signer)
);

// Earn a gem
vm.expectEmit(true, true, false, false);
emit GemEarned(address(this), block.timestamp);
deployedGemGameContract.earnGem();
}

function deploy() external {
address signer = vm.envAddress("DEPLOYER_ADDRESS");
address deployer = vm.envAddress("OWNABLE_CREATE3_FACTORY_ADDRESS");
address factory = vm.envAddress("OWNABLE_CREATE3_FACTORY_ADDRESS");
address defaultAdmin = vm.envAddress("DEFAULT_ADMIN");
address pauser = vm.envAddress("PAUSER");
address unpauser = vm.envAddress("UNPAUSER");
string memory salt = vm.envString("GEM_GAME_SALT");

DeploymentArgs memory deploymentArgs = DeploymentArgs({signer: signer, deployer: deployer, salt: salt});
DeploymentArgs memory deploymentArgs = DeploymentArgs({signer: signer, factory: factory, salt: salt});

GemGameContractArgs memory gemGameContractArgs =
GemGameContractArgs({defaultAdmin: defaultAdmin, pauser: pauser, unpauser: unpauser});

_deploy(deploymentArgs);
_deploy(deploymentArgs, gemGameContractArgs);
}

function _deploy(DeploymentArgs memory args) internal returns (GemGame gemGameContract) {
IDeployer ownableCreate3 = IDeployer(args.deployer);
function _deploy(DeploymentArgs memory deploymentArgs, GemGameContractArgs memory gemGameContractArgs)
internal
returns (GemGame gemGameContract)
{
IDeployer ownableCreate3 = IDeployer(deploymentArgs.factory);

// Create deployment bytecode and encode constructor args
bytes memory bytecode = abi.encodePacked(type(GemGame).creationCode);
bytes memory deploymentBytecode = abi.encodePacked(
type(GemGame).creationCode,
abi.encode(gemGameContractArgs.defaultAdmin, gemGameContractArgs.pauser, gemGameContractArgs.unpauser)
);

bytes32 saltBytes = keccak256(abi.encode(args.salt));
bytes32 saltBytes = keccak256(abi.encode(deploymentArgs.salt));

/// @dev Deploy the contract via the Ownable CREATE3 factory
vm.startBroadcast(args.signer);
vm.startBroadcast(deploymentArgs.signer);

address gemGameContractAddress = ownableCreate3.deploy(bytecode, saltBytes);
address gemGameContractAddress = ownableCreate3.deploy(deploymentBytecode, saltBytes);
gemGameContract = GemGame(gemGameContractAddress);

vm.stopBroadcast();
Expand Down
58 changes: 52 additions & 6 deletions test/games/gems/GemGame.t.sol
Original file line number Diff line number Diff line change
@@ -1,22 +1,68 @@
// Copyright Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: Apache 2.0
pragma solidity ^0.8.20;
// solhint-disable not-rely-on-time

pragma solidity ^0.8.19;

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

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

GemGame gemGame;
GemGame private _gemGame;

function setUp() public {
gemGame = new GemGame();
_gemGame = new GemGame(address(this), address(this), address(this));
}

function testEarnGem_EmitsGemEarnedEvent() public {
function testEarnGemEmitsGemEarnedEvent() public {
vm.expectEmit(true, true, false, false);
emit GemEarned(address(this), block.timestamp);
gemGame.earnGem();
_gemGame.earnGem();
}

function testEarnGemContractPausedReverts() public {
// pause the contract
_gemGame.pause();

// attempt to earn a gem
vm.expectRevert(ContractPaused.selector);
_gemGame.earnGem();
}

function testPausePausesContract() public {
// pause the contract
_gemGame.pause();

assertEq(_gemGame.paused(), true, "GemGame should be paused");
}

function testPauseWithoutPauseRoleReverts() public {
// revoke the pause role
_gemGame.revokeRole(keccak256("PAUSE"), address(this));

// attempt to pause
vm.expectRevert(Unauthorized.selector);
_gemGame.pause();
}

function testUnpauseUnpausesContract() public {
// pause the contract
_gemGame.pause();

// unpause the contract
_gemGame.unpause();

assertEq(_gemGame.paused(), false, "GemGame should be unpaused");
}

function testUnpauseWithoutPauseRoleReverts() public {
// revoke the unpause role
_gemGame.revokeRole(keccak256("UNPAUSE"), address(this));

// attempt to unpause
vm.expectRevert(Unauthorized.selector);
_gemGame.unpause();
}
}