Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion lib/openzeppelin-contracts
2 changes: 1 addition & 1 deletion lib/openzeppelin-contracts-upgradeable
13 changes: 13 additions & 0 deletions scripts/DeployAllTEEVerifiers.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ contract DeployAllTEEVerifiers is Script {
// Owner address for the auto-deployed ProxyAdmin contracts
address proxyAdminOwner = vm.envOr("PROXY_ADMIN_OWNER", msg.sender);

// Optional guardian addresses (comma-separated)
// Uses Forge's built-in envOr with comma delimiter to parse address arrays
address[] memory emptyGuardians = new address[](0);
address[] memory guardians = vm.envOr("GUARDIANS", ",", emptyGuardians);

// ============ Step 1: Deploy TEEVerifier ============
// Deploy implementation
EspressoTEEVerifier teeVerifierImpl = new EspressoTEEVerifier();
Expand Down Expand Up @@ -131,6 +136,14 @@ contract DeployAllTEEVerifiers is Script {
"TEEVerifier updated with SGX and Nitro verifier addresses"
);

// Add guardians if provided
for (uint256 i = 0; i < guardians.length; i++) {
if (guardians[i] != address(0)) {
teeVerifier.addGuardian(guardians[i]);
console2.log("Added guardian:", guardians[i]);
}
}

vm.stopBroadcast();

string memory chainId = vm.toString(block.chainid);
Expand Down
14 changes: 14 additions & 0 deletions scripts/DeployTEEVerifier.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ contract DeployTEEVerifier is Script {
// If not set, defaults to msg.sender
address proxyAdminOwner = vm.envOr("PROXY_ADMIN_OWNER", msg.sender);

// Optional guardian addresses (comma-separated)
// Uses Forge's built-in envOr with comma delimiter to parse address arrays
address[] memory emptyGuardians = new address[](0);
address[] memory guardians = vm.envOr("GUARDIANS", ",", emptyGuardians);

// 1. Deploy TEEVerifier implementation
EspressoTEEVerifier teeVerifierImpl = new EspressoTEEVerifier();
console2.log(
Expand All @@ -51,6 +56,15 @@ contract DeployTEEVerifier is Script {
);
console2.log("TEEVerifier proxy deployed at:", address(proxy));

// Add guardians if provided
EspressoTEEVerifier teeVerifier = EspressoTEEVerifier(address(proxy));
for (uint256 i = 0; i < guardians.length; i++) {
if (guardians[i] != address(0)) {
teeVerifier.addGuardian(guardians[i]);
console2.log("Added guardian:", guardians[i]);
}
}

vm.stopBroadcast();

// Save deployment artifacts (outside of broadcast to avoid gas costs)
Expand Down
2 changes: 1 addition & 1 deletion src/EspressoNitroTEEVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ contract EspressoNitroTEEVerifier is IEspressoNitroTEEVerifier, TEEHelper {
}

function _setNitroEnclaveVerifier(address nitroEnclaveVerifier_) internal {
if (nitroEnclaveVerifier_ == address(0)) {
if (nitroEnclaveVerifier_ == address(0) || nitroEnclaveVerifier_.code.length == 0) {
revert InvalidNitroEnclaveVerifierAddress();
}
_nitroLayout().nitroEnclaveVerifier = INitroEnclaveVerifier(nitroEnclaveVerifier_);
Expand Down
21 changes: 10 additions & 11 deletions src/EspressoTEEVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.0;

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import {OwnableWithGuardiansUpgradeable} from "./OwnableWithGuardiansUpgradeable.sol";
import {IEspressoSGXTEEVerifier} from "./interface/IEspressoSGXTEEVerifier.sol";
import {IEspressoNitroTEEVerifier} from "./interface/IEspressoNitroTEEVerifier.sol";
import {IEspressoTEEVerifier} from "./interface/IEspressoTEEVerifier.sol";
Expand All @@ -13,7 +13,7 @@ import {ServiceType} from "./types/Types.sol";
* @author Espresso Systems (https://espresso.systems)
* @notice This contract is used to resgister a signer which has been attested by the TEE
*/
contract EspressoTEEVerifier is Ownable2StepUpgradeable, IEspressoTEEVerifier {
contract EspressoTEEVerifier is OwnableWithGuardiansUpgradeable, IEspressoTEEVerifier {
/// @custom:storage-location erc7201:espresso.storage.EspressoTEEVerifier
struct EspressoTEEVerifierStorage {
IEspressoSGXTEEVerifier espressoSGXTEEVerifier;
Expand Down Expand Up @@ -42,8 +42,7 @@ contract EspressoTEEVerifier is Ownable2StepUpgradeable, IEspressoTEEVerifier {
EspressoTEEVerifierStorage storage $ = _layout();
$.espressoSGXTEEVerifier = _espressoSGXTEEVerifier;
$.espressoNitroTEEVerifier = _espressoNitroTEEVerifier;
__Ownable2Step_init();
_transferOwnership(_owner);
__OwnableWithGuardians_init(_owner);
}

/**
Expand Down Expand Up @@ -118,9 +117,9 @@ contract EspressoTEEVerifier is Ownable2StepUpgradeable, IEspressoTEEVerifier {
}
}

/*
@notice Set the EspressoSGXTEEVerifier
@param _espressoSGXTEEVerifier The address of the EspressoSGXTEEVerifier
/**
* @notice Set the EspressoSGXTEEVerifier
* @param _espressoSGXTEEVerifier The address of the EspressoSGXTEEVerifier
*/
function setEspressoSGXTEEVerifier(IEspressoSGXTEEVerifier _espressoSGXTEEVerifier)
public
Expand Down Expand Up @@ -149,15 +148,15 @@ contract EspressoTEEVerifier is Ownable2StepUpgradeable, IEspressoTEEVerifier {
}

/**
* @notice Allows the owner to set enclave hashes
* @notice Allows the owner or guardian to set enclave hashes
* @param enclaveHash The enclave hash to set
* @param valid Whether the enclave hash is valid or not
* @param teeType The type of TEE
* @param service The service type (BatchPoster or CaffNode)
*/
function setEnclaveHash(bytes32 enclaveHash, bool valid, TeeType teeType, ServiceType service)
external
onlyOwner
onlyGuardianOrOwner
{
EspressoTEEVerifierStorage storage $ = _layout();
if (teeType == TeeType.SGX) {
Expand All @@ -168,7 +167,7 @@ contract EspressoTEEVerifier is Ownable2StepUpgradeable, IEspressoTEEVerifier {
}

/**
* @notice Allows the owner to delete enclave hashes
* @notice Allows the owner or guardian to delete enclave hashes
* @param enclaveHashes The list of enclave hashes to delete
* @param teeType The type of TEE
* @param service The service type (BatchPoster or CaffNode)
Expand All @@ -177,7 +176,7 @@ contract EspressoTEEVerifier is Ownable2StepUpgradeable, IEspressoTEEVerifier {
bytes32[] memory enclaveHashes,
TeeType teeType,
ServiceType service
) external onlyOwner {
) external onlyGuardianOrOwner {
EspressoTEEVerifierStorage storage $ = _layout();
if (teeType == TeeType.SGX) {
$.espressoSGXTEEVerifier.deleteEnclaveHashes(enclaveHashes, service);
Expand Down
162 changes: 162 additions & 0 deletions src/OwnableWithGuardiansUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {
Ownable2StepUpgradeable
} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import {
AccessControlEnumerableUpgradeable
} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

/**
* @title OwnableWithGuardiansUpgradeable
* @author Espresso Systems (https://espresso.systems)
* @notice Abstract contract combining Ownable2Step with a guardian role system for emergency operations.
* @dev This contract provides:
* - 2-step ownership transfer (transferOwnership + acceptOwnership)
* - Multiple guardian addresses for time-sensitive operations
* - TransparentUpgradeableProxy pattern compatibility
* - EIP-7201 namespaced storage (via OZ upgradeable contracts)
*
* Inheriting contracts must:
* 1. Call __OwnableWithGuardians_init(initialOwner) in their initializer
*/
abstract contract OwnableWithGuardiansUpgradeable is
Initializable,
Ownable2StepUpgradeable,
AccessControlEnumerableUpgradeable
{
/// @notice Role identifier for guardians who can execute time-sensitive operations
bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE");

/// @notice Emitted when a guardian is added
event GuardianAdded(address indexed guardian);

/// @notice Emitted when a guardian is removed
event GuardianRemoved(address indexed guardian);

/// @notice Error thrown when caller is not a guardian
error NotGuardian(address caller);

/// @notice Error thrown when caller is neither guardian nor owner
error NotGuardianOrOwner(address caller);

/// @notice Error thrown when trying to add zero address as guardian
error InvalidGuardianAddress();

/**
* @dev Initializes the contract with an initial owner.
* @param initialOwner The address that will be set as the initial owner and admin
*/
function __OwnableWithGuardians_init(address initialOwner) internal onlyInitializing {
__Ownable_init(initialOwner);
__AccessControl_init();
__AccessControlEnumerable_init();
__OwnableWithGuardians_init_unchained(initialOwner);
}

function __OwnableWithGuardians_init_unchained(address initialOwner) internal onlyInitializing {
// Grant the initial owner the default admin role for managing guardians
_grantRole(DEFAULT_ADMIN_ROLE, initialOwner);

// Explicitly set DEFAULT_ADMIN_ROLE as the admin of GUARDIAN_ROLE for clarity
_setRoleAdmin(GUARDIAN_ROLE, DEFAULT_ADMIN_ROLE);
}

/**
* @notice Modifier that restricts function access to guardian addresses only
*/
modifier onlyGuardian() {
if (!hasRole(GUARDIAN_ROLE, msg.sender)) {
revert NotGuardian(msg.sender);
}
_;
}

/**
* @notice Modifier that restricts function access to either guardian or owner
* @dev Useful for time-sensitive operations that can be performed by either role
*/
modifier onlyGuardianOrOwner() {
if (!hasRole(GUARDIAN_ROLE, msg.sender) && msg.sender != owner()) {
revert NotGuardianOrOwner(msg.sender);
}
_;
}

/**
* @notice Adds a new guardian address
* @dev Only callable by the contract owner. Reverts if guardian is zero address.
* No-op if already a guardian.
* @param guardian The address to grant guardian privileges
*/
function addGuardian(address guardian) external onlyOwner {
Comment thread
Sneh1999 marked this conversation as resolved.
if (guardian == address(0)) {
revert InvalidGuardianAddress();
}
if (hasRole(GUARDIAN_ROLE, guardian)) {
return; // Already a guardian, no-op
}
grantRole(GUARDIAN_ROLE, guardian);
emit GuardianAdded(guardian);
}

/**
* @notice Removes a guardian address
* @dev Only callable by the contract owner. No-op if address is not a guardian.
* @param guardian The address to revoke guardian privileges from
*/
function removeGuardian(address guardian) external onlyOwner {
if (!hasRole(GUARDIAN_ROLE, guardian)) {
return; // Not a guardian, no-op
}
revokeRole(GUARDIAN_ROLE, guardian);
emit GuardianRemoved(guardian);
}

/**
* @notice Checks if an address is a guardian
* @param account The address to check
* @return bool True if the address is a guardian, false otherwise
*/
function isGuardian(address account) public view returns (bool) {
return hasRole(GUARDIAN_ROLE, account);
}

/**
* @notice Returns all guardian addresses
* @return address[] Array of all guardian addresses
*/
function getGuardians() public view returns (address[] memory) {
uint256 count = getRoleMemberCount(GUARDIAN_ROLE);
address[] memory guardians = new address[](count);
for (uint256 i = 0; i < count; i++) {
guardians[i] = getRoleMember(GUARDIAN_ROLE, i);
}
return guardians;
}

/**
* @notice Returns the total number of guardians
* @return uint256 The count of guardian addresses
*/
function guardianCount() public view returns (uint256) {
return getRoleMemberCount(GUARDIAN_ROLE);
}

/**
* @dev Override required by Solidity for multiple inheritance
* @notice Ensures owner retains admin role even after ownership transfer
*/
function _transferOwnership(address newOwner) internal virtual override {
address previousOwner = owner();
super._transferOwnership(newOwner);

// Transfer admin role to new owner
if (previousOwner != address(0)) {
_revokeRole(DEFAULT_ADMIN_ROLE, previousOwner);
}
_grantRole(DEFAULT_ADMIN_ROLE, newOwner);
}
}
1 change: 1 addition & 0 deletions src/TEEHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {ServiceType} from "./types/Types.sol";
import "./interface/ITEEHelper.sol";

abstract contract TEEHelper is ITEEHelper, Initializable {
/// @custom:storage-location erc7201:espresso.storage.TEEHelper
struct TEEHelperStorage {
mapping(ServiceType => mapping(bytes32 enclaveHash => bool valid)) registeredEnclaveHashes;
mapping(ServiceType => mapping(address signer => bool valid)) registeredServices;
Expand Down
Loading