Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.
Closed
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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# Required for deployment
PRIVATE_KEY=0x...
ARBITRUM_SEPOLIA_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc

# Optional: Arbiscan verification
ETHERSCAN_API_KEY=

# Mainnet deployment roles
PROTOCOL_ADDRESS=0x...
COMPLIANCE_ADDRESS=0x...
MULTISIG_ADDRESS=0x...

# Reclaim Protocol credentials (get from https://dev.reclaimprotocol.org/)
RECLAIM_APP_ID=
RECLAIM_APP_SECRET=
Expand Down
131 changes: 131 additions & 0 deletions contracts/access/ReineiraAccessControl.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

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

/// @title ReineiraAccessControl
/// @notice Shared access control module for ReineiraOS plugin contracts.
/// @dev Provides role-based permissions with four roles:
/// - DEFAULT_ADMIN_ROLE: full system control, intended for multisig
/// - PROTOCOL_ROLE: authorized protocol contracts (ConfidentialEscrow, etc.)
/// - COMPLIANCE_ROLE: regulatory operations (pause, emergency stop)
/// - UPGRADE_ROLE: UUPS proxy upgrades
///
/// All roles except DEFAULT_ADMIN_ROLE are managed by DEFAULT_ADMIN_ROLE.
/// The deployer receives DEFAULT_ADMIN_ROLE at construction and should
/// transfer it to a multisig after deployment.
abstract contract ReineiraAccessControl is AccessControl, Pausable {
/// @notice Role for protocol contracts authorized to configure conditions/policies.
bytes32 public constant PROTOCOL_ROLE = keccak256("PROTOCOL_ROLE");

/// @notice Role for compliance/regulatory operations (pause, emergency).
bytes32 public constant COMPLIANCE_ROLE = keccak256("COMPLIANCE_ROLE");

/// @notice Role for performing UUPS proxy upgrades.
bytes32 public constant UPGRADE_ROLE = keccak256("UPGRADE_ROLE");

/// @dev Revert when caller lacks PROTOCOL_ROLE.
error CallerNotProtocol();

/// @dev Revert when caller lacks COMPLIANCE_ROLE.
error CallerNotCompliance();

/// @dev Revert when caller lacks DEFAULT_ADMIN_ROLE.
error CallerNotAdmin();

/// @dev Revert when caller lacks UPGRADE_ROLE.
error CallerNotUpgrader();

/// @notice Emitted when a protocol address is granted or revoked.
event ProtocolAddressSet(address indexed protocol, bool enabled);

/// @notice Emitted when the contract is paused by compliance.
event CompliancePaused(address indexed account);

/// @notice Emitted when the contract is unpaused by compliance.
event ComplianceUnpaused(address indexed account);

/// @notice Restrict to PROTOCOL_ROLE holders.
modifier onlyProtocol() {
if (!hasRole(PROTOCOL_ROLE, msg.sender)) revert CallerNotProtocol();
_;
}

/// @notice Restrict to COMPLIANCE_ROLE holders.
modifier onlyCompliance() {
if (!hasRole(COMPLIANCE_ROLE, msg.sender)) revert CallerNotCompliance();
_;
}

/// @notice Restrict to DEFAULT_ADMIN_ROLE holders.
modifier onlyAdmin() {
if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert CallerNotAdmin();
_;
}

/// @notice Restrict to UPGRADE_ROLE holders.
modifier onlyUpgrader() {
if (!hasRole(UPGRADE_ROLE, msg.sender)) revert CallerNotUpgrader();
_;
}

/// @param admin Initial admin address (should be transferred to multisig post-deploy).
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_setRoleAdmin(PROTOCOL_ROLE, DEFAULT_ADMIN_ROLE);
_setRoleAdmin(COMPLIANCE_ROLE, DEFAULT_ADMIN_ROLE);
_setRoleAdmin(UPGRADE_ROLE, DEFAULT_ADMIN_ROLE);
}

/// @notice Pause the contract. Only compliance owner.
/// @dev When paused, `isConditionMet` and `onConditionSet` revert.
function pause() external onlyCompliance {
_pause();
emit CompliancePaused(msg.sender);
}

/// @notice Unpause the contract. Only compliance owner.
function unpause() external onlyCompliance {
_unpause();
emit ComplianceUnpaused(msg.sender);
}

/// @notice Grant PROTOCOL_ROLE to a protocol contract.
/// @param protocol Address to authorize.
function grantProtocolRole(address protocol) external onlyAdmin {
_grantRole(PROTOCOL_ROLE, protocol);
emit ProtocolAddressSet(protocol, true);
}

/// @notice Revoke PROTOCOL_ROLE from a protocol contract.
/// @param protocol Address to deauthorize.
function revokeProtocolRole(address protocol) external onlyAdmin {
_revokeRole(PROTOCOL_ROLE, protocol);
emit ProtocolAddressSet(protocol, false);
}

/// @notice Grant COMPLIANCE_ROLE to an address.
/// @param compliance Address to authorize for compliance operations.
function grantComplianceRole(address compliance) external onlyAdmin {
_grantRole(COMPLIANCE_ROLE, compliance);
}

/// @notice Revoke COMPLIANCE_ROLE from an address.
/// @param compliance Address to deauthorize.
function revokeComplianceRole(address compliance) external onlyAdmin {
_revokeRole(COMPLIANCE_ROLE, compliance);
}

/// @notice Grant UPGRADE_ROLE to an address.
/// @param upgrader Address to authorize for proxy upgrades.
function grantUpgradeRole(address upgrader) external onlyAdmin {
_grantRole(UPGRADE_ROLE, upgrader);
}

/// @notice Revoke UPGRADE_ROLE from an address.
/// @param upgrader Address to deauthorize.
function revokeUpgradeRole(address upgrader) external onlyAdmin {
_revokeRole(UPGRADE_ROLE, upgrader);
}
}
2 changes: 1 addition & 1 deletion contracts/resolvers/ChainlinkConditionBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ abstract contract ChainlinkConditionBase is IOracleConditionResolver, ERC165 {
}

/// @inheritdoc IConditionResolver
function isConditionMet(uint256 escrowId) external view virtual returns (bool) {
function isConditionMet(uint256 escrowId) public view virtual returns (bool) {
ChainlinkStorage storage $ = _getChainlinkStorage();
if (!$.configs[escrowId].configured) revert ConditionNotConfigured();

Expand Down
35 changes: 21 additions & 14 deletions contracts/resolvers/ChainlinkFunctionsResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@
pragma solidity ^0.8.24;

import {IConditionResolver} from "../interfaces/IConditionResolver.sol";
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import {ReineiraAccessControl} from "../access/ReineiraAccessControl.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol";
import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol";

/// @title ChainlinkFunctionsResolver
/// @notice Condition resolver using Chainlink Functions for custom off-chain computation
/// @dev Allows escrows to be released based on results from off-chain API calls and computation
/// executed in a decentralized oracle network (DON)
/// Inherits ReineiraAccessControl for protocol-gated configuration and compliance pausability.
///
/// ## How It Works
/// 1. Configure escrow with source code, subscription ID, and expected result
/// 2. Anyone can trigger the request to execute the off-chain computation
/// 3. Chainlink DON executes the code and returns the result
/// 4. If result matches expected value, condition is fulfilled
/// 1. Deploy resolver with admin address and grant PROTOCOL_ROLE to ConfidentialEscrow
/// 2. Configure escrow with source code, subscription ID, and expected result
/// 3. Anyone can trigger the request to execute the off-chain computation
/// 4. Chainlink DON executes the code and returns the result
/// 5. If result matches expected value, condition is fulfilled
///
/// ## Use Cases
/// - Verify API responses (e.g., GitHub stars > 1000, Twitter followers > 10k)
Expand All @@ -33,7 +35,7 @@ import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/l
/// See: https://docs.chain.link/chainlink-functions/supported-networks
/// Arbitrum Sepolia:
/// - Router: 0x234a5fb5Bd614a7AA2FfAB244D603abFA0Ac5C5C
contract ChainlinkFunctionsResolver is IConditionResolver, FunctionsClient, ERC165 {
contract ChainlinkFunctionsResolver is IConditionResolver, FunctionsClient, ReineiraAccessControl {
using FunctionsRequest for FunctionsRequest.Request;

/// @notice Configuration for each escrow's Chainlink Functions condition
Expand Down Expand Up @@ -75,7 +77,9 @@ contract ChainlinkFunctionsResolver is IConditionResolver, FunctionsClient, ERC1
error EmptySource();
error InvalidSubscriptionId();

constructor(address router) FunctionsClient(router) {}
/// @param router Chainlink Functions router address.
/// @param admin Initial admin address.
constructor(address router, address admin) FunctionsClient(router) ReineiraAccessControl(admin) {}

function _getFunctionsStorage() private pure returns (FunctionsStorage storage $) {
assembly {
Expand All @@ -95,7 +99,7 @@ contract ChainlinkFunctionsResolver is IConditionResolver, FunctionsClient, ERC1
/// )
/// @param escrowId The escrow identifier
/// @param data ABI-encoded configuration
function onConditionSet(uint256 escrowId, bytes calldata data) external {
function onConditionSet(uint256 escrowId, bytes calldata data) external onlyProtocol whenNotPaused {
FunctionsStorage storage $ = _getFunctionsStorage();
if ($.configs[escrowId].configured) revert ConditionAlreadySet();

Expand Down Expand Up @@ -131,10 +135,11 @@ contract ChainlinkFunctionsResolver is IConditionResolver, FunctionsClient, ERC1
}

/// @notice Execute the Chainlink Functions request for an escrow
/// @dev Anyone can call this to trigger the off-chain computation
/// @dev Anyone can call this to trigger the off-chain computation.
/// Reverts when paused.
/// @param escrowId The escrow identifier
/// @return requestId The Chainlink Functions request ID
function executeRequest(uint256 escrowId) external returns (bytes32 requestId) {
function executeRequest(uint256 escrowId) external whenNotPaused returns (bytes32 requestId) {
FunctionsStorage storage $ = _getFunctionsStorage();
Config storage config = $.configs[escrowId];

Expand Down Expand Up @@ -188,7 +193,7 @@ contract ChainlinkFunctionsResolver is IConditionResolver, FunctionsClient, ERC1
/// @notice Check if the condition is met
/// @param escrowId The escrow identifier
/// @return True if the Chainlink Functions result matches the expected value
function isConditionMet(uint256 escrowId) external view returns (bool) {
function isConditionMet(uint256 escrowId) external view whenNotPaused returns (bool) {
FunctionsStorage storage $ = _getFunctionsStorage();
return $.configs[escrowId].fulfilled;
}
Expand Down Expand Up @@ -217,8 +222,10 @@ contract ChainlinkFunctionsResolver is IConditionResolver, FunctionsClient, ERC1
return $.configs[escrowId].lastRequestId;
}

/// @inheritdoc ERC165
function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
/// @notice ERC-165 interface detection.
/// @param interfaceId Interface identifier.
/// @return True if the contract implements the interface.
function supportsInterface(bytes4 interfaceId) public view override(AccessControl) returns (bool) {
return interfaceId == type(IConditionResolver).interfaceId || super.supportsInterface(interfaceId);
}
}
52 changes: 41 additions & 11 deletions contracts/resolvers/ChainlinkPriceFeedResolver.sol
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IConditionResolver} from "../interfaces/IConditionResolver.sol";
import {IOracleConditionResolver} from "../interfaces/IOracleConditionResolver.sol";
import {ChainlinkConditionBase} from "./ChainlinkConditionBase.sol";
import {ReineiraAccessControl} from "../access/ReineiraAccessControl.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

/// @title ChainlinkPriceFeedResolver
/// @notice Concrete resolver using Chainlink Price Feeds for condition evaluation
/// @dev Allows escrows to be released based on price feed thresholds
/// Example: Release funds when ETH/USD > $2000
/// @dev Allows escrows to be released based on price feed thresholds.
/// Inherits ReineiraAccessControl for protocol-gated configuration and compliance pausability.
///
/// ## Usage Example
/// 1. Deploy this resolver
/// 2. Configure escrow with: abi.encode(feedAddress, threshold, op, maxStaleness)
/// - feedAddress: Chainlink price feed address (e.g., ETH/USD on Arbitrum Sepolia)
/// 1. Deploy this resolver with an admin address
/// 2. Grant PROTOCOL_ROLE to the ConfidentialEscrow contract
/// 3. Configure escrow with: abi.encode(feedAddress, threshold, op, maxStaleness)
/// - feedAddress: Chainlink price feed address (e.g., ETH/USD on Arbitrum)
/// - threshold: Price threshold in feed decimals (e.g., 2000 * 10^8 for $2000)
/// - op: Comparison operator (0=GT, 1=GTE, 2=LT, 3=LTE, 4=EQ, 5=NEQ)
/// - maxStaleness: Maximum age of data in seconds (e.g., 3600 for 1 hour)
Expand All @@ -22,7 +27,10 @@ import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interf
/// Arbitrum Sepolia Example:
/// - ETH/USD: 0xd30e2101a97dcbAeBCBC04F14C3f624E67A35165
/// - BTC/USD: 0x56a43EB56Da12C0dc1D972ACb089c06a5dEF8e69
contract ChainlinkPriceFeedResolver is ChainlinkConditionBase {
/// Arbitrum Mainnet:
/// - ETH/USD: 0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612
/// - BTC/USD: 0x6ce185860a4963106506C203335A2910413708e9
contract ChainlinkPriceFeedResolver is ChainlinkConditionBase, ReineiraAccessControl {
/// @custom:storage-location erc7201:reineira.storage.ChainlinkPriceFeedResolver
struct PriceFeedStorage {
mapping(uint256 => address) feedAddresses;
Expand All @@ -36,6 +44,9 @@ contract ChainlinkPriceFeedResolver is ChainlinkConditionBase {

error InvalidFeedAddress();

/// @param admin Initial admin address.
constructor(address admin) ReineiraAccessControl(admin) {}

function _getPriceFeedStorage() private pure returns (PriceFeedStorage storage $) {
assembly {
$.slot := PRICE_FEED_STORAGE_LOCATION
Expand All @@ -44,11 +55,8 @@ contract ChainlinkPriceFeedResolver is ChainlinkConditionBase {

/// @notice Configure the price feed condition for an escrow
/// @dev Data format: abi.encode(address feedAddress, int256 threshold, uint8 op, uint256 maxStaleness)
/// feedAddress: Chainlink price feed contract address
/// threshold: Price threshold in feed decimals
/// op: Comparison operator (0-5)
/// maxStaleness: Maximum data age in seconds
function onConditionSet(uint256 escrowId, bytes calldata data) external {
/// Restricted to PROTOCOL_ROLE and blocked when paused.
function onConditionSet(uint256 escrowId, bytes calldata data) external onlyProtocol whenNotPaused {
(address feedAddress, int256 threshold, uint8 op, uint256 maxStaleness) =
abi.decode(data, (address, int256, uint8, uint256));

Expand All @@ -66,6 +74,13 @@ contract ChainlinkPriceFeedResolver is ChainlinkConditionBase {
/// @notice Get the price feed address for an escrow
/// @param escrowId The escrow identifier
/// @return The Chainlink price feed address

/// @inheritdoc ChainlinkConditionBase
/// @dev Reverts when paused.
function isConditionMet(uint256 escrowId) public view override whenNotPaused returns (bool) {
return super.isConditionMet(escrowId);
}

function getFeedAddress(uint256 escrowId) external view returns (address) {
PriceFeedStorage storage $ = _getPriceFeedStorage();
return $.feedAddresses[escrowId];
Expand All @@ -76,4 +91,19 @@ contract ChainlinkPriceFeedResolver is ChainlinkConditionBase {
PriceFeedStorage storage $ = _getPriceFeedStorage();
return AggregatorV3Interface($.feedAddresses[escrowId]);
}

/// @notice ERC-165 interface detection.
/// @param interfaceId Interface identifier.
/// @return True if the contract implements the interface.
function supportsInterface(bytes4 interfaceId)
public
view
override(ChainlinkConditionBase, AccessControl)
returns (bool)
{
return interfaceId == type(IConditionResolver).interfaceId
|| interfaceId == type(IOracleConditionResolver).interfaceId
|| ChainlinkConditionBase.supportsInterface(interfaceId)
|| AccessControl.supportsInterface(interfaceId);
}
}
Loading
Loading