-
Notifications
You must be signed in to change notification settings - Fork 38
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
pano-skylakis
wants to merge
6
commits into
main
Choose a base branch
from
ps/session-activity-factory
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
63 changes: 63 additions & 0 deletions
63
contracts/games/session-activity/SessionActivityDeployer.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// 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 | ||
* @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, address indexed deployedContract, string indexed name); | ||
|
||
/// @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; | ||
|
||
/** | ||
* @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 returns (SessionActivity) { | ||
// Ensure the caller has the deployer role | ||
if (!hasRole(_DEPLOYER_ROLE, msg.sender)) revert Unauthorized(); | ||
|
||
// 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); | ||
emit SessionActivityDeployed(msg.sender, address(sessionActivityContract), name); | ||
|
||
return sessionActivityContract; | ||
} | ||
} |
169 changes: 169 additions & 0 deletions
169
script/games/session-activity/DeploySessionActivityDeployer.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
// Copyright (c) Immutable Pty Ltd 2018 - 2023 | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
pragma solidity ^0.8.20; | ||
|
||
import "forge-std/Test.sol"; | ||
import {SessionActivity} from "../../../contracts/games/session-activity/SessionActivity.sol"; | ||
import { | ||
SessionActivityDeployer, | ||
Unauthorized | ||
} from "../../../contracts/games/session-activity/SessionActivityDeployer.sol"; | ||
|
||
/** | ||
* @title IDeployer Interface | ||
* @notice This interface defines the contract responsible for deploying and optionally initializing new contracts | ||
* via a specified deployment method. | ||
* @dev Credit to axelarnetwork https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/main/contracts/interfaces/IDeployer.sol | ||
*/ | ||
interface IDeployer { | ||
function deploy(bytes memory bytecode, bytes32 salt) external payable returns (address deployedAddress_); | ||
function deployAndInit(bytes memory bytecode, bytes32 salt, bytes calldata init) | ||
external | ||
payable | ||
returns (address deployedAddress_); | ||
function deployedAddress(bytes calldata bytecode, address sender, bytes32 salt) | ||
external | ||
view | ||
returns (address deployedAddress_); | ||
} | ||
|
||
interface IAccessControlledDeployer { | ||
function deploy(IDeployer deployer, bytes memory bytecode, bytes32 salt) external payable returns (address); | ||
} | ||
|
||
struct DeploymentArgs { | ||
address signer; | ||
address create3Factory; | ||
address accessControlledDeployer; | ||
string salt; | ||
} | ||
|
||
struct SessionActivityDeployerArgs { | ||
address admin; | ||
address deployer; | ||
address pauser; | ||
address unpauser; | ||
} | ||
|
||
contract DeploySessionActivityDeployer is Test { | ||
event SessionActivityRecorded(address indexed account, uint256 timestamp); | ||
event SessionActivityDeployed(address indexed account, address indexed deployedContract, string indexed name); | ||
|
||
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: 0xE4D45C0277762CaD4EC40bE69406068DAE74E17d, | ||
create3Factory: 0xFB1Ecc73c3f3F505d66C055A3571362DE001D9C0, | ||
accessControlledDeployer: 0x0B5B1d92259b13D516cCd5a6E63d7D94Ea2A4836, | ||
salt: "salty" | ||
}); | ||
|
||
SessionActivityDeployerArgs memory sessionActivityDeployerArgs = SessionActivityDeployerArgs({ | ||
pauser: makeAddr("pause"), | ||
unpauser: makeAddr("unpause"), | ||
admin: makeAddr("admin"), | ||
deployer: makeAddr("deployer") | ||
}); | ||
|
||
// Run deployment against forked testnet | ||
SessionActivityDeployer deployerContract = _deploy(deploymentArgs, sessionActivityDeployerArgs); | ||
|
||
// Assert roles are assigned correctly | ||
assertEq(true, deployerContract.hasRole(keccak256("DEPLOYER"), sessionActivityDeployerArgs.deployer)); | ||
assertEq( | ||
true, deployerContract.hasRole(deployerContract.DEFAULT_ADMIN_ROLE(), sessionActivityDeployerArgs.admin) | ||
); | ||
|
||
// The DEFAULT_ADMIN_ROLE should be revoked from the deployer account and the factory contract address | ||
assertEq(false, deployerContract.hasRole(deployerContract.DEFAULT_ADMIN_ROLE(), deploymentArgs.signer)); | ||
assertEq(false, deployerContract.hasRole(deployerContract.DEFAULT_ADMIN_ROLE(), deploymentArgs.create3Factory)); | ||
|
||
// Try to deploy a contract without the deployer role expecting a revert | ||
vm.prank(makeAddr("notdeployer")); | ||
vm.expectRevert(Unauthorized.selector); | ||
deployerContract.deploy("atestname"); | ||
|
||
// Deploy a contract with the deployer role | ||
vm.prank(sessionActivityDeployerArgs.deployer); | ||
vm.expectEmit(true, false, true, false); | ||
emit SessionActivityDeployed(sessionActivityDeployerArgs.deployer, address(0), "atestname"); | ||
SessionActivity deployedSessionActivityContract = deployerContract.deploy("atestname"); | ||
|
||
// Asset roles are assigned correctly on the child contract | ||
assertEq(true, deployedSessionActivityContract.hasRole(keccak256("PAUSE"), sessionActivityDeployerArgs.pauser)); | ||
assertEq( | ||
true, deployedSessionActivityContract.hasRole(keccak256("UNPAUSE"), sessionActivityDeployerArgs.unpauser) | ||
); | ||
assertEq( | ||
true, | ||
deployedSessionActivityContract.hasRole( | ||
deployedSessionActivityContract.DEFAULT_ADMIN_ROLE(), sessionActivityDeployerArgs.admin | ||
) | ||
); | ||
|
||
// Record a session activity | ||
vm.expectEmit(true, true, true, false); | ||
emit SessionActivityRecorded(address(this), block.timestamp); | ||
deployedSessionActivityContract.recordSessionActivity(); | ||
} | ||
|
||
function deploy() external { | ||
address signer = vm.envAddress("SIGNER_ADDRESS"); | ||
address create3Factory = vm.envAddress("OWNABLE_CREATE3_FACTORY_ADDRESS"); | ||
address accessControlledDeployer = vm.envAddress("ACCESS_CONTROLLED_DEPLOYER_ADDRESS"); | ||
string memory salt = vm.envString("SESSION_ACTIVITY_DEPLOYER_SALT"); | ||
|
||
DeploymentArgs memory deploymentArgs = DeploymentArgs({ | ||
signer: signer, | ||
create3Factory: create3Factory, | ||
salt: salt, | ||
accessControlledDeployer: accessControlledDeployer | ||
}); | ||
|
||
address defaultAdmin = vm.envAddress("DEFAULT_ADMIN"); | ||
address deployer = vm.envAddress("DEPLOYER"); | ||
address pauser = vm.envAddress("PAUSER"); | ||
address unpauser = vm.envAddress("UNPAUSER"); | ||
|
||
SessionActivityDeployerArgs memory sessionActivityDeployerArgs = | ||
SessionActivityDeployerArgs({admin: defaultAdmin, deployer: deployer, pauser: pauser, unpauser: unpauser}); | ||
|
||
_deploy(deploymentArgs, sessionActivityDeployerArgs); | ||
} | ||
|
||
function _deploy( | ||
DeploymentArgs memory deploymentArgs, | ||
SessionActivityDeployerArgs memory sessionActivityDeployerArgs | ||
) internal returns (SessionActivityDeployer sessionActivityDeployerContract) { | ||
IAccessControlledDeployer accessControlledDeployer = | ||
IAccessControlledDeployer(deploymentArgs.accessControlledDeployer); | ||
IDeployer create3Factory = IDeployer(deploymentArgs.create3Factory); | ||
|
||
// Create deployment bytecode and encode constructor args | ||
bytes memory deploymentBytecode = abi.encodePacked( | ||
type(SessionActivityDeployer).creationCode, | ||
abi.encode( | ||
sessionActivityDeployerArgs.admin, | ||
sessionActivityDeployerArgs.deployer, | ||
sessionActivityDeployerArgs.pauser, | ||
sessionActivityDeployerArgs.unpauser | ||
) | ||
); | ||
|
||
bytes32 saltBytes = keccak256(abi.encode(deploymentArgs.salt)); | ||
|
||
/// @dev Deploy the contract via the Ownable CREATE3 factory | ||
vm.startBroadcast(deploymentArgs.signer); | ||
|
||
address sessionActivityDeployerAddress = | ||
accessControlledDeployer.deploy(create3Factory, deploymentBytecode, saltBytes); | ||
sessionActivityDeployerContract = SessionActivityDeployer(sessionActivityDeployerAddress); | ||
|
||
vm.stopBroadcast(); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe have setPauserAndUnpauser() function?