// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (access/AccessControl.sol)
pragma solidity ^0.8.20;
import {IAccessControl} from "./IAccessControl.sol";
import {Context} from "../utils/Context.sol";
import {ERC165} from "../utils/introspection/ERC165.sol";
/**
* @dev Contract module that allows children to implement role-based access
* control mechanisms. This is a lightweight version that doesn't allow enumerating role
* members except through off-chain means by accessing the contract event logs. Some
* applications may benefit from on-chain enumerability, for those cases see
* {AccessControlEnumerable}.
*
* Roles are referred to by their `bytes32` identifier. These should be exposed
* in the external API and be unique. The best way to achieve this is by
* using `public constant` hash digests:
*
* ```solidity
* bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
* ```
*
* Roles can be used to represent a set of permissions. To restrict access to a
* function call, use {hasRole}:
*
* ```solidity
* function foo() public {
* require(hasRole(MY_ROLE, msg.sender));
* ...
* }
* ```
*
* Roles can be granted and revoked dynamically via the {grantRole} and
* {revokeRole} functions. Each role has an associated admin role, and only
* accounts that have a role's admin role can call {grantRole} and {revokeRole}.
*
* By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
* that only accounts with this role will be able to grant or revoke other
* roles. More complex role relationships can be created by using
* {_setRoleAdmin}.
*
* WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
* grant and revoke this role. Extra precautions should be taken to secure
* accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
* to enforce additional security measures for this role.
*/
abstract contract AccessControl is Context, IAccessControl, ERC165 {
struct RoleData {
mapping(address => bool) members;
bytes32 adminRole;
}
mapping(bytes32 => RoleData) private _roles;
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
/**
* @dev Modifier that checks that an account has a specific role. Reverts
* with an {AccessControlUnauthorizedAccount} error including the required role.
*/
modifier onlyRole(bytes32 role) {
_checkRole(role);
_;
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) public view virtual returns (bool) {
return _roles[role].members[account];
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
* is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
*/
function _checkRole(bytes32 role) internal view virtual {
_checkRole(role, _msgSender());
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
* is missing `role`.
*/
function _checkRole(bytes32 role, address account) internal view virtual {
if (!hasRole(role, account)) {
revert AccessControlUnauthorizedAccount(account, role);
}
}
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
return _roles[role].adminRole;
}
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleGranted} event.
*/
function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleRevoked} event.
*/
function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been revoked `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*
* May emit a {RoleRevoked} event.
*/
function renounceRole(bytes32 role, address callerConfirmation) public virtual {
if (callerConfirmation != _msgSender()) {
revert AccessControlBadConfirmation();
}
_revokeRole(role, callerConfirmation);
}
/**
* @dev Sets `adminRole` as ``role``'s admin role.
*
* Emits a {RoleAdminChanged} event.
*/
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
bytes32 previousAdminRole = getRoleAdmin(role);
_roles[role].adminRole = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
/**
* @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
*
* Internal function without access restriction.
*
* May emit a {RoleGranted} event.
*/
function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
if (!hasRole(role, account)) {
_roles[role].members[account] = true;
emit RoleGranted(role, account, _msgSender());
return true;
} else {
return false;
}
}
/**
* @dev Attempts to revoke `role` to `account` and returns a boolean indicating if `role` was revoked.
*
* Internal function without access restriction.
*
* May emit a {RoleRevoked} event.
*/
function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
if (hasRole(role, account)) {
_roles[role].members[account] = false;
emit RoleRevoked(role, account, _msgSender());
return true;
} else {
return false;
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (access/AccessControl.sol)
pragma solidity ^0.8.20;
import { IAccessControlUpgradeable } from "./IAccessControlUpgradeable.sol";
import { ContextUpgradeable } from "../utils/ContextUpgradeable.sol";
import { ERC165Upgradeable } from "../utils/introspection/ERC165Upgradeable.sol";
import "../proxy/utils/Initializable.sol";
/**
* @dev Contract module that allows children to implement role-based access
* control mechanisms. This is a lightweight version that doesn't allow enumerating role
* members except through off-chain means by accessing the contract event logs. Some
* applications may benefit from on-chain enumerability, for those cases see
* {AccessControlEnumerable}.
*
* Roles are referred to by their `bytes32` identifier. These should be exposed
* in the external API and be unique. The best way to achieve this is by
* using `public constant` hash digests:
*
* ```solidity
* bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
* ```
*
* Roles can be used to represent a set of permissions. To restrict access to a
* function call, use {hasRole}:
*
* ```solidity
* function foo() public {
* require(hasRole(MY_ROLE, msg.sender));
* ...
* }
* ```
*
* Roles can be granted and revoked dynamically via the {grantRole} and
* {revokeRole} functions. Each role has an associated admin role, and only
* accounts that have a role's admin role can call {grantRole} and {revokeRole}.
*
* By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
* that only accounts with this role will be able to grant or revoke other
* roles. More complex role relationships can be created by using
* {_setRoleAdmin}.
*
* WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
* grant and revoke this role. Extra precautions should be taken to secure
* accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
* to enforce additional security measures for this role.
*/
abstract contract AccessControlUpgradeable is Initializable, ContextUpgradeable, IAccessControlUpgradeable, ERC165Upgradeable {
function __AccessControl_init() internal onlyInitializing {
}
function __AccessControl_init_unchained() internal onlyInitializing {
}
struct RoleData {
mapping(address => bool) members;
bytes32 adminRole;
}
mapping(bytes32 => RoleData) private _roles;
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
/**
* @dev Modifier that checks that an account has a specific role. Reverts
* with an {AccessControlUnauthorizedAccount} error including the required role.
*/
modifier onlyRole(bytes32 role) {
_checkRole(role);
_;
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IAccessControlUpgradeable).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) public view virtual returns (bool) {
return _roles[role].members[account];
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
* is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
*/
function _checkRole(bytes32 role) internal view virtual {
_checkRole(role, _msgSender());
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
* is missing `role`.
*/
function _checkRole(bytes32 role, address account) internal view virtual {
if (!hasRole(role, account)) {
revert AccessControlUnauthorizedAccount(account, role);
}
}
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
return _roles[role].adminRole;
}
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleGranted} event.
*/
function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleRevoked} event.
*/
function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been revoked `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*
* May emit a {RoleRevoked} event.
*/
function renounceRole(bytes32 role, address callerConfirmation) public virtual {
if (callerConfirmation != _msgSender()) {
revert AccessControlBadConfirmation();
}
_revokeRole(role, callerConfirmation);
}
/**
* @dev Sets `adminRole` as ``role``'s admin role.
*
* Emits a {RoleAdminChanged} event.
*/
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
bytes32 previousAdminRole = getRoleAdmin(role);
_roles[role].adminRole = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
/**
* @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
*
* Internal function without access restriction.
*
* May emit a {RoleGranted} event.
*/
function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
if (!hasRole(role, account)) {
_roles[role].members[account] = true;
emit RoleGranted(role, account, _msgSender());
return true;
} else {
return false;
}
}
/**
* @dev Attempts to revoke `role` to `account` and returns a boolean indicating if `role` was revoked.
*
* Internal function without access restriction.
*
* May emit a {RoleRevoked} event.
*/
function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
if (hasRole(role, account)) {
_roles[role].members[account] = false;
emit RoleRevoked(role, account, _msgSender());
return true;
} else {
return false;
}
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[49] private __gap;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Clones} from "openzeppelin-contracts/contracts/proxy/Clones.sol";
import {AccessControl} from "openzeppelin-contracts/contracts/access/AccessControl.sol";
import {SharpFactsAggregator} from "../src/SharpFactsAggregator.sol";
/// @title AggregatorsFactory
/// @author Herodotus Dev
/// @notice A factory contract for creating new SharpFactsAggregator contracts
/// and upgrading new one's starter template
contract AggregatorsFactory is AccessControl {
// Blank contract template address
SharpFactsAggregator public template;
// Timelock mechanism for upgrades proposals
struct UpgradeProposalTimelock {
uint256 timestamp;
SharpFactsAggregator newTemplate;
}
// Upgrades timelocks
mapping(uint256 => UpgradeProposalTimelock) public upgrades;
// Upgrades tracker
uint256 public upgradesCount;
// Delay before an upgrade can be performed
uint256 public constant DELAY = 3 days;
// Aggregators indexing
uint256 public aggregatorsCount;
// Aggregators by id
mapping(uint256 => SharpFactsAggregator) public aggregatorsById;
// Access control
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
// Default roots for new aggregators:
// poseidon_hash(1, "brave new world")
bytes32 public constant POSEIDON_MMR_INITIAL_ROOT =
0x06759138078831011e3bc0b4a135af21c008dda64586363531697207fb5a2bae;
// keccak_hash(1, "brave new world")
bytes32 public constant KECCAK_MMR_INITIAL_ROOT =
0x5d8d23518dd388daa16925ff9475c5d1c06430d21e0422520d6a56402f42937b;
// Events
event UpgradeProposal(SharpFactsAggregator newTemplate);
event Upgrade(
SharpFactsAggregator oldTemplate,
SharpFactsAggregator newTemplate
);
event AggregatorCreation(
SharpFactsAggregator aggregator,
uint256 newAggregatorId,
uint256 detachedFromAggregatorId
);
/// Creates a new Factory contract and grants OPERATOR_ROLE to the deployer
/// @param initialTemplate The address of the template contract to clone
constructor(SharpFactsAggregator initialTemplate) {
template = initialTemplate;
_setRoleAdmin(OPERATOR_ROLE, OPERATOR_ROLE);
_grantRole(OPERATOR_ROLE, _msgSender());
}
/// @notice Reverts if the caller is not an operator
modifier onlyOperator() {
require(
hasRole(OPERATOR_ROLE, _msgSender()),
"Caller is not an operator"
);
_;
}
/**
* Creates a new aggregator contract by cloning the template contract
* @param aggregatorId The id of an existing aggregator to attach to (0 for none)
*/
function createAggregator(
uint256 aggregatorId
) external onlyOperator returns (address) {
SharpFactsAggregator.AggregatorState memory initialAggregatorState;
if (aggregatorId != 0) {
// Attach from existing aggregator
require(aggregatorId <= aggregatorsCount, "Invalid aggregator ID");
address existingAggregatorAddr = address(
aggregatorsById[aggregatorId]
);
require(
existingAggregatorAddr != address(0),
"Aggregator not found"
);
SharpFactsAggregator existingAggregator = SharpFactsAggregator(
existingAggregatorAddr
);
(
bytes32 poseidonMmrRoot,
bytes32 keccakMmrRoot,
uint256 mmrSize,
bytes32 continuableParentHash
) = existingAggregator.aggregatorState();
initialAggregatorState.poseidonMmrRoot = poseidonMmrRoot;
initialAggregatorState.keccakMmrRoot = keccakMmrRoot;
initialAggregatorState.mmrSize = mmrSize;
initialAggregatorState
.continuableParentHash = continuableParentHash;
} else {
// Create a new aggregator (detach from existing ones)
initialAggregatorState = SharpFactsAggregator.AggregatorState({
poseidonMmrRoot: POSEIDON_MMR_INITIAL_ROOT,
keccakMmrRoot: KECCAK_MMR_INITIAL_ROOT,
mmrSize: 1,
continuableParentHash: bytes32(0)
});
}
// Initialize the newly created aggregator
bytes memory data = abi.encodeWithSignature(
"initialize((bytes32,bytes32,uint256,bytes32))",
initialAggregatorState
);
// Clone the template contract
address clone = Clones.clone(address(template));
// The data is the encoded initialize function (with initial parameters)
(bool success, ) = clone.call(data);
require(success, "Aggregator initialization failed");
aggregatorsById[++aggregatorsCount] = SharpFactsAggregator(clone);
emit AggregatorCreation(
SharpFactsAggregator(clone),
aggregatorsCount,
aggregatorId
);
// Grant roles to the caller so that roles are not stuck in the Factory
SharpFactsAggregator(clone).grantRole(
keccak256("OPERATOR_ROLE"),
_msgSender()
);
SharpFactsAggregator(clone).grantRole(
keccak256("UNLOCKER_ROLE"),
_msgSender()
);
return clone;
}
/**
* Proposes an upgrade to the template (blank aggregator) contract
* @param newTemplate The address of the new template contract to use for future aggregators
*/
function proposeUpgrade(
SharpFactsAggregator newTemplate
) external onlyOperator {
upgrades[++upgradesCount] = UpgradeProposalTimelock(
block.timestamp + DELAY,
newTemplate
);
emit UpgradeProposal(newTemplate);
}
/**
* Upgrades the template (blank aggregator) contract
* @param updateId The id of the upgrade proposal to execute
*/
function upgrade(uint256 updateId) external onlyOperator {
require(updateId == upgradesCount, "Invalid updateId");
uint256 timeLockTimestamp = upgrades[updateId].timestamp;
require(timeLockTimestamp != 0, "TimeLock not set");
require(block.timestamp >= timeLockTimestamp, "TimeLock not expired");
address oldTemplate = address(template);
template = SharpFactsAggregator(upgrades[updateId].newTemplate);
// Clear timelock
upgrades[updateId] = UpgradeProposalTimelock(
0,
SharpFactsAggregator(address(0))
);
emit Upgrade(SharpFactsAggregator(oldTemplate), template);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (proxy/Clones.sol)
pragma solidity ^0.8.20;
/**
* @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for
* deploying minimal proxy contracts, also known as "clones".
*
* > To simply and cheaply clone contract functionality in an immutable way, this standard specifies
* > a minimal bytecode implementation that delegates all calls to a known, fixed address.
*
* The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2`
* (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the
* deterministic method.
*/
library Clones {
/**
* @dev A clone instance deployment failed.
*/
error ERC1167FailedCreateClone();
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create opcode, which should never revert.
*/
function clone(address implementation) internal returns (address instance) {
/// @solidity memory-safe-assembly
assembly {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create(0, 0x09, 0x37)
}
if (instance == address(0)) {
revert ERC1167FailedCreateClone();
}
}
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create2 opcode and a `salt` to deterministically deploy
* the clone. Using the same `implementation` and `salt` multiple time will revert, since
* the clones cannot be deployed twice at the same address.
*/
function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
/// @solidity memory-safe-assembly
assembly {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create2(0, 0x09, 0x37, salt)
}
if (instance == address(0)) {
revert ERC1167FailedCreateClone();
}
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
/// @solidity memory-safe-assembly
assembly {
let ptr := mload(0x40)
mstore(add(ptr, 0x38), deployer)
mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
mstore(add(ptr, 0x14), implementation)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73)
mstore(add(ptr, 0x58), salt)
mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37))
predicted := keccak256(add(ptr, 0x43), 0x55)
}
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes32 salt
) internal view returns (address predicted) {
return predictDeterministicAddress(implementation, salt, address(this));
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
pragma solidity ^0.8.20;
import "../proxy/utils/Initializable.sol";
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract ContextUpgradeable is Initializable {
function __Context_init() internal onlyInitializing {
}
function __Context_init_unchained() internal onlyInitializing {
}
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[50] private __gap;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol)
pragma solidity ^0.8.20;
import {IERC165} from "./IERC165.sol";
/**
* @dev Implementation of the {IERC165} interface.
*
* Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
* for the additional interface id that will be supported. For example:
*
* ```solidity
* function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
* return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
* }
* ```
*/
abstract contract ERC165 is IERC165 {
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return interfaceId == type(IERC165).interfaceId;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol)
pragma solidity ^0.8.20;
import { IERC165Upgradeable } from "./IERC165Upgradeable.sol";
import "../../proxy/utils/Initializable.sol";
/**
* @dev Implementation of the {IERC165} interface.
*
* Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
* for the additional interface id that will be supported. For example:
*
* ```solidity
* function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
* return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
* }
* ```
*/
abstract contract ERC165Upgradeable is Initializable, IERC165Upgradeable {
function __ERC165_init() internal onlyInitializing {
}
function __ERC165_init_unchained() internal onlyInitializing {
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return interfaceId == type(IERC165Upgradeable).interfaceId;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[50] private __gap;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (access/IAccessControl.sol)
pragma solidity ^0.8.20;
/**
* @dev External interface of AccessControl declared to support ERC165 detection.
*/
interface IAccessControl {
/**
* @dev The `account` is missing a role.
*/
error AccessControlUnauthorizedAccount(address account, bytes32 neededRole);
/**
* @dev The caller of a function is not the expected one.
*
* NOTE: Don't confuse with {AccessControlUnauthorizedAccount}.
*/
error AccessControlBadConfirmation();
/**
* @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`
*
* `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite
* {RoleAdminChanged} not being emitted signaling this.
*/
event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);
/**
* @dev Emitted when `account` is granted `role`.
*
* `sender` is the account that originated the contract call, an admin role
* bearer except when using {AccessControl-_setupRole}.
*/
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
/**
* @dev Emitted when `account` is revoked `role`.
*
* `sender` is the account that originated the contract call:
* - if using `revokeRole`, it is the admin role bearer
* - if using `renounceRole`, it is the role bearer (i.e. `account`)
*/
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) external view returns (bool);
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {AccessControl-_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) external view returns (bytes32);
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function grantRole(bytes32 role, address account) external;
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function revokeRole(bytes32 role, address account) external;
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been granted `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*/
function renounceRole(bytes32 role, address callerConfirmation) external;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (access/IAccessControl.sol)
pragma solidity ^0.8.20;
/**
* @dev External interface of AccessControl declared to support ERC165 detection.
*/
interface IAccessControlUpgradeable {
/**
* @dev The `account` is missing a role.
*/
error AccessControlUnauthorizedAccount(address account, bytes32 neededRole);
/**
* @dev The caller of a function is not the expected one.
*
* NOTE: Don't confuse with {AccessControlUnauthorizedAccount}.
*/
error AccessControlBadConfirmation();
/**
* @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`
*
* `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite
* {RoleAdminChanged} not being emitted signaling this.
*/
event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);
/**
* @dev Emitted when `account` is granted `role`.
*
* `sender` is the account that originated the contract call, an admin role
* bearer except when using {AccessControl-_setupRole}.
*/
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
/**
* @dev Emitted when `account` is revoked `role`.
*
* `sender` is the account that originated the contract call:
* - if using `revokeRole`, it is the admin role bearer
* - if using `renounceRole`, it is the role bearer (i.e. `account`)
*/
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) external view returns (bool);
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {AccessControl-_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) external view returns (bytes32);
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function grantRole(bytes32 role, address account) external;
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function revokeRole(bytes32 role, address account) external;
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been granted `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*/
function renounceRole(bytes32 role, address callerConfirmation) external;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165Upgradeable {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IFactsRegistry {
function isValid(bytes32 fact) external view returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/Initializable.sol)
pragma solidity ^0.8.20;
/**
* @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
* behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an
* external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
* function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
*
* The initialization functions use a version number. Once a version number is used, it is consumed and cannot be
* reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in
* case an upgrade adds a module that needs to be initialized.
*
* For example:
*
* [.hljs-theme-light.nopadding]
* ```solidity
* contract MyToken is ERC20Upgradeable {
* function initialize() initializer public {
* __ERC20_init("MyToken", "MTK");
* }
* }
*
* contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {
* function initializeV2() reinitializer(2) public {
* __ERC20Permit_init("MyToken");
* }
* }
* ```
*
* TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
* possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.
*
* CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
* that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
*
* [CAUTION]
* ====
* Avoid leaving a contract uninitialized.
*
* An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation
* contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke
* the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:
*
* [.hljs-theme-light.nopadding]
* ```
* /// @custom:oz-upgrades-unsafe-allow constructor
* constructor() {
* _disableInitializers();
* }
* ```
* ====
*/
abstract contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
*/
uint8 private _initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private _initializing;
/**
* @dev The contract is already initialized.
*/
error AlreadyInitialized();
/**
* @dev The contract is not initializing.
*/
error NotInitializing();
/**
* @dev Triggered when the contract has been initialized or reinitialized.
*/
event Initialized(uint8 version);
/**
* @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
* `onlyInitializing` functions can be used to initialize parent contracts.
*
* Similar to `reinitializer(1)`, except that functions marked with `initializer` can be nested in the context of a
* constructor.
*
* Emits an {Initialized} event.
*/
modifier initializer() {
bool isTopLevelCall = !_initializing;
if (!(isTopLevelCall && _initialized < 1) && !(address(this).code.length == 0 && _initialized == 1)) {
revert AlreadyInitialized();
}
_initialized = 1;
if (isTopLevelCall) {
_initializing = true;
}
_;
if (isTopLevelCall) {
_initializing = false;
emit Initialized(1);
}
}
/**
* @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the
* contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be
* used to initialize parent contracts.
*
* A reinitializer may be used after the original initialization step. This is essential to configure modules that
* are added through upgrades and that require initialization.
*
* When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer`
* cannot be nested. If one is invoked in the context of another, execution will revert.
*
* Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in
* a contract, executing them in the right order is up to the developer or operator.
*
* WARNING: setting the version to 255 will prevent any future reinitialization.
*
* Emits an {Initialized} event.
*/
modifier reinitializer(uint8 version) {
if (_initializing || _initialized >= version) {
revert AlreadyInitialized();
}
_initialized = version;
_initializing = true;
_;
_initializing = false;
emit Initialized(version);
}
/**
* @dev Modifier to protect an initialization function so that it can only be invoked by functions with the
* {initializer} and {reinitializer} modifiers, directly or indirectly.
*/
modifier onlyInitializing() {
_checkInitializing();
_;
}
/**
* @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}.
*/
function _checkInitializing() internal view virtual {
if (!_initializing) {
revert NotInitializing();
}
}
/**
* @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
* Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
* to any version. It is recommended to use this to lock implementation contracts that are designed to be called
* through proxies.
*
* Emits an {Initialized} event the first time it is successfully executed.
*/
function _disableInitializers() internal virtual {
if (_initializing) {
revert AlreadyInitialized();
}
if (_initialized != type(uint8).max) {
_initialized = type(uint8).max;
emit Initialized(type(uint8).max);
}
}
/**
* @dev Returns the highest version that has been initialized. See {reinitializer}.
*/
function _getInitializedVersion() internal view returns (uint8) {
return _initialized;
}
/**
* @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
*/
function _isInitializing() internal view returns (bool) {
return _initializing;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
import {AccessControlUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol";
import {IFactsRegistry} from "./interfaces/IFactsRegistry.sol";
import {Uint256Splitter} from "./lib/Uint256Splitter.sol";
/// @title SharpFactsAggregator
/// @dev Aggregator contract to handle SHARP job outputs and update the global aggregator state.
/// @author Herodotus Dev
/// ------------------
/// Example:
/// Blocks inside brackets are the ones processed during their SHARP job execution
// 7 [8 9 10] 11
/// n = 10
/// r = 3
/// `r` is the number of blocks processed on a single SHARP job execution
/// `blockNMinusRPlusOneParentHash` = 8.parentHash (oldestHash)
/// `blockNPlusOneParentHash` = 11.parentHash (newestHash)
/// ------------------
contract SharpFactsAggregator is Initializable, AccessControlUpgradeable {
// Using inline library for efficient splitting and joining of uint256 values
using Uint256Splitter for uint256;
// Role definitions for access control
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
bytes32 public constant UNLOCKER_ROLE = keccak256("UNLOCKER_ROLE");
uint256 public constant MINIMUM_BLOCKS_CONFIRMATIONS = 20;
uint256 public constant MAXIMUM_BLOCKS_CONFIRMATIONS = 255;
// Sharp Facts Registry
IFactsRegistry public immutable FACTS_REGISTRY;
// Cairo program hash (i.e., the off-chain block headers accumulator program)
bytes32 public constant PROGRAM_HASH =
bytes32(
uint256(
0x01eca36d586f5356fba096edbf7414017d51cd0ed24b8fde80f78b61a9216ed2
)
);
// Global aggregator state
struct AggregatorState {
bytes32 poseidonMmrRoot;
bytes32 keccakMmrRoot;
uint256 mmrSize;
bytes32 continuableParentHash;
}
// Current __global__ state of this aggregator
AggregatorState public aggregatorState;
// Mapping to keep track of block number to its parent hash
mapping(uint256 => bytes32) public blockNumberToParentHash;
// Flag to control operator role requirements
bool public isOperatorRequired;
// Representation of the Cairo program's output (raw unpacked)
struct JobOutput {
uint256 fromBlockNumberHigh;
uint256 toBlockNumberLow;
bytes32 blockNPlusOneParentHashLow;
bytes32 blockNPlusOneParentHashHigh;
bytes32 blockNMinusRPlusOneParentHashLow;
bytes32 blockNMinusRPlusOneParentHashHigh;
bytes32 mmrPreviousRootPoseidon;
bytes32 mmrPreviousRootKeccakLow;
bytes32 mmrPreviousRootKeccakHigh;
uint256 mmrPreviousSize;
bytes32 mmrNewRootPoseidon;
bytes32 mmrNewRootKeccakLow;
bytes32 mmrNewRootKeccakHigh;
uint256 mmrNewSize;
}
// Packed representation of the Cairo program's output (for gas efficiency)
struct JobOutputPacked {
uint256 blockNumbersPacked;
bytes32 blockNPlusOneParentHash;
bytes32 blockNMinusRPlusOneParentHash;
bytes32 mmrPreviousRootPoseidon;
bytes32 mmrPreviousRootKeccak;
bytes32 mmrNewRootPoseidon;
bytes32 mmrNewRootKeccak;
uint256 mmrSizesPacked;
}
// Custom errors for better error handling and clarity
error NotEnoughBlockConfirmations();
error TooManyBlocksConfirmations();
error NotEnoughJobs();
error UnknownParentHash();
error AggregationError(string message); // Generic error with a message
error AggregationBlockMismatch();
error GenesisBlockReached();
error InvalidFact();
// Event emitted when a new range is registered
// (i.e, when we want to allow aggregating from a more recent block)
event NewRangeRegistered(
uint256 targetBlock,
bytes32 targetBlockParentHash
);
// Event emitted when __at least__ one SHARP job is aggregated
event Aggregate(
uint256 fromBlockNumberHigh,
uint256 toBlockNumberLow,
bytes32 poseidonMmrRoot,
bytes32 keccakMmrRoot,
uint256 mmrSize,
bytes32 continuableParentHash
);
event OperatorRequirementChange(bool newRequirement);
constructor(IFactsRegistry factsRegistry) {
FACTS_REGISTRY = factsRegistry;
}
/**
* @notice Initializes the contract with given parameters.
* @param initialAggregatorState Initial state of the aggregator (i.e., initial trees state).
*/
function initialize(
AggregatorState calldata initialAggregatorState
) public initializer {
__AccessControl_init();
aggregatorState = initialAggregatorState;
_setRoleAdmin(OPERATOR_ROLE, OPERATOR_ROLE);
_setRoleAdmin(UNLOCKER_ROLE, OPERATOR_ROLE);
// Grant operator role to the contract deployer
// to be able to define new aggregate ranges
_grantRole(OPERATOR_ROLE, _msgSender());
_grantRole(UNLOCKER_ROLE, _msgSender());
// Set operator role requirement to true by default
isOperatorRequired = true;
}
/// @notice Reverts if the caller is not an operator and the operator role requirement is enabled
modifier onlyOperator() {
if (isOperatorRequired) {
require(
hasRole(OPERATOR_ROLE, _msgSender()),
"Caller is not an operator"
);
}
_;
}
/// @notice Reverts if the caller is not an unlocker
modifier onlyUnlocker() {
require(
hasRole(UNLOCKER_ROLE, _msgSender()),
"Caller is not an unlocker"
);
_;
}
/// @dev Modifies the contract's operator requirement
function setOperatorRequired(
bool _isOperatorRequired
) external onlyUnlocker {
isOperatorRequired = _isOperatorRequired;
emit OperatorRequirementChange(_isOperatorRequired);
}
/// Registers a new range to aggregate from
/// @notice Caches a recent block hash (MINIMUM_BLOCKS_CONFIRMATIONS to -MAXIMUM_BLOCKS_CONFIRMATIONS from present), relying on the global `blockhash` Solidity function
/// @param blocksConfirmations Number of blocks preceding the current block
function registerNewRange(
uint256 blocksConfirmations
) external onlyOperator {
// Minimum blocks confirmations to avoid reorgs
if (blocksConfirmations < MINIMUM_BLOCKS_CONFIRMATIONS) {
revert NotEnoughBlockConfirmations();
}
// Maximum MAXIMUM_BLOCKS_CONFIRMATIONS blocks confirmations to capture
// an available block hash with Solidity `blockhash()`
if (blocksConfirmations > MAXIMUM_BLOCKS_CONFIRMATIONS) {
revert TooManyBlocksConfirmations();
}
// Determine the target block number (i.e. the child block)
uint256 targetBlock = block.number - blocksConfirmations;
// Extract its parent hash.
bytes32 targetBlockParentHash = blockhash(targetBlock - 1);
// If the parent hash is not available, revert
// (This should never happen under the current EVM rules)
if (targetBlockParentHash == bytes32(0)) {
revert UnknownParentHash();
}
// Cache the parent hash so that we can later on continue accumlating from it
blockNumberToParentHash[targetBlock] = targetBlockParentHash;
// If we cannot aggregate further in the past (e.g., genesis block is reached or it's a new tree)
if (aggregatorState.continuableParentHash == bytes32(0)) {
// Set the aggregator state's `continuableParentHash` to the target block's parent hash
// so we can easily continue aggregating from it without specifying `rightBoundStartBlock` in `aggregateSharpJobs`
aggregatorState.continuableParentHash = targetBlockParentHash;
}
emit NewRangeRegistered(targetBlock, targetBlockParentHash);
}
/// @notice Aggregate SHARP jobs outputs (min. 1) to update the global aggregator state
/// @param rightBoundStartBlock The reference block to start from. Defaults to continuing from the global state if set to `0`
/// @param outputs Array of SHARP jobs outputs (packed for Solidity)
function aggregateSharpJobs(
uint256 rightBoundStartBlock,
JobOutputPacked[] calldata outputs
) external onlyOperator {
// Ensuring at least one job output is provided
if (outputs.length < 1) {
revert NotEnoughJobs();
}
bytes32 rightBoundStartBlockParentHash = bytes32(0);
// Start from a different block than the current state if `rightBoundStartBlock` is specified
if (rightBoundStartBlock != 0) {
// Retrieve from cache the parent hash of the block to start from
rightBoundStartBlockParentHash = blockNumberToParentHash[
rightBoundStartBlock
];
// If not present in the cache, hash is not authenticated and we cannot continue from it
if (rightBoundStartBlockParentHash == bytes32(0)) {
revert UnknownParentHash();
}
}
JobOutputPacked calldata firstOutput = outputs[0];
// Ensure the first job is continuable
ensureContinuable(rightBoundStartBlockParentHash, firstOutput);
if (rightBoundStartBlockParentHash != bytes32(0)) {
(uint256 fromBlockHighStart, ) = firstOutput
.blockNumbersPacked
.split128();
// We check that block numbers are consecutives
if (fromBlockHighStart != rightBoundStartBlock - 1) {
revert AggregationBlockMismatch();
}
}
uint256 limit = outputs.length - 1;
// Iterate over the jobs outputs (aside from the last one)
// and ensure jobs are correctly linked and valid
for (uint256 i = 0; i < limit; ++i) {
JobOutputPacked calldata curOutput = outputs[i];
JobOutputPacked calldata nextOutput = outputs[i + 1];
ensureValidFact(curOutput);
ensureConsecutiveJobs(curOutput, nextOutput);
}
JobOutputPacked calldata lastOutput = outputs[limit];
ensureValidFact(lastOutput);
// We save the latest output in the contract state for future calls
(, uint256 mmrNewSize) = lastOutput.mmrSizesPacked.split128();
aggregatorState.poseidonMmrRoot = lastOutput.mmrNewRootPoseidon;
aggregatorState.keccakMmrRoot = lastOutput.mmrNewRootKeccak;
aggregatorState.mmrSize = mmrNewSize;
aggregatorState.continuableParentHash = lastOutput
.blockNMinusRPlusOneParentHash;
(uint256 fromBlock, ) = firstOutput.blockNumbersPacked.split128();
(, uint256 toBlock) = lastOutput.blockNumbersPacked.split128();
emit Aggregate(
fromBlock,
toBlock,
lastOutput.mmrNewRootPoseidon,
lastOutput.mmrNewRootKeccak,
mmrNewSize,
lastOutput.blockNMinusRPlusOneParentHash
);
}
/// @notice Ensures the fact is registered on SHARP Facts Registry
/// @param output SHARP job output (packed for Solidity)
function ensureValidFact(JobOutputPacked memory output) internal view {
(uint256 fromBlock, uint256 toBlock) = output
.blockNumbersPacked
.split128();
(uint256 mmrPreviousSize, uint256 mmrNewSize) = output
.mmrSizesPacked
.split128();
(
uint256 blockNPlusOneParentHashLow,
uint256 blockNPlusOneParentHashHigh
) = uint256(output.blockNPlusOneParentHash).split128();
(
uint256 blockNMinusRPlusOneParentHashLow,
uint256 blockNMinusRPlusOneParentHashHigh
) = uint256(output.blockNMinusRPlusOneParentHash).split128();
(
uint256 mmrPreviousRootKeccakLow,
uint256 mmrPreviousRootKeccakHigh
) = uint256(output.mmrPreviousRootKeccak).split128();
(uint256 mmrNewRootKeccakLow, uint256 mmrNewRootKeccakHigh) = uint256(
output.mmrNewRootKeccak
).split128();
// We assemble the outputs in a uint256 array
uint256[] memory outputs = new uint256[](14);
outputs[0] = fromBlock;
outputs[1] = toBlock;
outputs[2] = blockNPlusOneParentHashLow;
outputs[3] = blockNPlusOneParentHashHigh;
outputs[4] = blockNMinusRPlusOneParentHashLow;
outputs[5] = blockNMinusRPlusOneParentHashHigh;
outputs[6] = uint256(output.mmrPreviousRootPoseidon);
outputs[7] = mmrPreviousRootKeccakLow;
outputs[8] = mmrPreviousRootKeccakHigh;
outputs[9] = mmrPreviousSize;
outputs[10] = uint256(output.mmrNewRootPoseidon);
outputs[11] = mmrNewRootKeccakLow;
outputs[12] = mmrNewRootKeccakHigh;
outputs[13] = mmrNewSize;
// We hash the outputs
bytes32 outputHash = keccak256(abi.encodePacked(outputs));
// We compute the deterministic fact bytes32 value
bytes32 fact = keccak256(abi.encode(PROGRAM_HASH, outputHash));
// We ensure this fact has been registered on SHARP Facts Registry
if (!FACTS_REGISTRY.isValid(fact)) {
revert InvalidFact();
}
}
/// @notice Ensures the job output is cryptographically sound to continue from
/// @param rightBoundStartParentHash The parent hash of the block to start from
/// @param output The job output to check
function ensureContinuable(
bytes32 rightBoundStartParentHash,
JobOutputPacked memory output
) internal view {
(uint256 mmrPreviousSize, ) = output.mmrSizesPacked.split128();
// Check that the job's previous Poseidon MMR root is the same as the one stored in the contract state
if (output.mmrPreviousRootPoseidon != aggregatorState.poseidonMmrRoot)
revert AggregationError("Poseidon root mismatch");
// Check that the job's previous Keccak MMR root is the same as the one stored in the contract state
if (output.mmrPreviousRootKeccak != aggregatorState.keccakMmrRoot)
revert AggregationError("Keccak root mismatch");
// Check that the job's previous MMR size is the same as the one stored in the contract state
if (mmrPreviousSize != aggregatorState.mmrSize)
revert AggregationError("MMR size mismatch");
if (rightBoundStartParentHash == bytes32(0)) {
// If the right bound start parent hash __is not__ specified,
// we check that the job's `blockN + 1 parent hash` is matching with the previously stored parent hash
if (
output.blockNPlusOneParentHash !=
aggregatorState.continuableParentHash
) {
revert AggregationError("Global state: Parent hash mismatch");
}
} else {
// If the right bound start parent hash __is__ specified,
// we check that the job's `blockN + 1 parent hash` is matching with a previously stored parent hash
if (output.blockNPlusOneParentHash != rightBoundStartParentHash) {
revert AggregationError("Parent hash mismatch");
}
}
}
/// @notice Ensures the job outputs are correctly linked
/// @param output The job output to check
/// @param nextOutput The next job output to check
function ensureConsecutiveJobs(
JobOutputPacked memory output,
JobOutputPacked memory nextOutput
) internal pure {
(, uint256 toBlock) = output.blockNumbersPacked.split128();
// We cannot aggregate further past the genesis block
if (toBlock == 0) {
revert GenesisBlockReached();
}
(uint256 nextFromBlock, ) = nextOutput.blockNumbersPacked.split128();
// We check that the next job's `from block` is the same as the previous job's `to block + 1`
if (toBlock - 1 != nextFromBlock) revert AggregationBlockMismatch();
(, uint256 outputMmrNewSize) = output.mmrSizesPacked.split128();
(uint256 nextOutputMmrPreviousSize, ) = nextOutput
.mmrSizesPacked
.split128();
// We check that the previous job's new Poseidon MMR root matches the next job's previous Poseidon MMR root
if (output.mmrNewRootPoseidon != nextOutput.mmrPreviousRootPoseidon)
revert AggregationError("Poseidon root mismatch");
// We check that the previous job's new Keccak MMR root matches the next job's previous Keccak MMR root
if (output.mmrNewRootKeccak != nextOutput.mmrPreviousRootKeccak)
revert AggregationError("Keccak root mismatch");
// We check that the previous job's new MMR size matches the next job's previous MMR size
if (outputMmrNewSize != nextOutputMmrPreviousSize)
revert AggregationError("MMR size mismatch");
// We check that the previous job's lowest block hash matches the next job's highest block hash
if (
output.blockNMinusRPlusOneParentHash !=
nextOutput.blockNPlusOneParentHash
) revert AggregationError("Parent hash mismatch");
}
/// @dev Helper function to verify a fact based on a job output
function verifyFact(uint256[] memory outputs) external view returns (bool) {
bytes32 outputHash = keccak256(abi.encodePacked(outputs));
bytes32 fact = keccak256(abi.encode(PROGRAM_HASH, outputHash));
return FACTS_REGISTRY.isValid(fact);
}
/// @notice Returns the current root hash of the Keccak Merkle Mountain Range (MMR) tree
function getMMRKeccakRoot() external view returns (bytes32) {
return aggregatorState.keccakMmrRoot;
}
/// @notice Returns the current root hash of the Poseidon Merkle Mountain Range (MMR) tree
function getMMRPoseidonRoot() external view returns (bytes32) {
return aggregatorState.poseidonMmrRoot;
}
/// @notice Returns the current size of the Merkle Mountain Range (MMR) trees
function getMMRSize() external view returns (uint256) {
return aggregatorState.mmrSize;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
library Uint256Splitter {
uint256 constant _MASK = type(uint128).max;
/// @notice Splits a uint256 into two uint128s (low, high) represented as uint256s.
/// @param a The uint256 to split.
function split128(
uint256 a
) internal pure returns (uint256 lower, uint256 upper) {
return (a & _MASK, a >> 128);
}
/// @notice Merges two uint128s (low, high) into one uint256.
/// @param lower The lower uint256. The caller is required to pass a value that is less than 2^128 - 1.
/// @param upper The upper uint256.
function merge128(
uint256 lower,
uint256 upper
) internal pure returns (uint256 a) {
require(lower <= _MASK, "Uint256Splitter: lower exceeds uint128");
// return (upper << 128) | lower;
assembly {
a := or(shl(128, upper), lower)
}
}
}
{
"compilationTarget": {
"src/AggregatorsFactory.sol": "AggregatorsFactory"
},
"evmVersion": "paris",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 20000
},
"remappings": [
":ds-test/=lib/forge-std/lib/ds-test/src/",
":erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/",
":forge-std/=lib/forge-std/src/",
":openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/",
":openzeppelin-contracts/=lib/openzeppelin-contracts/",
":openzeppelin/=lib/openzeppelin-contracts-upgradeable/contracts/"
]
}
[{"inputs":[{"internalType":"contract SharpFactsAggregator","name":"initialTemplate","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AccessControlBadConfirmation","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"bytes32","name":"neededRole","type":"bytes32"}],"name":"AccessControlUnauthorizedAccount","type":"error"},{"inputs":[],"name":"ERC1167FailedCreateClone","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"contract SharpFactsAggregator","name":"aggregator","type":"address"},{"indexed":false,"internalType":"uint256","name":"newAggregatorId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"detachedFromAggregatorId","type":"uint256"}],"name":"AggregatorCreation","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"contract SharpFactsAggregator","name":"oldTemplate","type":"address"},{"indexed":false,"internalType":"contract SharpFactsAggregator","name":"newTemplate","type":"address"}],"name":"Upgrade","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"contract SharpFactsAggregator","name":"newTemplate","type":"address"}],"name":"UpgradeProposal","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DELAY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"KECCAK_MMR_INITIAL_ROOT","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"OPERATOR_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"POSEIDON_MMR_INITIAL_ROOT","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"aggregatorsById","outputs":[{"internalType":"contract SharpFactsAggregator","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"aggregatorsCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"aggregatorId","type":"uint256"}],"name":"createAggregator","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract SharpFactsAggregator","name":"newTemplate","type":"address"}],"name":"proposeUpgrade","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"callerConfirmation","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"template","outputs":[{"internalType":"contract SharpFactsAggregator","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"updateId","type":"uint256"}],"name":"upgrade","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"upgrades","outputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"contract SharpFactsAggregator","name":"newTemplate","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"upgradesCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]