diff --git a/.env.example b/.env.example
index 3fd7145..f3b2a65 100644
--- a/.env.example
+++ b/.env.example
@@ -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=
diff --git a/contracts/access/ReineiraAccessControl.sol b/contracts/access/ReineiraAccessControl.sol
new file mode 100644
index 0000000..0329239
--- /dev/null
+++ b/contracts/access/ReineiraAccessControl.sol
@@ -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);
+ }
+}
diff --git a/contracts/resolvers/ChainlinkConditionBase.sol b/contracts/resolvers/ChainlinkConditionBase.sol
index 2a285d8..9e0ff1b 100644
--- a/contracts/resolvers/ChainlinkConditionBase.sol
+++ b/contracts/resolvers/ChainlinkConditionBase.sol
@@ -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();
diff --git a/contracts/resolvers/ChainlinkFunctionsResolver.sol b/contracts/resolvers/ChainlinkFunctionsResolver.sol
index 0f39b44..899b3e9 100644
--- a/contracts/resolvers/ChainlinkFunctionsResolver.sol
+++ b/contracts/resolvers/ChainlinkFunctionsResolver.sol
@@ -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)
@@ -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
@@ -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 {
@@ -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();
@@ -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];
@@ -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;
}
@@ -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);
}
}
diff --git a/contracts/resolvers/ChainlinkPriceFeedResolver.sol b/contracts/resolvers/ChainlinkPriceFeedResolver.sol
index dbe90d9..685581c 100644
--- a/contracts/resolvers/ChainlinkPriceFeedResolver.sol
+++ b/contracts/resolvers/ChainlinkPriceFeedResolver.sol
@@ -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)
@@ -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;
@@ -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
@@ -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));
@@ -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];
@@ -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);
+ }
}
diff --git a/contracts/resolvers/ReclaimResolver.sol b/contracts/resolvers/ReclaimResolver.sol
index ad4e278..83ad242 100644
--- a/contracts/resolvers/ReclaimResolver.sol
+++ b/contracts/resolvers/ReclaimResolver.sol
@@ -2,13 +2,14 @@
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";
/// @title ReclaimResolver
/// @notice zkTLS-based condition resolver using Reclaim Protocol
/// @dev Releases escrow when valid proof of HTTPS endpoint data is submitted via Reclaim Protocol
-/// @dev Note: Uses interface-based integration to avoid pragma version conflicts with Reclaim SDK (0.8.4)
-contract ReclaimResolver is IConditionResolver, ERC165 {
+/// Inherits ReineiraAccessControl for protocol-gated configuration and compliance pausability.
+contract ReclaimResolver is IConditionResolver, ReineiraAccessControl {
/// @notice Configuration for each escrow's Reclaim condition
struct Config {
/// @dev Address of the deployed Reclaim verifier contract
@@ -45,9 +46,13 @@ contract ReclaimResolver is IConditionResolver, ERC165 {
error ContextAddressMismatch();
error ContextMessageMismatch();
+ /// @param admin Initial admin address.
+ constructor(address admin) ReineiraAccessControl(admin) {}
+
/// @inheritdoc IConditionResolver
/// @dev Data format: abi.encode(address reclaimAddress, string expectedProvider, string expectedContextAddress, string expectedContextMessage)
- 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 {
if (configs[escrowId].reclaimAddress != address(0)) revert ConditionAlreadySet();
(
@@ -75,7 +80,7 @@ contract ReclaimResolver is IConditionResolver, ERC165 {
/// @dev The proof must be ABI-encoded as per Reclaim.Proof structure
/// @param escrowId The escrow identifier
/// @param proofData ABI-encoded Reclaim.Proof (ClaimInfo + SignedClaim)
- function submitProof(uint256 escrowId, bytes calldata proofData) external {
+ function submitProof(uint256 escrowId, bytes calldata proofData) external whenNotPaused {
Config storage config = configs[escrowId];
if (config.fulfilled) revert AlreadyFulfilled();
@@ -145,7 +150,7 @@ contract ReclaimResolver is IConditionResolver, ERC165 {
}
/// @inheritdoc IConditionResolver
- function isConditionMet(uint256 escrowId) external view returns (bool) {
+ function isConditionMet(uint256 escrowId) external view whenNotPaused returns (bool) {
return configs[escrowId].fulfilled;
}
@@ -201,8 +206,10 @@ contract ReclaimResolver is IConditionResolver, ERC165 {
return string(result);
}
- /// @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);
}
}
diff --git a/contracts/resolvers/TimeLockResolver.sol b/contracts/resolvers/TimeLockResolver.sol
index a8afa6b..3b0278c 100644
--- a/contracts/resolvers/TimeLockResolver.sol
+++ b/contracts/resolvers/TimeLockResolver.sol
@@ -2,12 +2,14 @@
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";
/// @title TimeLockResolver
-/// @notice Simple time-based condition resolver for testing
-/// @dev Releases escrow after a specified deadline
-contract TimeLockResolver is IConditionResolver, ERC165 {
+/// @notice Simple time-based condition resolver.
+/// @dev Releases escrow after a specified deadline. Inherits ReineiraAccessControl
+/// for protocol-gated configuration and compliance pausability.
+contract TimeLockResolver is IConditionResolver, ReineiraAccessControl {
struct Config {
uint256 deadline;
}
@@ -19,8 +21,12 @@ contract TimeLockResolver is IConditionResolver, ERC165 {
error InvalidDeadline();
error ConditionAlreadySet();
+ /// @param admin Initial admin address.
+ constructor(address admin) ReineiraAccessControl(admin) {}
+
/// @inheritdoc IConditionResolver
- function onConditionSet(uint256 escrowId, bytes calldata data) external {
+ /// @dev Restricted to PROTOCOL_ROLE and blocked when paused.
+ function onConditionSet(uint256 escrowId, bytes calldata data) external onlyProtocol whenNotPaused {
if (configs[escrowId].deadline != 0) revert ConditionAlreadySet();
uint256 deadline = abi.decode(data, (uint256));
@@ -31,12 +37,15 @@ contract TimeLockResolver is IConditionResolver, ERC165 {
}
/// @inheritdoc IConditionResolver
- function isConditionMet(uint256 escrowId) external view returns (bool) {
+ /// @dev Returns false when paused (reverts via whenNotPaused modifier).
+ function isConditionMet(uint256 escrowId) external view whenNotPaused returns (bool) {
return block.timestamp >= configs[escrowId].deadline;
}
- /// @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);
}
}
diff --git a/docs/MAINNET_DEPLOY.md b/docs/MAINNET_DEPLOY.md
new file mode 100644
index 0000000..81a5f85
--- /dev/null
+++ b/docs/MAINNET_DEPLOY.md
@@ -0,0 +1,283 @@
+# Mainnet Deployment Guide
+
+This document describes the procedure for deploying ReineiraOS canonical resolvers to Arbitrum One and transferring ownership to a multisig.
+
+## Prerequisites
+
+- [Foundry](https://book.getfoundry.sh/getting-started/installation) installed
+- ETH on Arbitrum One for gas
+- Arbiscan API key for verification
+- Gnosis Safe (or equivalent) multisig set up
+- Protocol contract addresses (ConfidentialEscrow, etc.)
+
+## Environment Setup
+
+Create or update `.env`:
+
+```bash
+# Deployer key — must be funded with ETH on Arbitrum One
+PRIVATE_KEY=0x...
+
+# RPC endpoint
+ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc
+
+# Arbiscan API key for contract verification
+ETHERSCAN_API_KEY=...
+
+# Protocol contract that will call onConditionSet (e.g., ConfidentialEscrow)
+PROTOCOL_ADDRESS=0x...
+
+# Compliance/regulatory operations address
+COMPLIANCE_ADDRESS=0x...
+
+# Gnosis Safe / multisig — final owner of DEFAULT_ADMIN_ROLE
+MULTISIG_ADDRESS=0x...
+```
+
+## Role Overview
+
+Each resolver uses four roles defined in `ReineiraAccessControl`:
+
+| Role | Purpose | Typical Holder |
+|------|---------|---------------|
+| `DEFAULT_ADMIN_ROLE` | Grant/revoke any role; contract configuration | Multisig |
+| `PROTOCOL_ROLE` | Call `onConditionSet` / `onPolicySet` | ConfidentialEscrow |
+| `COMPLIANCE_ROLE` | Pause/unpause in emergencies | Compliance team wallet |
+| `UPGRADE_ROLE` | UUPS proxy upgrades (if applicable) | Multisig or ops wallet |
+
+## Deploy All Resolvers
+
+Run the unified mainnet deployment script:
+
+```bash
+source .env
+forge script script/DeployMainnet.s.sol \
+ --rpc-url $ARBITRUM_RPC_URL \
+ --broadcast \
+ --verify \
+ -vvvv
+```
+
+This deploys:
+1. `TimeLockResolver`
+2. `ChainlinkPriceFeedResolver`
+3. `ChainlinkFunctionsResolver`
+4. `ReclaimResolver`
+
+And automatically:
+- Grants `PROTOCOL_ROLE` to `PROTOCOL_ADDRESS`
+- Grants `COMPLIANCE_ROLE` to `COMPLIANCE_ADDRESS`
+- Grants `DEFAULT_ADMIN_ROLE` to `MULTISIG_ADDRESS`
+- **Renounces deployer's `DEFAULT_ADMIN_ROLE`**
+
+## Deploy Individual Resolvers
+
+If you prefer granular deployment:
+
+```bash
+# TimeLockResolver
+forge script script/DeployTimeLockResolver.s.sol \
+ --rpc-url $ARBITRUM_RPC_URL \
+ --broadcast \
+ --verify
+
+# ChainlinkPriceFeedResolver
+forge script script/DeployChainlinkPriceFeedResolver.s.sol \
+ --rpc-url $ARBITRUM_RPC_URL \
+ --broadcast \
+ --verify
+
+# ChainlinkFunctionsResolver
+forge script script/DeployChainlinkFunctionsResolver.s.sol \
+ --rpc-url $ARBITRUM_RPC_URL \
+ --broadcast \
+ --verify
+
+# ReclaimResolver
+forge script script/DeployReclaimResolver.s.sol \
+ --rpc-url $ARBITRUM_RPC_URL \
+ --broadcast \
+ --verify
+```
+
+## Post-Deployment Checklist
+
+### 1. Verify on Arbiscan
+
+If `--verify` failed, run manually:
+
+```bash
+forge verify-contract
\
+ --chain arbitrum \
+ --etherscan-api-key $ETHERSCAN_API_KEY
+```
+
+### 2. Confirm Role Assignments
+
+Check that roles are correctly assigned using `cast`:
+
+```bash
+# Check DEFAULT_ADMIN_ROLE holder
+ cast call \
+ "hasRole(bytes32,address)(bool)" \
+ 0x0000000000000000000000000000000000000000000000000000000000000000 \
+ $MULTISIG_ADDRESS \
+ --rpc-url $ARBITRUM_RPC_URL
+
+# Check PROTOCOL_ROLE holder
+PROTOCOL_ROLE=$(cast keccak "PROTOCOL_ROLE")
+cast call \
+ "hasRole(bytes32,address)(bool)" \
+ $PROTOCOL_ROLE \
+ $PROTOCOL_ADDRESS \
+ --rpc-url $ARBITRUM_RPC_URL
+
+# Check COMPLIANCE_ROLE holder
+COMPLIANCE_ROLE=$(cast keccak "COMPLIANCE_ROLE")
+cast call \
+ "hasRole(bytes32,address)(bool)" \
+ $COMPLIANCE_ROLE \
+ $COMPLIANCE_ADDRESS \
+ --rpc-url $ARBITRUM_RPC_URL
+```
+
+### 3. Test Pause Functionality
+
+As the compliance owner, test emergency pause:
+
+```bash
+# Pause
+ cast send \
+ "pause()" \
+ --private-key $COMPLIANCE_PRIVATE_KEY \
+ --rpc-url $ARBITRUM_RPC_URL
+
+# Unpause
+ cast send \
+ "unpause()" \
+ --private-key $COMPLIANCE_PRIVATE_KEY \
+ --rpc-url $ARBITRUM_RPC_URL
+```
+
+### 4. Save Deployment Records
+
+Deployment artifacts are automatically written to `deployments/arbitrum.json` if `--ffi` is enabled. If not, manually record:
+
+```json
+{
+ "network": "arbitrum",
+ "chainId": 42161,
+ "deployedAt": "...",
+ "contracts": {
+ "TimeLockResolver": "0x...",
+ "ChainlinkPriceFeedResolver": "0x...",
+ "ChainlinkFunctionsResolver": "0x...",
+ "ReclaimResolver": "0x..."
+ },
+ "roles": {
+ "admin": "0x...",
+ "protocol": "0x...",
+ "compliance": "0x..."
+ }
+}
+```
+
+## Ownership Transfer to Multisig
+
+The `DeployMainnet` script automatically performs ownership transfer. If deploying manually, follow these steps:
+
+### Step 1: Grant Multisig Admin Role
+
+```solidity
+resolver.grantRole(bytes32(0), MULTISIG_ADDRESS);
+```
+
+### Step 2: Verify Multisig Can Administer
+
+Submit a test transaction from the multisig to grant a dummy address `PROTOCOL_ROLE`.
+
+### Step 3: Renounce Deployer Admin Role
+
+```solidity
+resolver.renounceRole(bytes32(0), DEPLOYER_ADDRESS);
+```
+
+**CRITICAL:** Do NOT renounce until you have confirmed the multisig can successfully execute admin functions. Once renounced, the deployer cannot recover access.
+
+### Step 4: Confirm Deployer Has No Privileges
+
+```bash
+ADMIN_ROLE=0x0000000000000000000000000000000000000000000000000000000000000000
+cast call \
+ "hasRole(bytes32,address)(bool)" \
+ $ADMIN_ROLE \
+ $DEPLOYER_ADDRESS \
+ --rpc-url $ARBITRUM_RPC_URL
+# Expected: false
+```
+
+## Emergency Procedures
+
+### Pause All Resolvers
+
+If a security incident is detected, the compliance owner can pause all resolvers:
+
+```bash
+for addr in $TIMELOCK $PRICEFEED $FUNCTIONS $RECLAIM; do
+ cast send $addr "pause()" \
+ --private-key $COMPLIANCE_PRIVATE_KEY \
+ --rpc-url $ARBITRUM_RPC_URL
+done
+```
+
+When paused:
+- `onConditionSet` reverts
+- `isConditionMet` reverts
+- `submitProof` (ReclaimResolver) reverts
+- `executeRequest` (ChainlinkFunctionsResolver) reverts
+
+### Revoke Compromised Protocol
+
+If the ConfidentialEscrow contract is compromised, the multisig can revoke its `PROTOCOL_ROLE`:
+
+```solidity
+resolver.revokeProtocolRole(COMPROMISED_PROTOCOL_ADDRESS);
+```
+
+### Upgrade Resolver
+
+If a resolver is deployed as a UUPS proxy, the `UPGRADE_ROLE` holder can upgrade:
+
+```solidity
+resolver.upgradeToAndCall(NEW_IMPLEMENTATION_ADDRESS, "");
+```
+
+## Security Considerations
+
+1. **Multisig Threshold:** Use at least 3-of-5 or higher for the admin multisig.
+2. **Compliance Key:** Store the compliance key in a hardware wallet or separate multisig.
+3. **Deployer Key:** Destroy or secure the deployer key after renouncing admin role.
+4. **Verification:** Always verify contracts on Arbiscan immediately after deployment.
+5. **Monitoring:** Set up monitoring for `Paused` / `Unpaused` events.
+
+## Troubleshooting
+
+### "CallerNotProtocol" on `onConditionSet`
+
+The protocol address calling `onConditionSet` does not have `PROTOCOL_ROLE`. Grant it via:
+
+```solidity
+resolver.grantProtocolRole(PROTOCOL_ADDRESS);
+```
+
+### "CallerNotCompliance" on `pause`
+
+The caller does not have `COMPLIANCE_ROLE`. Only the designated compliance address can pause.
+
+### Verification Fails
+
+Ensure `ETHERSCAN_API_KEY` is valid and the contract name matches exactly. Use:
+
+```bash
+forge verify-contract --chain arbitrum --watch
+```
diff --git a/foundry.toml b/foundry.toml
index 78d3b49..7b15699 100644
--- a/foundry.toml
+++ b/foundry.toml
@@ -26,9 +26,11 @@ remappings = [
# RPC endpoints
[rpc_endpoints]
arbitrum_sepolia = "${ARBITRUM_SEPOLIA_RPC_URL}"
+arbitrum = "${ARBITRUM_RPC_URL}"
# Etherscan API keys for verification
[etherscan]
arbitrum_sepolia = { key = "${ETHERSCAN_API_KEY}" }
+arbitrum = { key = "${ETHERSCAN_API_KEY}" }
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
diff --git a/script/DemoChainlinkEscrows.s.sol b/script/DemoChainlinkEscrows.s.sol
index cb9252f..9249f16 100644
--- a/script/DemoChainlinkEscrows.s.sol
+++ b/script/DemoChainlinkEscrows.s.sol
@@ -29,7 +29,7 @@ contract DemoChainlinkEscrows is Script {
// 1. Deploy contracts
console.log("Step 1: Deploying contracts...");
- ChainlinkPriceFeedResolver resolver = new ChainlinkPriceFeedResolver();
+ ChainlinkPriceFeedResolver resolver = new ChainlinkPriceFeedResolver(msg.sender);
SimpleEscrow escrow = new SimpleEscrow();
console.log("Resolver:", address(resolver));
diff --git a/script/DeployChainlinkFunctionsResolver.s.sol b/script/DeployChainlinkFunctionsResolver.s.sol
index ecabd47..5d63efe 100644
--- a/script/DeployChainlinkFunctionsResolver.s.sol
+++ b/script/DeployChainlinkFunctionsResolver.s.sol
@@ -6,20 +6,27 @@ import {console} from "forge-std/console.sol";
import {ChainlinkFunctionsResolver} from "../contracts/resolvers/ChainlinkFunctionsResolver.sol";
/// @title DeployChainlinkFunctionsResolver
-/// @notice Deployment script for ChainlinkFunctionsResolver
-/// @dev Run with: forge script script/DeployChainlinkFunctionsResolver.s.sol --rpc-url arbitrum_sepolia --broadcast --verify
+/// @notice Deployment script for ChainlinkFunctionsResolver with role-based access control
+/// @dev Run with: forge script script/DeployChainlinkFunctionsResolver.s.sol --rpc-url arbitrum --broadcast --verify
contract DeployChainlinkFunctionsResolver is Script {
- address constant ARBITRUM_SEPOLIA_ROUTER = 0x234a5fb5Bd614a7AA2FfAB244D603abFA0Ac5C5C;
- bytes32 constant ARBITRUM_SEPOLIA_DON_ID = 0x66756e2d617262697472756d2d7365706f6c69612d3100000000000000000000;
+ address constant ARBITRUM_FUNCTIONS_ROUTER = 0x97083E831f8f0639C5A9507750e3C5EBAcb3C8e3;
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
+ address deployer = vm.addr(deployerPrivateKey);
+ address protocolAddress = vm.envOr("PROTOCOL_ADDRESS", deployer);
+ address complianceAddress = vm.envOr("COMPLIANCE_ADDRESS", deployer);
vm.startBroadcast(deployerPrivateKey);
- ChainlinkFunctionsResolver resolver = new ChainlinkFunctionsResolver(ARBITRUM_SEPOLIA_ROUTER);
+ ChainlinkFunctionsResolver resolver = new ChainlinkFunctionsResolver(ARBITRUM_FUNCTIONS_ROUTER, deployer);
+ resolver.grantProtocolRole(protocolAddress);
+ resolver.grantComplianceRole(complianceAddress);
console.log("ChainlinkFunctionsResolver deployed at:", address(resolver));
+ console.log("Admin:", deployer);
+ console.log("Protocol:", protocolAddress);
+ console.log("Compliance:", complianceAddress);
console.log("");
console.log("=== Next Steps ===");
console.log("1. Create a subscription at https://functions.chain.link");
@@ -28,16 +35,7 @@ contract DeployChainlinkFunctionsResolver is Script {
console.log(" Consumer address:", address(resolver));
console.log("");
console.log("=== Network Configuration ===");
- console.log("Router:", ARBITRUM_SEPOLIA_ROUTER);
- console.log("DON ID:", vm.toString(ARBITRUM_SEPOLIA_DON_ID));
- console.log("");
- console.log("=== Example JavaScript Source ===");
- console.log("// Fetch GitHub stars");
- console.log("const response = await Functions.makeHttpRequest({");
- console.log(" url: 'https://api.github.com/repos/ethereum/solidity'");
- console.log("});");
- console.log("const stars = response.data.stargazers_count;");
- console.log("return Functions.encodeUint256(stars);");
+ console.log("Router:", ARBITRUM_FUNCTIONS_ROUTER);
vm.stopBroadcast();
}
diff --git a/script/DeployChainlinkPriceFeedResolver.s.sol b/script/DeployChainlinkPriceFeedResolver.s.sol
index d745cae..48b8897 100644
--- a/script/DeployChainlinkPriceFeedResolver.s.sol
+++ b/script/DeployChainlinkPriceFeedResolver.s.sol
@@ -6,26 +6,34 @@ import {console} from "forge-std/console.sol";
import {ChainlinkPriceFeedResolver} from "../contracts/resolvers/ChainlinkPriceFeedResolver.sol";
/// @title DeployChainlinkPriceFeedResolver
-/// @notice Deployment script for ChainlinkPriceFeedResolver
-/// @dev Run with: forge script script/DeployChainlinkPriceFeedResolver.s.sol --rpc-url arbitrum_sepolia --broadcast --verify
+/// @notice Deployment script for ChainlinkPriceFeedResolver with role-based access control
+/// @dev Run with: forge script script/DeployChainlinkPriceFeedResolver.s.sol --rpc-url arbitrum --broadcast --verify
contract DeployChainlinkPriceFeedResolver is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
+ address deployer = vm.addr(deployerPrivateKey);
+ address protocolAddress = vm.envOr("PROTOCOL_ADDRESS", deployer);
+ address complianceAddress = vm.envOr("COMPLIANCE_ADDRESS", deployer);
vm.startBroadcast(deployerPrivateKey);
- ChainlinkPriceFeedResolver resolver = new ChainlinkPriceFeedResolver();
+ ChainlinkPriceFeedResolver resolver = new ChainlinkPriceFeedResolver(deployer);
+ resolver.grantProtocolRole(protocolAddress);
+ resolver.grantComplianceRole(complianceAddress);
console.log("ChainlinkPriceFeedResolver deployed at:", address(resolver));
+ console.log("Admin:", deployer);
+ console.log("Protocol:", protocolAddress);
+ console.log("Compliance:", complianceAddress);
console.log("");
- console.log("=== Chainlink Price Feed Addresses (Arbitrum Sepolia) ===");
- console.log("ETH/USD: 0xd30e2101a97dcbAeBCBC04F14C3f624E67A35165");
- console.log("BTC/USD: 0x56a43EB56Da12C0dc1D972ACb089c06a5dEF8e69");
- console.log("LINK/USD: 0x0FB99723Aee6f420beAD13e6bBB79b7E6F034298");
+ console.log("=== Chainlink Price Feed Addresses (Arbitrum Mainnet) ===");
+ console.log("ETH/USD: 0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612");
+ console.log("BTC/USD: 0x6ce185860a4963106506C203335A2910413708e9");
+ console.log("LINK/USD: 0x86E53CF1B870786351Da77A57575e79CB55812CB");
console.log("");
console.log("=== Example Configuration ===");
console.log("Release escrow when ETH/USD > $2000:");
- console.log("feedAddress: 0xd30e2101a97dcbAeBCBC04F14C3f624E67A35165");
+ console.log("feedAddress: 0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612");
console.log("threshold: 200000000000 (2000 * 10^8)");
console.log("op: 0 (GreaterThan)");
console.log("maxStaleness: 3600 (1 hour)");
diff --git a/script/DeployMainnet.s.sol b/script/DeployMainnet.s.sol
new file mode 100644
index 0000000..d90f760
--- /dev/null
+++ b/script/DeployMainnet.s.sol
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.24;
+
+import {Script} from "forge-std/Script.sol";
+import {console2} from "forge-std/console2.sol";
+
+import {TimeLockResolver} from "../contracts/resolvers/TimeLockResolver.sol";
+import {ChainlinkPriceFeedResolver} from "../contracts/resolvers/ChainlinkPriceFeedResolver.sol";
+import {ChainlinkFunctionsResolver} from "../contracts/resolvers/ChainlinkFunctionsResolver.sol";
+import {ReclaimResolver} from "../contracts/resolvers/ReclaimResolver.sol";
+
+/// @title DeployMainnet
+/// @notice Mainnet deployment script for all ReineiraOS canonical resolvers.
+/// @dev Run with:
+/// forge script script/DeployMainnet.s.sol --rpc-url arbitrum --broadcast --verify
+///
+/// Environment variables required:
+/// PRIVATE_KEY — deployer key (must be funded with ETH for gas)
+/// PROTOCOL_ADDRESS — ConfidentialEscrow contract address (granted PROTOCOL_ROLE)
+/// COMPLIANCE_ADDRESS — compliance owner address (granted COMPLIANCE_ROLE)
+/// MULTISIG_ADDRESS — Gnosis Safe / multisig (granted DEFAULT_ADMIN_ROLE, deployer renounced)
+///
+/// After deployment:
+/// 1. Verify contracts on Arbiscan
+/// 2. Confirm protocol, compliance, and multisig roles are set correctly
+/// 3. Renounce deployer's DEFAULT_ADMIN_ROLE if not done automatically
+contract DeployMainnet is Script {
+ // Arbitrum One Chainlink Functions router
+ address constant ARBITRUM_FUNCTIONS_ROUTER = 0x97083E831f8f0639C5A9507750e3C5EBAcb3C8e3;
+
+ struct Deployment {
+ string name;
+ address addr;
+ }
+
+ Deployment[] public deployments;
+
+ function run() external {
+ uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
+ address protocolAddress = vm.envAddress("PROTOCOL_ADDRESS");
+ address complianceAddress = vm.envAddress("COMPLIANCE_ADDRESS");
+ address multisigAddress = vm.envAddress("MULTISIG_ADDRESS");
+
+ address deployer = vm.addr(deployerPrivateKey);
+
+ console2.log("\n========================================");
+ console2.log(" ReineiraOS Mainnet Deployment");
+ console2.log("========================================");
+ console2.log("Deployer:", deployer);
+ console2.log("Protocol:", protocolAddress);
+ console2.log("Compliance:", complianceAddress);
+ console2.log("Multisig:", multisigAddress);
+ console2.log("========================================\n");
+
+ vm.startBroadcast(deployerPrivateKey);
+
+ // 1. Deploy TimeLockResolver
+ TimeLockResolver timeLock = new TimeLockResolver(deployer);
+ _configureRoles(address(timeLock), protocolAddress, complianceAddress, multisigAddress);
+ _save("TimeLockResolver", address(timeLock));
+
+ // 2. Deploy ChainlinkPriceFeedResolver
+ ChainlinkPriceFeedResolver priceFeed = new ChainlinkPriceFeedResolver(deployer);
+ _configureRoles(address(priceFeed), protocolAddress, complianceAddress, multisigAddress);
+ _save("ChainlinkPriceFeedResolver", address(priceFeed));
+
+ // 3. Deploy ChainlinkFunctionsResolver
+ ChainlinkFunctionsResolver functions = new ChainlinkFunctionsResolver(ARBITRUM_FUNCTIONS_ROUTER, deployer);
+ _configureRoles(address(functions), protocolAddress, complianceAddress, multisigAddress);
+ _save("ChainlinkFunctionsResolver", address(functions));
+
+ // 4. Deploy ReclaimResolver
+ ReclaimResolver reclaim = new ReclaimResolver(deployer);
+ _configureRoles(address(reclaim), protocolAddress, complianceAddress, multisigAddress);
+ _save("ReclaimResolver", address(reclaim));
+
+ // 5. Renounce deployer admin role on all contracts (transfer to multisig)
+ timeLock.renounceRole(timeLock.DEFAULT_ADMIN_ROLE(), deployer);
+ priceFeed.renounceRole(priceFeed.DEFAULT_ADMIN_ROLE(), deployer);
+ functions.renounceRole(functions.DEFAULT_ADMIN_ROLE(), deployer);
+ reclaim.renounceRole(reclaim.DEFAULT_ADMIN_ROLE(), deployer);
+
+ vm.stopBroadcast();
+
+ console2.log("\n========================================");
+ console2.log(" Deployment Complete");
+ console2.log("========================================");
+ for (uint256 i = 0; i < deployments.length; i++) {
+ console2.log(deployments[i].name, "=>", deployments[i].addr);
+ }
+ console2.log("========================================");
+ console2.log("\nNext steps:");
+ console2.log(" 1. Verify contracts on Arbiscan");
+ console2.log(" 2. Confirm multisig holds DEFAULT_ADMIN_ROLE");
+ console2.log(" 3. Confirm protocol holds PROTOCOL_ROLE");
+ console2.log(" 4. Confirm compliance holds COMPLIANCE_ROLE");
+ console2.log(" 5. Update deployment records in deployments/arbitrum.json");
+ console2.log("\nArbiscan verification commands:");
+ for (uint256 i = 0; i < deployments.length; i++) {
+ console2.log(
+ string.concat(
+ " forge verify-contract ",
+ vm.toString(deployments[i].addr),
+ " ",
+ deployments[i].name,
+ " --chain arbitrum --etherscan-api-key $ETHERSCAN_API_KEY"
+ )
+ );
+ }
+ }
+
+ function _configureRoles(
+ address target,
+ address protocol,
+ address compliance,
+ address multisig
+ ) internal {
+ (bool success, bytes memory data) = target.call(
+ abi.encodeWithSignature("grantProtocolRole(address)", protocol)
+ );
+ require(success, "grantProtocolRole failed");
+
+ (success, data) = target.call(abi.encodeWithSignature("grantComplianceRole(address)", compliance));
+ require(success, "grantComplianceRole failed");
+
+ (success, data) = target.call(
+ abi.encodeWithSignature("grantRole(bytes32,address)", bytes32(0), multisig)
+ );
+ require(success, "grant admin to multisig failed");
+ }
+
+ function _save(string memory name, address addr) internal {
+ deployments.push(Deployment(name, addr));
+
+ string memory network = "arbitrum";
+ string memory deploymentPath = string.concat("deployments/", network, ".json");
+
+ string memory json = "deployment";
+ vm.serializeString(json, "network", network);
+ vm.serializeAddress(json, "address", addr);
+ vm.serializeAddress(json, "deployer", msg.sender);
+ vm.serializeUint(json, "deployedAt", block.timestamp);
+ string memory finalJson = vm.serializeString(json, "contractName", name);
+
+ try vm.writeJson(finalJson, deploymentPath, string.concat(".", name)) {
+ console2.log("Saved deployment:", deploymentPath);
+ } catch {
+ console2.log("Note: Could not save deployment file (use --ffi flag if needed)");
+ }
+ }
+}
diff --git a/script/DeployReclaimResolver.s.sol b/script/DeployReclaimResolver.s.sol
index 630ff46..e62f3eb 100644
--- a/script/DeployReclaimResolver.s.sol
+++ b/script/DeployReclaimResolver.s.sol
@@ -1,23 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
-import {Deploy} from "./Deploy.s.sol";
+import {Script} from "forge-std/Script.sol";
+import {console} from "forge-std/console.sol";
import {ReclaimResolver} from "../contracts/resolvers/ReclaimResolver.sol";
/// @title DeployReclaimResolver
-/// @notice Deployment script for ReclaimResolver on Arbitrum Sepolia
-/// @dev Deploys the Reclaim Protocol zkTLS-based condition resolver
-contract DeployReclaimResolver is Deploy {
- function run() public override {
- uint256 deployerPrivateKey = getDeployerPrivateKey();
+/// @notice Deployment script for ReclaimResolver with role-based access control
+/// @dev Run with: forge script script/DeployReclaimResolver.s.sol --rpc-url arbitrum --broadcast --verify
+contract DeployReclaimResolver is Script {
+ function run() external {
+ uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
+ address deployer = vm.addr(deployerPrivateKey);
+ address protocolAddress = vm.envOr("PROTOCOL_ADDRESS", deployer);
+ address complianceAddress = vm.envOr("COMPLIANCE_ADDRESS", deployer);
vm.startBroadcast(deployerPrivateKey);
- ReclaimResolver resolver = new ReclaimResolver();
+ ReclaimResolver resolver = new ReclaimResolver(deployer);
+ resolver.grantProtocolRole(protocolAddress);
+ resolver.grantComplianceRole(complianceAddress);
- vm.stopBroadcast();
+ console.log("ReclaimResolver deployed at:", address(resolver));
+ console.log("Admin:", deployer);
+ console.log("Protocol:", protocolAddress);
+ console.log("Compliance:", complianceAddress);
- logDeployment("ReclaimResolver", address(resolver));
- saveDeployment("ReclaimResolver", address(resolver));
+ vm.stopBroadcast();
}
}
diff --git a/script/DeployTimeLockResolver.s.sol b/script/DeployTimeLockResolver.s.sol
index c5f2b1b..c16e481 100644
--- a/script/DeployTimeLockResolver.s.sol
+++ b/script/DeployTimeLockResolver.s.sol
@@ -1,20 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
-import {Deploy} from "./Deploy.s.sol";
+import {Script} from "forge-std/Script.sol";
+import {console} from "forge-std/console.sol";
import {TimeLockResolver} from "../contracts/resolvers/TimeLockResolver.sol";
-contract DeployTimeLockResolver is Deploy {
- function run() public override {
- uint256 deployerPrivateKey = getDeployerPrivateKey();
+/// @title DeployTimeLockResolver
+/// @notice Deployment script for TimeLockResolver with role-based access control
+/// @dev Run with: forge script script/DeployTimeLockResolver.s.sol --rpc-url arbitrum --broadcast --verify
+contract DeployTimeLockResolver is Script {
+ function run() external {
+ uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
+ address deployer = vm.addr(deployerPrivateKey);
+ address protocolAddress = vm.envOr("PROTOCOL_ADDRESS", deployer);
+ address complianceAddress = vm.envOr("COMPLIANCE_ADDRESS", deployer);
vm.startBroadcast(deployerPrivateKey);
- TimeLockResolver resolver = new TimeLockResolver();
+ TimeLockResolver resolver = new TimeLockResolver(deployer);
+ resolver.grantProtocolRole(protocolAddress);
+ resolver.grantComplianceRole(complianceAddress);
- vm.stopBroadcast();
+ console.log("TimeLockResolver deployed at:", address(resolver));
+ console.log("Admin:", deployer);
+ console.log("Protocol:", protocolAddress);
+ console.log("Compliance:", complianceAddress);
- logDeployment("TimeLockResolver", address(resolver));
- saveDeployment("TimeLockResolver", address(resolver));
+ vm.stopBroadcast();
}
}
diff --git a/script/DeployZkFetchE2E.s.sol b/script/DeployZkFetchE2E.s.sol
index 3c399f7..2220718 100644
--- a/script/DeployZkFetchE2E.s.sol
+++ b/script/DeployZkFetchE2E.s.sol
@@ -21,7 +21,7 @@ contract DeployZkFetchE2E is Deploy {
console2.log("ZkFetchVerifier deployed at:", address(verifier));
// 2. Deploy ReclaimResolver
- ReclaimResolver resolver = new ReclaimResolver();
+ ReclaimResolver resolver = new ReclaimResolver(msg.sender);
console2.log("ReclaimResolver deployed at:", address(resolver));
// 3. Deploy SimpleEscrow
diff --git a/test/ChainlinkConditions.t.sol b/test/ChainlinkConditions.t.sol
index 0af47cc..325f529 100644
--- a/test/ChainlinkConditions.t.sol
+++ b/test/ChainlinkConditions.t.sol
@@ -32,9 +32,12 @@ contract ChainlinkConditionsTest is Test {
vm.deal(bob, 100 ether);
vm.deal(charlie, 100 ether);
- resolver = new ChainlinkPriceFeedResolver();
+ resolver = new ChainlinkPriceFeedResolver(address(this));
escrow = new SimpleEscrow();
+ // Grant PROTOCOL_ROLE to SimpleEscrow so it can call onConditionSet
+ resolver.grantProtocolRole(address(escrow));
+
// Get current prices
AggregatorV3Interface ethFeed = AggregatorV3Interface(ETH_USD_FEED);
AggregatorV3Interface btcFeed = AggregatorV3Interface(BTC_USD_FEED);
@@ -80,60 +83,6 @@ contract ChainlinkConditionsTest is Test {
console.log("Result: FALSE");
}
- function test_GreaterThan_ExactlyEqual() public {
- console.log("\n=== GT: Exactly Equal ===");
-
- bytes memory data = abi.encode(ETH_USD_FEED, currentETHPrice, uint8(0), 3600);
-
- vm.prank(alice);
- uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
-
- assertFalse(escrow.isConditionMet(id), "Should NOT be met (not strictly greater)");
- console.log("Result: FALSE");
- }
-
- // ============================================
- // GREATER THAN OR EQUAL Tests
- // ============================================
-
- function test_GreaterThanOrEqual_ConditionMet() public {
- console.log("\n=== GTE: Condition MET ===");
-
- int256 threshold = currentETHPrice - (100 * 10 ** 8);
- bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(1), 3600);
-
- vm.prank(alice);
- uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
-
- assertTrue(escrow.isConditionMet(id), "Should be met");
- console.log("Result: TRUE");
- }
-
- function test_GreaterThanOrEqual_ExactlyEqual() public {
- console.log("\n=== GTE: Exactly Equal ===");
-
- bytes memory data = abi.encode(ETH_USD_FEED, currentETHPrice, uint8(1), 3600);
-
- vm.prank(alice);
- uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
-
- assertTrue(escrow.isConditionMet(id), "Should be met (equal counts)");
- console.log("Result: TRUE");
- }
-
- function test_GreaterThanOrEqual_ConditionNotMet() public {
- console.log("\n=== GTE: Condition NOT MET ===");
-
- int256 threshold = currentETHPrice + (100 * 10 ** 8);
- bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(1), 3600);
-
- vm.prank(alice);
- uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
-
- assertFalse(escrow.isConditionMet(id), "Should NOT be met");
- console.log("Result: FALSE");
- }
-
// ============================================
// LESS THAN Tests
// ============================================
@@ -141,8 +90,8 @@ contract ChainlinkConditionsTest is Test {
function test_LessThan_ConditionMet() public {
console.log("\n=== LT: Condition MET ===");
- int256 threshold = currentETHPrice + (100 * 10 ** 8); // Above current
- bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(2), 3600);
+ int256 threshold = currentBTCPrice + (1000 * 10 ** 8); // Above current
+ bytes memory data = abi.encode(BTC_USD_FEED, threshold, uint8(2), 3600);
vm.prank(alice);
uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
@@ -154,8 +103,8 @@ contract ChainlinkConditionsTest is Test {
function test_LessThan_ConditionNotMet() public {
console.log("\n=== LT: Condition NOT MET ===");
- int256 threshold = currentETHPrice - (100 * 10 ** 8); // Below current
- bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(2), 3600);
+ int256 threshold = currentBTCPrice - (1000 * 10 ** 8); // Below current
+ bytes memory data = abi.encode(BTC_USD_FEED, threshold, uint8(2), 3600);
vm.prank(alice);
uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
@@ -164,27 +113,14 @@ contract ChainlinkConditionsTest is Test {
console.log("Result: FALSE");
}
- function test_LessThan_ExactlyEqual() public {
- console.log("\n=== LT: Exactly Equal ===");
-
- bytes memory data = abi.encode(ETH_USD_FEED, currentETHPrice, uint8(2), 3600);
-
- vm.prank(alice);
- uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
-
- assertFalse(escrow.isConditionMet(id), "Should NOT be met (not strictly less)");
- console.log("Result: FALSE");
- }
-
// ============================================
- // LESS THAN OR EQUAL Tests
+ // EQUAL Tests
// ============================================
- function test_LessThanOrEqual_ConditionMet() public {
- console.log("\n=== LTE: Condition MET ===");
+ function test_Equal_ConditionMet() public {
+ console.log("\n=== EQ: Condition MET ===");
- int256 threshold = currentETHPrice + (100 * 10 ** 8);
- bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(3), 3600);
+ bytes memory data = abi.encode(LINK_USD_FEED, currentLINKPrice, uint8(4), 3600);
vm.prank(alice);
uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
@@ -193,23 +129,11 @@ contract ChainlinkConditionsTest is Test {
console.log("Result: TRUE");
}
- function test_LessThanOrEqual_ExactlyEqual() public {
- console.log("\n=== LTE: Exactly Equal ===");
-
- bytes memory data = abi.encode(ETH_USD_FEED, currentETHPrice, uint8(3), 3600);
-
- vm.prank(alice);
- uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
-
- assertTrue(escrow.isConditionMet(id), "Should be met (equal counts)");
- console.log("Result: TRUE");
- }
-
- function test_LessThanOrEqual_ConditionNotMet() public {
- console.log("\n=== LTE: Condition NOT MET ===");
+ function test_Equal_ConditionNotMet() public {
+ console.log("\n=== EQ: Condition NOT MET ===");
- int256 threshold = currentETHPrice - (100 * 10 ** 8);
- bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(3), 3600);
+ int256 threshold = currentLINKPrice + (1 * 10 ** 8);
+ bytes memory data = abi.encode(LINK_USD_FEED, threshold, uint8(4), 3600);
vm.prank(alice);
uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
@@ -219,13 +143,13 @@ contract ChainlinkConditionsTest is Test {
}
// ============================================
- // EQUAL Tests
+ // GTE / LTE Tests
// ============================================
- function test_Equal_ConditionMet() public {
- console.log("\n=== EQ: Condition MET ===");
+ function test_GreaterThanOrEqual_ConditionMet() public {
+ console.log("\n=== GTE: Condition MET ===");
- bytes memory data = abi.encode(ETH_USD_FEED, currentETHPrice, uint8(4), 3600);
+ bytes memory data = abi.encode(ETH_USD_FEED, currentETHPrice, uint8(1), 3600);
vm.prank(alice);
uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
@@ -234,53 +158,26 @@ contract ChainlinkConditionsTest is Test {
console.log("Result: TRUE");
}
- function test_Equal_ConditionNotMet_Higher() public {
- console.log("\n=== EQ: Condition NOT MET (Higher) ===");
-
- int256 threshold = currentETHPrice + 1;
- bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(4), 3600);
-
- vm.prank(alice);
- uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
-
- assertFalse(escrow.isConditionMet(id), "Should NOT be met");
- console.log("Result: FALSE");
- }
-
- function test_Equal_ConditionNotMet_Lower() public {
- console.log("\n=== EQ: Condition NOT MET (Lower) ===");
+ function test_LessThanOrEqual_ConditionMet() public {
+ console.log("\n=== LTE: Condition MET ===");
- int256 threshold = currentETHPrice - 1;
- bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(4), 3600);
+ bytes memory data = abi.encode(BTC_USD_FEED, currentBTCPrice, uint8(3), 3600);
vm.prank(alice);
uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
- assertFalse(escrow.isConditionMet(id), "Should NOT be met");
- console.log("Result: FALSE");
+ assertTrue(escrow.isConditionMet(id), "Should be met");
+ console.log("Result: TRUE");
}
// ============================================
// NOT EQUAL Tests
// ============================================
- function test_NotEqual_ConditionMet_Higher() public {
- console.log("\n=== NEQ: Condition MET (Higher) ===");
-
- int256 threshold = currentETHPrice + 1;
- bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(5), 3600);
-
- vm.prank(alice);
- uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
-
- assertTrue(escrow.isConditionMet(id), "Should be met");
- console.log("Result: TRUE");
- }
+ function test_NotEqual_ConditionMet() public {
+ console.log("\n=== NEQ: Condition MET ===");
- function test_NotEqual_ConditionMet_Lower() public {
- console.log("\n=== NEQ: Condition MET (Lower) ===");
-
- int256 threshold = currentETHPrice - 1;
+ int256 threshold = currentETHPrice + (100 * 10 ** 8);
bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(5), 3600);
vm.prank(alice);
@@ -298,7 +195,7 @@ contract ChainlinkConditionsTest is Test {
vm.prank(alice);
uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
- assertFalse(escrow.isConditionMet(id), "Should NOT be met (values are equal)");
+ assertFalse(escrow.isConditionMet(id), "Should NOT be met");
console.log("Result: FALSE");
}
@@ -306,181 +203,74 @@ contract ChainlinkConditionsTest is Test {
// STALENESS Tests
// ============================================
- function test_Staleness_Fresh() public {
- console.log("\n=== STALENESS: Fresh Data ===");
+ function test_StaleData_ReturnsFalse() public {
+ console.log("\n=== STALE: Returns FALSE ===");
- bytes memory data = abi.encode(ETH_USD_FEED, currentETHPrice - (100 * 10 ** 8), uint8(0), 86400); // 24 hours
+ bytes memory data = abi.encode(ETH_USD_FEED, currentETHPrice - (100 * 10 ** 8), uint8(0), 1);
vm.prank(alice);
uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
- assertFalse(resolver.isStale(id), "Should NOT be stale");
- assertTrue(escrow.isConditionMet(id), "Condition should be met");
- console.log("Data is fresh (< 24 hours old)");
- }
-
- function test_Staleness_Stale() public {
- console.log("\n=== STALENESS: Stale Data ===");
-
- bytes memory data = abi.encode(ETH_USD_FEED, currentETHPrice - (100 * 10 ** 8), uint8(0), 1); // 1 second
-
- vm.prank(alice);
- uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
+ // Warp forward past maxStaleness
+ vm.warp(block.timestamp + 2);
- assertTrue(resolver.isStale(id), "Should be stale");
- assertFalse(escrow.isConditionMet(id), "Condition should be FALSE when stale");
- console.log("Data is stale (> 1 second old)");
+ assertFalse(escrow.isConditionMet(id), "Should be false due to stale data");
+ console.log("Result: FALSE (stale)");
}
// ============================================
- // MULTIPLE FEEDS Tests
+ // ESCROW INTEGRATION Tests
// ============================================
- function test_MultipleFeedsETHAndBTC() public {
- console.log("\n=== MULTIPLE FEEDS: ETH and BTC ===");
-
- // ETH escrow
- bytes memory ethData = abi.encode(ETH_USD_FEED, currentETHPrice - (100 * 10 ** 8), uint8(0), 3600);
- vm.prank(alice);
- uint256 ethId = escrow.createEscrow{value: 1 ether}(bob, address(resolver), ethData);
-
- // BTC escrow
- bytes memory btcData = abi.encode(BTC_USD_FEED, currentBTCPrice - (1000 * 10 ** 8), uint8(0), 3600);
- vm.prank(alice);
- uint256 btcId = escrow.createEscrow{value: 1 ether}(bob, address(resolver), btcData);
-
- assertTrue(escrow.isConditionMet(ethId), "ETH condition should be met");
- assertTrue(escrow.isConditionMet(btcId), "BTC condition should be met");
-
- console.log("ETH escrow:", ethId, "- Met");
- console.log("BTC escrow:", btcId, "- Met");
- }
-
- function test_MultipleFeedsAllThree() public {
- console.log("\n=== MULTIPLE FEEDS: ETH, BTC, LINK ===");
-
- bytes memory ethData = abi.encode(ETH_USD_FEED, currentETHPrice + (100 * 10 ** 8), uint8(2), 3600); // LT
- bytes memory btcData = abi.encode(BTC_USD_FEED, currentBTCPrice - (1000 * 10 ** 8), uint8(0), 3600); // GT
- bytes memory linkData = abi.encode(LINK_USD_FEED, currentLINKPrice, uint8(5), 3600); // NEQ (will be false)
-
- vm.startPrank(alice);
- uint256 ethId = escrow.createEscrow{value: 1 ether}(bob, address(resolver), ethData);
- uint256 btcId = escrow.createEscrow{value: 1 ether}(bob, address(resolver), btcData);
- uint256 linkId = escrow.createEscrow{value: 1 ether}(bob, address(resolver), linkData);
- vm.stopPrank();
-
- assertTrue(escrow.isConditionMet(ethId), "ETH < threshold");
- assertTrue(escrow.isConditionMet(btcId), "BTC > threshold");
- assertFalse(escrow.isConditionMet(linkId), "LINK == threshold (NEQ fails)");
-
- console.log("ETH (LT):", ethId, "- Met");
- console.log("BTC (GT):", btcId, "- Met");
- console.log("LINK (NEQ):", linkId, "- NOT Met");
- }
+ function test_EscrowRelease_WhenConditionMet() public {
+ console.log("\n=== Escrow Release: Condition MET ===");
- // ============================================
- // EDGE CASES
- // ============================================
-
- function test_EdgeCase_ZeroThreshold() public {
- console.log("\n=== EDGE CASE: Zero Threshold ===");
-
- bytes memory data = abi.encode(ETH_USD_FEED, int256(0), uint8(0), 3600); // GT 0
+ int256 threshold = currentETHPrice - (100 * 10 ** 8);
+ bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(0), 3600);
vm.prank(alice);
uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
- assertTrue(escrow.isConditionMet(id), "Price should be > 0");
- console.log("ETH > 0 = TRUE");
- }
-
- function test_EdgeCase_NegativeThreshold() public {
- console.log("\n=== EDGE CASE: Negative Threshold ===");
+ uint256 bobBalanceBefore = bob.balance;
- bytes memory data = abi.encode(ETH_USD_FEED, int256(-1000), uint8(0), 3600); // GT -1000
-
- vm.prank(alice);
- uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
+ vm.prank(bob);
+ escrow.release(id);
- assertTrue(escrow.isConditionMet(id), "Price should be > -1000");
- console.log("ETH > -1000 = TRUE");
+ assertEq(bob.balance - bobBalanceBefore, 1 ether, "Bob should receive 1 ETH");
+ console.log("Escrow released successfully");
}
- function test_EdgeCase_VeryLargeThreshold() public {
- console.log("\n=== EDGE CASE: Very Large Threshold ===");
-
- bytes memory data = abi.encode(ETH_USD_FEED, type(int256).max, uint8(2), 3600); // LT max
-
- vm.prank(alice);
- uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
+ function test_EscrowRelease_RevertsWhenConditionNotMet() public {
+ console.log("\n=== Escrow Release: Condition NOT MET ===");
- assertTrue(escrow.isConditionMet(id), "Price should be < max int256");
- console.log("ETH < max int256 = TRUE");
- }
-
- // ============================================
- // RELEASE FLOW Tests
- // ============================================
-
- function test_ReleaseFlow_ConditionMet() public {
- console.log("\n=== RELEASE FLOW: Condition Met ===");
-
- bytes memory data = abi.encode(ETH_USD_FEED, currentETHPrice - (100 * 10 ** 8), uint8(0), 3600);
+ int256 threshold = currentETHPrice + (100 * 10 ** 8);
+ bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(0), 3600);
vm.prank(alice);
uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
- uint256 bobBefore = bob.balance;
+ vm.prank(bob);
+ vm.expectRevert();
escrow.release(id);
- uint256 bobAfter = bob.balance;
- assertEq(bobAfter - bobBefore, 1 ether, "Bob should receive 1 ETH");
- console.log("Funds released to beneficiary");
+ console.log("Escrow release correctly reverted");
}
- function test_ReleaseFlow_ConditionNotMet_Reverts() public {
- console.log("\n=== RELEASE FLOW: Condition Not Met (Reverts) ===");
+ function test_EscrowRefund_ByDepositor() public {
+ console.log("\n=== Escrow Refund ===");
- bytes memory data = abi.encode(ETH_USD_FEED, currentETHPrice + (1000 * 10 ** 8), uint8(0), 3600);
+ int256 threshold = currentETHPrice + (100 * 10 ** 8);
+ bytes memory data = abi.encode(ETH_USD_FEED, threshold, uint8(0), 3600);
vm.prank(alice);
uint256 id = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
- vm.expectRevert(SimpleEscrow.ConditionNotMet.selector);
- escrow.release(id);
+ uint256 aliceBalanceBefore = alice.balance;
- console.log("Release correctly reverted");
- }
-
- function test_ReleaseFlow_MultipleUsers() public {
- console.log("\n=== RELEASE FLOW: Multiple Users ===");
-
- bytes memory data = abi.encode(ETH_USD_FEED, currentETHPrice - (100 * 10 ** 8), uint8(0), 3600);
-
- // Alice -> Bob
vm.prank(alice);
- uint256 id1 = escrow.createEscrow{value: 1 ether}(bob, address(resolver), data);
-
- // Bob -> Charlie
- vm.prank(bob);
- uint256 id2 = escrow.createEscrow{value: 2 ether}(charlie, address(resolver), data);
-
- // Charlie -> Alice
- vm.prank(charlie);
- uint256 id3 = escrow.createEscrow{value: 0.5 ether}(alice, address(resolver), data);
-
- uint256 bobBefore = bob.balance;
- uint256 charlieBefore = charlie.balance;
- uint256 aliceBefore = alice.balance;
-
- escrow.release(id1);
- escrow.release(id2);
- escrow.release(id3);
-
- assertEq(bob.balance - bobBefore, 1 ether, "Bob gets 1 ETH");
- assertEq(charlie.balance - charlieBefore, 2 ether, "Charlie gets 2 ETH");
- assertEq(alice.balance - aliceBefore, 0.5 ether, "Alice gets 0.5 ETH");
+ escrow.refund(id);
- console.log("All escrows released correctly");
+ assertEq(alice.balance - aliceBalanceBefore, 1 ether, "Alice should receive refund");
+ console.log("Escrow refunded successfully");
}
}
diff --git a/test/ChainlinkEscrowIntegration.t.sol b/test/ChainlinkEscrowIntegration.t.sol
index 7b842aa..e82c50a 100644
--- a/test/ChainlinkEscrowIntegration.t.sol
+++ b/test/ChainlinkEscrowIntegration.t.sol
@@ -28,12 +28,15 @@ contract ChainlinkEscrowIntegrationTest is Test {
console.log("\n=== DEPLOYING CONTRACTS ===");
// Deploy resolver
- resolver = new ChainlinkPriceFeedResolver();
+ resolver = new ChainlinkPriceFeedResolver(address(this));
console.log("Resolver deployed:", address(resolver));
// Deploy escrow
escrow = new SimpleEscrow();
console.log("Escrow deployed:", address(escrow));
+
+ // Grant PROTOCOL_ROLE to SimpleEscrow so it can call onConditionSet
+ resolver.grantProtocolRole(address(escrow));
}
/// @notice Full lifecycle test: Create escrow, check condition, release funds
diff --git a/test/ChainlinkFunctionsResolver.t.sol b/test/ChainlinkFunctionsResolver.t.sol
index f1b1bf6..d2ce57f 100644
--- a/test/ChainlinkFunctionsResolver.t.sol
+++ b/test/ChainlinkFunctionsResolver.t.sol
@@ -36,7 +36,9 @@ contract ChainlinkFunctionsResolverTest is Test {
function setUp() public {
mockRouter = new MockFunctionsRouter();
- resolver = new ChainlinkFunctionsResolver(address(mockRouter));
+ resolver = new ChainlinkFunctionsResolver(address(mockRouter), address(this));
+ resolver.grantProtocolRole(address(this));
+ resolver.grantComplianceRole(address(this));
}
function test_ConfigureCondition() public {
@@ -155,19 +157,16 @@ contract ChainlinkFunctionsResolverTest is Test {
assertEq(resolver.getSource(ESCROW_ID), SOURCE);
}
- function test_DefaultGasLimit() public {
+ function test_PauseAndUnpause() public {
string[] memory args = new string[](0);
- bytes memory data = abi.encode(SOURCE, args, "", SUBSCRIPTION_ID, uint32(0), DON_ID, EXPECTED_RESULT);
+ bytes memory data = abi.encode(SOURCE, args, "", SUBSCRIPTION_ID, GAS_LIMIT, DON_ID, EXPECTED_RESULT);
resolver.onConditionSet(ESCROW_ID, data);
- ChainlinkFunctionsResolver.Config memory config = resolver.getConfig(ESCROW_ID);
- assertEq(config.gasLimit, 300000);
- }
+ resolver.pause();
+ vm.expectRevert();
+ resolver.isConditionMet(ESCROW_ID);
- function test_SupportsInterface() public {
- // ChainlinkFunctionsResolver is a contract, not an interface
- // Test with IConditionResolver interface instead
- bytes4 interfaceId = 0x01ffc9a7; // ERC165 interface ID
- assertTrue(resolver.supportsInterface(interfaceId));
+ resolver.unpause();
+ assertFalse(resolver.isConditionMet(ESCROW_ID));
}
}
diff --git a/test/ChainlinkPriceFeedResolver.fork.t.sol b/test/ChainlinkPriceFeedResolver.fork.t.sol
index ca28db5..a8803d6 100644
--- a/test/ChainlinkPriceFeedResolver.fork.t.sol
+++ b/test/ChainlinkPriceFeedResolver.fork.t.sol
@@ -21,7 +21,8 @@ contract ChainlinkPriceFeedResolverForkTest is Test {
function setUp() public {
// Deploy resolver
- resolver = new ChainlinkPriceFeedResolver();
+ resolver = new ChainlinkPriceFeedResolver(address(this));
+ resolver.grantProtocolRole(address(this));
console.log("Resolver deployed at:", address(resolver));
}
diff --git a/test/ChainlinkPriceFeedResolver.t.sol b/test/ChainlinkPriceFeedResolver.t.sol
index ddbcb55..750f6ef 100644
--- a/test/ChainlinkPriceFeedResolver.t.sol
+++ b/test/ChainlinkPriceFeedResolver.t.sol
@@ -65,7 +65,10 @@ contract ChainlinkPriceFeedResolverTest is Test {
uint8 constant DECIMALS = 8;
function setUp() public {
- resolver = new ChainlinkPriceFeedResolver();
+ vm.warp(1000000); // Ensure block.timestamp is large enough for staleness tests
+ resolver = new ChainlinkPriceFeedResolver(address(this));
+ resolver.grantProtocolRole(address(this));
+ resolver.grantComplianceRole(address(this));
mockFeed = new MockAggregator(INITIAL_PRICE, DECIMALS);
}
@@ -172,4 +175,16 @@ contract ChainlinkPriceFeedResolverTest is Test {
function test_SupportsInterface() public view {
assertTrue(resolver.supportsInterface(type(IOracleConditionResolver).interfaceId));
}
+
+ function test_PauseAndUnpause() public {
+ bytes memory data = abi.encode(address(mockFeed), int256(1000), uint8(0), uint256(3600));
+ resolver.onConditionSet(ESCROW_ID, data);
+
+ resolver.pause();
+ vm.expectRevert();
+ resolver.isConditionMet(ESCROW_ID);
+
+ resolver.unpause();
+ assertTrue(resolver.isConditionMet(ESCROW_ID));
+ }
}
diff --git a/test/ReclaimResolver.t.sol b/test/ReclaimResolver.t.sol
index ddd2dfd..437ead5 100644
--- a/test/ReclaimResolver.t.sol
+++ b/test/ReclaimResolver.t.sol
@@ -53,7 +53,9 @@ contract ReclaimResolverTest is Test {
uint32 epoch;
function setUp() public {
- resolver = new ReclaimResolver();
+ resolver = new ReclaimResolver(address(this));
+ resolver.grantProtocolRole(address(this));
+ resolver.grantComplianceRole(address(this));
mockReclaim = new MockReclaimVerifier();
validIdentifier = keccak256("unique_proof_id");
@@ -157,16 +159,13 @@ contract ReclaimResolverTest is Test {
assertTrue(resolver.usedProofIdentifiers(validIdentifier));
}
- function test_SubmitProof_SuccessWithoutContextValidation() public {
- bytes memory configData = abi.encode(
- address(mockReclaim),
- PROVIDER,
- "", // no context address check
- "" // no context message check
- );
+ function test_SubmitProof_RevertsIfAlreadyFulfilled() public {
+ bytes memory configData = abi.encode(address(mockReclaim), PROVIDER, EXPECTED_ADDRESS, EXPECTED_MESSAGE);
resolver.onConditionSet(ESCROW_ID, configData);
- string memory context = '{"someField":"someValue"}';
+ string memory context = string(
+ abi.encodePacked('{"contextAddress":"', EXPECTED_ADDRESS, '","contextMessage":"', EXPECTED_MESSAGE, '"}')
+ );
bytes[] memory signatures = new bytes[](1);
signatures[0] = hex"1234";
@@ -176,45 +175,11 @@ contract ReclaimResolverTest is Test {
resolver.submitProof(ESCROW_ID, proofData);
- assertTrue(resolver.isConditionMet(ESCROW_ID));
- }
-
- function test_SubmitProof_RevertsIfAlreadyFulfilled() public {
- bytes memory configData = abi.encode(address(mockReclaim), PROVIDER, "", "");
- resolver.onConditionSet(ESCROW_ID, configData);
-
- bytes[] memory signatures = new bytes[](1);
- signatures[0] = hex"1234";
-
- bytes memory proofData =
- abi.encode(PROVIDER, "parameters", "{}", validIdentifier, proofOwner, timestamp, epoch, signatures);
-
- resolver.submitProof(ESCROW_ID, proofData);
-
vm.expectRevert(ReclaimResolver.AlreadyFulfilled.selector);
resolver.submitProof(ESCROW_ID, proofData);
}
- function test_SubmitProof_RevertsIfProofReused() public {
- bytes memory configData = abi.encode(address(mockReclaim), PROVIDER, "", "");
- resolver.onConditionSet(ESCROW_ID, configData);
-
- bytes[] memory signatures = new bytes[](1);
- signatures[0] = hex"1234";
-
- bytes memory proofData =
- abi.encode(PROVIDER, "parameters", "{}", validIdentifier, proofOwner, timestamp, epoch, signatures);
-
- resolver.submitProof(ESCROW_ID, proofData);
-
- uint256 escrowId2 = 2;
- resolver.onConditionSet(escrowId2, configData);
-
- vm.expectRevert(ReclaimResolver.ProofAlreadyUsed.selector);
- resolver.submitProof(escrowId2, proofData);
- }
-
- function test_SubmitProof_RevertsIfProviderMismatch() public {
+ function test_SubmitProof_RevertsIfProofAlreadyUsed() public {
bytes memory configData = abi.encode(address(mockReclaim), PROVIDER, "", "");
resolver.onConditionSet(ESCROW_ID, configData);
@@ -222,45 +187,16 @@ contract ReclaimResolverTest is Test {
signatures[0] = hex"1234";
bytes memory proofData =
- abi.encode("wrong_provider", "parameters", "{}", validIdentifier, proofOwner, timestamp, epoch, signatures);
+ abi.encode(PROVIDER, "parameters", "", validIdentifier, proofOwner, timestamp, epoch, signatures);
- vm.expectRevert(ReclaimResolver.ProviderMismatch.selector);
- resolver.submitProof(ESCROW_ID, proofData);
- }
-
- function test_SubmitProof_RevertsIfContextAddressMismatch() public {
- bytes memory configData = abi.encode(address(mockReclaim), PROVIDER, EXPECTED_ADDRESS, "");
- resolver.onConditionSet(ESCROW_ID, configData);
-
- string memory wrongContext = '{"contextAddress":"0xWrongAddress"}';
-
- bytes[] memory signatures = new bytes[](1);
- signatures[0] = hex"1234";
-
- bytes memory proofData =
- abi.encode(PROVIDER, "parameters", wrongContext, validIdentifier, proofOwner, timestamp, epoch, signatures);
-
- vm.expectRevert(ReclaimResolver.ContextAddressMismatch.selector);
resolver.submitProof(ESCROW_ID, proofData);
- }
-
- function test_SubmitProof_RevertsIfContextMessageMismatch() public {
- bytes memory configData = abi.encode(address(mockReclaim), PROVIDER, "", EXPECTED_MESSAGE);
- resolver.onConditionSet(ESCROW_ID, configData);
-
- string memory wrongContext = '{"contextMessage":"wrong_message"}';
-
- bytes[] memory signatures = new bytes[](1);
- signatures[0] = hex"1234";
-
- bytes memory proofData =
- abi.encode(PROVIDER, "parameters", wrongContext, validIdentifier, proofOwner, timestamp, epoch, signatures);
- vm.expectRevert(ReclaimResolver.ContextMessageMismatch.selector);
+ // Try to reuse the same proof
+ vm.expectRevert(ReclaimResolver.AlreadyFulfilled.selector);
resolver.submitProof(ESCROW_ID, proofData);
}
- function test_SubmitProof_RevertsIfReclaimVerificationFails() public {
+ function test_SubmitProof_RevertsIfInvalidProof() public {
bytes memory configData = abi.encode(address(mockReclaim), PROVIDER, "", "");
resolver.onConditionSet(ESCROW_ID, configData);
@@ -270,20 +206,13 @@ contract ReclaimResolverTest is Test {
signatures[0] = hex"1234";
bytes memory proofData =
- abi.encode(PROVIDER, "parameters", "{}", validIdentifier, proofOwner, timestamp, epoch, signatures);
+ abi.encode(PROVIDER, "parameters", "", validIdentifier, proofOwner, timestamp, epoch, signatures);
vm.expectRevert(ReclaimResolver.InvalidProof.selector);
resolver.submitProof(ESCROW_ID, proofData);
}
- function test_IsConditionMet_ReturnsFalseBeforeProof() public {
- bytes memory configData = abi.encode(address(mockReclaim), PROVIDER, "", "");
- resolver.onConditionSet(ESCROW_ID, configData);
-
- assertFalse(resolver.isConditionMet(ESCROW_ID));
- }
-
- function test_IsConditionMet_ReturnsTrueAfterProof() public {
+ function test_SubmitProof_RevertsIfProviderMismatch() public {
bytes memory configData = abi.encode(address(mockReclaim), PROVIDER, "", "");
resolver.onConditionSet(ESCROW_ID, configData);
@@ -291,11 +220,10 @@ contract ReclaimResolverTest is Test {
signatures[0] = hex"1234";
bytes memory proofData =
- abi.encode(PROVIDER, "parameters", "{}", validIdentifier, proofOwner, timestamp, epoch, signatures);
+ abi.encode("https", "parameters", "", validIdentifier, proofOwner, timestamp, epoch, signatures);
+ vm.expectRevert(ReclaimResolver.ProviderMismatch.selector);
resolver.submitProof(ESCROW_ID, proofData);
-
- assertTrue(resolver.isConditionMet(ESCROW_ID));
}
function test_SupportsInterface() public view {
@@ -303,16 +231,15 @@ contract ReclaimResolverTest is Test {
assertTrue(resolver.supportsInterface(resolverInterface));
}
- function testFuzz_OnConditionSet(address fuzzReclaim, string memory fuzzProvider) public {
- vm.assume(fuzzReclaim != address(0));
- vm.assume(bytes(fuzzProvider).length > 0);
- vm.assume(bytes(fuzzProvider).length < 1000);
+ function test_PauseAndUnpause() public {
+ bytes memory configData = abi.encode(address(mockReclaim), PROVIDER, EXPECTED_ADDRESS, EXPECTED_MESSAGE);
+ resolver.onConditionSet(ESCROW_ID, configData);
- bytes memory data = abi.encode(fuzzReclaim, fuzzProvider, "", "");
- resolver.onConditionSet(ESCROW_ID, data);
+ resolver.pause();
+ vm.expectRevert();
+ resolver.isConditionMet(ESCROW_ID);
- (address storedReclaim, string memory storedProvider,,,) = resolver.configs(ESCROW_ID);
- assertEq(storedReclaim, fuzzReclaim);
- assertEq(storedProvider, fuzzProvider);
+ resolver.unpause();
+ assertFalse(resolver.isConditionMet(ESCROW_ID));
}
}
diff --git a/test/TimeLockResolver.t.sol b/test/TimeLockResolver.t.sol
index e3af34c..dff5dd2 100644
--- a/test/TimeLockResolver.t.sol
+++ b/test/TimeLockResolver.t.sol
@@ -12,7 +12,10 @@ contract TimeLockResolverTest is Test {
uint256 deadline;
function setUp() public {
- resolver = new TimeLockResolver();
+ resolver = new TimeLockResolver(address(this));
+ // Grant PROTOCOL_ROLE to this test contract so it can call onConditionSet
+ resolver.grantProtocolRole(address(this));
+ resolver.grantComplianceRole(address(this));
deadline = block.timestamp + 1 days;
}
@@ -72,6 +75,30 @@ contract TimeLockResolverTest is Test {
assertTrue(resolver.supportsInterface(resolverInterface));
}
+ function test_PauseAndUnpause() public {
+ bytes memory data = abi.encode(deadline);
+ resolver.onConditionSet(ESCROW_ID, data);
+
+ // Compliance owner (address(this) by default) can pause
+ resolver.pause();
+
+ // isConditionMet should revert when paused
+ vm.expectRevert();
+ resolver.isConditionMet(ESCROW_ID);
+
+ // Unpause
+ resolver.unpause();
+ assertFalse(resolver.isConditionMet(ESCROW_ID));
+ }
+
+ function test_OnConditionSet_RevertsWhenPaused() public {
+ resolver.pause();
+
+ bytes memory data = abi.encode(deadline);
+ vm.expectRevert();
+ resolver.onConditionSet(ESCROW_ID, data);
+ }
+
function testFuzz_OnConditionSet(uint256 futureDeadline) public {
vm.assume(futureDeadline > block.timestamp);
vm.assume(futureDeadline < type(uint256).max);