Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Session Activity Factory #230

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Empty file.
67 changes: 67 additions & 0 deletions contracts/games/session-activity/SessionActivity.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: Apache 2
// solhint-disable not-rely-on-time

pragma solidity ^0.8.19;

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

error Unauthorized();
error ContractPaused();

/**
* @title SessionActivity - A simple contract that emits an event for the purpose of recording session activity on-chain
* @author Immutable
* @dev The SessionActivity contract is not designed to be upgradeable or extended.
*/
contract SessionActivity is AccessControlEnumerable, Pausable {
/// @notice Indicates that session activity has been recorded for an account
event SessionActivityRecorded(address indexed account, uint256 timestamp);

/// @notice The name of the contract
string public name;

/// @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, string memory _name) {
_grantRole(DEFAULT_ADMIN_ROLE, _admin);
_grantRole(_PAUSE, _pauser);
_grantRole(_UNPAUSE, _unpauser);
name = _name;
}

/**
* @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 `SessionActivityRecorded` event
*/
function recordSessionActivity() external {
if (paused()) revert ContractPaused();
emit SessionActivityRecorded(msg.sender, block.timestamp);
}
}
90 changes: 90 additions & 0 deletions contracts/games/session-activity/SessionActivityDeployer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: Apache 2
// solhint-disable not-rely-on-time

pragma solidity ^0.8.19;

import {AccessControlEnumerable} from "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
import {SessionActivity} from "./SessionActivity.sol";

error Unauthorized();
error NameAlreadyRegistered();

/**
* @title SessionActivityDeployer - A factory contract that deploys SessionActivity contracts and tracks their addresses and names
* @author Immutable
* @dev The SessionActivityDeployer contract is not designed to be upgradeable or extended.
*/
contract SessionActivityDeployer is AccessControlEnumerable {
/// @notice Indicates that an account has registered session activity
event SessionActivityDeployed(address indexed account, string indexed name);

/// @notice Mapping of deployed SessionActivity contract addresses to their names
/// @dev To get a list of all deployed contract names, iterate over the deployedContracts array and use this mapping
mapping(address deployedContract => string name) public sessionActivityNames;

/// @notice Mapping of SessionActivity contract names to their addresses
/// @dev To get a list of all deployed contract addresses, iterate over the names array and use this mapping
mapping(string name => address deployedContract) public sessionActivityContracts;

/// @notice Array of deployed SessionActivity contracts
address[] public deployedContracts;
Copy link
Contributor

Choose a reason for hiding this comment

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

This declaration will result in an accessor that returns an array. If there are a lot of values, this could result in a out of gas error (yes, for a view call)

Copy link
Contributor

Choose a reason for hiding this comment

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

That is, if we get to 100,000 (or maybe just 10,000) values. If we feel that it is unlikely to ever happen, then it is probably OK. An alternative would be to have a function to return how many contracts have been deployed, and another function to request, starting at contract N, return information for up to M contracts.


/// @notice Array of deployed SessionActivity contract names
string[] public names;
Copy link
Contributor

Choose a reason for hiding this comment

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

see above


/// @notice Role to allow deploying SessionActivity contracts
bytes32 private constant _DEPLOYER_ROLE = keccak256("DEPLOYER");

/// @notice The address for the pauser role on the SessionActivity contract
address private _pauser;

/// @notice The address for the unpauser role on the SessionActivity contract
address private _unpauser;
Comment on lines +26 to +29
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking for specific feedback and suggestions for effective role management for the Factory + deployed contracts.

Copy link
Contributor

Choose a reason for hiding this comment

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

would the same pauser and unpauser be used all the time?
What happens is the pauser or unpauser account are compromised?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe have setPauserAndUnpauser() function?


/**
* @notice Sets the DEFAULT_ADMIN, PAUSE and UNPAUSE roles
* @param admin The address for the admin role
* @param deployer The address for the deployer role
* @param pauser The address for the pauser role on the SessionActivity contract
* @param unpauser The address for the unpauser role on the SessionActivity contract
*/
constructor(address admin, address deployer, address pauser, address unpauser) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(_DEPLOYER_ROLE, deployer);
_pauser = pauser;
_unpauser = unpauser;
}

/**
* @notice Deploys a new SessionActivity contract
* @param name The name of the SessionActivity contract
* @dev Only accounts granted the _DEPLOYER_ROLE can call this function
*/
function deploy(string memory name) public {
// Ensure the caller has the deployer role
if (!hasRole(_DEPLOYER_ROLE, msg.sender)) revert Unauthorized();

// Loop through names and ensure the provided name is unique
for (uint256 i = 0; i < names.length; i++) {
if (keccak256(abi.encodePacked(names[i])) == keccak256(abi.encodePacked(name))) {
revert NameAlreadyRegistered();
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Something more robust may be needed here. Is Guild Wars the same as Guildwars or GuildWars?

Another consideration: would we ever want to deploy a separate contract for the same game?

Copy link
Contributor

Choose a reason for hiding this comment

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

This construct is super gas inefficient:

  • There is a for loop
  • The bytes32 for keccak256(abi.encodePacked(name)) happens each loop. This is likely to result in a memory expansion for each time through the loop which will cost $$$LOTS
  • keccak256(abi.encodePacked(names[i])) is also going to result in lots of memory expansion

Copy link
Contributor

Choose a reason for hiding this comment

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

Change this code to:
if (sessionActivityContracts[name] != address(0)) {
revert NameAlreadyRegistered();
}


// Get the existing admin role
address admin = getRoleMember(DEFAULT_ADMIN_ROLE, 0);

// Deploy the session activity contract
SessionActivity sessionActivityContract = new SessionActivity(admin, _pauser, _unpauser, name);

// Register the contract address and name
sessionActivityNames[address(sessionActivityContract)] = name;
deployedContracts.push(address(sessionActivityContract));

sessionActivityContracts[name] = address(sessionActivityContract);
names.push(name);

emit SessionActivityDeployed(msg.sender, name);
}
}
Loading