From 7c498217dff9ecd0f109a9ec3f2a1e62e2e0d2d8 Mon Sep 17 00:00:00 2001 From: christian harrington Date: Fri, 31 Oct 2025 12:57:23 +0000 Subject: [PATCH 01/11] crewrapper alpha --- .../contracts/wrappers/CreAuthWrapper.sol | 298 +++++++ .../contracts/wrappers/datacacheExample.sol | 757 ++++++++++++++++++ 2 files changed, 1055 insertions(+) create mode 100644 pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol create mode 100644 pkg/pool-quantamm/contracts/wrappers/datacacheExample.sol diff --git a/pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol b/pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol new file mode 100644 index 00000000..71275f88 --- /dev/null +++ b/pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { IReceiver } from "@chainlink/contracts/src/v0.8/keystone/interfaces/IReceiver.sol"; +import { OwnerIsCreator } from "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol"; +import { ITypeAndVersion } from "@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol"; + +import { IERC165 } from "@openzeppelin/contracts@5.0.2/interfaces/IERC165.sol"; +import { IERC20 } from "@openzeppelin/contracts@5.0.2/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts@5.0.2/token/ERC20/utils/SafeERC20.sol"; + +import { IUpdateWeightRunner } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/IUpdateWeightRunner.sol"; + +contract CreAuthWrapper is IReceiver, ITypeAndVersion, OwnerIsCreator { + using SafeERC20 for IERC20; + + constructor(address updateWeightRunnerAddress) OwnerIsCreator() { + updateWeightRunner = IUpdateWeightRunner(updateWeightRunnerAddress); + } + + IUpdateWeightRunner public immutable updateWeightRunner; + + string public constant override typeAndVersion = "CreAuthWrapper 1.0.0"; + + // solhint-disable-next-line + uint256 public constant override version = 1; + + /// Cache State + + struct WorkflowMetadata { + address allowedSender; // ─╮ Address of the allowed sender + address allowedWorkflowOwner; // ─╮ Address of the workflow owner + bytes10 allowedWorkflowName; // ──╯ Name of the workflow + } + + struct FeedConfig { + // DataId. + string description; // Description of the BTF that is used + WorkflowMetadata[] workflowMetadata; // Metadata for the feed + } + + /// Addresses that are permitted to configure all feeds + mapping(address feedAdmin => bool isFeedAdmin) private s_feedAdmins; + + mapping(address poolId => FeedConfig) private s_feedConfigs; + + mapping(bytes32 poolHash => bool) private s_writePermissions; + + event FeedConfigRemoved(address indexed pool); + event TargetWeightsForwarded(address indexed pool, address sender, address workflowOwner, bytes10 workflowName); + event FeedAdminSet(address indexed feedAdmin, bool indexed isAdmin); + event InvalidUpdatePermission(address indexed pool, address sender, address workflowOwner, bytes10 workflowName); + error ArrayLengthMismatch(); + error EmptyConfig(); + error FeedNotConfigured(address pool); + error InvalidAddress(address addr); + error InvalidWorkflowName(bytes10 workflowName); + error UnauthorizedCaller(address caller); + error NoMappingForSender(address proxy); + + modifier onlyFeedAdmin() { + if (!s_feedAdmins[msg.sender]) revert UnauthorizedCaller(msg.sender); + _; + } + + /// ================================================================ + /// @notice Get the workflow metadata of a feed + /// @param pool The address of the pool for the feed + /// @param startIndex The cursor to start fetching the metadata from + /// @param maxCount The number of metadata to fetch + /// @return workflowMetadata The metadata of the feed + function getFeedMetadata( + address pool, + uint256 startIndex, + uint256 maxCount + ) external view returns (WorkflowMetadata[] memory workflowMetadata) { + FeedConfig storage feedConfig = s_feedConfigs[pool]; + + uint256 workflowMetadataLength = feedConfig.workflowMetadata.length; + + if (workflowMetadataLength == 0) { + revert FeedNotConfigured(pool); + } + + if (startIndex >= workflowMetadataLength) return new WorkflowMetadata[](0); + uint256 endIndex = startIndex + maxCount; + endIndex = endIndex > workflowMetadataLength || maxCount == 0 ? workflowMetadataLength : endIndex; + + workflowMetadata = new WorkflowMetadata[](endIndex - startIndex); + for (uint256 idx; idx < workflowMetadata.length; idx++) { + workflowMetadata[idx] = feedConfig.workflowMetadata[idx + startIndex]; + } + + return workflowMetadata; + } + + /// @notice Checks to see if this data ID, msg.sender, workflow owner, and workflow name are permissioned + /// @param pool The address of the pool for the feed + /// @param workflowMetadata workflow metadata + function checkFeedPermission( + address pool, + WorkflowMetadata memory workflowMetadata + ) external view returns (bool hasPermission) { + bytes32 permission = _createPoolHash( + pool, + workflowMetadata.allowedSender, + workflowMetadata.allowedWorkflowOwner, + workflowMetadata.allowedWorkflowName + ); + return s_writePermissions[permission]; + } + + // ================================================================ + // │ Contract Config Interface │ + // ================================================================ + + /// @notice Initializes the config for a pool feed + /// @param pools The addresses of the pools to configure + /// @param descriptions The descriptions of the feeds + /// @param workflowMetadata List of workflow metadata (owners, senders, and names) for every feed + function setPoolFeedConfigs( + address[] calldata pools, + string[] calldata descriptions, + WorkflowMetadata[] calldata workflowMetadata + ) external onlyFeedAdmin { + if (workflowMetadata.length == 0 || pools.length == 0) { + revert EmptyConfig(); + } + + if (pools.length != descriptions.length) { + revert ArrayLengthMismatch(); + } + + for (uint256 i; i < pools.length; ++i) { + address pool = pools[i]; + if (pool == address(0)) revert InvalidAddress(pool); + FeedConfig storage feedConfig = s_feedConfigs[pool]; + + if (feedConfig.workflowMetadata.length > 0) { + // Feed is already configured, remove the previous config + for (uint256 j; j < feedConfig.workflowMetadata.length; ++j) { + WorkflowMetadata memory feedCurrentWorkflowMetadata = feedConfig.workflowMetadata[j]; + bytes32 poolHash = _createPoolHash( + pool, + feedCurrentWorkflowMetadata.allowedSender, + feedCurrentWorkflowMetadata.allowedWorkflowOwner, + feedCurrentWorkflowMetadata.allowedWorkflowName + ); + delete s_writePermissions[poolHash]; + } + + delete s_feedConfigs[pool]; + + emit FeedConfigRemoved(pool); + } + + for (uint256 j; j < workflowMetadata.length; ++j) { + WorkflowMetadata memory feedWorkflowMetadata = workflowMetadata[j]; + // Do those checks only once for the first data id + if (i == 0) { + if (feedWorkflowMetadata.allowedSender == address(0)) { + revert InvalidAddress(feedWorkflowMetadata.allowedSender); + } + if (feedWorkflowMetadata.allowedWorkflowOwner == address(0)) { + revert InvalidAddress(feedWorkflowMetadata.allowedWorkflowOwner); + } + if (feedWorkflowMetadata.allowedWorkflowName == bytes10(0)) { + revert InvalidWorkflowName(feedWorkflowMetadata.allowedWorkflowName); + } + } + + bytes32 poolHash = _createPoolHash( + pool, + feedWorkflowMetadata.allowedSender, + feedWorkflowMetadata.allowedWorkflowOwner, + feedWorkflowMetadata.allowedWorkflowName + ); + s_writePermissions[poolHash] = true; + feedConfig.workflowMetadata.push(feedWorkflowMetadata); + } + + feedConfig.description = descriptions[i]; + } + } + + /// @notice Removes feeds and all associated data, for a set of feeds + /// @param pools And array of data IDs to delete the data and configs of + function removeFeedConfigs(address[] calldata pools) external onlyFeedAdmin { + for (uint256 i; i < pools.length; ++i) { + address pool = pools[i]; + if (s_feedConfigs[pool].workflowMetadata.length == 0) revert FeedNotConfigured(pool); + + for (uint256 j; j < s_feedConfigs[pool].workflowMetadata.length; ++j) { + WorkflowMetadata memory feedWorkflowMetadata = s_feedConfigs[pool].workflowMetadata[j]; + bytes32 poolHash = _createPoolHash( + pool, + feedWorkflowMetadata.allowedSender, + feedWorkflowMetadata.allowedWorkflowOwner, + feedWorkflowMetadata.allowedWorkflowName + ); + delete s_writePermissions[poolHash]; + } + + delete s_feedConfigs[pool]; + + emit FeedConfigRemoved(pool); + } + } + + /// @notice Sets a feed admin for all feeds, only callable by the Owner + /// @param feedAdmin The feed admin + function setFeedAdmin(address feedAdmin, bool isAdmin) external onlyOwner { + if (feedAdmin == address(0)) revert InvalidAddress(feedAdmin); + + s_feedAdmins[feedAdmin] = isAdmin; + emit FeedAdminSet(feedAdmin, isAdmin); + } + + /// @notice Returns a bool is an address has feed admin permission for all feeds + /// @param feedAdmin The feed admin + /// @return isFeedAdmin bool if the address is the feed admin for all feeds + function isFeedAdmin(address feedAdmin) external view returns (bool) { + return s_feedAdmins[feedAdmin]; + } + + // ================================================================ + // │ Helper Methods │ + // ================================================================ + + /// @notice Extracts the workflow name and the workflow owner from the metadata parameter of onReport + /// @param metadata The metadata in bytes format + /// @return workflowOwner The owner of the workflow + /// @return workflowName The name of the workflow + function _getWorkflowMetaData(bytes memory metadata) internal pure returns (address, bytes10) { + address workflowOwner; + bytes10 workflowName; + // (first 32 bytes contain length of the byte array) + // workflow_cid // offset 32, size 32 + // workflow_name // offset 64, size 10 + // workflow_owner // offset 74, size 20 + // pool_name // offset 94, size 2 + assembly { + // no shifting needed for bytes10 type + workflowName := mload(add(metadata, 64)) + // shift right by 12 bytes to get the actual value + workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) + } + return (workflowOwner, workflowName); + } + + /// @notice Creates a pool hash used to permission write access + /// @param poolId The pool ID for the feed + /// @param sender The msg.sender of the transaction calling into onpool + /// @param workflowOwner The owner of the workflow + /// @param workflowName The name of the workflow + /// @return poolHash The keccak256 hash of the abi.encoded inputs + function _createPoolHash( + address pool, + address sender, + address workflowOwner, + bytes10 workflowName + ) internal pure returns (bytes32) { + return keccak256(abi.encode(pool, sender, workflowOwner, workflowName)); + } + + /// @notice Wrapper that verifies metadata and write permissions, then forwards to UpdateWeightRunner.setTargetWeightManually. + function setTargetWeightManuallyWithMeta( + int256[] calldata _weights, // 1e18-scaled, non-negative + address _poolAddress, + uint40 _lastInterpolationTimePossible, + uint256 _numberOfAssets, + WeightUpdateMeta calldata _meta + ) external { + (address workflowOwner, bytes10 workflowName) = _getWorkflowMetaData(_meta); + + bytes32 permission = _createPoolHash( + _poolAddress, + msg.sender, + workflowOwner, + workflowName + ); + + if (!s_writePermissions[permission]) { + emit InvalidUpdatePermission(_poolAddress, msg.sender, workflowOwner, workflowName); + revert UnauthorizedCaller(msg.sender); + } + + // 6) Forward the call to the UpdateWeightRunner + _updateWeightRunner.setTargetWeightManually(_weights, _poolAddress, _lastInterpolationTimePossible, _numberOfAssets); + + emit TargetWeightsForwarded( + _poolAddress, + msg.sender, + workflowOwner, + workflowName + ); + } +} diff --git a/pkg/pool-quantamm/contracts/wrappers/datacacheExample.sol b/pkg/pool-quantamm/contracts/wrappers/datacacheExample.sol new file mode 100644 index 00000000..6e33d13e --- /dev/null +++ b/pkg/pool-quantamm/contracts/wrappers/datacacheExample.sol @@ -0,0 +1,757 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IReceiver} from "../keystone/interfaces/IReceiver.sol"; +import {OwnerIsCreator} from "../shared/access/OwnerIsCreator.sol"; +import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol"; +import {IDataFeedsCache} from "./interfaces/IDataFeedsCache.sol"; +import {ITokenRecover} from "./interfaces/ITokenRecover.sol"; + +import {IERC165} from "@openzeppelin/contracts@5.0.2/interfaces/IERC165.sol"; +import {IERC20} from "@openzeppelin/contracts@5.0.2/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts@5.0.2/token/ERC20/utils/SafeERC20.sol"; + +contract DataFeedsCache is IDataFeedsCache, IReceiver, ITokenRecover, ITypeAndVersion, OwnerIsCreator { + using SafeERC20 for IERC20; + + string public constant override typeAndVersion = "DataFeedsCache 1.0.0"; + + // solhint-disable-next-line + uint256 public constant override version = 7; + + /// Cache State + + struct WorkflowMetadata { + address allowedSender; // Address of the sender allowed to send new reports + address allowedWorkflowOwner; // ─╮ Address of the workflow owner + bytes10 allowedWorkflowName; // ──╯ Name of the workflow + } + + struct FeedConfig { + uint8[] bundleDecimals; // Only appliciable to Bundle reports - Decimal reports have decimals encoded into the + // DataId. + string description; // Description of the feed (e.g. "LINK / USD") + WorkflowMetadata[] workflowMetadata; // Metadata for the feed + } + + struct ReceivedBundleReport { + bytes32 dataId; // Data ID of the feed from the received report + uint32 timestamp; // Timestamp of the feed from the received report + bytes bundle; // Report data in raw bytes + } + + struct ReceivedDecimalReport { + bytes32 dataId; // Data ID of the feed from the received report + uint32 timestamp; // ─╮ Timestamp of the feed from the received report + uint224 answer; // ───╯ Report data in uint224 + } + + struct StoredBundleReport { + bytes bundle; // The latest bundle report stored for a feed + uint32 timestamp; // The timestamp of the latest bundle report + } + + struct StoredDecimalReport { + uint224 answer; // ───╮ The latest decimal report stored for a feed + uint32 timestamp; // ─╯ The timestamp of the latest decimal report + } + + /// The message sender determines which feed is being requested, as each proxy has a single associated feed + mapping(address aggProxy => bytes16 dataId) private s_aggregatorProxyToDataId; + + /// The latest decimal reports for each decimal feed. This will always equal + /// s_decimalReports[s_dataIdToRoundId[dataId]][dataId] + mapping(bytes16 dataId => StoredDecimalReport) private s_latestDecimalReports; + + /// Decimal reports for each feed, per round + mapping(uint256 roundId => mapping(bytes16 dataId => StoredDecimalReport)) private s_decimalReports; + + /// The latest bundle reports for each bundle feed + mapping(bytes16 dataId => StoredBundleReport) private s_latestBundleReports; + + /// The latest round id for each feed + mapping(bytes16 dataId => uint256 roundId) private s_dataIdToRoundId; + + /// Addresses that are permitted to configure all feeds + mapping(address feedAdmin => bool isFeedAdmin) private s_feedAdmins; + + mapping(bytes16 dataId => FeedConfig) private s_feedConfigs; + + /// Whether a given Sender and Workflow have permission to write feed updates. + /// reportHash is the keccak256 hash of the abi.encoded(dataId, sender, workflowOwner and workflowName) + mapping(bytes32 reportHash => bool) private s_writePermissions; + + event BundleReportUpdated(bytes16 indexed dataId, uint256 indexed timestamp, bytes bundle); + event DecimalReportUpdated( + bytes16 indexed dataId, uint256 indexed roundId, uint256 indexed timestamp, uint224 answer + ); + event DecimalFeedConfigSet( + bytes16 indexed dataId, uint8 decimals, string description, WorkflowMetadata[] workflowMetadata + ); + event BundleFeedConfigSet( + bytes16 indexed dataId, uint8[] decimals, string description, WorkflowMetadata[] workflowMetadata + ); + event FeedConfigRemoved(bytes16 indexed dataId); + event TokenRecovered(address indexed token, address indexed to, uint256 amount); + + event FeedAdminSet(address indexed feedAdmin, bool indexed isAdmin); + + event ProxyDataIdRemoved(address indexed proxy, bytes16 indexed dataId); + event ProxyDataIdUpdated(address indexed proxy, bytes16 indexed dataId); + + event InvalidUpdatePermission(bytes16 indexed dataId, address sender, address workflowOwner, bytes10 workflowName); + event StaleDecimalReport(bytes16 indexed dataId, uint256 reportTimestamp, uint256 latestTimestamp); + event StaleBundleReport(bytes16 indexed dataId, uint256 reportTimestamp, uint256 latestTimestamp); + + error ArrayLengthMismatch(); + error EmptyConfig(); + error ErrorSendingNative(address to, uint256 amount, bytes data); + error FeedNotConfigured(bytes16 dataId); + error InsufficientBalance(uint256 balance, uint256 requiredBalance); + error InvalidAddress(address addr); + error InvalidDataId(); + error InvalidWorkflowName(bytes10 workflowName); + error UnauthorizedCaller(address caller); + error NoMappingForSender(address proxy); + + modifier onlyFeedAdmin() { + if (!s_feedAdmins[msg.sender]) revert UnauthorizedCaller(msg.sender); + _; + } + + /// @inheritdoc IERC165 + function supportsInterface( + bytes4 interfaceId + ) public pure returns (bool) { + return ( + interfaceId == type(IDataFeedsCache).interfaceId || interfaceId == type(IERC165).interfaceId + || interfaceId == type(IReceiver).interfaceId || interfaceId == type(ITokenRecover).interfaceId + || interfaceId == type(ITypeAndVersion).interfaceId + ); + } + + /// @notice Get the workflow metadata of a feed + /// @param dataId data ID of the feed + /// @param startIndex The cursor to start fetching the metadata from + /// @param maxCount The number of metadata to fetch + /// @return workflowMetadata The metadata of the feed + function getFeedMetadata( + bytes16 dataId, + uint256 startIndex, + uint256 maxCount + ) external view returns (WorkflowMetadata[] memory workflowMetadata) { + FeedConfig storage feedConfig = s_feedConfigs[dataId]; + + uint256 workflowMetadataLength = feedConfig.workflowMetadata.length; + + if (workflowMetadataLength == 0) { + revert FeedNotConfigured(dataId); + } + + if (startIndex >= workflowMetadataLength) return new WorkflowMetadata[](0); + uint256 endIndex = startIndex + maxCount; + endIndex = endIndex > workflowMetadataLength || maxCount == 0 ? workflowMetadataLength : endIndex; + + workflowMetadata = new WorkflowMetadata[](endIndex - startIndex); + for (uint256 idx; idx < workflowMetadata.length; idx++) { + workflowMetadata[idx] = feedConfig.workflowMetadata[idx + startIndex]; + } + + return workflowMetadata; + } + + /// @notice Checks to see if this data ID, msg.sender, workflow owner, and workflow name are permissioned + /// @param dataId The data ID for the feed + /// @param workflowMetadata workflow metadata + function checkFeedPermission( + bytes16 dataId, + WorkflowMetadata memory workflowMetadata + ) external view returns (bool hasPermission) { + bytes32 permission = _createReportHash( + dataId, + workflowMetadata.allowedSender, + workflowMetadata.allowedWorkflowOwner, + workflowMetadata.allowedWorkflowName + ); + return s_writePermissions[permission]; + } + + // ================================================================ + // │ Contract Config Interface │ + // ================================================================ + + /// @notice Initializes the config for a decimal feed + /// @param dataIds The data IDs of the feeds to configure + /// @param descriptions The descriptions of the feeds + /// @param workflowMetadata List of workflow metadata (owners, senders, and names) for every feed + function setDecimalFeedConfigs( + bytes16[] calldata dataIds, + string[] calldata descriptions, + WorkflowMetadata[] calldata workflowMetadata + ) external onlyFeedAdmin { + if (workflowMetadata.length == 0 || dataIds.length == 0) { + revert EmptyConfig(); + } + + if (dataIds.length != descriptions.length) { + revert ArrayLengthMismatch(); + } + + for (uint256 i; i < dataIds.length; ++i) { + bytes16 dataId = dataIds[i]; + if (dataId == bytes16(0)) revert InvalidDataId(); + FeedConfig storage feedConfig = s_feedConfigs[dataId]; + + if (feedConfig.workflowMetadata.length > 0) { + // Feed is already configured, remove the previous config + for (uint256 j; j < feedConfig.workflowMetadata.length; ++j) { + WorkflowMetadata memory feedCurrentWorkflowMetadata = feedConfig.workflowMetadata[j]; + bytes32 reportHash = _createReportHash( + dataId, + feedCurrentWorkflowMetadata.allowedSender, + feedCurrentWorkflowMetadata.allowedWorkflowOwner, + feedCurrentWorkflowMetadata.allowedWorkflowName + ); + delete s_writePermissions[reportHash]; + } + + delete s_feedConfigs[dataId]; + + emit FeedConfigRemoved(dataId); + } + + for (uint256 j; j < workflowMetadata.length; ++j) { + WorkflowMetadata memory feedWorkflowMetadata = workflowMetadata[j]; + // Do those checks only once for the first data id + if (i == 0) { + if (feedWorkflowMetadata.allowedSender == address(0)) { + revert InvalidAddress(feedWorkflowMetadata.allowedSender); + } + if (feedWorkflowMetadata.allowedWorkflowOwner == address(0)) { + revert InvalidAddress(feedWorkflowMetadata.allowedWorkflowOwner); + } + if (feedWorkflowMetadata.allowedWorkflowName == bytes10(0)) { + revert InvalidWorkflowName(feedWorkflowMetadata.allowedWorkflowName); + } + } + + bytes32 reportHash = _createReportHash( + dataId, + feedWorkflowMetadata.allowedSender, + feedWorkflowMetadata.allowedWorkflowOwner, + feedWorkflowMetadata.allowedWorkflowName + ); + s_writePermissions[reportHash] = true; + feedConfig.workflowMetadata.push(feedWorkflowMetadata); + } + + feedConfig.description = descriptions[i]; + + emit DecimalFeedConfigSet({ + dataId: dataId, + decimals: _getDecimals(dataId), + description: descriptions[i], + workflowMetadata: workflowMetadata + }); + } + } + + /// @notice Initializes the config for a bundle feed + /// @param dataIds The data IDs of the feeds to configure + /// @param descriptions The descriptions of the feeds + /// @param decimalsMatrix The number of decimals for each data point in the bundle for the feed + /// @param workflowMetadata List of workflow metadata (owners, senders, and names) for every feed + function setBundleFeedConfigs( + bytes16[] calldata dataIds, + string[] calldata descriptions, + uint8[][] calldata decimalsMatrix, + WorkflowMetadata[] calldata workflowMetadata + ) external onlyFeedAdmin { + if (workflowMetadata.length == 0 || dataIds.length == 0) { + revert EmptyConfig(); + } + + if (dataIds.length != descriptions.length || dataIds.length != decimalsMatrix.length) { + revert ArrayLengthMismatch(); + } + + for (uint256 i; i < dataIds.length; ++i) { + bytes16 dataId = dataIds[i]; + if (dataId == bytes16(0)) revert InvalidDataId(); + FeedConfig storage feedConfig = s_feedConfigs[dataId]; + + if (feedConfig.workflowMetadata.length > 0) { + // Feed is already configured, remove the previous config + for (uint256 j; j < feedConfig.workflowMetadata.length; ++j) { + WorkflowMetadata memory feedCurrentWorkflowMetadata = feedConfig.workflowMetadata[j]; + bytes32 reportHash = _createReportHash( + dataId, + feedCurrentWorkflowMetadata.allowedSender, + feedCurrentWorkflowMetadata.allowedWorkflowOwner, + feedCurrentWorkflowMetadata.allowedWorkflowName + ); + delete s_writePermissions[reportHash]; + } + + delete s_feedConfigs[dataId]; + + emit FeedConfigRemoved(dataId); + } + + for (uint256 j; j < workflowMetadata.length; ++j) { + WorkflowMetadata memory feedWorkflowMetadata = workflowMetadata[j]; + // Do those checks only once for the first data id + if (i == 0) { + if (feedWorkflowMetadata.allowedSender == address(0)) { + revert InvalidAddress(feedWorkflowMetadata.allowedSender); + } + if (feedWorkflowMetadata.allowedWorkflowOwner == address(0)) { + revert InvalidAddress(feedWorkflowMetadata.allowedWorkflowOwner); + } + if (feedWorkflowMetadata.allowedWorkflowName == bytes10(0)) { + revert InvalidWorkflowName(feedWorkflowMetadata.allowedWorkflowName); + } + } + + bytes32 reportHash = _createReportHash( + dataId, + feedWorkflowMetadata.allowedSender, + feedWorkflowMetadata.allowedWorkflowOwner, + feedWorkflowMetadata.allowedWorkflowName + ); + s_writePermissions[reportHash] = true; + feedConfig.workflowMetadata.push(feedWorkflowMetadata); + } + + feedConfig.bundleDecimals = decimalsMatrix[i]; + feedConfig.description = descriptions[i]; + + emit BundleFeedConfigSet({ + dataId: dataId, + decimals: decimalsMatrix[i], + description: descriptions[i], + workflowMetadata: workflowMetadata + }); + } + } + + /// @notice Removes feeds and all associated data, for a set of feeds + /// @param dataIds And array of data IDs to delete the data and configs of + function removeFeedConfigs( + bytes16[] calldata dataIds + ) external onlyFeedAdmin { + for (uint256 i; i < dataIds.length; ++i) { + bytes16 dataId = dataIds[i]; + if (s_feedConfigs[dataId].workflowMetadata.length == 0) revert FeedNotConfigured(dataId); + + for (uint256 j; j < s_feedConfigs[dataId].workflowMetadata.length; ++j) { + WorkflowMetadata memory feedWorkflowMetadata = s_feedConfigs[dataId].workflowMetadata[j]; + bytes32 reportHash = _createReportHash( + dataId, + feedWorkflowMetadata.allowedSender, + feedWorkflowMetadata.allowedWorkflowOwner, + feedWorkflowMetadata.allowedWorkflowName + ); + delete s_writePermissions[reportHash]; + } + + delete s_feedConfigs[dataId]; + + emit FeedConfigRemoved(dataId); + } + } + + /// @notice Sets a feed admin for all feeds, only callable by the Owner + /// @param feedAdmin The feed admin + function setFeedAdmin(address feedAdmin, bool isAdmin) external onlyOwner { + if (feedAdmin == address(0)) revert InvalidAddress(feedAdmin); + + s_feedAdmins[feedAdmin] = isAdmin; + emit FeedAdminSet(feedAdmin, isAdmin); + } + + /// @notice Returns a bool is an address has feed admin permission for all feeds + /// @param feedAdmin The feed admin + /// @return isFeedAdmin bool if the address is the feed admin for all feeds + function isFeedAdmin( + address feedAdmin + ) external view returns (bool) { + return s_feedAdmins[feedAdmin]; + } + + /// @inheritdoc IDataFeedsCache + function updateDataIdMappingsForProxies( + address[] calldata proxies, + bytes16[] calldata dataIds + ) external onlyFeedAdmin { + uint256 numberOfProxies = proxies.length; + if (numberOfProxies != dataIds.length) revert ArrayLengthMismatch(); + + for (uint256 i; i < numberOfProxies; i++) { + s_aggregatorProxyToDataId[proxies[i]] = dataIds[i]; + + emit ProxyDataIdUpdated(proxies[i], dataIds[i]); + } + } + + /// @inheritdoc IDataFeedsCache + function getDataIdForProxy( + address proxy + ) external view returns (bytes16 dataId) { + return s_aggregatorProxyToDataId[proxy]; + } + + /// @inheritdoc IDataFeedsCache + function removeDataIdMappingsForProxies( + address[] calldata proxies + ) external onlyFeedAdmin { + uint256 numberOfProxies = proxies.length; + + for (uint256 i; i < numberOfProxies; i++) { + address proxy = proxies[i]; + bytes16 dataId = s_aggregatorProxyToDataId[proxy]; + delete s_aggregatorProxyToDataId[proxy]; + emit ProxyDataIdRemoved(proxy, dataId); + } + } + + // ================================================================ + // │ Token Transfer Interface │ + // ================================================================ + + /// @inheritdoc ITokenRecover + function recoverTokens(IERC20 token, address to, uint256 amount) external onlyOwner { + if (address(token) == address(0)) { + if (amount > address(this).balance) { + revert InsufficientBalance(address(this).balance, amount); + } + (bool success, bytes memory data) = to.call{value: amount}(""); + if (!success) revert ErrorSendingNative(to, amount, data); + } else { + if (amount > token.balanceOf(address(this))) { + revert InsufficientBalance(token.balanceOf(address(this)), amount); + } + token.safeTransfer(to, amount); + } + emit TokenRecovered(address(token), to, amount); + } + + // ================================================================ + // │ Cache Update Interface │ + // ================================================================ + + /// @inheritdoc IReceiver + function onReport(bytes calldata metadata, bytes calldata report) external { + (address workflowOwner, bytes10 workflowName) = _getWorkflowMetaData(metadata); + + // The first 32 bytes is the offset to the array + // The second 32 bytes is the length of the array + uint256 numReports = uint256(bytes32(report[32:64])); + + // Decimal reports contain 96 bytes per report + // The total length should equal to the sum of: + // 32 bytes for the offset + // 32 bytes for the number of reports + // the number of reports times 96 + if (report.length == numReports * 96 + 64) { + ReceivedDecimalReport[] memory decodedDecimalReports = abi.decode(report, (ReceivedDecimalReport[])); + for (uint256 i; i < numReports; ++i) { + ReceivedDecimalReport memory decodedDecimalReport = decodedDecimalReports[i]; + // single dataId can have multiple permissions, to be updated by multiple Workflows + bytes16 dataId = bytes16(decodedDecimalReport.dataId); + bytes32 permission = _createReportHash(dataId, msg.sender, workflowOwner, workflowName); + if (!s_writePermissions[permission]) { + emit InvalidUpdatePermission(dataId, msg.sender, workflowOwner, workflowName); + continue; + } + + if (decodedDecimalReport.timestamp <= s_latestDecimalReports[dataId].timestamp) { + emit StaleDecimalReport(dataId, decodedDecimalReport.timestamp, s_latestDecimalReports[dataId].timestamp); + continue; + } + + StoredDecimalReport memory decimalReport = + StoredDecimalReport({answer: decodedDecimalReport.answer, timestamp: decodedDecimalReport.timestamp}); + + uint256 roundId = ++s_dataIdToRoundId[dataId]; + + s_latestDecimalReports[dataId] = decimalReport; + s_decimalReports[roundId][dataId] = decimalReport; + + emit DecimalReportUpdated(dataId, roundId, decimalReport.timestamp, decimalReport.answer); + + // Needed for DF1 backward compatibility + emit NewRound(roundId, address(0), decodedDecimalReport.timestamp); + emit AnswerUpdated(int256(uint256(decodedDecimalReport.answer)), roundId, block.timestamp); + } + } + // Bundle reports contain more bytes for the offsets + // The total length should equal to the sum of: + // 32 bytes for the offset + // 32 bytes for the number of reports + // the number of reports times 224 + else { + //For byte reports decode using ReceivedFeedReportBundle struct + ReceivedBundleReport[] memory decodedBundleReports = abi.decode(report, (ReceivedBundleReport[])); + for (uint256 i; i < decodedBundleReports.length; ++i) { + ReceivedBundleReport memory decodedBundleReport = decodedBundleReports[i]; + bytes16 dataId = bytes16(decodedBundleReport.dataId); + // same dataId can have multiple permissions + bytes32 permission = _createReportHash(dataId, msg.sender, workflowOwner, workflowName); + if (!s_writePermissions[permission]) { + emit InvalidUpdatePermission(dataId, msg.sender, workflowOwner, workflowName); + continue; + } + + if (decodedBundleReport.timestamp <= s_latestBundleReports[dataId].timestamp) { + emit StaleBundleReport(dataId, decodedBundleReport.timestamp, s_latestBundleReports[dataId].timestamp); + continue; + } + + StoredBundleReport memory bundleReport = + StoredBundleReport({bundle: decodedBundleReport.bundle, timestamp: decodedBundleReport.timestamp}); + + s_latestBundleReports[dataId] = bundleReport; + + emit BundleReportUpdated(dataId, bundleReport.timestamp, bundleReport.bundle); + } + } + } + // ================================================================ + // │ Helper Methods │ + // ================================================================ + + /// @notice Gets the Decimals of the feed from the data Id + /// @param dataId The data ID for the feed + /// @return feedDecimals The number of decimals the feed has + function _getDecimals( + bytes16 dataId + ) internal pure returns (uint8 feedDecimals) { + // Get the report type from data id. Report type has index of 7 + bytes1 reportType = _getDataType(dataId, 7); + + // For decimal reports convert to uint8, then shift + if (reportType >= hex"20" && reportType <= hex"60") { + return uint8(reportType) - 32; + } + + // If not decimal type, return 0 + return 0; + } + + /// @notice Extracts the workflow name and the workflow owner from the metadata parameter of onReport + /// @param metadata The metadata in bytes format + /// @return workflowOwner The owner of the workflow + /// @return workflowName The name of the workflow + function _getWorkflowMetaData( + bytes memory metadata + ) internal pure returns (address, bytes10) { + address workflowOwner; + bytes10 workflowName; + // (first 32 bytes contain length of the byte array) + // workflow_cid // offset 32, size 32 + // workflow_name // offset 64, size 10 + // workflow_owner // offset 74, size 20 + // report_name // offset 94, size 2 + assembly { + // no shifting needed for bytes10 type + workflowName := mload(add(metadata, 64)) + // shift right by 12 bytes to get the actual value + workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) + } + return (workflowOwner, workflowName); + } + + /// @notice Extracts a byte from the data ID, to check data types + /// @param dataId The data ID for the feed + /// @param index The index of the byte to extract from the data Id + /// @return dataType result The keccak256 hash of the abi.encoded inputs + function _getDataType(bytes16 dataId, uint256 index) internal pure returns (bytes1 dataType) { + // Convert bytes16 to bytes + return abi.encodePacked(dataId)[index]; + } + + /// @notice Creates a report hash used to permission write access + /// @param dataId The data ID for the feed + /// @param sender The msg.sender of the transaction calling into onReport + /// @param workflowOwner The owner of the workflow + /// @param workflowName The name of the workflow + /// @return reportHash The keccak256 hash of the abi.encoded inputs + function _createReportHash( + bytes16 dataId, + address sender, + address workflowOwner, + bytes10 workflowName + ) internal pure returns (bytes32) { + return keccak256(abi.encode(dataId, sender, workflowOwner, workflowName)); + } + + // ================================================================ + // │ Data Access Interface │ + // ================================================================ + + /// Bundle Feed Interface + + function latestBundle() external view returns (bytes memory bundle) { + bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; + if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); + + return (s_latestBundleReports[dataId].bundle); + } + + function bundleDecimals() external view returns (uint8[] memory bundleFeedDecimals) { + bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; + if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); + + return s_feedConfigs[dataId].bundleDecimals; + } + + function latestBundleTimestamp() external view returns (uint256 timestamp) { + bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; + if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); + + return s_latestBundleReports[dataId].timestamp; + } + + /// AggregatorInterface + + function latestAnswer() external view returns (int256 answer) { + bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; + if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); + + return int256(uint256(s_latestDecimalReports[dataId].answer)); + } + + function latestTimestamp() external view returns (uint256 timestamp) { + bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; + if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); + + return s_latestDecimalReports[dataId].timestamp; + } + + function latestRound() external view returns (uint256 round) { + bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; + if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); + + return s_dataIdToRoundId[dataId]; + } + + function getAnswer( + uint256 roundId + ) external view returns (int256 answer) { + bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; + if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); + + return int256(uint256(s_decimalReports[roundId][dataId].answer)); + } + + function getTimestamp( + uint256 roundId + ) external view returns (uint256 timestamp) { + bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; + if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); + + return s_decimalReports[roundId][dataId].timestamp; + } + + /// AggregatorV3Interface + + function decimals() external view returns (uint8 feedDecimals) { + bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; + if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); + return _getDecimals(dataId); + } + + function description() external view returns (string memory feedDescription) { + bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; + if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); + + return s_feedConfigs[dataId].description; + } + + function getRoundData( + uint80 roundId + ) external view returns (uint80 id, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { + bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; + if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); + + uint256 timestamp = s_decimalReports[uint256(roundId)][dataId].timestamp; + + return (roundId, int256(uint256(s_decimalReports[uint256(roundId)][dataId].answer)), timestamp, timestamp, roundId); + } + + function latestRoundData() + external + view + returns (uint80 id, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; + if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); + + uint80 roundId = uint80(s_dataIdToRoundId[dataId]); + uint256 timestamp = s_latestDecimalReports[dataId].timestamp; + + return (roundId, int256(uint256(s_latestDecimalReports[dataId].answer)), timestamp, timestamp, roundId); + } + + /// Direct access + function getLatestBundle( + bytes16 dataId + ) external view returns (bytes memory bundle) { + if (dataId == bytes16(0)) revert InvalidDataId(); + return (s_latestBundleReports[dataId].bundle); + } + + function getBundleDecimals( + bytes16 dataId + ) external view returns (uint8[] memory bundleFeedDecimals) { + if (dataId == bytes16(0)) revert InvalidDataId(); + return s_feedConfigs[dataId].bundleDecimals; + } + + function getLatestBundleTimestamp( + bytes16 dataId + ) external view returns (uint256 timestamp) { + if (dataId == bytes16(0)) revert InvalidDataId(); + return s_latestBundleReports[dataId].timestamp; + } + + function getLatestAnswer( + bytes16 dataId + ) external view returns (int256 answer) { + if (dataId == bytes16(0)) revert InvalidDataId(); + return int256(uint256(s_latestDecimalReports[dataId].answer)); + } + + function getLatestTimestamp( + bytes16 dataId + ) external view returns (uint256 timestamp) { + if (dataId == bytes16(0)) revert InvalidDataId(); + return s_latestDecimalReports[dataId].timestamp; + } + + function getLatestRoundData( + bytes16 dataId + ) external view returns (uint80 id, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { + if (dataId == bytes16(0)) revert InvalidDataId(); + + uint80 roundId = uint80(s_dataIdToRoundId[dataId]); + uint256 timestamp = s_latestDecimalReports[dataId].timestamp; + + return (roundId, int256(uint256(s_latestDecimalReports[dataId].answer)), timestamp, timestamp, roundId); + } + + function getDecimals( + bytes16 dataId + ) external pure returns (uint8 feedDecimals) { + if (dataId == bytes16(0)) revert InvalidDataId(); + return _getDecimals(dataId); + } + + function getDescription( + bytes16 dataId + ) external view returns (string memory feedDescription) { + if (dataId == bytes16(0)) revert InvalidDataId(); + return s_feedConfigs[dataId].description; + } +} \ No newline at end of file From 5848d00a6de4d1ad3a843f55262572729c606731 Mon Sep 17 00:00:00 2001 From: christian harrington Date: Thu, 13 Nov 2025 00:03:50 +0000 Subject: [PATCH 02/11] make to a non generic wrapper --- .../contracts/wrappers/CreAuthWrapper.sol | 298 ------- .../wrappers/TruflationCreAuthWrapper.sol | 210 +++++ .../contracts/wrappers/datacacheExample.sol | 757 ------------------ 3 files changed, 210 insertions(+), 1055 deletions(-) delete mode 100644 pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol create mode 100644 pkg/pool-quantamm/contracts/wrappers/TruflationCreAuthWrapper.sol delete mode 100644 pkg/pool-quantamm/contracts/wrappers/datacacheExample.sol diff --git a/pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol b/pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol deleted file mode 100644 index 71275f88..00000000 --- a/pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol +++ /dev/null @@ -1,298 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import { IReceiver } from "@chainlink/contracts/src/v0.8/keystone/interfaces/IReceiver.sol"; -import { OwnerIsCreator } from "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol"; -import { ITypeAndVersion } from "@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol"; - -import { IERC165 } from "@openzeppelin/contracts@5.0.2/interfaces/IERC165.sol"; -import { IERC20 } from "@openzeppelin/contracts@5.0.2/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts@5.0.2/token/ERC20/utils/SafeERC20.sol"; - -import { IUpdateWeightRunner } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/IUpdateWeightRunner.sol"; - -contract CreAuthWrapper is IReceiver, ITypeAndVersion, OwnerIsCreator { - using SafeERC20 for IERC20; - - constructor(address updateWeightRunnerAddress) OwnerIsCreator() { - updateWeightRunner = IUpdateWeightRunner(updateWeightRunnerAddress); - } - - IUpdateWeightRunner public immutable updateWeightRunner; - - string public constant override typeAndVersion = "CreAuthWrapper 1.0.0"; - - // solhint-disable-next-line - uint256 public constant override version = 1; - - /// Cache State - - struct WorkflowMetadata { - address allowedSender; // ─╮ Address of the allowed sender - address allowedWorkflowOwner; // ─╮ Address of the workflow owner - bytes10 allowedWorkflowName; // ──╯ Name of the workflow - } - - struct FeedConfig { - // DataId. - string description; // Description of the BTF that is used - WorkflowMetadata[] workflowMetadata; // Metadata for the feed - } - - /// Addresses that are permitted to configure all feeds - mapping(address feedAdmin => bool isFeedAdmin) private s_feedAdmins; - - mapping(address poolId => FeedConfig) private s_feedConfigs; - - mapping(bytes32 poolHash => bool) private s_writePermissions; - - event FeedConfigRemoved(address indexed pool); - event TargetWeightsForwarded(address indexed pool, address sender, address workflowOwner, bytes10 workflowName); - event FeedAdminSet(address indexed feedAdmin, bool indexed isAdmin); - event InvalidUpdatePermission(address indexed pool, address sender, address workflowOwner, bytes10 workflowName); - error ArrayLengthMismatch(); - error EmptyConfig(); - error FeedNotConfigured(address pool); - error InvalidAddress(address addr); - error InvalidWorkflowName(bytes10 workflowName); - error UnauthorizedCaller(address caller); - error NoMappingForSender(address proxy); - - modifier onlyFeedAdmin() { - if (!s_feedAdmins[msg.sender]) revert UnauthorizedCaller(msg.sender); - _; - } - - /// ================================================================ - /// @notice Get the workflow metadata of a feed - /// @param pool The address of the pool for the feed - /// @param startIndex The cursor to start fetching the metadata from - /// @param maxCount The number of metadata to fetch - /// @return workflowMetadata The metadata of the feed - function getFeedMetadata( - address pool, - uint256 startIndex, - uint256 maxCount - ) external view returns (WorkflowMetadata[] memory workflowMetadata) { - FeedConfig storage feedConfig = s_feedConfigs[pool]; - - uint256 workflowMetadataLength = feedConfig.workflowMetadata.length; - - if (workflowMetadataLength == 0) { - revert FeedNotConfigured(pool); - } - - if (startIndex >= workflowMetadataLength) return new WorkflowMetadata[](0); - uint256 endIndex = startIndex + maxCount; - endIndex = endIndex > workflowMetadataLength || maxCount == 0 ? workflowMetadataLength : endIndex; - - workflowMetadata = new WorkflowMetadata[](endIndex - startIndex); - for (uint256 idx; idx < workflowMetadata.length; idx++) { - workflowMetadata[idx] = feedConfig.workflowMetadata[idx + startIndex]; - } - - return workflowMetadata; - } - - /// @notice Checks to see if this data ID, msg.sender, workflow owner, and workflow name are permissioned - /// @param pool The address of the pool for the feed - /// @param workflowMetadata workflow metadata - function checkFeedPermission( - address pool, - WorkflowMetadata memory workflowMetadata - ) external view returns (bool hasPermission) { - bytes32 permission = _createPoolHash( - pool, - workflowMetadata.allowedSender, - workflowMetadata.allowedWorkflowOwner, - workflowMetadata.allowedWorkflowName - ); - return s_writePermissions[permission]; - } - - // ================================================================ - // │ Contract Config Interface │ - // ================================================================ - - /// @notice Initializes the config for a pool feed - /// @param pools The addresses of the pools to configure - /// @param descriptions The descriptions of the feeds - /// @param workflowMetadata List of workflow metadata (owners, senders, and names) for every feed - function setPoolFeedConfigs( - address[] calldata pools, - string[] calldata descriptions, - WorkflowMetadata[] calldata workflowMetadata - ) external onlyFeedAdmin { - if (workflowMetadata.length == 0 || pools.length == 0) { - revert EmptyConfig(); - } - - if (pools.length != descriptions.length) { - revert ArrayLengthMismatch(); - } - - for (uint256 i; i < pools.length; ++i) { - address pool = pools[i]; - if (pool == address(0)) revert InvalidAddress(pool); - FeedConfig storage feedConfig = s_feedConfigs[pool]; - - if (feedConfig.workflowMetadata.length > 0) { - // Feed is already configured, remove the previous config - for (uint256 j; j < feedConfig.workflowMetadata.length; ++j) { - WorkflowMetadata memory feedCurrentWorkflowMetadata = feedConfig.workflowMetadata[j]; - bytes32 poolHash = _createPoolHash( - pool, - feedCurrentWorkflowMetadata.allowedSender, - feedCurrentWorkflowMetadata.allowedWorkflowOwner, - feedCurrentWorkflowMetadata.allowedWorkflowName - ); - delete s_writePermissions[poolHash]; - } - - delete s_feedConfigs[pool]; - - emit FeedConfigRemoved(pool); - } - - for (uint256 j; j < workflowMetadata.length; ++j) { - WorkflowMetadata memory feedWorkflowMetadata = workflowMetadata[j]; - // Do those checks only once for the first data id - if (i == 0) { - if (feedWorkflowMetadata.allowedSender == address(0)) { - revert InvalidAddress(feedWorkflowMetadata.allowedSender); - } - if (feedWorkflowMetadata.allowedWorkflowOwner == address(0)) { - revert InvalidAddress(feedWorkflowMetadata.allowedWorkflowOwner); - } - if (feedWorkflowMetadata.allowedWorkflowName == bytes10(0)) { - revert InvalidWorkflowName(feedWorkflowMetadata.allowedWorkflowName); - } - } - - bytes32 poolHash = _createPoolHash( - pool, - feedWorkflowMetadata.allowedSender, - feedWorkflowMetadata.allowedWorkflowOwner, - feedWorkflowMetadata.allowedWorkflowName - ); - s_writePermissions[poolHash] = true; - feedConfig.workflowMetadata.push(feedWorkflowMetadata); - } - - feedConfig.description = descriptions[i]; - } - } - - /// @notice Removes feeds and all associated data, for a set of feeds - /// @param pools And array of data IDs to delete the data and configs of - function removeFeedConfigs(address[] calldata pools) external onlyFeedAdmin { - for (uint256 i; i < pools.length; ++i) { - address pool = pools[i]; - if (s_feedConfigs[pool].workflowMetadata.length == 0) revert FeedNotConfigured(pool); - - for (uint256 j; j < s_feedConfigs[pool].workflowMetadata.length; ++j) { - WorkflowMetadata memory feedWorkflowMetadata = s_feedConfigs[pool].workflowMetadata[j]; - bytes32 poolHash = _createPoolHash( - pool, - feedWorkflowMetadata.allowedSender, - feedWorkflowMetadata.allowedWorkflowOwner, - feedWorkflowMetadata.allowedWorkflowName - ); - delete s_writePermissions[poolHash]; - } - - delete s_feedConfigs[pool]; - - emit FeedConfigRemoved(pool); - } - } - - /// @notice Sets a feed admin for all feeds, only callable by the Owner - /// @param feedAdmin The feed admin - function setFeedAdmin(address feedAdmin, bool isAdmin) external onlyOwner { - if (feedAdmin == address(0)) revert InvalidAddress(feedAdmin); - - s_feedAdmins[feedAdmin] = isAdmin; - emit FeedAdminSet(feedAdmin, isAdmin); - } - - /// @notice Returns a bool is an address has feed admin permission for all feeds - /// @param feedAdmin The feed admin - /// @return isFeedAdmin bool if the address is the feed admin for all feeds - function isFeedAdmin(address feedAdmin) external view returns (bool) { - return s_feedAdmins[feedAdmin]; - } - - // ================================================================ - // │ Helper Methods │ - // ================================================================ - - /// @notice Extracts the workflow name and the workflow owner from the metadata parameter of onReport - /// @param metadata The metadata in bytes format - /// @return workflowOwner The owner of the workflow - /// @return workflowName The name of the workflow - function _getWorkflowMetaData(bytes memory metadata) internal pure returns (address, bytes10) { - address workflowOwner; - bytes10 workflowName; - // (first 32 bytes contain length of the byte array) - // workflow_cid // offset 32, size 32 - // workflow_name // offset 64, size 10 - // workflow_owner // offset 74, size 20 - // pool_name // offset 94, size 2 - assembly { - // no shifting needed for bytes10 type - workflowName := mload(add(metadata, 64)) - // shift right by 12 bytes to get the actual value - workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) - } - return (workflowOwner, workflowName); - } - - /// @notice Creates a pool hash used to permission write access - /// @param poolId The pool ID for the feed - /// @param sender The msg.sender of the transaction calling into onpool - /// @param workflowOwner The owner of the workflow - /// @param workflowName The name of the workflow - /// @return poolHash The keccak256 hash of the abi.encoded inputs - function _createPoolHash( - address pool, - address sender, - address workflowOwner, - bytes10 workflowName - ) internal pure returns (bytes32) { - return keccak256(abi.encode(pool, sender, workflowOwner, workflowName)); - } - - /// @notice Wrapper that verifies metadata and write permissions, then forwards to UpdateWeightRunner.setTargetWeightManually. - function setTargetWeightManuallyWithMeta( - int256[] calldata _weights, // 1e18-scaled, non-negative - address _poolAddress, - uint40 _lastInterpolationTimePossible, - uint256 _numberOfAssets, - WeightUpdateMeta calldata _meta - ) external { - (address workflowOwner, bytes10 workflowName) = _getWorkflowMetaData(_meta); - - bytes32 permission = _createPoolHash( - _poolAddress, - msg.sender, - workflowOwner, - workflowName - ); - - if (!s_writePermissions[permission]) { - emit InvalidUpdatePermission(_poolAddress, msg.sender, workflowOwner, workflowName); - revert UnauthorizedCaller(msg.sender); - } - - // 6) Forward the call to the UpdateWeightRunner - _updateWeightRunner.setTargetWeightManually(_weights, _poolAddress, _lastInterpolationTimePossible, _numberOfAssets); - - emit TargetWeightsForwarded( - _poolAddress, - msg.sender, - workflowOwner, - workflowName - ); - } -} diff --git a/pkg/pool-quantamm/contracts/wrappers/TruflationCreAuthWrapper.sol b/pkg/pool-quantamm/contracts/wrappers/TruflationCreAuthWrapper.sol new file mode 100644 index 00000000..b09431c9 --- /dev/null +++ b/pkg/pool-quantamm/contracts/wrappers/TruflationCreAuthWrapper.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { ITypeAndVersion } from "@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IUpdateWeightRunner } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/IUpdateWeightRunner.sol"; + +contract TruflationCreAuthWrapper is ITypeAndVersion, Ownable { + constructor( + address updateWeightRunnerAddress, + int256[] memory fullInflationRegimeWeight, + int256[] memory mixedInflationRegimeWeight, + int256[] memory noInflationRegimeWeight + ) Ownable(msg.sender) { + updateWeightRunner = IUpdateWeightRunner(updateWeightRunnerAddress); + + require(fullInflationRegimeWeight.length == 2, "Invalid Full inflation regime weight"); + require(mixedInflationRegimeWeight.length == 2, "Invalid Mixed inflation regime weight"); + require(noInflationRegimeWeight.length == 2, "Invalid No inflation regime weight"); + + BTC_FULL_INFLATION_REGIME_WEIGHT = fullInflationRegimeWeight[0]; + USDC_FULL_INFLATION_REGIME_WEIGHT = fullInflationRegimeWeight[1]; + BTC_MIXED_INFLATION_REGIME_WEIGHT = mixedInflationRegimeWeight[0]; + USDC_MIXED_INFLATION_REGIME_WEIGHT = mixedInflationRegimeWeight[1]; + BTC_NO_INFLATION_REGIME_WEIGHT = noInflationRegimeWeight[0]; + USDC_NO_INFLATION_REGIME_WEIGHT = noInflationRegimeWeight[1]; + } + + IUpdateWeightRunner public updateWeightRunner; + + uint40 public lastInterpolationTimePossible; + + string public constant override typeAndVersion = "CreAuthWrapper 1.0.0"; + + int256 public immutable BTC_FULL_INFLATION_REGIME_WEIGHT; + int256 public immutable USDC_FULL_INFLATION_REGIME_WEIGHT; + int256 public immutable BTC_MIXED_INFLATION_REGIME_WEIGHT; + int256 public immutable USDC_MIXED_INFLATION_REGIME_WEIGHT; + int256 public immutable BTC_NO_INFLATION_REGIME_WEIGHT; + int256 public immutable USDC_NO_INFLATION_REGIME_WEIGHT; + + struct WorkflowMetadata { + address allowedSender; + address allowedWorkflowOwner; + bytes10 allowedWorkflowName; + } + + bytes32 public s_writePermission; + + event WritePermissionRemoved(bytes32 indexed pool); + event TargetWeightsForwarded( + uint indexed regime, + address indexed pool, + address sender, + address workflowOwner, + bytes10 workflowName + ); + event WriteAdminSet(address indexed feedAdmin, bool indexed isAdmin); + event InvalidUpdatePermission(address indexed pool, address sender, address workflowOwner, bytes10 workflowName); + + error EmptyConfig(); + error InvalidAddress(address addr); + error InvalidWorkflowName(bytes10 workflowName); + error UnauthorizedCaller(address caller); + + /// @notice Checks to see if this data ID, msg.sender, workflow owner, and workflow name are permissioned + /// @param pool The address of the pool for the feed + /// @param workflowMetadata workflow metadata + function checkFeedPermission( + address pool, + WorkflowMetadata memory workflowMetadata + ) external view returns (bool hasPermission) { + bytes32 permission = _createPoolHash( + pool, + workflowMetadata.allowedSender, + workflowMetadata.allowedWorkflowOwner, + workflowMetadata.allowedWorkflowName + ); + + return s_writePermission == permission; + } + + /// @notice Sets the UpdateWeightRunner contract address. Admin breakglass feature available to owner only. + /// @param updateWeightRunnerAddress The address of the UpdateWeightRunner contract + function setUpdateWeightRunner(address updateWeightRunnerAddress) external onlyOwner { + require(updateWeightRunnerAddress != address(0), "INVADDR"); + updateWeightRunner = IUpdateWeightRunner(updateWeightRunnerAddress); + } + + /// @notice Initializes the config for a pool feed + /// @param pool The address of the pool for the feed + /// @param workflowMetadata List of workflow metadata (owners, senders, and names) for every feed + function setWriteConfig(address pool, WorkflowMetadata calldata workflowMetadata) external onlyOwner { + if (workflowMetadata.allowedSender == address(0)) { + revert InvalidAddress(workflowMetadata.allowedSender); + } + if (workflowMetadata.allowedWorkflowOwner == address(0)) { + revert InvalidAddress(workflowMetadata.allowedWorkflowOwner); + } + if (workflowMetadata.allowedWorkflowName == bytes10(0)) { + revert InvalidWorkflowName(workflowMetadata.allowedWorkflowName); + } + + bytes32 poolHash = _createPoolHash( + pool, + workflowMetadata.allowedSender, + workflowMetadata.allowedWorkflowOwner, + workflowMetadata.allowedWorkflowName + ); + + s_writePermission = poolHash; + + emit WriteAdminSet(workflowMetadata.allowedSender, true); + } + + /// @notice Removes the write permission config for a pool feed + function removeWritePermissionConfig() external onlyOwner { + if (s_writePermission == bytes32(0)) { + revert EmptyConfig(); + } + + emit WritePermissionRemoved(s_writePermission); + + s_writePermission = bytes32(0); + } + + /// @notice Extracts the workflow name and the workflow owner from the metadata parameter of onReport + /// @param metadata The metadata in bytes format + /// @return workflowOwner The owner of the workflow + /// @return workflowName The name of the workflow + function _getWorkflowMetaData(bytes memory metadata) internal pure returns (address, bytes10) { + address workflowOwner; + bytes10 workflowName; + // (first 32 bytes contain length of the byte array) + // workflow_cid // offset 32, size 32 + // workflow_name // offset 64, size 10 + // workflow_owner // offset 74, size 20 + // pool_name // offset 94, size 2 + assembly { + // no shifting needed for bytes10 type + workflowName := mload(add(metadata, 64)) + // shift right by 12 bytes to get the actual value + workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) + } + + return (workflowOwner, workflowName); + } + + /// @notice Creates a pool hash used to permission write access + /// @param poolId The pool ID for the feed + /// @param sender The msg.sender of the transaction calling into onpool + /// @param workflowOwner The owner of the workflow + /// @param workflowName The name of the workflow + /// @return poolHash The keccak256 hash of the abi.encoded inputs + function _createPoolHash( + address poolId, + address sender, + address workflowOwner, + bytes10 workflowName + ) internal pure returns (bytes32) { + return keccak256(abi.encode(poolId, sender, workflowOwner, workflowName)); + } + + enum InflationRegime { + BTC_INFLATION, + MIXED_INFLATION, + NO_INFLATION + } + + function setTargetWeightsByRegimeWithMeta( + uint newRegime, + address _poolAddress, + int256[] calldata _weights, + bytes calldata _meta + ) external { + require( + newRegime == uint(InflationRegime.BTC_INFLATION) || + newRegime == uint(InflationRegime.MIXED_INFLATION) || + newRegime == uint(InflationRegime.NO_INFLATION), + "INVALID_REGIME" + ); + + (address workflowOwner, bytes10 workflowName) = _getWorkflowMetaData(_meta); + + bytes32 permission = _createPoolHash(_poolAddress, msg.sender, workflowOwner, workflowName); + + if (s_writePermission != permission) { + emit InvalidUpdatePermission(_poolAddress, msg.sender, workflowOwner, workflowName); + revert UnauthorizedCaller(msg.sender); + } + + if (newRegime == uint40(InflationRegime.BTC_INFLATION)) { + require(_weights.length == 2, "ARRAY_LENGTH_MISMATCH"); + require(_weights[0] == BTC_FULL_INFLATION_REGIME_WEIGHT, "INVALID_WEIGHT"); + require(_weights[1] == USDC_FULL_INFLATION_REGIME_WEIGHT, "INVALID_WEIGHT"); + } else if (newRegime == uint40(InflationRegime.MIXED_INFLATION)) { + require(_weights.length == 2, "ARRAY_LENGTH_MISMATCH"); + require(_weights[0] == BTC_MIXED_INFLATION_REGIME_WEIGHT, "INVALID_WEIGHT"); + require(_weights[1] == USDC_MIXED_INFLATION_REGIME_WEIGHT, "INVALID_WEIGHT"); + } else { + require(_weights.length == 2, "ARRAY_LENGTH_MISMATCH"); + require(_weights[0] == BTC_NO_INFLATION_REGIME_WEIGHT, "INVALID_WEIGHT"); + require(_weights[1] == USDC_NO_INFLATION_REGIME_WEIGHT, "INVALID_WEIGHT"); + } + + updateWeightRunner.setTargetWeightsManually(_weights, _poolAddress, lastInterpolationTimePossible, uint(2)); + + emit TargetWeightsForwarded(newRegime, _poolAddress, msg.sender, workflowOwner, workflowName); + } +} diff --git a/pkg/pool-quantamm/contracts/wrappers/datacacheExample.sol b/pkg/pool-quantamm/contracts/wrappers/datacacheExample.sol deleted file mode 100644 index 6e33d13e..00000000 --- a/pkg/pool-quantamm/contracts/wrappers/datacacheExample.sol +++ /dev/null @@ -1,757 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {IReceiver} from "../keystone/interfaces/IReceiver.sol"; -import {OwnerIsCreator} from "../shared/access/OwnerIsCreator.sol"; -import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol"; -import {IDataFeedsCache} from "./interfaces/IDataFeedsCache.sol"; -import {ITokenRecover} from "./interfaces/ITokenRecover.sol"; - -import {IERC165} from "@openzeppelin/contracts@5.0.2/interfaces/IERC165.sol"; -import {IERC20} from "@openzeppelin/contracts@5.0.2/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts@5.0.2/token/ERC20/utils/SafeERC20.sol"; - -contract DataFeedsCache is IDataFeedsCache, IReceiver, ITokenRecover, ITypeAndVersion, OwnerIsCreator { - using SafeERC20 for IERC20; - - string public constant override typeAndVersion = "DataFeedsCache 1.0.0"; - - // solhint-disable-next-line - uint256 public constant override version = 7; - - /// Cache State - - struct WorkflowMetadata { - address allowedSender; // Address of the sender allowed to send new reports - address allowedWorkflowOwner; // ─╮ Address of the workflow owner - bytes10 allowedWorkflowName; // ──╯ Name of the workflow - } - - struct FeedConfig { - uint8[] bundleDecimals; // Only appliciable to Bundle reports - Decimal reports have decimals encoded into the - // DataId. - string description; // Description of the feed (e.g. "LINK / USD") - WorkflowMetadata[] workflowMetadata; // Metadata for the feed - } - - struct ReceivedBundleReport { - bytes32 dataId; // Data ID of the feed from the received report - uint32 timestamp; // Timestamp of the feed from the received report - bytes bundle; // Report data in raw bytes - } - - struct ReceivedDecimalReport { - bytes32 dataId; // Data ID of the feed from the received report - uint32 timestamp; // ─╮ Timestamp of the feed from the received report - uint224 answer; // ───╯ Report data in uint224 - } - - struct StoredBundleReport { - bytes bundle; // The latest bundle report stored for a feed - uint32 timestamp; // The timestamp of the latest bundle report - } - - struct StoredDecimalReport { - uint224 answer; // ───╮ The latest decimal report stored for a feed - uint32 timestamp; // ─╯ The timestamp of the latest decimal report - } - - /// The message sender determines which feed is being requested, as each proxy has a single associated feed - mapping(address aggProxy => bytes16 dataId) private s_aggregatorProxyToDataId; - - /// The latest decimal reports for each decimal feed. This will always equal - /// s_decimalReports[s_dataIdToRoundId[dataId]][dataId] - mapping(bytes16 dataId => StoredDecimalReport) private s_latestDecimalReports; - - /// Decimal reports for each feed, per round - mapping(uint256 roundId => mapping(bytes16 dataId => StoredDecimalReport)) private s_decimalReports; - - /// The latest bundle reports for each bundle feed - mapping(bytes16 dataId => StoredBundleReport) private s_latestBundleReports; - - /// The latest round id for each feed - mapping(bytes16 dataId => uint256 roundId) private s_dataIdToRoundId; - - /// Addresses that are permitted to configure all feeds - mapping(address feedAdmin => bool isFeedAdmin) private s_feedAdmins; - - mapping(bytes16 dataId => FeedConfig) private s_feedConfigs; - - /// Whether a given Sender and Workflow have permission to write feed updates. - /// reportHash is the keccak256 hash of the abi.encoded(dataId, sender, workflowOwner and workflowName) - mapping(bytes32 reportHash => bool) private s_writePermissions; - - event BundleReportUpdated(bytes16 indexed dataId, uint256 indexed timestamp, bytes bundle); - event DecimalReportUpdated( - bytes16 indexed dataId, uint256 indexed roundId, uint256 indexed timestamp, uint224 answer - ); - event DecimalFeedConfigSet( - bytes16 indexed dataId, uint8 decimals, string description, WorkflowMetadata[] workflowMetadata - ); - event BundleFeedConfigSet( - bytes16 indexed dataId, uint8[] decimals, string description, WorkflowMetadata[] workflowMetadata - ); - event FeedConfigRemoved(bytes16 indexed dataId); - event TokenRecovered(address indexed token, address indexed to, uint256 amount); - - event FeedAdminSet(address indexed feedAdmin, bool indexed isAdmin); - - event ProxyDataIdRemoved(address indexed proxy, bytes16 indexed dataId); - event ProxyDataIdUpdated(address indexed proxy, bytes16 indexed dataId); - - event InvalidUpdatePermission(bytes16 indexed dataId, address sender, address workflowOwner, bytes10 workflowName); - event StaleDecimalReport(bytes16 indexed dataId, uint256 reportTimestamp, uint256 latestTimestamp); - event StaleBundleReport(bytes16 indexed dataId, uint256 reportTimestamp, uint256 latestTimestamp); - - error ArrayLengthMismatch(); - error EmptyConfig(); - error ErrorSendingNative(address to, uint256 amount, bytes data); - error FeedNotConfigured(bytes16 dataId); - error InsufficientBalance(uint256 balance, uint256 requiredBalance); - error InvalidAddress(address addr); - error InvalidDataId(); - error InvalidWorkflowName(bytes10 workflowName); - error UnauthorizedCaller(address caller); - error NoMappingForSender(address proxy); - - modifier onlyFeedAdmin() { - if (!s_feedAdmins[msg.sender]) revert UnauthorizedCaller(msg.sender); - _; - } - - /// @inheritdoc IERC165 - function supportsInterface( - bytes4 interfaceId - ) public pure returns (bool) { - return ( - interfaceId == type(IDataFeedsCache).interfaceId || interfaceId == type(IERC165).interfaceId - || interfaceId == type(IReceiver).interfaceId || interfaceId == type(ITokenRecover).interfaceId - || interfaceId == type(ITypeAndVersion).interfaceId - ); - } - - /// @notice Get the workflow metadata of a feed - /// @param dataId data ID of the feed - /// @param startIndex The cursor to start fetching the metadata from - /// @param maxCount The number of metadata to fetch - /// @return workflowMetadata The metadata of the feed - function getFeedMetadata( - bytes16 dataId, - uint256 startIndex, - uint256 maxCount - ) external view returns (WorkflowMetadata[] memory workflowMetadata) { - FeedConfig storage feedConfig = s_feedConfigs[dataId]; - - uint256 workflowMetadataLength = feedConfig.workflowMetadata.length; - - if (workflowMetadataLength == 0) { - revert FeedNotConfigured(dataId); - } - - if (startIndex >= workflowMetadataLength) return new WorkflowMetadata[](0); - uint256 endIndex = startIndex + maxCount; - endIndex = endIndex > workflowMetadataLength || maxCount == 0 ? workflowMetadataLength : endIndex; - - workflowMetadata = new WorkflowMetadata[](endIndex - startIndex); - for (uint256 idx; idx < workflowMetadata.length; idx++) { - workflowMetadata[idx] = feedConfig.workflowMetadata[idx + startIndex]; - } - - return workflowMetadata; - } - - /// @notice Checks to see if this data ID, msg.sender, workflow owner, and workflow name are permissioned - /// @param dataId The data ID for the feed - /// @param workflowMetadata workflow metadata - function checkFeedPermission( - bytes16 dataId, - WorkflowMetadata memory workflowMetadata - ) external view returns (bool hasPermission) { - bytes32 permission = _createReportHash( - dataId, - workflowMetadata.allowedSender, - workflowMetadata.allowedWorkflowOwner, - workflowMetadata.allowedWorkflowName - ); - return s_writePermissions[permission]; - } - - // ================================================================ - // │ Contract Config Interface │ - // ================================================================ - - /// @notice Initializes the config for a decimal feed - /// @param dataIds The data IDs of the feeds to configure - /// @param descriptions The descriptions of the feeds - /// @param workflowMetadata List of workflow metadata (owners, senders, and names) for every feed - function setDecimalFeedConfigs( - bytes16[] calldata dataIds, - string[] calldata descriptions, - WorkflowMetadata[] calldata workflowMetadata - ) external onlyFeedAdmin { - if (workflowMetadata.length == 0 || dataIds.length == 0) { - revert EmptyConfig(); - } - - if (dataIds.length != descriptions.length) { - revert ArrayLengthMismatch(); - } - - for (uint256 i; i < dataIds.length; ++i) { - bytes16 dataId = dataIds[i]; - if (dataId == bytes16(0)) revert InvalidDataId(); - FeedConfig storage feedConfig = s_feedConfigs[dataId]; - - if (feedConfig.workflowMetadata.length > 0) { - // Feed is already configured, remove the previous config - for (uint256 j; j < feedConfig.workflowMetadata.length; ++j) { - WorkflowMetadata memory feedCurrentWorkflowMetadata = feedConfig.workflowMetadata[j]; - bytes32 reportHash = _createReportHash( - dataId, - feedCurrentWorkflowMetadata.allowedSender, - feedCurrentWorkflowMetadata.allowedWorkflowOwner, - feedCurrentWorkflowMetadata.allowedWorkflowName - ); - delete s_writePermissions[reportHash]; - } - - delete s_feedConfigs[dataId]; - - emit FeedConfigRemoved(dataId); - } - - for (uint256 j; j < workflowMetadata.length; ++j) { - WorkflowMetadata memory feedWorkflowMetadata = workflowMetadata[j]; - // Do those checks only once for the first data id - if (i == 0) { - if (feedWorkflowMetadata.allowedSender == address(0)) { - revert InvalidAddress(feedWorkflowMetadata.allowedSender); - } - if (feedWorkflowMetadata.allowedWorkflowOwner == address(0)) { - revert InvalidAddress(feedWorkflowMetadata.allowedWorkflowOwner); - } - if (feedWorkflowMetadata.allowedWorkflowName == bytes10(0)) { - revert InvalidWorkflowName(feedWorkflowMetadata.allowedWorkflowName); - } - } - - bytes32 reportHash = _createReportHash( - dataId, - feedWorkflowMetadata.allowedSender, - feedWorkflowMetadata.allowedWorkflowOwner, - feedWorkflowMetadata.allowedWorkflowName - ); - s_writePermissions[reportHash] = true; - feedConfig.workflowMetadata.push(feedWorkflowMetadata); - } - - feedConfig.description = descriptions[i]; - - emit DecimalFeedConfigSet({ - dataId: dataId, - decimals: _getDecimals(dataId), - description: descriptions[i], - workflowMetadata: workflowMetadata - }); - } - } - - /// @notice Initializes the config for a bundle feed - /// @param dataIds The data IDs of the feeds to configure - /// @param descriptions The descriptions of the feeds - /// @param decimalsMatrix The number of decimals for each data point in the bundle for the feed - /// @param workflowMetadata List of workflow metadata (owners, senders, and names) for every feed - function setBundleFeedConfigs( - bytes16[] calldata dataIds, - string[] calldata descriptions, - uint8[][] calldata decimalsMatrix, - WorkflowMetadata[] calldata workflowMetadata - ) external onlyFeedAdmin { - if (workflowMetadata.length == 0 || dataIds.length == 0) { - revert EmptyConfig(); - } - - if (dataIds.length != descriptions.length || dataIds.length != decimalsMatrix.length) { - revert ArrayLengthMismatch(); - } - - for (uint256 i; i < dataIds.length; ++i) { - bytes16 dataId = dataIds[i]; - if (dataId == bytes16(0)) revert InvalidDataId(); - FeedConfig storage feedConfig = s_feedConfigs[dataId]; - - if (feedConfig.workflowMetadata.length > 0) { - // Feed is already configured, remove the previous config - for (uint256 j; j < feedConfig.workflowMetadata.length; ++j) { - WorkflowMetadata memory feedCurrentWorkflowMetadata = feedConfig.workflowMetadata[j]; - bytes32 reportHash = _createReportHash( - dataId, - feedCurrentWorkflowMetadata.allowedSender, - feedCurrentWorkflowMetadata.allowedWorkflowOwner, - feedCurrentWorkflowMetadata.allowedWorkflowName - ); - delete s_writePermissions[reportHash]; - } - - delete s_feedConfigs[dataId]; - - emit FeedConfigRemoved(dataId); - } - - for (uint256 j; j < workflowMetadata.length; ++j) { - WorkflowMetadata memory feedWorkflowMetadata = workflowMetadata[j]; - // Do those checks only once for the first data id - if (i == 0) { - if (feedWorkflowMetadata.allowedSender == address(0)) { - revert InvalidAddress(feedWorkflowMetadata.allowedSender); - } - if (feedWorkflowMetadata.allowedWorkflowOwner == address(0)) { - revert InvalidAddress(feedWorkflowMetadata.allowedWorkflowOwner); - } - if (feedWorkflowMetadata.allowedWorkflowName == bytes10(0)) { - revert InvalidWorkflowName(feedWorkflowMetadata.allowedWorkflowName); - } - } - - bytes32 reportHash = _createReportHash( - dataId, - feedWorkflowMetadata.allowedSender, - feedWorkflowMetadata.allowedWorkflowOwner, - feedWorkflowMetadata.allowedWorkflowName - ); - s_writePermissions[reportHash] = true; - feedConfig.workflowMetadata.push(feedWorkflowMetadata); - } - - feedConfig.bundleDecimals = decimalsMatrix[i]; - feedConfig.description = descriptions[i]; - - emit BundleFeedConfigSet({ - dataId: dataId, - decimals: decimalsMatrix[i], - description: descriptions[i], - workflowMetadata: workflowMetadata - }); - } - } - - /// @notice Removes feeds and all associated data, for a set of feeds - /// @param dataIds And array of data IDs to delete the data and configs of - function removeFeedConfigs( - bytes16[] calldata dataIds - ) external onlyFeedAdmin { - for (uint256 i; i < dataIds.length; ++i) { - bytes16 dataId = dataIds[i]; - if (s_feedConfigs[dataId].workflowMetadata.length == 0) revert FeedNotConfigured(dataId); - - for (uint256 j; j < s_feedConfigs[dataId].workflowMetadata.length; ++j) { - WorkflowMetadata memory feedWorkflowMetadata = s_feedConfigs[dataId].workflowMetadata[j]; - bytes32 reportHash = _createReportHash( - dataId, - feedWorkflowMetadata.allowedSender, - feedWorkflowMetadata.allowedWorkflowOwner, - feedWorkflowMetadata.allowedWorkflowName - ); - delete s_writePermissions[reportHash]; - } - - delete s_feedConfigs[dataId]; - - emit FeedConfigRemoved(dataId); - } - } - - /// @notice Sets a feed admin for all feeds, only callable by the Owner - /// @param feedAdmin The feed admin - function setFeedAdmin(address feedAdmin, bool isAdmin) external onlyOwner { - if (feedAdmin == address(0)) revert InvalidAddress(feedAdmin); - - s_feedAdmins[feedAdmin] = isAdmin; - emit FeedAdminSet(feedAdmin, isAdmin); - } - - /// @notice Returns a bool is an address has feed admin permission for all feeds - /// @param feedAdmin The feed admin - /// @return isFeedAdmin bool if the address is the feed admin for all feeds - function isFeedAdmin( - address feedAdmin - ) external view returns (bool) { - return s_feedAdmins[feedAdmin]; - } - - /// @inheritdoc IDataFeedsCache - function updateDataIdMappingsForProxies( - address[] calldata proxies, - bytes16[] calldata dataIds - ) external onlyFeedAdmin { - uint256 numberOfProxies = proxies.length; - if (numberOfProxies != dataIds.length) revert ArrayLengthMismatch(); - - for (uint256 i; i < numberOfProxies; i++) { - s_aggregatorProxyToDataId[proxies[i]] = dataIds[i]; - - emit ProxyDataIdUpdated(proxies[i], dataIds[i]); - } - } - - /// @inheritdoc IDataFeedsCache - function getDataIdForProxy( - address proxy - ) external view returns (bytes16 dataId) { - return s_aggregatorProxyToDataId[proxy]; - } - - /// @inheritdoc IDataFeedsCache - function removeDataIdMappingsForProxies( - address[] calldata proxies - ) external onlyFeedAdmin { - uint256 numberOfProxies = proxies.length; - - for (uint256 i; i < numberOfProxies; i++) { - address proxy = proxies[i]; - bytes16 dataId = s_aggregatorProxyToDataId[proxy]; - delete s_aggregatorProxyToDataId[proxy]; - emit ProxyDataIdRemoved(proxy, dataId); - } - } - - // ================================================================ - // │ Token Transfer Interface │ - // ================================================================ - - /// @inheritdoc ITokenRecover - function recoverTokens(IERC20 token, address to, uint256 amount) external onlyOwner { - if (address(token) == address(0)) { - if (amount > address(this).balance) { - revert InsufficientBalance(address(this).balance, amount); - } - (bool success, bytes memory data) = to.call{value: amount}(""); - if (!success) revert ErrorSendingNative(to, amount, data); - } else { - if (amount > token.balanceOf(address(this))) { - revert InsufficientBalance(token.balanceOf(address(this)), amount); - } - token.safeTransfer(to, amount); - } - emit TokenRecovered(address(token), to, amount); - } - - // ================================================================ - // │ Cache Update Interface │ - // ================================================================ - - /// @inheritdoc IReceiver - function onReport(bytes calldata metadata, bytes calldata report) external { - (address workflowOwner, bytes10 workflowName) = _getWorkflowMetaData(metadata); - - // The first 32 bytes is the offset to the array - // The second 32 bytes is the length of the array - uint256 numReports = uint256(bytes32(report[32:64])); - - // Decimal reports contain 96 bytes per report - // The total length should equal to the sum of: - // 32 bytes for the offset - // 32 bytes for the number of reports - // the number of reports times 96 - if (report.length == numReports * 96 + 64) { - ReceivedDecimalReport[] memory decodedDecimalReports = abi.decode(report, (ReceivedDecimalReport[])); - for (uint256 i; i < numReports; ++i) { - ReceivedDecimalReport memory decodedDecimalReport = decodedDecimalReports[i]; - // single dataId can have multiple permissions, to be updated by multiple Workflows - bytes16 dataId = bytes16(decodedDecimalReport.dataId); - bytes32 permission = _createReportHash(dataId, msg.sender, workflowOwner, workflowName); - if (!s_writePermissions[permission]) { - emit InvalidUpdatePermission(dataId, msg.sender, workflowOwner, workflowName); - continue; - } - - if (decodedDecimalReport.timestamp <= s_latestDecimalReports[dataId].timestamp) { - emit StaleDecimalReport(dataId, decodedDecimalReport.timestamp, s_latestDecimalReports[dataId].timestamp); - continue; - } - - StoredDecimalReport memory decimalReport = - StoredDecimalReport({answer: decodedDecimalReport.answer, timestamp: decodedDecimalReport.timestamp}); - - uint256 roundId = ++s_dataIdToRoundId[dataId]; - - s_latestDecimalReports[dataId] = decimalReport; - s_decimalReports[roundId][dataId] = decimalReport; - - emit DecimalReportUpdated(dataId, roundId, decimalReport.timestamp, decimalReport.answer); - - // Needed for DF1 backward compatibility - emit NewRound(roundId, address(0), decodedDecimalReport.timestamp); - emit AnswerUpdated(int256(uint256(decodedDecimalReport.answer)), roundId, block.timestamp); - } - } - // Bundle reports contain more bytes for the offsets - // The total length should equal to the sum of: - // 32 bytes for the offset - // 32 bytes for the number of reports - // the number of reports times 224 - else { - //For byte reports decode using ReceivedFeedReportBundle struct - ReceivedBundleReport[] memory decodedBundleReports = abi.decode(report, (ReceivedBundleReport[])); - for (uint256 i; i < decodedBundleReports.length; ++i) { - ReceivedBundleReport memory decodedBundleReport = decodedBundleReports[i]; - bytes16 dataId = bytes16(decodedBundleReport.dataId); - // same dataId can have multiple permissions - bytes32 permission = _createReportHash(dataId, msg.sender, workflowOwner, workflowName); - if (!s_writePermissions[permission]) { - emit InvalidUpdatePermission(dataId, msg.sender, workflowOwner, workflowName); - continue; - } - - if (decodedBundleReport.timestamp <= s_latestBundleReports[dataId].timestamp) { - emit StaleBundleReport(dataId, decodedBundleReport.timestamp, s_latestBundleReports[dataId].timestamp); - continue; - } - - StoredBundleReport memory bundleReport = - StoredBundleReport({bundle: decodedBundleReport.bundle, timestamp: decodedBundleReport.timestamp}); - - s_latestBundleReports[dataId] = bundleReport; - - emit BundleReportUpdated(dataId, bundleReport.timestamp, bundleReport.bundle); - } - } - } - // ================================================================ - // │ Helper Methods │ - // ================================================================ - - /// @notice Gets the Decimals of the feed from the data Id - /// @param dataId The data ID for the feed - /// @return feedDecimals The number of decimals the feed has - function _getDecimals( - bytes16 dataId - ) internal pure returns (uint8 feedDecimals) { - // Get the report type from data id. Report type has index of 7 - bytes1 reportType = _getDataType(dataId, 7); - - // For decimal reports convert to uint8, then shift - if (reportType >= hex"20" && reportType <= hex"60") { - return uint8(reportType) - 32; - } - - // If not decimal type, return 0 - return 0; - } - - /// @notice Extracts the workflow name and the workflow owner from the metadata parameter of onReport - /// @param metadata The metadata in bytes format - /// @return workflowOwner The owner of the workflow - /// @return workflowName The name of the workflow - function _getWorkflowMetaData( - bytes memory metadata - ) internal pure returns (address, bytes10) { - address workflowOwner; - bytes10 workflowName; - // (first 32 bytes contain length of the byte array) - // workflow_cid // offset 32, size 32 - // workflow_name // offset 64, size 10 - // workflow_owner // offset 74, size 20 - // report_name // offset 94, size 2 - assembly { - // no shifting needed for bytes10 type - workflowName := mload(add(metadata, 64)) - // shift right by 12 bytes to get the actual value - workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) - } - return (workflowOwner, workflowName); - } - - /// @notice Extracts a byte from the data ID, to check data types - /// @param dataId The data ID for the feed - /// @param index The index of the byte to extract from the data Id - /// @return dataType result The keccak256 hash of the abi.encoded inputs - function _getDataType(bytes16 dataId, uint256 index) internal pure returns (bytes1 dataType) { - // Convert bytes16 to bytes - return abi.encodePacked(dataId)[index]; - } - - /// @notice Creates a report hash used to permission write access - /// @param dataId The data ID for the feed - /// @param sender The msg.sender of the transaction calling into onReport - /// @param workflowOwner The owner of the workflow - /// @param workflowName The name of the workflow - /// @return reportHash The keccak256 hash of the abi.encoded inputs - function _createReportHash( - bytes16 dataId, - address sender, - address workflowOwner, - bytes10 workflowName - ) internal pure returns (bytes32) { - return keccak256(abi.encode(dataId, sender, workflowOwner, workflowName)); - } - - // ================================================================ - // │ Data Access Interface │ - // ================================================================ - - /// Bundle Feed Interface - - function latestBundle() external view returns (bytes memory bundle) { - bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; - if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); - - return (s_latestBundleReports[dataId].bundle); - } - - function bundleDecimals() external view returns (uint8[] memory bundleFeedDecimals) { - bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; - if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); - - return s_feedConfigs[dataId].bundleDecimals; - } - - function latestBundleTimestamp() external view returns (uint256 timestamp) { - bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; - if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); - - return s_latestBundleReports[dataId].timestamp; - } - - /// AggregatorInterface - - function latestAnswer() external view returns (int256 answer) { - bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; - if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); - - return int256(uint256(s_latestDecimalReports[dataId].answer)); - } - - function latestTimestamp() external view returns (uint256 timestamp) { - bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; - if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); - - return s_latestDecimalReports[dataId].timestamp; - } - - function latestRound() external view returns (uint256 round) { - bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; - if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); - - return s_dataIdToRoundId[dataId]; - } - - function getAnswer( - uint256 roundId - ) external view returns (int256 answer) { - bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; - if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); - - return int256(uint256(s_decimalReports[roundId][dataId].answer)); - } - - function getTimestamp( - uint256 roundId - ) external view returns (uint256 timestamp) { - bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; - if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); - - return s_decimalReports[roundId][dataId].timestamp; - } - - /// AggregatorV3Interface - - function decimals() external view returns (uint8 feedDecimals) { - bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; - if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); - return _getDecimals(dataId); - } - - function description() external view returns (string memory feedDescription) { - bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; - if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); - - return s_feedConfigs[dataId].description; - } - - function getRoundData( - uint80 roundId - ) external view returns (uint80 id, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { - bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; - if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); - - uint256 timestamp = s_decimalReports[uint256(roundId)][dataId].timestamp; - - return (roundId, int256(uint256(s_decimalReports[uint256(roundId)][dataId].answer)), timestamp, timestamp, roundId); - } - - function latestRoundData() - external - view - returns (uint80 id, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) - { - bytes16 dataId = s_aggregatorProxyToDataId[msg.sender]; - if (dataId == bytes16(0)) revert NoMappingForSender(msg.sender); - - uint80 roundId = uint80(s_dataIdToRoundId[dataId]); - uint256 timestamp = s_latestDecimalReports[dataId].timestamp; - - return (roundId, int256(uint256(s_latestDecimalReports[dataId].answer)), timestamp, timestamp, roundId); - } - - /// Direct access - function getLatestBundle( - bytes16 dataId - ) external view returns (bytes memory bundle) { - if (dataId == bytes16(0)) revert InvalidDataId(); - return (s_latestBundleReports[dataId].bundle); - } - - function getBundleDecimals( - bytes16 dataId - ) external view returns (uint8[] memory bundleFeedDecimals) { - if (dataId == bytes16(0)) revert InvalidDataId(); - return s_feedConfigs[dataId].bundleDecimals; - } - - function getLatestBundleTimestamp( - bytes16 dataId - ) external view returns (uint256 timestamp) { - if (dataId == bytes16(0)) revert InvalidDataId(); - return s_latestBundleReports[dataId].timestamp; - } - - function getLatestAnswer( - bytes16 dataId - ) external view returns (int256 answer) { - if (dataId == bytes16(0)) revert InvalidDataId(); - return int256(uint256(s_latestDecimalReports[dataId].answer)); - } - - function getLatestTimestamp( - bytes16 dataId - ) external view returns (uint256 timestamp) { - if (dataId == bytes16(0)) revert InvalidDataId(); - return s_latestDecimalReports[dataId].timestamp; - } - - function getLatestRoundData( - bytes16 dataId - ) external view returns (uint80 id, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { - if (dataId == bytes16(0)) revert InvalidDataId(); - - uint80 roundId = uint80(s_dataIdToRoundId[dataId]); - uint256 timestamp = s_latestDecimalReports[dataId].timestamp; - - return (roundId, int256(uint256(s_latestDecimalReports[dataId].answer)), timestamp, timestamp, roundId); - } - - function getDecimals( - bytes16 dataId - ) external pure returns (uint8 feedDecimals) { - if (dataId == bytes16(0)) revert InvalidDataId(); - return _getDecimals(dataId); - } - - function getDescription( - bytes16 dataId - ) external view returns (string memory feedDescription) { - if (dataId == bytes16(0)) revert InvalidDataId(); - return s_feedConfigs[dataId].description; - } -} \ No newline at end of file From 2ac44d649c4143915f5e453db87ff074bf3a7d6f Mon Sep 17 00:00:00 2001 From: christian harrington Date: Thu, 13 Nov 2025 11:03:47 +0000 Subject: [PATCH 03/11] remove weight checks given potential smoothing --- .../wrappers/TruflationCreAuthWrapper.sol | 184 +++++++----------- 1 file changed, 70 insertions(+), 114 deletions(-) diff --git a/pkg/pool-quantamm/contracts/wrappers/TruflationCreAuthWrapper.sol b/pkg/pool-quantamm/contracts/wrappers/TruflationCreAuthWrapper.sol index b09431c9..7caded19 100644 --- a/pkg/pool-quantamm/contracts/wrappers/TruflationCreAuthWrapper.sol +++ b/pkg/pool-quantamm/contracts/wrappers/TruflationCreAuthWrapper.sol @@ -5,57 +5,33 @@ import { ITypeAndVersion } from "@chainlink/contracts/src/v0.8/shared/interfaces import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IUpdateWeightRunner } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/IUpdateWeightRunner.sol"; -contract TruflationCreAuthWrapper is ITypeAndVersion, Ownable { - constructor( - address updateWeightRunnerAddress, - int256[] memory fullInflationRegimeWeight, - int256[] memory mixedInflationRegimeWeight, - int256[] memory noInflationRegimeWeight - ) Ownable(msg.sender) { +/// @title The Wrapper needed to bridge between CRE auth and update weighr runner +/// @dev CRE has a single address per chain that can execute functions. +/// While this means that msg.sender can be constant it also means anyone can setup +/// a CRE workflow and it will trigger from the same msg.sender. +/// The metadata is set by the workflow sender and cannot be manipulated. +/// Checking the metadata as well as the msg.sender means that the auth is complete +contract CREAuthWrapper is ITypeAndVersion, Ownable { + constructor(address updateWeightRunnerAddress) Ownable(msg.sender) { updateWeightRunner = IUpdateWeightRunner(updateWeightRunnerAddress); - - require(fullInflationRegimeWeight.length == 2, "Invalid Full inflation regime weight"); - require(mixedInflationRegimeWeight.length == 2, "Invalid Mixed inflation regime weight"); - require(noInflationRegimeWeight.length == 2, "Invalid No inflation regime weight"); - - BTC_FULL_INFLATION_REGIME_WEIGHT = fullInflationRegimeWeight[0]; - USDC_FULL_INFLATION_REGIME_WEIGHT = fullInflationRegimeWeight[1]; - BTC_MIXED_INFLATION_REGIME_WEIGHT = mixedInflationRegimeWeight[0]; - USDC_MIXED_INFLATION_REGIME_WEIGHT = mixedInflationRegimeWeight[1]; - BTC_NO_INFLATION_REGIME_WEIGHT = noInflationRegimeWeight[0]; - USDC_NO_INFLATION_REGIME_WEIGHT = noInflationRegimeWeight[1]; } IUpdateWeightRunner public updateWeightRunner; - uint40 public lastInterpolationTimePossible; - string public constant override typeAndVersion = "CreAuthWrapper 1.0.0"; - int256 public immutable BTC_FULL_INFLATION_REGIME_WEIGHT; - int256 public immutable USDC_FULL_INFLATION_REGIME_WEIGHT; - int256 public immutable BTC_MIXED_INFLATION_REGIME_WEIGHT; - int256 public immutable USDC_MIXED_INFLATION_REGIME_WEIGHT; - int256 public immutable BTC_NO_INFLATION_REGIME_WEIGHT; - int256 public immutable USDC_NO_INFLATION_REGIME_WEIGHT; - struct WorkflowMetadata { + address poolAddress; address allowedSender; address allowedWorkflowOwner; bytes10 allowedWorkflowName; } - bytes32 public s_writePermission; - - event WritePermissionRemoved(bytes32 indexed pool); - event TargetWeightsForwarded( - uint indexed regime, - address indexed pool, - address sender, - address workflowOwner, - bytes10 workflowName - ); - event WriteAdminSet(address indexed feedAdmin, bool indexed isAdmin); + WorkflowMetadata public s_writePermission; + + event WritePermissionRemoved(WorkflowMetadata indexed pool); + event TargetWeightsForwarded(address indexed pool, address sender, address workflowOwner, bytes10 workflowName); + event WriteAdminSet(address indexed poolAdmin, bool indexed isAdmin); event InvalidUpdatePermission(address indexed pool, address sender, address workflowOwner, bytes10 workflowName); error EmptyConfig(); @@ -64,20 +40,26 @@ contract TruflationCreAuthWrapper is ITypeAndVersion, Ownable { error UnauthorizedCaller(address caller); /// @notice Checks to see if this data ID, msg.sender, workflow owner, and workflow name are permissioned - /// @param pool The address of the pool for the feed /// @param workflowMetadata workflow metadata - function checkFeedPermission( - address pool, + function checkCREAuthentication( WorkflowMetadata memory workflowMetadata ) external view returns (bool hasPermission) { - bytes32 permission = _createPoolHash( - pool, - workflowMetadata.allowedSender, - workflowMetadata.allowedWorkflowOwner, - workflowMetadata.allowedWorkflowName - ); - - return s_writePermission == permission; + return checkCREAuth(workflowMetadata, s_writePermission); + } + + /// @notice Internal function to check CRE authentication + /// @param workflowMetadata workflow metadata + /// @param permissionWorkflowMetaData permissioned workflow metadata by the owner + /// @return hasPermission boolean indicating if permission is granted + function checkCREAuth( + WorkflowMetadata memory workflowMetadata, + WorkflowMetadata memory permissionWorkflowMetaData + ) internal pure returns (bool hasPermission) { + return + workflowMetadata.allowedSender == permissionWorkflowMetaData.allowedSender && + workflowMetadata.allowedWorkflowOwner == permissionWorkflowMetaData.allowedWorkflowOwner && + workflowMetadata.allowedWorkflowName == permissionWorkflowMetaData.allowedWorkflowName && + workflowMetadata.poolAddress == permissionWorkflowMetaData.poolAddress; } /// @notice Sets the UpdateWeightRunner contract address. Admin breakglass feature available to owner only. @@ -87,10 +69,9 @@ contract TruflationCreAuthWrapper is ITypeAndVersion, Ownable { updateWeightRunner = IUpdateWeightRunner(updateWeightRunnerAddress); } - /// @notice Initializes the config for a pool feed - /// @param pool The address of the pool for the feed - /// @param workflowMetadata List of workflow metadata (owners, senders, and names) for every feed - function setWriteConfig(address pool, WorkflowMetadata calldata workflowMetadata) external onlyOwner { + /// @notice Initializes the config for a pool + /// @param workflowMetadata List of workflow metadata (poolAddress, owners, senders, and names) + function setWriteConfig(WorkflowMetadata calldata workflowMetadata) external onlyOwner { if (workflowMetadata.allowedSender == address(0)) { revert InvalidAddress(workflowMetadata.allowedSender); } @@ -101,30 +82,32 @@ contract TruflationCreAuthWrapper is ITypeAndVersion, Ownable { revert InvalidWorkflowName(workflowMetadata.allowedWorkflowName); } - bytes32 poolHash = _createPoolHash( - pool, - workflowMetadata.allowedSender, - workflowMetadata.allowedWorkflowOwner, - workflowMetadata.allowedWorkflowName - ); + if (workflowMetadata.poolAddress == address(0)) { + revert InvalidAddress(workflowMetadata.poolAddress); + } - s_writePermission = poolHash; + s_writePermission = workflowMetadata; emit WriteAdminSet(workflowMetadata.allowedSender, true); } - /// @notice Removes the write permission config for a pool feed + /// @notice Removes the write permission config for a pool function removeWritePermissionConfig() external onlyOwner { - if (s_writePermission == bytes32(0)) { + if (s_writePermission.allowedSender == address(0)) { revert EmptyConfig(); } emit WritePermissionRemoved(s_writePermission); - s_writePermission = bytes32(0); + s_writePermission = WorkflowMetadata({ + poolAddress: address(0), + allowedSender: address(0), + allowedWorkflowOwner: address(0), + allowedWorkflowName: bytes10(0) + }); } - /// @notice Extracts the workflow name and the workflow owner from the metadata parameter of onReport + /// @notice Extracts the workflow name and the workflow owner from the metadata parameter of setTargetWeightsByRegimeWithMeta /// @param metadata The metadata in bytes format /// @return workflowOwner The owner of the workflow /// @return workflowName The name of the workflow @@ -146,65 +129,38 @@ contract TruflationCreAuthWrapper is ITypeAndVersion, Ownable { return (workflowOwner, workflowName); } - /// @notice Creates a pool hash used to permission write access - /// @param poolId The pool ID for the feed - /// @param sender The msg.sender of the transaction calling into onpool - /// @param workflowOwner The owner of the workflow - /// @param workflowName The name of the workflow - /// @return poolHash The keccak256 hash of the abi.encoded inputs - function _createPoolHash( - address poolId, - address sender, - address workflowOwner, - bytes10 workflowName - ) internal pure returns (bytes32) { - return keccak256(abi.encode(poolId, sender, workflowOwner, workflowName)); - } - - enum InflationRegime { - BTC_INFLATION, - MIXED_INFLATION, - NO_INFLATION - } - + /// @notice The main function that checks the CRE permissions and then passes through the weights + /// @param _poolAddress target pool address + /// @param _weights the new target weights + /// @param _lastInterpolationTimePossible the update interval + /// @param _meta the CRE metadata required to authenticate + /// @dev this wrapper assumes complete trust in the correct workflow in terms of weights and last interpolation time function setTargetWeightsByRegimeWithMeta( - uint newRegime, address _poolAddress, int256[] calldata _weights, + uint40 _lastInterpolationTimePossible, bytes calldata _meta ) external { - require( - newRegime == uint(InflationRegime.BTC_INFLATION) || - newRegime == uint(InflationRegime.MIXED_INFLATION) || - newRegime == uint(InflationRegime.NO_INFLATION), - "INVALID_REGIME" - ); - (address workflowOwner, bytes10 workflowName) = _getWorkflowMetaData(_meta); - bytes32 permission = _createPoolHash(_poolAddress, msg.sender, workflowOwner, workflowName); - - if (s_writePermission != permission) { + WorkflowMetadata memory workflowMetadata = WorkflowMetadata({ + poolAddress: _poolAddress, + allowedSender: msg.sender, //main check against the singleton CRE address + allowedWorkflowOwner: workflowOwner, //check against rogue workflows + allowedWorkflowName: workflowName + }); + + if (checkCREAuth(workflowMetadata, s_writePermission)) { + updateWeightRunner.setTargetWeightsManually( + _weights, + _poolAddress, + _lastInterpolationTimePossible, + _weights.length + ); + emit TargetWeightsForwarded(_poolAddress, msg.sender, workflowOwner, workflowName); + } else { emit InvalidUpdatePermission(_poolAddress, msg.sender, workflowOwner, workflowName); revert UnauthorizedCaller(msg.sender); } - - if (newRegime == uint40(InflationRegime.BTC_INFLATION)) { - require(_weights.length == 2, "ARRAY_LENGTH_MISMATCH"); - require(_weights[0] == BTC_FULL_INFLATION_REGIME_WEIGHT, "INVALID_WEIGHT"); - require(_weights[1] == USDC_FULL_INFLATION_REGIME_WEIGHT, "INVALID_WEIGHT"); - } else if (newRegime == uint40(InflationRegime.MIXED_INFLATION)) { - require(_weights.length == 2, "ARRAY_LENGTH_MISMATCH"); - require(_weights[0] == BTC_MIXED_INFLATION_REGIME_WEIGHT, "INVALID_WEIGHT"); - require(_weights[1] == USDC_MIXED_INFLATION_REGIME_WEIGHT, "INVALID_WEIGHT"); - } else { - require(_weights.length == 2, "ARRAY_LENGTH_MISMATCH"); - require(_weights[0] == BTC_NO_INFLATION_REGIME_WEIGHT, "INVALID_WEIGHT"); - require(_weights[1] == USDC_NO_INFLATION_REGIME_WEIGHT, "INVALID_WEIGHT"); - } - - updateWeightRunner.setTargetWeightsManually(_weights, _poolAddress, lastInterpolationTimePossible, uint(2)); - - emit TargetWeightsForwarded(newRegime, _poolAddress, msg.sender, workflowOwner, workflowName); } } From 4b45f246d70c20af45f57310ac601304b68ae96b Mon Sep 17 00:00:00 2001 From: christian harrington Date: Thu, 13 Nov 2025 11:04:34 +0000 Subject: [PATCH 04/11] new generic auth wrapper --- .../wrappers/{TruflationCreAuthWrapper.sol => CreAuthWrapper.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pkg/pool-quantamm/contracts/wrappers/{TruflationCreAuthWrapper.sol => CreAuthWrapper.sol} (100%) diff --git a/pkg/pool-quantamm/contracts/wrappers/TruflationCreAuthWrapper.sol b/pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol similarity index 100% rename from pkg/pool-quantamm/contracts/wrappers/TruflationCreAuthWrapper.sol rename to pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol From f1539f75927e03058531d8dd6d8f60d006a3f85f Mon Sep 17 00:00:00 2001 From: christian harrington Date: Thu, 13 Nov 2025 13:59:37 +0000 Subject: [PATCH 05/11] progress with rule wrapper --- .../contracts/UpdateWeightRunner.sol | 3 +- .../contracts/rules/CreAuthWrapper.sol | 122 +++++++++++++ .../contracts/wrappers/CreAuthWrapper.sol | 166 ------------------ 3 files changed, 124 insertions(+), 167 deletions(-) create mode 100644 pkg/pool-quantamm/contracts/rules/CreAuthWrapper.sol delete mode 100644 pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol diff --git a/pkg/pool-quantamm/contracts/UpdateWeightRunner.sol b/pkg/pool-quantamm/contracts/UpdateWeightRunner.sol index 7124865d..6d4db55b 100644 --- a/pkg/pool-quantamm/contracts/UpdateWeightRunner.sol +++ b/pkg/pool-quantamm/contracts/UpdateWeightRunner.sol @@ -646,7 +646,8 @@ contract UpdateWeightRunner is IUpdateWeightRunner { //it also centralises logic for weight vectors, just like normal rules, zk rules do not to duplicate logic somewhere else. _calculateMultiplerAndSetWeights(params); } -/// @notice Breakglass function to allow the admin or the pool manager to set the quantammAdmins weights manually + + /// @notice Breakglass function to allow the admin or the pool manager to set the quantammAdmins weights manually /// @param _weights the new weights /// @param _poolAddress the target pool /// @param _interpolationTime the time required to calcluate the multiplier diff --git a/pkg/pool-quantamm/contracts/rules/CreAuthWrapper.sol b/pkg/pool-quantamm/contracts/rules/CreAuthWrapper.sol new file mode 100644 index 00000000..8e3f31fa --- /dev/null +++ b/pkg/pool-quantamm/contracts/rules/CreAuthWrapper.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IReceiver} from "@chainlink/contracts/src/v0.8/keystone/interfaces/IReceiver.sol"; +import { ITypeAndVersion } from "@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { ICreReceiver } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/ICreReceiver.sol"; +import { IUpdateRule } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/IUpdateRule.sol"; // Ensure this path is correct +import { UpdateWeightRunner } from "../UpdateWeightRunner.sol"; +import "./base/QuantammMathGuard.sol"; + +/// @title The Wrapper needed to bridge between CRE auth and update weighr runner +/// @dev CRE has a single address per chain that can execute functions. +/// While this means that msg.sender can be constant it also means anyone can setup +/// a CRE workflow and it will trigger from the same msg.sender. +/// The metadata is set by the workflow sender and cannot be manipulated. +/// Checking the metadata as well as the msg.sender means that the auth is complete +contract CREUpdateRule is QuantAMMMathGuard, ICreReceiver, ITypeAndVersion, IUpdateRule { + constructor(address updateWeightRunnerAddress) ICreReceiver() { + updateWeightRunner = UpdateWeightRunner(updateWeightRunnerAddress); + } + + UpdateWeightRunner public updateWeightRunner; + + string public constant override typeAndVersion = "CreAuthWrapper 1.0.0"; + + struct WorkflowMetadata { + address poolAddress; + uint256[] weights; + uint40 lastInterpolationTimePossible; + } + + event TargetWeightsForwarded(address indexed pool, address sender); + + error InvalidAddress(address addr); + error NotImplemented(address sender); + + /// @notice Sets the UpdateWeightRunner contract address. Admin breakglass feature available to owner only. + /// @param updateWeightRunnerAddress The address of the UpdateWeightRunner contract + function setUpdateWeightRunner(address updateWeightRunnerAddress) external onlyOwner { + require(updateWeightRunnerAddress != address(0), "INVADDR"); + updateWeightRunner = UpdateWeightRunner(updateWeightRunnerAddress); + } + + /// @notice The main function that checks the CRE permissions and then passes through the weights + /// @param _poolAddress target pool address + /// @param _weights the new target weights + /// @param _lastInterpolationTimePossible the update interval + /// @dev this wrapper assumes complete trust in the correct workflow in terms of weights and last interpolation time + function setTargetWeightsByRegimeWithMeta( + address _poolAddress, + int256[] memory _weights, + uint40 _lastInterpolationTimePossible + ) internal { + updateWeightRunner.setTargetWeightsManually( + _weights, + _poolAddress, + _lastInterpolationTimePossible, + _weights.length + ); + emit TargetWeightsForwarded(_poolAddress, msg.sender); + } + + function _processReport(bytes calldata report) internal virtual override { + WorkflowMetadata memory meta = abi.decode(report, (WorkflowMetadata)); + if (meta.poolAddress == address(0) + //as the pool address is part of the report, make sure that the rule doesnt call another pool + && address(updateWeightRunner.rules(meta.poolAddress)) == address(this)) { + revert InvalidAddress(meta.poolAddress); + } + + UpdateWeightRunner.CalculateMuliplierAndSetWeightsLocal memory local; + local.poolAddress = meta.poolAddress; + local.updateInterval = int256(uint256(meta.lastInterpolationTimePossible)); + local.absoluteWeightGuardRail18 = 0; + + int256[] memory updated = new int256[](meta.weights.length); + for (uint256 i = 0; i < meta.weights.length; ++i) { + updated[i] = int256(meta.weights[i]); + } + //Guard weights is done in the base contract so regardless of the rule the logic will always be executed + int256[] memory updatedWeights = _guardQuantAMMWeights( + updated, + _prevWeights, + int128(uint128(_epsilonMax)), + int128(uint128(_absoluteWeightGuardRail)) + ); + + local.updatedWeights = updated; + local.currentWeights = new int256[](updated.length); + + updateWeightRunner.calculateMultiplierAndSetWeightsFromRule(local); + } + + function CalculateNewWeights( + int256[] calldata , + int256[] memory , + address , + int256[][] calldata, + uint64[] calldata, + uint64, + uint64 + ) external override returns (int256[] memory) { + //throw not silently pass as this if for the standard perform update call. CRE takes over this function + //this is as intended and why calculateMultiplierAndSetWeightsFromRule exists + revert NotImplemented(msg.sender); + } + + function initialisePoolRuleIntermediateValues( + address _poolAddress, + int256[] memory _newMovingAverages, + int256[] memory _newInitialValues, + uint _numberOfAssets + ) external override { + //this is fine to be empty, it will be called during pool creation, the pool and rule will initialise without throwing + } + + function validParameters(int256[][] calldata ) external view override returns (bool) { + // No parameters to validate in this wrapper + return true; + } +} diff --git a/pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol b/pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol deleted file mode 100644 index 7caded19..00000000 --- a/pkg/pool-quantamm/contracts/wrappers/CreAuthWrapper.sol +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import { ITypeAndVersion } from "@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { IUpdateWeightRunner } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/IUpdateWeightRunner.sol"; - -/// @title The Wrapper needed to bridge between CRE auth and update weighr runner -/// @dev CRE has a single address per chain that can execute functions. -/// While this means that msg.sender can be constant it also means anyone can setup -/// a CRE workflow and it will trigger from the same msg.sender. -/// The metadata is set by the workflow sender and cannot be manipulated. -/// Checking the metadata as well as the msg.sender means that the auth is complete -contract CREAuthWrapper is ITypeAndVersion, Ownable { - constructor(address updateWeightRunnerAddress) Ownable(msg.sender) { - updateWeightRunner = IUpdateWeightRunner(updateWeightRunnerAddress); - } - - IUpdateWeightRunner public updateWeightRunner; - - string public constant override typeAndVersion = "CreAuthWrapper 1.0.0"; - - struct WorkflowMetadata { - address poolAddress; - address allowedSender; - address allowedWorkflowOwner; - bytes10 allowedWorkflowName; - } - - WorkflowMetadata public s_writePermission; - - event WritePermissionRemoved(WorkflowMetadata indexed pool); - event TargetWeightsForwarded(address indexed pool, address sender, address workflowOwner, bytes10 workflowName); - event WriteAdminSet(address indexed poolAdmin, bool indexed isAdmin); - event InvalidUpdatePermission(address indexed pool, address sender, address workflowOwner, bytes10 workflowName); - - error EmptyConfig(); - error InvalidAddress(address addr); - error InvalidWorkflowName(bytes10 workflowName); - error UnauthorizedCaller(address caller); - - /// @notice Checks to see if this data ID, msg.sender, workflow owner, and workflow name are permissioned - /// @param workflowMetadata workflow metadata - function checkCREAuthentication( - WorkflowMetadata memory workflowMetadata - ) external view returns (bool hasPermission) { - return checkCREAuth(workflowMetadata, s_writePermission); - } - - /// @notice Internal function to check CRE authentication - /// @param workflowMetadata workflow metadata - /// @param permissionWorkflowMetaData permissioned workflow metadata by the owner - /// @return hasPermission boolean indicating if permission is granted - function checkCREAuth( - WorkflowMetadata memory workflowMetadata, - WorkflowMetadata memory permissionWorkflowMetaData - ) internal pure returns (bool hasPermission) { - return - workflowMetadata.allowedSender == permissionWorkflowMetaData.allowedSender && - workflowMetadata.allowedWorkflowOwner == permissionWorkflowMetaData.allowedWorkflowOwner && - workflowMetadata.allowedWorkflowName == permissionWorkflowMetaData.allowedWorkflowName && - workflowMetadata.poolAddress == permissionWorkflowMetaData.poolAddress; - } - - /// @notice Sets the UpdateWeightRunner contract address. Admin breakglass feature available to owner only. - /// @param updateWeightRunnerAddress The address of the UpdateWeightRunner contract - function setUpdateWeightRunner(address updateWeightRunnerAddress) external onlyOwner { - require(updateWeightRunnerAddress != address(0), "INVADDR"); - updateWeightRunner = IUpdateWeightRunner(updateWeightRunnerAddress); - } - - /// @notice Initializes the config for a pool - /// @param workflowMetadata List of workflow metadata (poolAddress, owners, senders, and names) - function setWriteConfig(WorkflowMetadata calldata workflowMetadata) external onlyOwner { - if (workflowMetadata.allowedSender == address(0)) { - revert InvalidAddress(workflowMetadata.allowedSender); - } - if (workflowMetadata.allowedWorkflowOwner == address(0)) { - revert InvalidAddress(workflowMetadata.allowedWorkflowOwner); - } - if (workflowMetadata.allowedWorkflowName == bytes10(0)) { - revert InvalidWorkflowName(workflowMetadata.allowedWorkflowName); - } - - if (workflowMetadata.poolAddress == address(0)) { - revert InvalidAddress(workflowMetadata.poolAddress); - } - - s_writePermission = workflowMetadata; - - emit WriteAdminSet(workflowMetadata.allowedSender, true); - } - - /// @notice Removes the write permission config for a pool - function removeWritePermissionConfig() external onlyOwner { - if (s_writePermission.allowedSender == address(0)) { - revert EmptyConfig(); - } - - emit WritePermissionRemoved(s_writePermission); - - s_writePermission = WorkflowMetadata({ - poolAddress: address(0), - allowedSender: address(0), - allowedWorkflowOwner: address(0), - allowedWorkflowName: bytes10(0) - }); - } - - /// @notice Extracts the workflow name and the workflow owner from the metadata parameter of setTargetWeightsByRegimeWithMeta - /// @param metadata The metadata in bytes format - /// @return workflowOwner The owner of the workflow - /// @return workflowName The name of the workflow - function _getWorkflowMetaData(bytes memory metadata) internal pure returns (address, bytes10) { - address workflowOwner; - bytes10 workflowName; - // (first 32 bytes contain length of the byte array) - // workflow_cid // offset 32, size 32 - // workflow_name // offset 64, size 10 - // workflow_owner // offset 74, size 20 - // pool_name // offset 94, size 2 - assembly { - // no shifting needed for bytes10 type - workflowName := mload(add(metadata, 64)) - // shift right by 12 bytes to get the actual value - workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) - } - - return (workflowOwner, workflowName); - } - - /// @notice The main function that checks the CRE permissions and then passes through the weights - /// @param _poolAddress target pool address - /// @param _weights the new target weights - /// @param _lastInterpolationTimePossible the update interval - /// @param _meta the CRE metadata required to authenticate - /// @dev this wrapper assumes complete trust in the correct workflow in terms of weights and last interpolation time - function setTargetWeightsByRegimeWithMeta( - address _poolAddress, - int256[] calldata _weights, - uint40 _lastInterpolationTimePossible, - bytes calldata _meta - ) external { - (address workflowOwner, bytes10 workflowName) = _getWorkflowMetaData(_meta); - - WorkflowMetadata memory workflowMetadata = WorkflowMetadata({ - poolAddress: _poolAddress, - allowedSender: msg.sender, //main check against the singleton CRE address - allowedWorkflowOwner: workflowOwner, //check against rogue workflows - allowedWorkflowName: workflowName - }); - - if (checkCREAuth(workflowMetadata, s_writePermission)) { - updateWeightRunner.setTargetWeightsManually( - _weights, - _poolAddress, - _lastInterpolationTimePossible, - _weights.length - ); - emit TargetWeightsForwarded(_poolAddress, msg.sender, workflowOwner, workflowName); - } else { - emit InvalidUpdatePermission(_poolAddress, msg.sender, workflowOwner, workflowName); - revert UnauthorizedCaller(msg.sender); - } - } -} From 51080b1deb693b881d5db62d5487744bc8b4da3b Mon Sep 17 00:00:00 2001 From: christian harrington Date: Mon, 17 Nov 2025 19:05:43 +0000 Subject: [PATCH 06/11] CRE update rule corrections and tests --- .../mock/mockRules/MockCreReceiver.sol | 19 + .../mock/mockRules/MockCreUpdateRule.sol | 11 + .../contracts/rules/CreReceiver.sol | 123 ++++ .../{CreAuthWrapper.sol => CreUpdateRule.sol} | 98 ++-- .../rules/base/QuantammMathGuard.sol | 2 +- .../test/foundry/rules/CreReceiver.t.sol | 542 ++++++++++++++++++ .../test/foundry/rules/CreReceiver_Fuzz.t.sol | 528 +++++++++++++++++ .../test/foundry/rules/CreUpdateRule.t.sol | 486 ++++++++++++++++ 8 files changed, 1763 insertions(+), 46 deletions(-) create mode 100644 pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol create mode 100644 pkg/pool-quantamm/contracts/mock/mockRules/MockCreUpdateRule.sol create mode 100644 pkg/pool-quantamm/contracts/rules/CreReceiver.sol rename pkg/pool-quantamm/contracts/rules/{CreAuthWrapper.sol => CreUpdateRule.sol} (52%) create mode 100644 pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol create mode 100644 pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol create mode 100644 pkg/pool-quantamm/test/foundry/rules/CreUpdateRule.t.sol diff --git a/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol b/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol new file mode 100644 index 00000000..3c38e884 --- /dev/null +++ b/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.24; +import "../../rules/CreReceiver.sol"; + +contract MockCreReceiver is CreReceiver { + bytes public lastReport; + bool public processCalled; + + constructor() CreReceiver() {} + + function ProcessReport(bytes calldata metaData) external { + _processReport(metaData); + } + + function _processReport(bytes calldata report) internal override { + processCalled = true; + lastReport = report; + } +} diff --git a/pkg/pool-quantamm/contracts/mock/mockRules/MockCreUpdateRule.sol b/pkg/pool-quantamm/contracts/mock/mockRules/MockCreUpdateRule.sol new file mode 100644 index 00000000..9c295843 --- /dev/null +++ b/pkg/pool-quantamm/contracts/mock/mockRules/MockCreUpdateRule.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.24; +import "../../rules/CreUpdateRule.sol"; + +contract MockCreUpdateRule is CreUpdateRule { + constructor(address _updateWeightRunner) CreUpdateRule(_updateWeightRunner) {} + + function ProcessReport(bytes memory metaData) external { + _processReportMemory(metaData); + } +} diff --git a/pkg/pool-quantamm/contracts/rules/CreReceiver.sol b/pkg/pool-quantamm/contracts/rules/CreReceiver.sol new file mode 100644 index 00000000..460b50cb --- /dev/null +++ b/pkg/pool-quantamm/contracts/rules/CreReceiver.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { + IERC165 +} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol"; +import { IReceiver } from "@chainlink/contracts/src/v0.8/keystone/interfaces/IReceiver.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +/// @title IReceiverTemplate - Abstract receiver with optional permission controls +/// @notice Provides flexible, updatable security checks for receiving workflow reports +/// @dev All permission fields default to zero (disabled). Use setter functions to enable checks. +/// https://docs.chain.link/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts#3-using-ireceivertemplate +abstract contract CreReceiver is IReceiver, Ownable { + // Optional permission fields (all default to zero = disabled) + address public forwarderAddress; // If set, only this address can call onReport + address public expectedAuthor; // If set, only reports from this workflow owner are accepted + bytes10 public expectedWorkflowName; // If set, only reports with this workflow name are accepted + bytes32 public expectedWorkflowId; // If set, only reports from this specific workflow ID are accepted + + // Custom errors + error InvalidSender(address sender, address expected); + error InvalidAuthor(address received, address expected); + error InvalidWorkflowName(bytes10 received, bytes10 expected); + error InvalidWorkflowId(bytes32 received, bytes32 expected); + + event ExpectedAuthorChanged(address indexed newAuthor, address indexed oldAuthor, address indexed changer); + event ExpectedWorkflowNameChanged(bytes10 indexed newName, bytes10 indexed oldName, address indexed changer); + event ExpectedWorkflowIdChanged(bytes32 indexed newId, bytes32 indexed oldId, address indexed changer); + event ForwarderAddressChanged(address indexed newForwarder, address indexed oldForwarder, address indexed changer); + + /// @notice Constructor sets msg.sender as the owner + /// @dev All permission fields are initialized to zero (disabled by default) + constructor() Ownable(msg.sender) {} + + /// @inheritdoc IReceiver + /// @dev Performs optional validation checks based on which permission fields are set + function onReport(bytes calldata metadata, bytes calldata report) external override { + // Security Check 1: Verify caller is the trusted Chainlink Forwarder (if configured) + if (forwarderAddress != address(0) && msg.sender != forwarderAddress) { + revert InvalidSender(msg.sender, forwarderAddress); + } + + // Security Checks 2-4: Verify workflow identity - ID, owner, and/or name (if any are configured) + if (expectedWorkflowId != bytes32(0) || expectedAuthor != address(0) || expectedWorkflowName != bytes10(0)) { + (bytes32 workflowId, bytes10 workflowName, address workflowOwner) = _decodeMetadata(metadata); + + if (expectedWorkflowId != bytes32(0) && workflowId != expectedWorkflowId) { + revert InvalidWorkflowId(workflowId, expectedWorkflowId); + } + if (expectedAuthor != address(0) && workflowOwner != expectedAuthor) { + revert InvalidAuthor(workflowOwner, expectedAuthor); + } + if (expectedWorkflowName != bytes10(0) && workflowName != expectedWorkflowName) { + revert InvalidWorkflowName(workflowName, expectedWorkflowName); + } + } + + _processReport(report); + } + + /// @notice Updates the forwarder address that is allowed to call onReport + /// @param _forwarder The new forwarder address (use address(0) to disable this check) + function setForwarderAddress(address _forwarder) external onlyOwner { + address oldForwarder = forwarderAddress; + forwarderAddress = _forwarder; + emit ForwarderAddressChanged(_forwarder, oldForwarder, msg.sender); + } + + /// @notice Updates the expected workflow owner address + /// @param _author The new expected author address (use address(0) to disable this check) + function setExpectedAuthor(address _author) external onlyOwner { + address oldAuthor = expectedAuthor; + expectedAuthor = _author; + emit ExpectedAuthorChanged(_author, oldAuthor, msg.sender); + } + + /// @notice Updates the expected workflow name + /// @param _name The new expected workflow name (use bytes10(0) to disable this check) + function setExpectedWorkflowName(bytes10 _name) external onlyOwner { + bytes10 oldName = expectedWorkflowName; + expectedWorkflowName = _name; + emit ExpectedWorkflowNameChanged(_name, oldName, msg.sender); + } + + /// @notice Updates the expected workflow ID + /// @param _id The new expected workflow ID (use bytes32(0) to disable this check) + function setExpectedWorkflowId(bytes32 _id) external onlyOwner { + bytes32 oldId = expectedWorkflowId; + expectedWorkflowId = _id; + emit ExpectedWorkflowIdChanged(_id, oldId, msg.sender); + } + + /// @notice Extracts all metadata fields from the onReport metadata parameter + /// @param metadata The metadata in bytes format + /// @return workflowId The unique identifier of the workflow (bytes32) + /// @return workflowName The name of the workflow (bytes10) + /// @return workflowOwner The owner address of the workflow + function _decodeMetadata( + bytes memory metadata + ) internal pure returns (bytes32 workflowId, bytes10 workflowName, address workflowOwner) { + // Metadata structure: + // - First 32 bytes: length of the byte array (standard for dynamic bytes) + // - Offset 32, size 32: workflow_id (bytes32) + // - Offset 64, size 10: workflow_name (bytes10) + // - Offset 74, size 20: workflow_owner (address) + assembly { + workflowId := mload(add(metadata, 32)) + workflowName := mload(add(metadata, 64)) + workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) + } + } + + /// @notice Abstract function to process the report data + /// @param report The report calldata containing your workflow's encoded data + /// @dev Implement this function with your contract's business logic + function _processReport(bytes calldata report) internal virtual; + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == type(IReceiver).interfaceId || interfaceId == type(IERC165).interfaceId; + } +} diff --git a/pkg/pool-quantamm/contracts/rules/CreAuthWrapper.sol b/pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol similarity index 52% rename from pkg/pool-quantamm/contracts/rules/CreAuthWrapper.sol rename to pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol index 8e3f31fa..c51a4734 100644 --- a/pkg/pool-quantamm/contracts/rules/CreAuthWrapper.sol +++ b/pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {IReceiver} from "@chainlink/contracts/src/v0.8/keystone/interfaces/IReceiver.sol"; +import { IReceiver } from "@chainlink/contracts/src/v0.8/keystone/interfaces/IReceiver.sol"; import { ITypeAndVersion } from "@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { ICreReceiver } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/ICreReceiver.sol"; +import { CreReceiver } from "./CreReceiver.sol"; import { IUpdateRule } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/IUpdateRule.sol"; // Ensure this path is correct import { UpdateWeightRunner } from "../UpdateWeightRunner.sol"; -import "./base/QuantammMathGuard.sol"; +import { QuantAMMMathGuard } from "./base/QuantammMathGuard.sol"; +import { IQuantAMMWeightedPool } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/IQuantAMMWeightedPool.sol"; /// @title The Wrapper needed to bridge between CRE auth and update weighr runner /// @dev CRE has a single address per chain that can execute functions. @@ -15,8 +16,8 @@ import "./base/QuantammMathGuard.sol"; /// a CRE workflow and it will trigger from the same msg.sender. /// The metadata is set by the workflow sender and cannot be manipulated. /// Checking the metadata as well as the msg.sender means that the auth is complete -contract CREUpdateRule is QuantAMMMathGuard, ICreReceiver, ITypeAndVersion, IUpdateRule { - constructor(address updateWeightRunnerAddress) ICreReceiver() { +contract CreUpdateRule is QuantAMMMathGuard, CreReceiver, ITypeAndVersion, IUpdateRule { + constructor(address updateWeightRunnerAddress) CreReceiver() { updateWeightRunner = UpdateWeightRunner(updateWeightRunnerAddress); } @@ -31,6 +32,7 @@ contract CREUpdateRule is QuantAMMMathGuard, ICreReceiver, ITypeAndVersion, IUpd } event TargetWeightsForwarded(address indexed pool, address sender); + event UpdateWeightRunnerChanged(address indexed newAddress, address indexed oldAddress, address indexed changer); error InvalidAddress(address addr); error NotImplemented(address sender); @@ -38,70 +40,75 @@ contract CREUpdateRule is QuantAMMMathGuard, ICreReceiver, ITypeAndVersion, IUpd /// @notice Sets the UpdateWeightRunner contract address. Admin breakglass feature available to owner only. /// @param updateWeightRunnerAddress The address of the UpdateWeightRunner contract function setUpdateWeightRunner(address updateWeightRunnerAddress) external onlyOwner { - require(updateWeightRunnerAddress != address(0), "INVADDR"); + if (updateWeightRunnerAddress == address(0)) { + revert InvalidAddress(updateWeightRunnerAddress); + } + address oldAddress = address(updateWeightRunner); + updateWeightRunner = UpdateWeightRunner(updateWeightRunnerAddress); + emit UpdateWeightRunnerChanged(updateWeightRunnerAddress, oldAddress, msg.sender); } - /// @notice The main function that checks the CRE permissions and then passes through the weights - /// @param _poolAddress target pool address - /// @param _weights the new target weights - /// @param _lastInterpolationTimePossible the update interval - /// @dev this wrapper assumes complete trust in the correct workflow in terms of weights and last interpolation time - function setTargetWeightsByRegimeWithMeta( - address _poolAddress, - int256[] memory _weights, - uint40 _lastInterpolationTimePossible - ) internal { - updateWeightRunner.setTargetWeightsManually( - _weights, - _poolAddress, - _lastInterpolationTimePossible, - _weights.length - ); - emit TargetWeightsForwarded(_poolAddress, msg.sender); + function _processReport(bytes calldata report) internal virtual override { + //Cannot change the process report structure however for testing we need to have a internal memory wrapper + _processReportMemory(report); } - function _processReport(bytes calldata report) internal virtual override { + function _processReportMemory(bytes memory report) internal virtual { + UpdateWeightRunner.CalculateMuliplierAndSetWeightsLocal memory local; + WorkflowMetadata memory meta = abi.decode(report, (WorkflowMetadata)); - if (meta.poolAddress == address(0) - //as the pool address is part of the report, make sure that the rule doesnt call another pool - && address(updateWeightRunner.rules(meta.poolAddress)) == address(this)) { + + //as the pool address is part of the report, make sure that the rule doesnt call another pool + if (meta.poolAddress == address(0) || address(updateWeightRunner.rules(meta.poolAddress)) != address(this)) { revert InvalidAddress(meta.poolAddress); } - - UpdateWeightRunner.CalculateMuliplierAndSetWeightsLocal memory local; + + uint256[] memory currentWeights = IQuantAMMWeightedPool(meta.poolAddress).getNormalizedWeights(); + local.currentWeights = new int256[](currentWeights.length); + + require(currentWeights.length == meta.weights.length, "WRONGLENGTH"); + UpdateWeightRunner.PoolRuleSettings memory poolSettings = updateWeightRunner.getPoolRuleSettings( + meta.poolAddress + ); + local.poolAddress = meta.poolAddress; - local.updateInterval = int256(uint256(meta.lastInterpolationTimePossible)); - local.absoluteWeightGuardRail18 = 0; + local.updateInterval = int256(uint256(poolSettings.timingSettings.updateInterval)); int256[] memory updated = new int256[](meta.weights.length); + for (uint256 i = 0; i < meta.weights.length; ++i) { + local.currentWeights[i] = int256(currentWeights[i]); updated[i] = int256(meta.weights[i]); } - //Guard weights is done in the base contract so regardless of the rule the logic will always be executed - int256[] memory updatedWeights = _guardQuantAMMWeights( + + //Guard weights is done in the base contract so regardless of the rule the logic will always be executed + int256[] memory guardedWeights = _guardQuantAMMWeights( updated, - _prevWeights, - int128(uint128(_epsilonMax)), - int128(uint128(_absoluteWeightGuardRail)) + local.currentWeights, + int128(uint128(poolSettings.epsilonMax)), + int128(uint128(poolSettings.absoluteWeightGuardRail)) ); - local.updatedWeights = updated; - local.currentWeights = new int256[](updated.length); + //current weights always the block weights + //updated weights are used to determine what the multipliers should be + local.updatedWeights = guardedWeights; updateWeightRunner.calculateMultiplierAndSetWeightsFromRule(local); + + emit TargetWeightsForwarded(local.poolAddress, msg.sender); } function CalculateNewWeights( - int256[] calldata , - int256[] memory , - address , + int256[] calldata, + int256[] memory, + address, int256[][] calldata, uint64[] calldata, uint64, uint64 - ) external override returns (int256[] memory) { - //throw not silently pass as this if for the standard perform update call. CRE takes over this function + ) external override view returns (int256[] memory) { + //throw not silently pass as this is for the standard perform update call. CRE takes over this function //this is as intended and why calculateMultiplierAndSetWeightsFromRule exists revert NotImplemented(msg.sender); } @@ -113,10 +120,11 @@ contract CREUpdateRule is QuantAMMMathGuard, ICreReceiver, ITypeAndVersion, IUpd uint _numberOfAssets ) external override { //this is fine to be empty, it will be called during pool creation, the pool and rule will initialise without throwing + //all parametisation should be within CRE workflow } - function validParameters(int256[][] calldata ) external view override returns (bool) { - // No parameters to validate in this wrapper + function validParameters(int256[][] calldata) external pure override returns (bool) { + // No parameters to validate in this wrapper. Again called during initialisation so dont want it to throw. return true; } } diff --git a/pkg/pool-quantamm/contracts/rules/base/QuantammMathGuard.sol b/pkg/pool-quantamm/contracts/rules/base/QuantammMathGuard.sol index f936e9ab..c651b6dd 100644 --- a/pkg/pool-quantamm/contracts/rules/base/QuantammMathGuard.sol +++ b/pkg/pool-quantamm/contracts/rules/base/QuantammMathGuard.sol @@ -18,7 +18,7 @@ abstract contract QuantAMMMathGuard { /// @param _absoluteWeightGuardRail Minimum allowed weight in the QuantAMM whitepaper function _guardQuantAMMWeights( int256[] memory _weights, - int256[] calldata _prevWeights, + int256[] memory _prevWeights, int256 _epsilonMax, int256 _absoluteWeightGuardRail ) internal pure returns (int256[] memory guardedNewWeights) { diff --git a/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol b/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol new file mode 100644 index 00000000..a79359be --- /dev/null +++ b/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol @@ -0,0 +1,542 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "forge-std/Test.sol"; +import { + IERC165 +} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol"; +import { IReceiver } from "@chainlink/contracts/src/v0.8/keystone/interfaces/IReceiver.sol"; + +import { MockCreReceiver } from "../../../contracts/mock/mockRules/MockCreReceiver.sol"; +import { CreReceiver } from "../../../contracts/rules/CreReceiver.sol"; + +contract CreReceiverTest is Test { + MockCreReceiver internal receiver; + + // Re-declare events so we can use expectEmit + event ExpectedAuthorChanged(address indexed newAuthor, address indexed oldAuthor, address indexed changer); + event ExpectedWorkflowNameChanged(bytes10 indexed newName, bytes10 indexed oldName, address indexed changer); + event ExpectedWorkflowIdChanged(bytes32 indexed newId, bytes32 indexed oldId, address indexed changer); + event ForwarderAddressChanged(address indexed newForwarder, address indexed oldForwarder, address indexed changer); + + function setUp() public { + receiver = new MockCreReceiver(); + } + + // ----------------------- + // Helpers + // ----------------------- + + function _encodeMetadata( + bytes32 workflowId, + bytes10 workflowName, + address workflowOwner + ) internal pure returns (bytes memory) { + return abi.encodePacked(workflowId, workflowName, workflowOwner); + } + + function _invalidSenderSelector() internal pure returns (bytes4) { + return bytes4(keccak256("InvalidSender(address,address)")); + } + + function _invalidAuthorSelector() internal pure returns (bytes4) { + return bytes4(keccak256("InvalidAuthor(address,address)")); + } + + function _invalidWorkflowIdSelector() internal pure returns (bytes4) { + return bytes4(keccak256("InvalidWorkflowId(bytes32,bytes32)")); + } + + function _invalidWorkflowNameSelector() internal pure returns (bytes4) { + return bytes4(keccak256("InvalidWorkflowName(bytes10,bytes10)")); + } + + function _ownableUnauthorizedAccountSelector() internal pure returns (bytes4) { + return bytes4(keccak256("OwnableUnauthorizedAccount(address)")); + } + + function testInitialState() public view { + assertEq(receiver.forwarderAddress(), address(0)); + assertEq(receiver.expectedAuthor(), address(0)); + assertEq(receiver.expectedWorkflowName(), bytes10(0)); + assertEq(receiver.expectedWorkflowId(), bytes32(0)); + + // CreReceiver constructor passes msg.sender into Ownable + assertEq(receiver.owner(), address(this)); + } + + function testSetForwarderAddressUpdatesStateAndEmitsEvent() public { + address newForwarder = address(0xF0F0); + address oldForwarder = receiver.forwarderAddress(); + + vm.expectEmit(true, true, true, false); + emit ForwarderAddressChanged(newForwarder, oldForwarder, address(this)); + + receiver.setForwarderAddress(newForwarder); + + assertEq(receiver.forwarderAddress(), newForwarder); + } + + function testSetForwarderAddressOnlyOwner() public { + address nonOwner = address(0xBEEF); + address newForwarder = address(0xF0F0); + + vm.prank(nonOwner); + vm.expectRevert(abi.encodeWithSelector(_ownableUnauthorizedAccountSelector(), nonOwner)); + receiver.setForwarderAddress(newForwarder); + } + + function testSetExpectedAuthorUpdatesStateAndEmitsEvent() public { + address newAuthor = address(0xA1); + address oldAuthor = receiver.expectedAuthor(); + + vm.expectEmit(true, true, true, false); + emit ExpectedAuthorChanged(newAuthor, oldAuthor, address(this)); + + receiver.setExpectedAuthor(newAuthor); + + assertEq(receiver.expectedAuthor(), newAuthor); + } + + function testSetExpectedAuthorOnlyOwner() public { + address nonOwner = address(0xBEEF); + address newAuthor = address(0xA1); + + vm.prank(nonOwner); + vm.expectRevert(abi.encodeWithSelector(_ownableUnauthorizedAccountSelector(), nonOwner)); + receiver.setExpectedAuthor(newAuthor); + } + + function testSetExpectedWorkflowNameUpdatesStateAndEmitsEvent() public { + bytes10 newName = bytes10("WF_LOW_001"); + bytes10 oldName = receiver.expectedWorkflowName(); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowNameChanged(newName, oldName, address(this)); + + receiver.setExpectedWorkflowName(newName); + + assertEq(receiver.expectedWorkflowName(), newName); + } + + function testSetExpectedWorkflowNameOnlyOwner() public { + address nonOwner = address(0xBEEF); + bytes10 newName = bytes10("WF_LOW_001"); + + vm.prank(nonOwner); + vm.expectRevert(abi.encodeWithSelector(_ownableUnauthorizedAccountSelector(), nonOwner)); + receiver.setExpectedWorkflowName(newName); + } + + function testSetExpectedWorkflowIdUpdatesStateAndEmitsEvent() public { + bytes32 newId = keccak256("new-workflow-id"); + bytes32 oldId = receiver.expectedWorkflowId(); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowIdChanged(newId, oldId, address(this)); + + receiver.setExpectedWorkflowId(newId); + + assertEq(receiver.expectedWorkflowId(), newId); + } + + function testSetExpectedWorkflowIdOnlyOwner() public { + address nonOwner = address(0xBEEF); + bytes32 newId = keccak256("new-workflow-id"); + + vm.prank(nonOwner); + vm.expectRevert(abi.encodeWithSelector(_ownableUnauthorizedAccountSelector(), nonOwner)); + receiver.setExpectedWorkflowId(newId); + } + + function testOnReportSucceedsWhenNoForwarderOrExpectationsSet() public { + bytes memory metadata = ""; + bytes memory report = abi.encodePacked(uint256(123)); + + receiver.onReport(metadata, report); + + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testOnReportRevertsForInvalidSenderWhenForwarderIsConfigured() public { + address trustedForwarder = address(0xF0F0); + receiver.setForwarderAddress(trustedForwarder); + + address badCaller = address(0xBAD); + bytes memory metadata = ""; + bytes memory report = abi.encodePacked(uint256(123)); + + vm.prank(badCaller); + vm.expectRevert(abi.encodeWithSelector(_invalidSenderSelector(), badCaller, trustedForwarder)); + receiver.onReport(metadata, report); + } + + function testOnReportAllowsTrustedForwarder() public { + address trustedForwarder = address(0xF0F0); + receiver.setForwarderAddress(trustedForwarder); + + bytes32 workflowId = keccak256("wf-id"); + bytes10 workflowName = bytes10("WF_LOW_001"); + address workflowOwner = address(this); + + bytes memory metadata = _encodeMetadata(workflowId, workflowName, workflowOwner); + bytes memory report = abi.encodePacked(uint256(999)); + + vm.prank(trustedForwarder); + receiver.onReport(metadata, report); + + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testOnReportRevertsForInvalidWorkflowId() public { + bytes32 expectedId = keccak256("expected-id"); + receiver.setExpectedWorkflowId(expectedId); + + bytes32 wrongId = keccak256("wrong-id"); + bytes10 workflowName = bytes10("WF_LOW_001"); + address workflowOwner = address(this); + + bytes memory metadata = _encodeMetadata(wrongId, workflowName, workflowOwner); + bytes memory report = abi.encodePacked(uint256(123)); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), wrongId, expectedId)); + receiver.onReport(metadata, report); + } + + function testOnReportSucceedsWithCorrectWorkflowId() public { + bytes32 expectedId = keccak256("expected-id"); + receiver.setExpectedWorkflowId(expectedId); + + bytes10 workflowName = bytes10("WF_LOW_001"); + address workflowOwner = address(this); + + bytes memory metadata = _encodeMetadata(expectedId, workflowName, workflowOwner); + bytes memory report = abi.encodePacked(uint256(123)); + + receiver.onReport(metadata, report); + + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testOnReportRevertsForInvalidAuthor() public { + address expectedAuthor = address(0xA1); + receiver.setExpectedAuthor(expectedAuthor); + + bytes32 workflowId = keccak256("wf-id"); + bytes10 workflowName = bytes10("WF_LOW_001"); + address wrongOwner = address(0xDEAD); + + bytes memory metadata = _encodeMetadata(workflowId, workflowName, wrongOwner); + bytes memory report = abi.encodePacked(uint256(123)); + + vm.expectRevert(abi.encodeWithSelector(_invalidAuthorSelector(), wrongOwner, expectedAuthor)); + receiver.onReport(metadata, report); + } + + function testOnReportSucceedsWithCorrectAuthor() public { + address expectedAuthor = address(0xA1); + receiver.setExpectedAuthor(expectedAuthor); + + bytes32 workflowId = keccak256("wf-id"); + bytes10 workflowName = bytes10("WF_LOW_001"); + + bytes memory metadata = _encodeMetadata(workflowId, workflowName, expectedAuthor); + bytes memory report = abi.encodePacked(uint256(123)); + + receiver.onReport(metadata, report); + + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testOnReportRevertsForInvalidWorkflowName() public { + bytes10 expectedName = bytes10("WF_EXPECT"); + receiver.setExpectedWorkflowName(expectedName); + + bytes32 workflowId = keccak256("wf-id"); + bytes10 wrongName = bytes10("WF_WRONG"); + address workflowOwner = address(this); + + bytes memory metadata = _encodeMetadata(workflowId, wrongName, workflowOwner); + bytes memory report = abi.encodePacked(uint256(123)); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), wrongName, expectedName)); + receiver.onReport(metadata, report); + } + + function testOnReportSucceedsWithCorrectWorkflowName() public { + bytes10 expectedName = bytes10("WF_EXPECT"); + receiver.setExpectedWorkflowName(expectedName); + + bytes32 workflowId = keccak256("wf-id"); + address workflowOwner = address(this); + + bytes memory metadata = _encodeMetadata(workflowId, expectedName, workflowOwner); + bytes memory report = abi.encodePacked(uint256(123)); + + receiver.onReport(metadata, report); + + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testOnReportSucceedsWhenAllChecksPass() public { + address trustedForwarder = address(0xF0F0); + receiver.setForwarderAddress(trustedForwarder); + + bytes32 expectedId = keccak256("expected-id"); + bytes10 expectedName = bytes10("WF_EXPECT"); + address expectedAuthor = address(0xA1); + + receiver.setExpectedWorkflowId(expectedId); + receiver.setExpectedWorkflowName(expectedName); + receiver.setExpectedAuthor(expectedAuthor); + + bytes memory metadata = _encodeMetadata(expectedId, expectedName, expectedAuthor); + bytes memory report = abi.encodePacked(uint256(777)); + + vm.prank(trustedForwarder); + receiver.onReport(metadata, report); + + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testDisableForwarderAllowsAnySender() public { + address trustedForwarder = address(0xF0F0); + receiver.setForwarderAddress(trustedForwarder); + + address nonForwarder = address(0xBEEF); + bytes memory metadata = ""; + bytes memory report = abi.encodePacked(uint256(111)); + + vm.prank(nonForwarder); + vm.expectRevert(abi.encodeWithSelector(_invalidSenderSelector(), nonForwarder, trustedForwarder)); + + receiver.onReport(metadata, report); + receiver.setForwarderAddress(address(0)); + + vm.prank(nonForwarder); + receiver.onReport(metadata, report); + + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testDisableExpectedAuthorAllowsAnyAuthor() public { + address expectedAuthor = address(0xA1); + receiver.setExpectedAuthor(expectedAuthor); + + bytes32 workflowId = keccak256("wf-id"); + bytes10 workflowName = bytes10("WF_AUTH"); + address wrongOwner = address(0xDEAD); + + bytes memory metadata = _encodeMetadata(workflowId, workflowName, wrongOwner); + bytes memory report = abi.encodePacked(uint256(222)); + + vm.expectRevert(abi.encodeWithSelector(_invalidAuthorSelector(), wrongOwner, expectedAuthor)); + receiver.onReport(metadata, report); + + receiver.setExpectedAuthor(address(0)); + receiver.onReport(metadata, report); + + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testDisableExpectedWorkflowIdAllowsAnyWorkflowId() public { + bytes32 expectedId = keccak256("expected-id"); + receiver.setExpectedWorkflowId(expectedId); + + bytes32 wrongId = keccak256("wrong-id"); + bytes10 workflowName = bytes10("WF_ID"); + address workflowOwner = address(this); + + bytes memory badMetadata = _encodeMetadata(wrongId, workflowName, workflowOwner); + bytes memory report = abi.encodePacked(uint256(333)); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), wrongId, expectedId)); + receiver.onReport(badMetadata, report); + + receiver.setExpectedWorkflowId(bytes32(0)); + + receiver.onReport(badMetadata, report); + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testDisableExpectedWorkflowNameAllowsAnyWorkflowName() public { + bytes10 expectedName = bytes10("WF_NAME_OK"); + receiver.setExpectedWorkflowName(expectedName); + + bytes32 workflowId = keccak256("wf-id"); + bytes10 wrongName = bytes10("BAD_NAME"); + address workflowOwner = address(this); + + bytes memory badMetadata = _encodeMetadata(workflowId, wrongName, workflowOwner); + bytes memory report = abi.encodePacked(uint256(444)); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), wrongName, expectedName)); + receiver.onReport(badMetadata, report); + + receiver.setExpectedWorkflowName(bytes10(0)); + + receiver.onReport(badMetadata, report); + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testOnReportIgnoresAuthorWhenExpectationZero() public { + // Expectation is zero by default + assertEq(receiver.expectedAuthor(), address(0)); + + bytes32 workflowId = keccak256("wf-id"); + bytes10 workflowName = bytes10("WF_AUTH0"); + address arbitraryOwner = address(0xDEAD); + + bytes memory metadata = _encodeMetadata(workflowId, workflowName, arbitraryOwner); + bytes memory report = abi.encodePacked(uint256(555)); + + receiver.onReport(metadata, report); + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testOnReportIgnoresWorkflowIdWhenExpectationZero() public { + assertEq(receiver.expectedWorkflowId(), bytes32(0)); + + bytes32 anyId = keccak256("any-id"); + bytes10 workflowName = bytes10("WF_ID0"); + address workflowOwner = address(this); + + bytes memory metadata = _encodeMetadata(anyId, workflowName, workflowOwner); + bytes memory report = abi.encodePacked(uint256(666)); + + receiver.onReport(metadata, report); + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testOnReportIgnoresWorkflowNameWhenExpectationZero() public { + assertEq(receiver.expectedWorkflowName(), bytes10(0)); + + bytes32 workflowId = keccak256("wf-id"); + bytes10 anyName = bytes10("ANY_NAME"); + address workflowOwner = address(this); + + bytes memory metadata = _encodeMetadata(workflowId, anyName, workflowOwner); + bytes memory report = abi.encodePacked(uint256(7777)); + + receiver.onReport(metadata, report); + assertTrue(receiver.processCalled()); + assertEq(receiver.lastReport(), report); + } + + function testOnReportRevertsWithInvalidWorkflowIdPrecedence() public { + bytes32 expectedId = keccak256("expected-id"); + receiver.setExpectedWorkflowId(expectedId); + + address expectedAuthor = address(0xA1); + receiver.setExpectedAuthor(expectedAuthor); + + bytes10 expectedName = bytes10("WF_EXPECT"); + receiver.setExpectedWorkflowName(expectedName); + + // All three are wrong: id, author, name + bytes32 wrongId = keccak256("wrong-id"); + address wrongAuthor = address(0xDEAD); + bytes10 wrongName = bytes10("WF_WRONG"); + + bytes memory metadata = _encodeMetadata(wrongId, wrongName, wrongAuthor); + bytes memory report = abi.encodePacked(uint256(888)); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), wrongId, expectedId)); + receiver.onReport(metadata, report); + } + + function testOnReportRevertsWithInvalidAuthorBeforeWorkflowName() public { + address expectedAuthor = address(0xA1); + receiver.setExpectedAuthor(expectedAuthor); + + bytes10 expectedName = bytes10("WF_EXPECT"); + receiver.setExpectedWorkflowName(expectedName); + + bytes32 workflowId = keccak256("wf-id"); + address wrongAuthor = address(0xDEAD); + bytes10 wrongName = bytes10("WF_WRONG"); + + bytes memory metadata = _encodeMetadata(workflowId, wrongName, wrongAuthor); + bytes memory report = abi.encodePacked(uint256(999)); + + vm.expectRevert(abi.encodeWithSelector(_invalidAuthorSelector(), wrongAuthor, expectedAuthor)); + receiver.onReport(metadata, report); + } + + function testSupportsInterfaceERC165AndIReceiverAndUnknown() public view { + assertTrue(receiver.supportsInterface(type(IERC165).interfaceId)); + assertTrue(receiver.supportsInterface(type(IReceiver).interfaceId)); + assertFalse(receiver.supportsInterface(0xffffffff)); + } + + function testOnReportRevertsInvalidWorkflowIdEvenWhenForwarderCorrect() public { + address trustedForwarder = address(0xF0F0); + receiver.setForwarderAddress(trustedForwarder); + + bytes32 expectedId = keccak256("expected-id"); + receiver.setExpectedWorkflowId(expectedId); + + bytes32 wrongId = keccak256("wrong-id"); + bytes10 workflowName = bytes10("WF_MIX"); + address workflowOwner = address(0xA1); + + bytes memory metadata = _encodeMetadata(wrongId, workflowName, workflowOwner); + bytes memory report = abi.encodePacked(uint256(4242)); + + vm.prank(trustedForwarder); + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), wrongId, expectedId)); + receiver.onReport(metadata, report); + } + + function testSetForwarderAddressEmitsWhenSettingSameValue() public { + address forwarder = address(0xF0F0); + receiver.setForwarderAddress(forwarder); + + vm.expectEmit(true, true, true, false); + emit ForwarderAddressChanged(forwarder, forwarder, address(this)); + + receiver.setForwarderAddress(forwarder); + } + + function testSetExpectedAuthorEmitsWhenSettingSameValue() public { + address author = address(0xA1); + receiver.setExpectedAuthor(author); + + vm.expectEmit(true, true, true, false); + emit ExpectedAuthorChanged(author, author, address(this)); + + receiver.setExpectedAuthor(author); + } + + function testSetExpectedWorkflowNameEmitsWhenSettingSameValue() public { + bytes10 name = bytes10("WF_REPEAT"); + receiver.setExpectedWorkflowName(name); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowNameChanged(name, name, address(this)); + + receiver.setExpectedWorkflowName(name); + } + + function testSetExpectedWorkflowIdEmitsWhenSettingSameValue() public { + bytes32 id = keccak256("same-id"); + receiver.setExpectedWorkflowId(id); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowIdChanged(id, id, address(this)); + + receiver.setExpectedWorkflowId(id); + } +} diff --git a/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol b/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol new file mode 100644 index 00000000..b9daa38d --- /dev/null +++ b/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol @@ -0,0 +1,528 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "forge-std/Test.sol"; +import { + IERC165 +} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol"; +import { IReceiver } from "@chainlink/contracts/src/v0.8/keystone/interfaces/IReceiver.sol"; + +import { MockCreReceiver } from "../../../contracts/mock/mockRules/MockCreReceiver.sol"; +import { CreReceiver } from "../../../contracts/rules/CreReceiver.sol"; + +contract CreReceiverFuzzTest is Test { + MockCreReceiver internal receiver; + + // Re-declare events so we can use expectEmit + event ExpectedAuthorChanged(address indexed newAuthor, address indexed oldAuthor, address indexed changer); + event ExpectedWorkflowNameChanged(bytes10 indexed newName, bytes10 indexed oldName, address indexed changer); + event ExpectedWorkflowIdChanged(bytes32 indexed newId, bytes32 indexed oldId, address indexed changer); + event ForwarderAddressChanged(address indexed newForwarder, address indexed oldForwarder, address indexed changer); + + function setUp() public { + receiver = new MockCreReceiver(); + } + + // ----------------------- + // Helpers + // ----------------------- + + function _encodeMetadata( + bytes32 workflowId, + bytes10 workflowName, + address workflowOwner + ) internal pure returns (bytes memory) { + return abi.encodePacked(workflowId, workflowName, workflowOwner); + } + + function _invalidSenderSelector() internal pure returns (bytes4) { + return bytes4(keccak256("InvalidSender(address,address)")); + } + + function _invalidAuthorSelector() internal pure returns (bytes4) { + return bytes4(keccak256("InvalidAuthor(address,address)")); + } + + function _invalidWorkflowIdSelector() internal pure returns (bytes4) { + return bytes4(keccak256("InvalidWorkflowId(bytes32,bytes32)")); + } + + function _invalidWorkflowNameSelector() internal pure returns (bytes4) { + return bytes4(keccak256("InvalidWorkflowName(bytes10,bytes10)")); + } + + function _ownableUnauthorizedAccountSelector() internal pure returns (bytes4) { + return bytes4(keccak256("OwnableUnauthorizedAccount(address)")); + } + + // ----------------------- + // Fuzz: admin setters + // ----------------------- + + function testFuzz_SetForwarderAddressUpdatesStateAndEmitsEvent(address newForwarder) public { + address oldForwarder = receiver.forwarderAddress(); + + vm.expectEmit(true, true, true, false); + emit ForwarderAddressChanged(newForwarder, oldForwarder, address(this)); + + receiver.setForwarderAddress(newForwarder); + + assertEq(receiver.forwarderAddress(), newForwarder); + } + + function testFuzz_SetForwarderAddressOnlyOwner(address nonOwner, address newForwarder) public { + vm.assume(nonOwner != address(this)); + + vm.prank(nonOwner); + vm.expectRevert(abi.encodeWithSelector(_ownableUnauthorizedAccountSelector(), nonOwner)); + receiver.setForwarderAddress(newForwarder); + } + + function testFuzz_SetExpectedAuthorUpdatesStateAndEmitsEvent(address newAuthor) public { + address oldAuthor = receiver.expectedAuthor(); + + vm.expectEmit(true, true, true, false); + emit ExpectedAuthorChanged(newAuthor, oldAuthor, address(this)); + + receiver.setExpectedAuthor(newAuthor); + + assertEq(receiver.expectedAuthor(), newAuthor); + } + + function testFuzz_SetExpectedAuthorOnlyOwner(address nonOwner, address newAuthor) public { + vm.assume(nonOwner != address(this)); + + vm.prank(nonOwner); + vm.expectRevert(abi.encodeWithSelector(_ownableUnauthorizedAccountSelector(), nonOwner)); + receiver.setExpectedAuthor(newAuthor); + } + + function testFuzz_SetExpectedWorkflowNameUpdatesStateAndEmitsEvent(bytes10 newName) public { + bytes10 oldName = receiver.expectedWorkflowName(); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowNameChanged(newName, oldName, address(this)); + + receiver.setExpectedWorkflowName(newName); + + assertEq(receiver.expectedWorkflowName(), newName); + } + + function testFuzz_SetExpectedWorkflowNameOnlyOwner(bytes10 newName, address nonOwner) public { + vm.assume(nonOwner != address(this)); + + vm.prank(nonOwner); + vm.expectRevert(abi.encodeWithSelector(_ownableUnauthorizedAccountSelector(), nonOwner)); + receiver.setExpectedWorkflowName(newName); + } + + function testFuzz_SetExpectedWorkflowIdUpdatesStateAndEmitsEvent(bytes32 newId) public { + bytes32 oldId = receiver.expectedWorkflowId(); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowIdChanged(newId, oldId, address(this)); + + receiver.setExpectedWorkflowId(newId); + + assertEq(receiver.expectedWorkflowId(), newId); + } + + function testFuzz_SetExpectedWorkflowIdOnlyOwner(bytes32 newId, address nonOwner) public { + vm.assume(nonOwner != address(this)); + + vm.prank(nonOwner); + vm.expectRevert(abi.encodeWithSelector(_ownableUnauthorizedAccountSelector(), nonOwner)); + receiver.setExpectedWorkflowId(newId); + } + + function testFuzz_SetForwarderAddressEmitsWhenSettingSameValue(address forwarder) public { + receiver.setForwarderAddress(forwarder); + + vm.expectEmit(true, true, true, false); + emit ForwarderAddressChanged(forwarder, forwarder, address(this)); + + receiver.setForwarderAddress(forwarder); + } + + function testFuzz_SetExpectedAuthorEmitsWhenSettingSameValue(address author) public { + receiver.setExpectedAuthor(author); + + vm.expectEmit(true, true, true, false); + emit ExpectedAuthorChanged(author, author, address(this)); + + receiver.setExpectedAuthor(author); + } + + function testFuzz_SetExpectedWorkflowNameEmitsWhenSettingSameValue(bytes10 name) public { + receiver.setExpectedWorkflowName(name); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowNameChanged(name, name, address(this)); + + receiver.setExpectedWorkflowName(name); + } + + function testFuzz_SetExpectedWorkflowIdEmitsWhenSettingSameValue(bytes32 id) public { + receiver.setExpectedWorkflowId(id); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowIdChanged(id, id, address(this)); + + receiver.setExpectedWorkflowId(id); + } + + function testFuzz_OnReportSucceedsWhenNoForwarderOrExpectationsSet( + bytes32 workflowId, + bytes10 workflowName, + address workflowOwner, + bytes memory report + ) public { + bytes memory metadata = _encodeMetadata(workflowId, workflowName, workflowOwner); + + receiver.onReport(metadata, report); + } + + function testFuzz_OnReportRevertsForInvalidSenderWhenForwarderConfigured( + address trustedForwarder, + address badCaller, + bytes32 workflowId, + bytes10 workflowName, + address workflowOwner, + bytes memory report + ) public { + vm.assume(trustedForwarder != address(0)); + vm.assume(badCaller != trustedForwarder); + + receiver.setForwarderAddress(trustedForwarder); + + bytes memory metadata = _encodeMetadata(workflowId, workflowName, workflowOwner); + + vm.prank(badCaller); + vm.expectRevert(abi.encodeWithSelector(_invalidSenderSelector(), badCaller, trustedForwarder)); + receiver.onReport(metadata, report); + } + + function testFuzz_OnReportAllowsTrustedForwarder( + address trustedForwarder, + bytes32 workflowId, + bytes10 workflowName, + address workflowOwner, + bytes memory report + ) public { + vm.assume(trustedForwarder != address(0)); + + receiver.setForwarderAddress(trustedForwarder); + + bytes memory metadata = _encodeMetadata(workflowId, workflowName, workflowOwner); + + vm.prank(trustedForwarder); + receiver.onReport(metadata, report); + } + + function testFuzz_DisableForwarderAllowsAnySender( + address trustedForwarder, + address nonForwarder, + bytes32 workflowId, + bytes10 workflowName, + address workflowOwner, + bytes memory report + ) public { + vm.assume(trustedForwarder != address(0)); + vm.assume(nonForwarder != trustedForwarder); + + receiver.setForwarderAddress(trustedForwarder); + bytes memory metadata = _encodeMetadata(workflowId, workflowName, workflowOwner); + + vm.prank(nonForwarder); + vm.expectRevert(abi.encodeWithSelector(_invalidSenderSelector(), nonForwarder, trustedForwarder)); + receiver.onReport(metadata, report); + + receiver.setForwarderAddress(address(0)); + + vm.prank(nonForwarder); + receiver.onReport(metadata, report); + } + + function testFuzz_OnReportRevertsForInvalidWorkflowId( + bytes32 expectedId, + bytes32 wrongId, + bytes10 workflowName, + address workflowOwner, + bytes memory report + ) public { + vm.assume(expectedId != bytes32(0)); + vm.assume(wrongId != expectedId); + + receiver.setExpectedWorkflowId(expectedId); + + bytes memory metadata = _encodeMetadata(wrongId, workflowName, workflowOwner); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), wrongId, expectedId)); + receiver.onReport(metadata, report); + } + + function testFuzz_OnReportSucceedsWithCorrectWorkflowId( + bytes32 expectedId, + bytes10 workflowName, + address workflowOwner, + bytes memory report + ) public { + vm.assume(expectedId != bytes32(0)); + + receiver.setExpectedWorkflowId(expectedId); + + bytes memory metadata = _encodeMetadata(expectedId, workflowName, workflowOwner); + + receiver.onReport(metadata, report); + } + + function testFuzz_DisableExpectedWorkflowIdAllowsAnyWorkflowId( + bytes32 expectedId, + bytes32 wrongId, + bytes10 workflowName, + address workflowOwner, + bytes memory report + ) public { + vm.assume(expectedId != bytes32(0)); + vm.assume(wrongId != expectedId); + + receiver.setExpectedWorkflowId(expectedId); + + bytes memory badMetadata = _encodeMetadata(wrongId, workflowName, workflowOwner); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), wrongId, expectedId)); + receiver.onReport(badMetadata, report); + + receiver.setExpectedWorkflowId(bytes32(0)); + + receiver.onReport(badMetadata, report); + } + + function testFuzz_OnReportRevertsForInvalidAuthor( + address expectedAuthor, + address wrongOwner, + bytes32 workflowId, + bytes10 workflowName, + bytes memory report + ) public { + vm.assume(expectedAuthor != address(0)); + vm.assume(wrongOwner != expectedAuthor); + + receiver.setExpectedAuthor(expectedAuthor); + + bytes memory metadata = _encodeMetadata(workflowId, workflowName, wrongOwner); + + vm.expectRevert(abi.encodeWithSelector(_invalidAuthorSelector(), wrongOwner, expectedAuthor)); + receiver.onReport(metadata, report); + } + + function testFuzz_OnReportSucceedsWithCorrectAuthor( + address expectedAuthor, + bytes32 workflowId, + bytes10 workflowName, + bytes memory report + ) public { + vm.assume(expectedAuthor != address(0)); + + receiver.setExpectedAuthor(expectedAuthor); + + bytes memory metadata = _encodeMetadata(workflowId, workflowName, expectedAuthor); + + receiver.onReport(metadata, report); + } + + function testFuzz_DisableExpectedAuthorAllowsAnyAuthor( + address expectedAuthor, + address wrongOwner, + bytes32 workflowId, + bytes10 workflowName, + bytes memory report + ) public { + vm.assume(expectedAuthor != address(0)); + vm.assume(wrongOwner != expectedAuthor); + + receiver.setExpectedAuthor(expectedAuthor); + + bytes memory metadata = _encodeMetadata(workflowId, workflowName, wrongOwner); + + vm.expectRevert(abi.encodeWithSelector(_invalidAuthorSelector(), wrongOwner, expectedAuthor)); + receiver.onReport(metadata, report); + + receiver.setExpectedAuthor(address(0)); + + receiver.onReport(metadata, report); + } + + function testFuzz_OnReportRevertsForInvalidWorkflowName( + bytes10 expectedName, + bytes10 wrongName, + bytes32 workflowId, + address workflowOwner, + bytes memory report + ) public { + vm.assume(expectedName != bytes10(0)); + vm.assume(wrongName != expectedName); + + receiver.setExpectedWorkflowName(expectedName); + + bytes memory metadata = _encodeMetadata(workflowId, wrongName, workflowOwner); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), wrongName, expectedName)); + receiver.onReport(metadata, report); + } + + function testFuzz_OnReportSucceedsWithCorrectWorkflowName( + bytes10 expectedName, + bytes32 workflowId, + address workflowOwner, + bytes memory report + ) public { + vm.assume(expectedName != bytes10(0)); + + receiver.setExpectedWorkflowName(expectedName); + + bytes memory metadata = _encodeMetadata(workflowId, expectedName, workflowOwner); + + receiver.onReport(metadata, report); + } + + function testFuzz_DisableExpectedWorkflowNameAllowsAnyWorkflowName( + bytes10 expectedName, + bytes10 wrongName, + bytes32 workflowId, + address workflowOwner, + bytes memory report + ) public { + vm.assume(expectedName != bytes10(0)); + vm.assume(wrongName != expectedName); + + receiver.setExpectedWorkflowName(expectedName); + + bytes memory badMetadata = _encodeMetadata(workflowId, wrongName, workflowOwner); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), wrongName, expectedName)); + receiver.onReport(badMetadata, report); + + receiver.setExpectedWorkflowName(bytes10(0)); + + receiver.onReport(badMetadata, report); + } + + function testFuzz_OnReportIgnoresAllExpectationsWhenZero( + bytes32 workflowId, + bytes10 workflowName, + address workflowOwner, + bytes memory report + ) public { + assertEq(receiver.forwarderAddress(), address(0)); + assertEq(receiver.expectedAuthor(), address(0)); + assertEq(receiver.expectedWorkflowName(), bytes10(0)); + assertEq(receiver.expectedWorkflowId(), bytes32(0)); + + bytes memory metadata = _encodeMetadata(workflowId, workflowName, workflowOwner); + + receiver.onReport(metadata, report); + } + + function testFuzz_OnReportRevertsWithInvalidWorkflowIdPrecedence( + bytes32 expectedId, + bytes32 wrongId, + bytes10 expectedName, + bytes10 wrongName, + address expectedAuthor, + address wrongAuthor, + bytes memory report + ) public { + vm.assume(expectedId != bytes32(0)); + vm.assume(expectedName != bytes10(0)); + vm.assume(expectedAuthor != address(0)); + + vm.assume(wrongId != expectedId); + vm.assume(wrongName != expectedName); + vm.assume(wrongAuthor != expectedAuthor); + + receiver.setExpectedWorkflowId(expectedId); + receiver.setExpectedAuthor(expectedAuthor); + receiver.setExpectedWorkflowName(expectedName); + + bytes memory metadata = _encodeMetadata(wrongId, wrongName, wrongAuthor); + + // WorkflowId is checked first + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), wrongId, expectedId)); + receiver.onReport(metadata, report); + } + + function testFuzz_OnReportRevertsWithInvalidAuthorBeforeWorkflowName( + address expectedAuthor, + address wrongAuthor, + bytes10 expectedName, + bytes10 wrongName, + bytes32 workflowId, + bytes memory report + ) public { + vm.assume(expectedAuthor != address(0)); + vm.assume(expectedName != bytes10(0)); + + vm.assume(wrongAuthor != expectedAuthor); + vm.assume(wrongName != expectedName); + + receiver.setExpectedAuthor(expectedAuthor); + receiver.setExpectedWorkflowName(expectedName); + + bytes memory metadata = _encodeMetadata(workflowId, wrongName, wrongAuthor); + + vm.expectRevert(abi.encodeWithSelector(_invalidAuthorSelector(), wrongAuthor, expectedAuthor)); + receiver.onReport(metadata, report); + } + + function testFuzz_OnReportRevertsInvalidWorkflowIdEvenWhenForwarderCorrect( + address trustedForwarder, + bytes32 expectedId, + bytes32 wrongId, + bytes10 workflowName, + address workflowOwner, + bytes memory report + ) public { + vm.assume(trustedForwarder != address(0)); + vm.assume(expectedId != bytes32(0)); + vm.assume(wrongId != expectedId); + + receiver.setForwarderAddress(trustedForwarder); + receiver.setExpectedWorkflowId(expectedId); + + bytes memory metadata = _encodeMetadata(wrongId, workflowName, workflowOwner); + + vm.prank(trustedForwarder); + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), wrongId, expectedId)); + receiver.onReport(metadata, report); + } + + function testFuzz_OnReportSucceedsWhenAllChecksPass( + address trustedForwarder, + bytes32 workflowId, + bytes10 workflowName, + address workflowOwner, + bytes memory report + ) public { + vm.assume(trustedForwarder != address(0)); + vm.assume(workflowId != bytes32(0)); + vm.assume(workflowName != bytes10(0)); + vm.assume(workflowOwner != address(0)); + + receiver.setForwarderAddress(trustedForwarder); + receiver.setExpectedWorkflowId(workflowId); + receiver.setExpectedWorkflowName(workflowName); + receiver.setExpectedAuthor(workflowOwner); + + bytes memory metadata = _encodeMetadata(workflowId, workflowName, workflowOwner); + + vm.prank(trustedForwarder); + receiver.onReport(metadata, report); + } + + function testFuzz_SupportsInterface(bytes4 interfaceId) public view { + bool expected = interfaceId == type(IReceiver).interfaceId || interfaceId == type(IERC165).interfaceId; + + assertEq(receiver.supportsInterface(interfaceId), expected); + } +} diff --git a/pkg/pool-quantamm/test/foundry/rules/CreUpdateRule.t.sol b/pkg/pool-quantamm/test/foundry/rules/CreUpdateRule.t.sol new file mode 100644 index 00000000..85808e1e --- /dev/null +++ b/pkg/pool-quantamm/test/foundry/rules/CreUpdateRule.t.sol @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "forge-std/Test.sol"; + +import "../../../contracts/mock/MockRuleInvoker.sol"; +import "../utils.t.sol"; +import { CreUpdateRule } from "../../../contracts/rules/CreUpdateRule.sol"; +import { MockCreUpdateRule } from "../../../contracts/mock/mockRules/MockCreUpdateRule.sol"; +import { MockUpdateWeightRunner } from "../../../contracts/mock/MockUpdateWeightRunner.sol"; +import { MockQuantAMMBasePool } from "../../../contracts/mock/MockQuantAMMBasePool.sol"; +import { MockChainlinkOracle } from "../../../contracts/mock/MockChainlinkOracles.sol"; + +contract CreUpdateRuleTest is Test, QuantAMMTestUtils { + MockCreUpdateRule public rule; + MockUpdateWeightRunner public updateWeightRunner; + MockQuantAMMBasePool public mockPool; + + address internal owner; + address internal addr1; + address internal addr2; + MockChainlinkOracle internal chainlinkOracle; + + uint40 constant UPDATE_INTERVAL = 1800; + + event TargetWeightsForwarded(address indexed pool, address sender); + event UpdateWeightRunnerChanged(address indexed newAddress, address indexed oldAddress, address indexed changer); + + function setUp() public { + (address ownerLocal, address addr1Local, address addr2Local) = (vm.addr(1), vm.addr(2), vm.addr(3)); + owner = ownerLocal; + addr1 = addr1Local; + addr2 = addr2Local; + // Deploying MockUpdateWeightRunner contract + updateWeightRunner = new MockUpdateWeightRunner(owner, addr2, false); + + // Deploying MockCreUpdateRule contract + rule = new MockCreUpdateRule(address(updateWeightRunner)); + rule.transferOwnership(owner); + // Deploy MockPool contract with some mock parameters + mockPool = new MockQuantAMMBasePool(UPDATE_INTERVAL, address(updateWeightRunner)); + } + + function testInitialiseIntermediateValueAlwaysPasses( + uint256 numAssets, + int256[] memory previousAlphas, + int256[] memory prevMovingAverages + ) public { + // Simulate setting number of assets and calculating intermediate values + vm.startPrank(owner); + rule.initialisePoolRuleIntermediateValues(address(mockPool), prevMovingAverages, previousAlphas, numAssets); + vm.stopPrank(); + } + + function testNoninitialisedParametersShouldBeAccepted() public view { + int256[][] memory parameters; + bool result = rule.validParameters(parameters); + assertTrue(result); + } + + function testEmptyParametersShouldNotBeAccepted() public view { + int256[][] memory parameters = new int256[][](1); + parameters[0] = new int256[](1); + bool result = rule.validParameters(parameters); + assertTrue(result); + } + + function testEmpty1DParametersShouldBeAccepted() public view { + int256[][] memory parameters = new int256[][](1); + bool result = rule.validParameters(parameters); + assertTrue(result); + } + + function test0InitialisedParametersShouldBeAccepted() public view { + int256[][] memory parameters = new int256[][](0); + bool result = rule.validParameters(parameters); + assertTrue(result); + } + + function testZeroShouldBeAccepted() public view { + int256[][] memory parameters = new int256[][](1); + parameters[0] = new int256[](1); + parameters[0][0] = PRBMathSD59x18.fromInt(0); + bool result = rule.validParameters(parameters); + assertTrue(result); + } + + function test_setUpdateWeightRunner_revertsForNonOwner(address nonOwner) public { + MockUpdateWeightRunner newRunner = new MockUpdateWeightRunner(owner, addr2, false); + vm.assume(nonOwner != owner); + vm.prank(nonOwner); + vm.expectRevert(); + rule.setUpdateWeightRunner(address(newRunner)); + } + + function test_setUpdateWeightRunner_revertsOnZeroAddress() public { + vm.startPrank(owner); + + vm.expectRevert(abi.encodeWithSelector(CreUpdateRule.InvalidAddress.selector, address(0))); + rule.setUpdateWeightRunner(address(0)); + vm.stopPrank(); + } + + function test_setUpdateWeightRunner_emitsEventAndUpdatesOldAddress() public { + MockUpdateWeightRunner newRunner = new MockUpdateWeightRunner(owner, addr2, false); + // First set, oldAddress should be zero + vm.expectEmit(false, false, false, true, address(rule)); + emit UpdateWeightRunnerChanged(address(newRunner), address(updateWeightRunner), address(owner)); + vm.prank(owner); + rule.setUpdateWeightRunner(address(newRunner)); + } + + function test_CalculateNewWeights_revertsWithNotImplemented_forTestContractCaller( + int256[] calldata prevWeights, + int256[] calldata data, + int256[][] calldata _parameters, + uint64[] calldata lambdaStore + ) public { + vm.expectRevert(abi.encodeWithSelector(CreUpdateRule.NotImplemented.selector, address(this))); + rule.CalculateNewWeights(prevWeights, data, address(0), _parameters, lambdaStore, 0, 0); + } + + function test_CalculateNewWeights_revertsWithNotImplemented_forPrankedCaller( + int256[] calldata prevWeights, + int256[] calldata data, + int256[][] calldata _parameters, + uint64[] calldata lambdaStore + ) public { + vm.prank(addr1); + vm.expectRevert(abi.encodeWithSelector(CreUpdateRule.NotImplemented.selector, addr1)); + rule.CalculateNewWeights(prevWeights, data, address(0xBEEF), _parameters, lambdaStore, 123, 456); + } + + function test_ProcessReport(uint256 firstWeight, uint40 lastInterpolationTimePossible) public { + int256[] memory initialWeights = new int256[](4); + initialWeights[0] = 0.0000000005e18; + initialWeights[1] = 0.0000000005e18; + initialWeights[2] = 0; + initialWeights[3] = 0; + + // Set initial weights + mockPool.setInitialWeights(initialWeights); + mockPool.setPoolRegistry(32); + vm.startPrank(owner); + updateWeightRunner.setApprovedActionsForPool(address(mockPool), 32); + vm.stopPrank(); + + int216 fixedValue = 1000; + chainlinkOracle = deployOracle(fixedValue, 0); + + vm.startPrank(owner); + updateWeightRunner.addOracle(OracleWrapper(chainlinkOracle)); + vm.stopPrank(); + + vm.startPrank(address(mockPool)); + + address[][] memory oracles = new address[][](2); + oracles[0] = new address[](1); + oracles[0][0] = address(chainlinkOracle); + oracles[1] = new address[](1); + oracles[1][0] = address(chainlinkOracle); + + uint64[] memory lambda = new uint64[](1); + uint40 updateInterval = uint40(bound(lastInterpolationTimePossible, 1, 15552000)); + updateWeightRunner.setRuleForPool( + IQuantAMMWeightedPool.PoolSettings({ + assets: new IERC20[](0), + rule: IUpdateRule(rule), + oracles: oracles, + updateInterval: updateInterval, + lambda: lambda, + epsilonMax: 0.9e18, + absoluteWeightGuardRail: 0.2e18, + maxTradeSizeRatio: 0.2e18, + ruleParameters: new int256[][](0), + poolManager: addr2 + }) + ); + vm.stopPrank(); + + vm.startPrank(owner); + uint256[] memory weights = new uint256[](2); + weights[0] = bound(uint256(firstWeight), 0.2e18, 0.8e18); + weights[1] = uint256(1e18) - weights[0]; + bytes memory metaData = abi.encode( + CreUpdateRule.WorkflowMetadata({ + poolAddress: address(mockPool), + weights: weights, + lastInterpolationTimePossible: updateInterval + }) + ); + + uint256[] memory currentPoolWeights = mockPool.getNormalizedWeights(); + assertEq(currentPoolWeights[0], uint256(initialWeights[0])); + assertEq(currentPoolWeights[1], uint256(initialWeights[1])); + + vm.expectEmit(false, false, false, true, address(rule)); + emit TargetWeightsForwarded(address(mockPool), owner); + rule.ProcessReport(metaData); + + int256[] memory newPoolWeights = mockPool.getWeights(); + assertEq(newPoolWeights[0], initialWeights[0]); + assertEq(newPoolWeights[1], initialWeights[1]); + + //We dont need to test the mathguard. Epsilon max is high and the fuzzing is bound within the limits. + //So we can just check that the multiplier is correctly calculated to make sure its goes through. + int256 expectedMultiplier0 = (int256(weights[0]) - int256(initialWeights[0])) / int256(int40(updateInterval)); + + int256 expectedMultiplier1 = (int256(weights[1]) - int256(initialWeights[1])) / int256(int40(updateInterval)); + + assertEq(newPoolWeights[2], expectedMultiplier0); + assertEq(newPoolWeights[3], expectedMultiplier1); + + vm.stopPrank(); + } + + function test_ProcessReport_revertsOnZeroPoolAddress() public { + // Arrange: simple valid weights + uint256[] memory weights = new uint256[](2); + weights[0] = 0.5e18; + weights[1] = 0.5e18; + + bytes memory metaData = abi.encode( + CreUpdateRule.WorkflowMetadata({ + poolAddress: address(0), + weights: weights, + lastInterpolationTimePossible: uint40(1) + }) + ); + + // Act / Assert + vm.expectRevert(abi.encodeWithSelector(CreUpdateRule.InvalidAddress.selector, address(0))); + rule.ProcessReport(metaData); + } + + function test_ProcessReport_revertsOnWeightsLengthMismatch(uint256 firstWeight, uint40 lastInterpolationTimePossible) public { + int256[] memory initialWeights = new int256[](4); + initialWeights[0] = 0.0000000005e18; + initialWeights[1] = 0.0000000005e18; + initialWeights[2] = 0; + initialWeights[3] = 0; + + // Set initial weights + mockPool.setInitialWeights(initialWeights); + mockPool.setPoolRegistry(32); + vm.startPrank(owner); + updateWeightRunner.setApprovedActionsForPool(address(mockPool), 32); + vm.stopPrank(); + + int216 fixedValue = 1000; + chainlinkOracle = deployOracle(fixedValue, 0); + + vm.startPrank(owner); + updateWeightRunner.addOracle(OracleWrapper(chainlinkOracle)); + vm.stopPrank(); + + vm.startPrank(address(mockPool)); + + address[][] memory oracles = new address[][](2); + oracles[0] = new address[](1); + oracles[0][0] = address(chainlinkOracle); + oracles[1] = new address[](1); + oracles[1][0] = address(chainlinkOracle); + + uint64[] memory lambda = new uint64[](1); + uint40 updateInterval = uint40(bound(lastInterpolationTimePossible, 1, 15552000)); + updateWeightRunner.setRuleForPool( + IQuantAMMWeightedPool.PoolSettings({ + assets: new IERC20[](0), + rule: IUpdateRule(rule), + oracles: oracles, + updateInterval: updateInterval, + lambda: lambda, + epsilonMax: 0.9e18, + absoluteWeightGuardRail: 0.2e18, + maxTradeSizeRatio: 0.2e18, + ruleParameters: new int256[][](0), + poolManager: addr2 + }) + ); + vm.stopPrank(); + + vm.startPrank(owner); + uint256[] memory weights = new uint256[](1); + weights[0] = bound(uint256(firstWeight), 0.2e18, 0.8e18); + bytes memory metaData = abi.encode( + CreUpdateRule.WorkflowMetadata({ + poolAddress: address(mockPool), + weights: weights, + lastInterpolationTimePossible: updateInterval + }) + ); + + uint256[] memory currentPoolWeights = mockPool.getNormalizedWeights(); + assertEq(currentPoolWeights[0], uint256(initialWeights[0])); + assertEq(currentPoolWeights[1], uint256(initialWeights[1])); + + vm.expectRevert(bytes("WRONGLENGTH")); + rule.ProcessReport(metaData); + + } + + function test_ProcessReport_revertsWhenPoolNotConfiguredInRunner() public { + // Arrange: create a brand-new pool that the runner doesn't know about + // Adjust constructor args if your mock differs, but from the traces: + // new MockQuantAMMBasePool(3600, 1 ether, address(rule)); + MockQuantAMMBasePool unconfiguredPool = new MockQuantAMMBasePool(UPDATE_INTERVAL, address(updateWeightRunner)); + + // Give it some initial normalized weights so getNormalizedWeights() doesn't revert + int256[] memory initialWeights = new int256[](2); + initialWeights[0] = 0.5e18; + initialWeights[1] = 0.5e18; + unconfiguredPool.setInitialWeights(initialWeights); + + uint256[] memory weights = new uint256[](2); + weights[0] = 0.5e18; + weights[1] = 0.5e18; + + bytes memory metaData = abi.encode( + CreUpdateRule.WorkflowMetadata({ + poolAddress: address(unconfiguredPool), + weights: weights, + lastInterpolationTimePossible: uint40(1) + }) + ); + + // Act / Assert + // We don't care about the exact revert reason here (it could be from + // getPoolRuleSettings or from calculateMultiplierAndSetWeightsFromRule), + // we just assert that a non-configured pool can't be updated "successfully". + vm.expectRevert(); + rule.ProcessReport(metaData); + } + + function test_ProcessReport_revertsWhenRunnerConfiguredForDifferentRule(uint256 firstWeight, uint40 lastInterpolationTimePossible) public { + int256[] memory initialWeights = new int256[](4); + initialWeights[0] = 0.0000000005e18; + initialWeights[1] = 0.0000000005e18; + initialWeights[2] = 0; + initialWeights[3] = 0; + + // Set initial weights + mockPool.setInitialWeights(initialWeights); + mockPool.setPoolRegistry(32); + vm.startPrank(owner); + updateWeightRunner.setApprovedActionsForPool(address(mockPool), 32); + vm.stopPrank(); + + int216 fixedValue = 1000; + chainlinkOracle = deployOracle(fixedValue, 0); + + vm.startPrank(owner); + updateWeightRunner.addOracle(OracleWrapper(chainlinkOracle)); + vm.stopPrank(); + + vm.startPrank(address(mockPool)); + + address[][] memory oracles = new address[][](2); + oracles[0] = new address[](1); + oracles[0][0] = address(chainlinkOracle); + oracles[1] = new address[](1); + oracles[1][0] = address(chainlinkOracle); + + uint64[] memory lambda = new uint64[](1); + uint40 updateInterval = uint40(bound(lastInterpolationTimePossible, 1, 15552000)); + MockCreUpdateRule differentRule = new MockCreUpdateRule(address(updateWeightRunner)); + updateWeightRunner.setRuleForPool( + IQuantAMMWeightedPool.PoolSettings({ + assets: new IERC20[](0), + rule: IUpdateRule(differentRule), + oracles: oracles, + updateInterval: updateInterval, + lambda: lambda, + epsilonMax: 0.9e18, + absoluteWeightGuardRail: 0.2e18, + maxTradeSizeRatio: 0.2e18, + ruleParameters: new int256[][](0), + poolManager: addr2 + }) + ); + vm.stopPrank(); + + vm.startPrank(owner); + uint256[] memory weights = new uint256[](2); + weights[0] = bound(uint256(firstWeight), 0.2e18, 0.8e18); + weights[1] = uint256(1e18) - weights[0]; + bytes memory metaData = abi.encode( + CreUpdateRule.WorkflowMetadata({ + poolAddress: address(mockPool), + weights: weights, + lastInterpolationTimePossible: updateInterval + }) + ); + + uint256[] memory currentPoolWeights = mockPool.getNormalizedWeights(); + assertEq(currentPoolWeights[0], uint256(initialWeights[0])); + assertEq(currentPoolWeights[1], uint256(initialWeights[1])); + + + // Act / Assert + // Now, when `rule` tries to call calculateMultiplierAndSetWeightsFromRule, + // UpdateWeightRunner should reject it because msg.sender != rules[pool]. + vm.expectRevert(abi.encodeWithSelector(CreUpdateRule.InvalidAddress.selector, address(mockPool))); + vm.startPrank(owner); + rule.ProcessReport(metaData); + vm.stopPrank(); + } + + + function test_ProcessReportUnauthorised(uint256 firstWeight, uint40 lastInterpolationTimePossible, uint256 permission) public { + int256[] memory initialWeights = new int256[](4); + initialWeights[0] = 0.0000000005e18; + initialWeights[1] = 0.0000000005e18; + initialWeights[2] = 0; + initialWeights[3] = 0; + + vm.assume(permission & 32 == 0 && permission != 0); // Ensure the permission bit for this function is not set + + // Set initial weights + mockPool.setInitialWeights(initialWeights); + mockPool.setPoolRegistry(permission); + vm.startPrank(owner); + updateWeightRunner.setApprovedActionsForPool(address(mockPool), permission); + vm.stopPrank(); + + int216 fixedValue = 1000; + chainlinkOracle = deployOracle(fixedValue, 0); + + vm.startPrank(owner); + updateWeightRunner.addOracle(OracleWrapper(chainlinkOracle)); + vm.stopPrank(); + + vm.startPrank(address(mockPool)); + + address[][] memory oracles = new address[][](2); + oracles[0] = new address[](1); + oracles[0][0] = address(chainlinkOracle); + oracles[1] = new address[](1); + oracles[1][0] = address(chainlinkOracle); + + uint64[] memory lambda = new uint64[](1); + uint40 updateInterval = uint40(bound(lastInterpolationTimePossible, 1, 15552000)); + updateWeightRunner.setRuleForPool( + IQuantAMMWeightedPool.PoolSettings({ + assets: new IERC20[](0), + rule: IUpdateRule(rule), + oracles: oracles, + updateInterval: updateInterval, + lambda: lambda, + epsilonMax: 0.9e18, + absoluteWeightGuardRail: 0.2e18, + maxTradeSizeRatio: 0.2e18, + ruleParameters: new int256[][](0), + poolManager: addr2 + }) + ); + vm.stopPrank(); + + vm.startPrank(owner); + uint256[] memory weights = new uint256[](2); + weights[0] = bound(uint256(firstWeight), 0.2e18, 0.8e18); + weights[1] = uint256(1e18) - weights[0]; + bytes memory metaData = abi.encode( + CreUpdateRule.WorkflowMetadata({ + poolAddress: address(mockPool), + weights: weights, + lastInterpolationTimePossible: updateInterval + }) + ); + + uint256[] memory currentPoolWeights = mockPool.getNormalizedWeights(); + assertEq(currentPoolWeights[0], uint256(initialWeights[0])); + assertEq(currentPoolWeights[1], uint256(initialWeights[1])); + + // Expect revert due to function not approved for pool. + vm.expectRevert(bytes("FUNCTIONNOTAPPROVEDFORPOOL")); + rule.ProcessReport(metaData); + + vm.stopPrank(); + } + + function deployOracle(int216 fixedValue, uint delay) internal returns (MockChainlinkOracle) { + MockChainlinkOracle oracle = new MockChainlinkOracle(fixedValue, delay); + return oracle; + } +} From 85a0bb83350c4b9a8b7546849c5272f2d728fbc4 Mon Sep 17 00:00:00 2001 From: christian harrington Date: Wed, 21 Jan 2026 16:38:21 +0000 Subject: [PATCH 07/11] initial change to auditted chainlink base class ie CreReceiver - tests require fixes given name is now mandatory --- .../mock/mockRules/MockCreReceiver.sol | 2 +- .../contracts/rules/CreReceiver.sol | 320 ++++++++++++------ .../contracts/rules/CreUpdateRule.sol | 2 +- .../test/foundry/rules/CreReceiver.t.sol | 66 ++-- .../test/foundry/rules/CreReceiver_Fuzz.t.sol | 64 ++-- 5 files changed, 299 insertions(+), 155 deletions(-) diff --git a/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol b/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol index 3c38e884..9782047a 100644 --- a/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol +++ b/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol @@ -6,7 +6,7 @@ contract MockCreReceiver is CreReceiver { bytes public lastReport; bool public processCalled; - constructor() CreReceiver() {} + constructor(address forwarderAddress) CreReceiver(forwarderAddress) {} function ProcessReport(bytes calldata metaData) external { _processReport(metaData); diff --git a/pkg/pool-quantamm/contracts/rules/CreReceiver.sol b/pkg/pool-quantamm/contracts/rules/CreReceiver.sol index 460b50cb..9cf2ae31 100644 --- a/pkg/pool-quantamm/contracts/rules/CreReceiver.sol +++ b/pkg/pool-quantamm/contracts/rules/CreReceiver.sol @@ -7,117 +7,239 @@ import { import { IReceiver } from "@chainlink/contracts/src/v0.8/keystone/interfaces/IReceiver.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -/// @title IReceiverTemplate - Abstract receiver with optional permission controls +/// @title ReceiverTemplate - Abstract receiver with optional permission controls /// @notice Provides flexible, updatable security checks for receiving workflow reports -/// @dev All permission fields default to zero (disabled). Use setter functions to enable checks. -/// https://docs.chain.link/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts#3-using-ireceivertemplate +/// @dev The forwarder address is required at construction time for security. +/// Additional permission fields can be configured using setter functions. abstract contract CreReceiver is IReceiver, Ownable { - // Optional permission fields (all default to zero = disabled) - address public forwarderAddress; // If set, only this address can call onReport - address public expectedAuthor; // If set, only reports from this workflow owner are accepted - bytes10 public expectedWorkflowName; // If set, only reports with this workflow name are accepted - bytes32 public expectedWorkflowId; // If set, only reports from this specific workflow ID are accepted - - // Custom errors - error InvalidSender(address sender, address expected); - error InvalidAuthor(address received, address expected); - error InvalidWorkflowName(bytes10 received, bytes10 expected); - error InvalidWorkflowId(bytes32 received, bytes32 expected); - - event ExpectedAuthorChanged(address indexed newAuthor, address indexed oldAuthor, address indexed changer); - event ExpectedWorkflowNameChanged(bytes10 indexed newName, bytes10 indexed oldName, address indexed changer); - event ExpectedWorkflowIdChanged(bytes32 indexed newId, bytes32 indexed oldId, address indexed changer); - event ForwarderAddressChanged(address indexed newForwarder, address indexed oldForwarder, address indexed changer); - - /// @notice Constructor sets msg.sender as the owner - /// @dev All permission fields are initialized to zero (disabled by default) - constructor() Ownable(msg.sender) {} - - /// @inheritdoc IReceiver - /// @dev Performs optional validation checks based on which permission fields are set - function onReport(bytes calldata metadata, bytes calldata report) external override { - // Security Check 1: Verify caller is the trusted Chainlink Forwarder (if configured) - if (forwarderAddress != address(0) && msg.sender != forwarderAddress) { - revert InvalidSender(msg.sender, forwarderAddress); - } - - // Security Checks 2-4: Verify workflow identity - ID, owner, and/or name (if any are configured) - if (expectedWorkflowId != bytes32(0) || expectedAuthor != address(0) || expectedWorkflowName != bytes10(0)) { - (bytes32 workflowId, bytes10 workflowName, address workflowOwner) = _decodeMetadata(metadata); - - if (expectedWorkflowId != bytes32(0) && workflowId != expectedWorkflowId) { - revert InvalidWorkflowId(workflowId, expectedWorkflowId); - } - if (expectedAuthor != address(0) && workflowOwner != expectedAuthor) { - revert InvalidAuthor(workflowOwner, expectedAuthor); - } - if (expectedWorkflowName != bytes10(0) && workflowName != expectedWorkflowName) { - revert InvalidWorkflowName(workflowName, expectedWorkflowName); - } - } - - _processReport(report); + // Required permission field at deployment, configurable after + address private s_forwarderAddress; // If set, only this address can call onReport + + // Optional permission fields (all default to zero = disabled) + address private s_expectedAuthor; // If set, only reports from this workflow owner are accepted + bytes10 private s_expectedWorkflowName; // Only validated when s_expectedAuthor is also set + bytes32 private s_expectedWorkflowId; // If set, only reports from this specific workflow ID are accepted + + // Hex character lookup table for bytes-to-hex conversion + bytes private constant HEX_CHARS = "0123456789abcdef"; + + // Custom errors + error InvalidForwarderAddress(); + error InvalidSender(address sender, address expected); + error InvalidAuthor(address received, address expected); + error InvalidWorkflowName(bytes10 received, bytes10 expected); + error InvalidWorkflowId(bytes32 received, bytes32 expected); + error WorkflowNameRequiresAuthorValidation(); + + // Events + event ForwarderAddressUpdated(address indexed previousForwarder, address indexed newForwarder); + event ExpectedAuthorUpdated(address indexed previousAuthor, address indexed newAuthor); + event ExpectedWorkflowNameUpdated(bytes10 indexed previousName, bytes10 indexed newName); + event ExpectedWorkflowIdUpdated(bytes32 indexed previousId, bytes32 indexed newId); + event SecurityWarning(string message); + + /// @notice Constructor sets msg.sender as the owner and configures the forwarder address + /// @param _forwarderAddress The address of the Chainlink Forwarder contract (cannot be address(0)) + /// @dev The forwarder address is required for security - it ensures only verified reports are processed + constructor( + address _forwarderAddress + ) Ownable(msg.sender) { + if (_forwarderAddress == address(0)) { + revert InvalidForwarderAddress(); } - - /// @notice Updates the forwarder address that is allowed to call onReport - /// @param _forwarder The new forwarder address (use address(0) to disable this check) - function setForwarderAddress(address _forwarder) external onlyOwner { - address oldForwarder = forwarderAddress; - forwarderAddress = _forwarder; - emit ForwarderAddressChanged(_forwarder, oldForwarder, msg.sender); + s_forwarderAddress = _forwarderAddress; + emit ForwarderAddressUpdated(address(0), _forwarderAddress); + } + + /// @notice Returns the configured forwarder address + /// @return The forwarder address (address(0) if disabled) + function getForwarderAddress() external view returns (address) { + return s_forwarderAddress; + } + + /// @notice Returns the expected workflow author address + /// @return The expected author address (address(0) if not set) + function getExpectedAuthor() external view returns (address) { + return s_expectedAuthor; + } + + /// @notice Returns the expected workflow name + /// @return The expected workflow name (bytes10(0) if not set) + function getExpectedWorkflowName() external view returns (bytes10) { + return s_expectedWorkflowName; + } + + /// @notice Returns the expected workflow ID + /// @return The expected workflow ID (bytes32(0) if not set) + function getExpectedWorkflowId() external view returns (bytes32) { + return s_expectedWorkflowId; + } + + /// @inheritdoc IReceiver + /// @dev Performs optional validation checks based on which permission fields are set + function onReport( + bytes calldata metadata, + bytes calldata report + ) external override { + // Security Check 1: Verify caller is the trusted Chainlink Forwarder (if configured) + if (s_forwarderAddress != address(0) && msg.sender != s_forwarderAddress) { + revert InvalidSender(msg.sender, s_forwarderAddress); } - /// @notice Updates the expected workflow owner address - /// @param _author The new expected author address (use address(0) to disable this check) - function setExpectedAuthor(address _author) external onlyOwner { - address oldAuthor = expectedAuthor; - expectedAuthor = _author; - emit ExpectedAuthorChanged(_author, oldAuthor, msg.sender); + // Security Checks 2-4: Verify workflow identity - ID, owner, and/or name (if any are configured) + if (s_expectedWorkflowId != bytes32(0) || s_expectedAuthor != address(0) || s_expectedWorkflowName != bytes10(0)) { + (bytes32 workflowId, bytes10 workflowName, address workflowOwner) = _decodeMetadata(metadata); + + if (s_expectedWorkflowId != bytes32(0) && workflowId != s_expectedWorkflowId) { + revert InvalidWorkflowId(workflowId, s_expectedWorkflowId); + } + if (s_expectedAuthor != address(0) && workflowOwner != s_expectedAuthor) { + revert InvalidAuthor(workflowOwner, s_expectedAuthor); + } + + // ================================================================ + // WORKFLOW NAME VALIDATION - REQUIRES AUTHOR VALIDATION + // ================================================================ + // Do not rely on workflow name validation alone. Workflow names are unique + // per owner, but not across owners. + // Furthermore, workflow names use 40-bit truncation (bytes10), making collisions possible. + // Therefore, workflow name validation REQUIRES author (workflow owner) validation. + // The code enforces this dependency at runtime. + // ================================================================ + if (s_expectedWorkflowName != bytes10(0)) { + // Author must be configured if workflow name is used + if (s_expectedAuthor == address(0)) { + revert WorkflowNameRequiresAuthorValidation(); + } + // Validate workflow name matches (author already validated above) + if (workflowName != s_expectedWorkflowName) { + revert InvalidWorkflowName(workflowName, s_expectedWorkflowName); + } + } } - /// @notice Updates the expected workflow name - /// @param _name The new expected workflow name (use bytes10(0) to disable this check) - function setExpectedWorkflowName(bytes10 _name) external onlyOwner { - bytes10 oldName = expectedWorkflowName; - expectedWorkflowName = _name; - emit ExpectedWorkflowNameChanged(_name, oldName, msg.sender); + _processReport(report); + } + + /// @notice Updates the forwarder address that is allowed to call onReport + /// @param _forwarder The new forwarder address + /// @dev WARNING: Setting to address(0) disables forwarder validation. + /// This makes your contract INSECURE - anyone can call onReport() with arbitrary data. + /// Only use address(0) if you fully understand the security implications. + function setForwarderAddress( + address _forwarder + ) external onlyOwner { + address previousForwarder = s_forwarderAddress; + + // Emit warning if disabling forwarder check + if (_forwarder == address(0)) { + emit SecurityWarning("Forwarder address set to zero - contract is now INSECURE"); } - /// @notice Updates the expected workflow ID - /// @param _id The new expected workflow ID (use bytes32(0) to disable this check) - function setExpectedWorkflowId(bytes32 _id) external onlyOwner { - bytes32 oldId = expectedWorkflowId; - expectedWorkflowId = _id; - emit ExpectedWorkflowIdChanged(_id, oldId, msg.sender); + s_forwarderAddress = _forwarder; + emit ForwarderAddressUpdated(previousForwarder, _forwarder); + } + + /// @notice Updates the expected workflow owner address + /// @param _author The new expected author address (use address(0) to disable this check) + function setExpectedAuthor( + address _author + ) external onlyOwner { + address previousAuthor = s_expectedAuthor; + s_expectedAuthor = _author; + emit ExpectedAuthorUpdated(previousAuthor, _author); + } + + /// @notice Updates the expected workflow name from a plaintext string + /// @param _name The workflow name as a string (use empty string "" to disable this check) + /// @dev IMPORTANT: Workflow name validation REQUIRES author validation to be enabled. + /// The workflow name uses only 40-bit truncation, making collision attacks feasible + /// when used alone. However, since workflow names are unique per owner, validating + /// both the name AND the author address provides adequate security. + /// You must call setExpectedAuthor() before or after calling this function. + /// The name is hashed using SHA256 and truncated to bytes10. + function setExpectedWorkflowName( + string calldata _name + ) external onlyOwner { + bytes10 previousName = s_expectedWorkflowName; + + if (bytes(_name).length == 0) { + s_expectedWorkflowName = bytes10(0); + emit ExpectedWorkflowNameUpdated(previousName, bytes10(0)); + return; } - /// @notice Extracts all metadata fields from the onReport metadata parameter - /// @param metadata The metadata in bytes format - /// @return workflowId The unique identifier of the workflow (bytes32) - /// @return workflowName The name of the workflow (bytes10) - /// @return workflowOwner The owner address of the workflow - function _decodeMetadata( - bytes memory metadata - ) internal pure returns (bytes32 workflowId, bytes10 workflowName, address workflowOwner) { - // Metadata structure: - // - First 32 bytes: length of the byte array (standard for dynamic bytes) - // - Offset 32, size 32: workflow_id (bytes32) - // - Offset 64, size 10: workflow_name (bytes10) - // - Offset 74, size 20: workflow_owner (address) - assembly { - workflowId := mload(add(metadata, 32)) - workflowName := mload(add(metadata, 64)) - workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) - } + // Convert workflow name to bytes10: + // SHA256 hash → hex encode → take first 10 chars → hex encode those chars + bytes32 hash = sha256(bytes(_name)); + bytes memory hexString = _bytesToHexString(abi.encodePacked(hash)); + bytes memory first10 = new bytes(10); + for (uint256 i = 0; i < 10; i++) { + first10[i] = hexString[i]; + } + s_expectedWorkflowName = bytes10(first10); + emit ExpectedWorkflowNameUpdated(previousName, s_expectedWorkflowName); + } + + /// @notice Updates the expected workflow ID + /// @param _id The new expected workflow ID (use bytes32(0) to disable this check) + function setExpectedWorkflowId( + bytes32 _id + ) external onlyOwner { + bytes32 previousId = s_expectedWorkflowId; + s_expectedWorkflowId = _id; + emit ExpectedWorkflowIdUpdated(previousId, _id); + } + + /// @notice Helper function to convert bytes to hex string + /// @param data The bytes to convert + /// @return The hex string representation + function _bytesToHexString( + bytes memory data + ) private pure returns (bytes memory) { + bytes memory hexString = new bytes(data.length * 2); + + for (uint256 i = 0; i < data.length; i++) { + hexString[i * 2] = HEX_CHARS[uint8(data[i] >> 4)]; + hexString[i * 2 + 1] = HEX_CHARS[uint8(data[i] & 0x0f)]; } - /// @notice Abstract function to process the report data - /// @param report The report calldata containing your workflow's encoded data - /// @dev Implement this function with your contract's business logic - function _processReport(bytes calldata report) internal virtual; - - /// @inheritdoc IERC165 - function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { - return interfaceId == type(IReceiver).interfaceId || interfaceId == type(IERC165).interfaceId; + return hexString; + } + + /// @notice Extracts all metadata fields from the onReport metadata parameter + /// @param metadata The metadata bytes encoded using abi.encodePacked(workflowId, workflowName, workflowOwner) + /// @return workflowId The unique identifier of the workflow (bytes32) + /// @return workflowName The name of the workflow (bytes10) + /// @return workflowOwner The owner address of the workflow + function _decodeMetadata( + bytes memory metadata + ) internal pure returns (bytes32 workflowId, bytes10 workflowName, address workflowOwner) { + // Metadata structure (encoded using abi.encodePacked by the Forwarder): + // - First 32 bytes: length of the byte array (standard for dynamic bytes) + // - Offset 32, size 32: workflow_id (bytes32) + // - Offset 64, size 10: workflow_name (bytes10) + // - Offset 74, size 20: workflow_owner (address) + assembly { + workflowId := mload(add(metadata, 32)) + workflowName := mload(add(metadata, 64)) + workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) } + return (workflowId, workflowName, workflowOwner); + } + + /// @notice Abstract function to process the report data + /// @param report The report calldata containing your workflow's encoded data + /// @dev Implement this function with your contract's business logic + function _processReport( + bytes calldata report + ) internal virtual; + + /// @inheritdoc IERC165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override returns (bool) { + return interfaceId == type(IReceiver).interfaceId || interfaceId == type(IERC165).interfaceId; + } } + diff --git a/pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol b/pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol index c51a4734..b10184e9 100644 --- a/pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol +++ b/pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol @@ -17,7 +17,7 @@ import { IQuantAMMWeightedPool } from "@balancer-labs/v3-interfaces/contracts/po /// The metadata is set by the workflow sender and cannot be manipulated. /// Checking the metadata as well as the msg.sender means that the auth is complete contract CreUpdateRule is QuantAMMMathGuard, CreReceiver, ITypeAndVersion, IUpdateRule { - constructor(address updateWeightRunnerAddress) CreReceiver() { + constructor(address updateWeightRunnerAddress) CreReceiver(updateWeightRunnerAddress) { updateWeightRunner = UpdateWeightRunner(updateWeightRunnerAddress); } diff --git a/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol b/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol index a79359be..91da41f8 100644 --- a/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol +++ b/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol @@ -20,7 +20,7 @@ contract CreReceiverTest is Test { event ForwarderAddressChanged(address indexed newForwarder, address indexed oldForwarder, address indexed changer); function setUp() public { - receiver = new MockCreReceiver(); + receiver = new MockCreReceiver(address(this)); } // ----------------------- @@ -56,10 +56,10 @@ contract CreReceiverTest is Test { } function testInitialState() public view { - assertEq(receiver.forwarderAddress(), address(0)); - assertEq(receiver.expectedAuthor(), address(0)); - assertEq(receiver.expectedWorkflowName(), bytes10(0)); - assertEq(receiver.expectedWorkflowId(), bytes32(0)); + assertEq(receiver.getForwarderAddress(), address(0)); + assertEq(receiver.getExpectedAuthor(), address(0)); + assertEq(receiver.getExpectedWorkflowName(), bytes10(0)); + assertEq(receiver.getExpectedWorkflowId(), bytes32(0)); // CreReceiver constructor passes msg.sender into Ownable assertEq(receiver.owner(), address(this)); @@ -67,14 +67,14 @@ contract CreReceiverTest is Test { function testSetForwarderAddressUpdatesStateAndEmitsEvent() public { address newForwarder = address(0xF0F0); - address oldForwarder = receiver.forwarderAddress(); + address oldForwarder = receiver.getForwarderAddress(); vm.expectEmit(true, true, true, false); emit ForwarderAddressChanged(newForwarder, oldForwarder, address(this)); receiver.setForwarderAddress(newForwarder); - assertEq(receiver.forwarderAddress(), newForwarder); + assertEq(receiver.getForwarderAddress(), newForwarder); } function testSetForwarderAddressOnlyOwner() public { @@ -88,14 +88,14 @@ contract CreReceiverTest is Test { function testSetExpectedAuthorUpdatesStateAndEmitsEvent() public { address newAuthor = address(0xA1); - address oldAuthor = receiver.expectedAuthor(); + address oldAuthor = receiver.getExpectedAuthor(); vm.expectEmit(true, true, true, false); emit ExpectedAuthorChanged(newAuthor, oldAuthor, address(this)); receiver.setExpectedAuthor(newAuthor); - assertEq(receiver.expectedAuthor(), newAuthor); + assertEq(receiver.getExpectedAuthor(), newAuthor); } function testSetExpectedAuthorOnlyOwner() public { @@ -109,14 +109,15 @@ contract CreReceiverTest is Test { function testSetExpectedWorkflowNameUpdatesStateAndEmitsEvent() public { bytes10 newName = bytes10("WF_LOW_001"); - bytes10 oldName = receiver.expectedWorkflowName(); + bytes10 oldName = receiver.getExpectedWorkflowName(); vm.expectEmit(true, true, true, false); emit ExpectedWorkflowNameChanged(newName, oldName, address(this)); - receiver.setExpectedWorkflowName(newName); + string memory newNameStr = string(abi.encodePacked(newName)); + receiver.setExpectedWorkflowName(newNameStr); - assertEq(receiver.expectedWorkflowName(), newName); + assertEq(receiver.getExpectedWorkflowName(), newName); } function testSetExpectedWorkflowNameOnlyOwner() public { @@ -125,19 +126,20 @@ contract CreReceiverTest is Test { vm.prank(nonOwner); vm.expectRevert(abi.encodeWithSelector(_ownableUnauthorizedAccountSelector(), nonOwner)); - receiver.setExpectedWorkflowName(newName); + string memory newNameStr = string(abi.encodePacked(newName)); + receiver.setExpectedWorkflowName(newNameStr); } function testSetExpectedWorkflowIdUpdatesStateAndEmitsEvent() public { bytes32 newId = keccak256("new-workflow-id"); - bytes32 oldId = receiver.expectedWorkflowId(); + bytes32 oldId = receiver.getExpectedWorkflowId(); vm.expectEmit(true, true, true, false); emit ExpectedWorkflowIdChanged(newId, oldId, address(this)); receiver.setExpectedWorkflowId(newId); - assertEq(receiver.expectedWorkflowId(), newId); + assertEq(receiver.getExpectedWorkflowId(), newId); } function testSetExpectedWorkflowIdOnlyOwner() public { @@ -254,7 +256,8 @@ contract CreReceiverTest is Test { function testOnReportRevertsForInvalidWorkflowName() public { bytes10 expectedName = bytes10("WF_EXPECT"); - receiver.setExpectedWorkflowName(expectedName); + string memory expectedNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(expectedNameStr); bytes32 workflowId = keccak256("wf-id"); bytes10 wrongName = bytes10("WF_WRONG"); @@ -269,7 +272,8 @@ contract CreReceiverTest is Test { function testOnReportSucceedsWithCorrectWorkflowName() public { bytes10 expectedName = bytes10("WF_EXPECT"); - receiver.setExpectedWorkflowName(expectedName); + string memory expectedNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(expectedNameStr); bytes32 workflowId = keccak256("wf-id"); address workflowOwner = address(this); @@ -292,7 +296,10 @@ contract CreReceiverTest is Test { address expectedAuthor = address(0xA1); receiver.setExpectedWorkflowId(expectedId); - receiver.setExpectedWorkflowName(expectedName); + + string memory expectedNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(expectedNameStr); + receiver.setExpectedAuthor(expectedAuthor); bytes memory metadata = _encodeMetadata(expectedId, expectedName, expectedAuthor); @@ -370,7 +377,8 @@ contract CreReceiverTest is Test { function testDisableExpectedWorkflowNameAllowsAnyWorkflowName() public { bytes10 expectedName = bytes10("WF_NAME_OK"); - receiver.setExpectedWorkflowName(expectedName); + string memory expectedNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(expectedNameStr); bytes32 workflowId = keccak256("wf-id"); bytes10 wrongName = bytes10("BAD_NAME"); @@ -382,7 +390,8 @@ contract CreReceiverTest is Test { vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), wrongName, expectedName)); receiver.onReport(badMetadata, report); - receiver.setExpectedWorkflowName(bytes10(0)); + string memory zeroNameStr = string(abi.encodePacked(bytes10(0))); + receiver.setExpectedWorkflowName(zeroNameStr); receiver.onReport(badMetadata, report); assertTrue(receiver.processCalled()); @@ -391,7 +400,7 @@ contract CreReceiverTest is Test { function testOnReportIgnoresAuthorWhenExpectationZero() public { // Expectation is zero by default - assertEq(receiver.expectedAuthor(), address(0)); + assertEq(receiver.getExpectedAuthor(), address(0)); bytes32 workflowId = keccak256("wf-id"); bytes10 workflowName = bytes10("WF_AUTH0"); @@ -406,7 +415,7 @@ contract CreReceiverTest is Test { } function testOnReportIgnoresWorkflowIdWhenExpectationZero() public { - assertEq(receiver.expectedWorkflowId(), bytes32(0)); + assertEq(receiver.getExpectedWorkflowId(), bytes32(0)); bytes32 anyId = keccak256("any-id"); bytes10 workflowName = bytes10("WF_ID0"); @@ -421,7 +430,7 @@ contract CreReceiverTest is Test { } function testOnReportIgnoresWorkflowNameWhenExpectationZero() public { - assertEq(receiver.expectedWorkflowName(), bytes10(0)); + assertEq(receiver.getExpectedWorkflowName(), bytes10(0)); bytes32 workflowId = keccak256("wf-id"); bytes10 anyName = bytes10("ANY_NAME"); @@ -443,7 +452,8 @@ contract CreReceiverTest is Test { receiver.setExpectedAuthor(expectedAuthor); bytes10 expectedName = bytes10("WF_EXPECT"); - receiver.setExpectedWorkflowName(expectedName); + string memory expectedNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(expectedNameStr); // All three are wrong: id, author, name bytes32 wrongId = keccak256("wrong-id"); @@ -462,7 +472,8 @@ contract CreReceiverTest is Test { receiver.setExpectedAuthor(expectedAuthor); bytes10 expectedName = bytes10("WF_EXPECT"); - receiver.setExpectedWorkflowName(expectedName); + string memory expectedNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(expectedNameStr); bytes32 workflowId = keccak256("wf-id"); address wrongAuthor = address(0xDEAD); @@ -522,12 +533,13 @@ contract CreReceiverTest is Test { function testSetExpectedWorkflowNameEmitsWhenSettingSameValue() public { bytes10 name = bytes10("WF_REPEAT"); - receiver.setExpectedWorkflowName(name); + string memory nameStr = string(abi.encodePacked(name)); + receiver.setExpectedWorkflowName(nameStr); vm.expectEmit(true, true, true, false); emit ExpectedWorkflowNameChanged(name, name, address(this)); - receiver.setExpectedWorkflowName(name); + receiver.setExpectedWorkflowName(nameStr); } function testSetExpectedWorkflowIdEmitsWhenSettingSameValue() public { diff --git a/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol b/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol index b9daa38d..22d785c5 100644 --- a/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol +++ b/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol @@ -20,7 +20,7 @@ contract CreReceiverFuzzTest is Test { event ForwarderAddressChanged(address indexed newForwarder, address indexed oldForwarder, address indexed changer); function setUp() public { - receiver = new MockCreReceiver(); + receiver = new MockCreReceiver(address(this)); } // ----------------------- @@ -60,14 +60,14 @@ contract CreReceiverFuzzTest is Test { // ----------------------- function testFuzz_SetForwarderAddressUpdatesStateAndEmitsEvent(address newForwarder) public { - address oldForwarder = receiver.forwarderAddress(); + address oldForwarder = receiver.getForwarderAddress(); vm.expectEmit(true, true, true, false); emit ForwarderAddressChanged(newForwarder, oldForwarder, address(this)); receiver.setForwarderAddress(newForwarder); - assertEq(receiver.forwarderAddress(), newForwarder); + assertEq(receiver.getForwarderAddress(), newForwarder); } function testFuzz_SetForwarderAddressOnlyOwner(address nonOwner, address newForwarder) public { @@ -79,14 +79,14 @@ contract CreReceiverFuzzTest is Test { } function testFuzz_SetExpectedAuthorUpdatesStateAndEmitsEvent(address newAuthor) public { - address oldAuthor = receiver.expectedAuthor(); + address oldAuthor = receiver.getExpectedAuthor(); vm.expectEmit(true, true, true, false); emit ExpectedAuthorChanged(newAuthor, oldAuthor, address(this)); receiver.setExpectedAuthor(newAuthor); - assertEq(receiver.expectedAuthor(), newAuthor); + assertEq(receiver.getExpectedAuthor(), newAuthor); } function testFuzz_SetExpectedAuthorOnlyOwner(address nonOwner, address newAuthor) public { @@ -98,14 +98,15 @@ contract CreReceiverFuzzTest is Test { } function testFuzz_SetExpectedWorkflowNameUpdatesStateAndEmitsEvent(bytes10 newName) public { - bytes10 oldName = receiver.expectedWorkflowName(); + bytes10 oldName = receiver.getExpectedWorkflowName(); vm.expectEmit(true, true, true, false); emit ExpectedWorkflowNameChanged(newName, oldName, address(this)); - receiver.setExpectedWorkflowName(newName); + string memory newNameStr = string(abi.encodePacked(newName)); + receiver.setExpectedWorkflowName(newNameStr); - assertEq(receiver.expectedWorkflowName(), newName); + assertEq(receiver.getExpectedWorkflowName(), newName); } function testFuzz_SetExpectedWorkflowNameOnlyOwner(bytes10 newName, address nonOwner) public { @@ -113,18 +114,20 @@ contract CreReceiverFuzzTest is Test { vm.prank(nonOwner); vm.expectRevert(abi.encodeWithSelector(_ownableUnauthorizedAccountSelector(), nonOwner)); - receiver.setExpectedWorkflowName(newName); + + string memory newNameStr = string(abi.encodePacked(newName)); + receiver.setExpectedWorkflowName(newNameStr); } function testFuzz_SetExpectedWorkflowIdUpdatesStateAndEmitsEvent(bytes32 newId) public { - bytes32 oldId = receiver.expectedWorkflowId(); + bytes32 oldId = receiver.getExpectedWorkflowId(); vm.expectEmit(true, true, true, false); emit ExpectedWorkflowIdChanged(newId, oldId, address(this)); receiver.setExpectedWorkflowId(newId); - assertEq(receiver.expectedWorkflowId(), newId); + assertEq(receiver.getExpectedWorkflowId(), newId); } function testFuzz_SetExpectedWorkflowIdOnlyOwner(bytes32 newId, address nonOwner) public { @@ -154,12 +157,13 @@ contract CreReceiverFuzzTest is Test { } function testFuzz_SetExpectedWorkflowNameEmitsWhenSettingSameValue(bytes10 name) public { - receiver.setExpectedWorkflowName(name); + string memory nameStr = string(abi.encodePacked(name)); + receiver.setExpectedWorkflowName(nameStr); vm.expectEmit(true, true, true, false); emit ExpectedWorkflowNameChanged(name, name, address(this)); - receiver.setExpectedWorkflowName(name); + receiver.setExpectedWorkflowName(nameStr); } function testFuzz_SetExpectedWorkflowIdEmitsWhenSettingSameValue(bytes32 id) public { @@ -363,7 +367,8 @@ contract CreReceiverFuzzTest is Test { vm.assume(expectedName != bytes10(0)); vm.assume(wrongName != expectedName); - receiver.setExpectedWorkflowName(expectedName); + string memory wfNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(wfNameStr); bytes memory metadata = _encodeMetadata(workflowId, wrongName, workflowOwner); @@ -379,7 +384,8 @@ contract CreReceiverFuzzTest is Test { ) public { vm.assume(expectedName != bytes10(0)); - receiver.setExpectedWorkflowName(expectedName); + string memory wfNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(wfNameStr); bytes memory metadata = _encodeMetadata(workflowId, expectedName, workflowOwner); @@ -396,14 +402,15 @@ contract CreReceiverFuzzTest is Test { vm.assume(expectedName != bytes10(0)); vm.assume(wrongName != expectedName); - receiver.setExpectedWorkflowName(expectedName); + string memory wfNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(wfNameStr); bytes memory badMetadata = _encodeMetadata(workflowId, wrongName, workflowOwner); vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), wrongName, expectedName)); receiver.onReport(badMetadata, report); - receiver.setExpectedWorkflowName(bytes10(0)); + receiver.setExpectedWorkflowName(wfNameStr); receiver.onReport(badMetadata, report); } @@ -414,10 +421,10 @@ contract CreReceiverFuzzTest is Test { address workflowOwner, bytes memory report ) public { - assertEq(receiver.forwarderAddress(), address(0)); - assertEq(receiver.expectedAuthor(), address(0)); - assertEq(receiver.expectedWorkflowName(), bytes10(0)); - assertEq(receiver.expectedWorkflowId(), bytes32(0)); + assertEq(receiver.getForwarderAddress(), address(0)); + assertEq(receiver.getExpectedAuthor(), address(0)); + assertEq(receiver.getExpectedWorkflowName(), bytes10(0)); + assertEq(receiver.getExpectedWorkflowId(), bytes32(0)); bytes memory metadata = _encodeMetadata(workflowId, workflowName, workflowOwner); @@ -443,7 +450,8 @@ contract CreReceiverFuzzTest is Test { receiver.setExpectedWorkflowId(expectedId); receiver.setExpectedAuthor(expectedAuthor); - receiver.setExpectedWorkflowName(expectedName); + string memory wfNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(wfNameStr); bytes memory metadata = _encodeMetadata(wrongId, wrongName, wrongAuthor); @@ -467,7 +475,8 @@ contract CreReceiverFuzzTest is Test { vm.assume(wrongName != expectedName); receiver.setExpectedAuthor(expectedAuthor); - receiver.setExpectedWorkflowName(expectedName); + string memory wfNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(wfNameStr); bytes memory metadata = _encodeMetadata(workflowId, wrongName, wrongAuthor); @@ -500,18 +509,19 @@ contract CreReceiverFuzzTest is Test { function testFuzz_OnReportSucceedsWhenAllChecksPass( address trustedForwarder, bytes32 workflowId, - bytes10 workflowName, address workflowOwner, - bytes memory report + bytes memory report, + bytes10 workflowName ) public { vm.assume(trustedForwarder != address(0)); vm.assume(workflowId != bytes32(0)); - vm.assume(workflowName != bytes10(0)); vm.assume(workflowOwner != address(0)); + string memory wfNameStr = string(abi.encodePacked(workflowName)); + receiver.setForwarderAddress(trustedForwarder); receiver.setExpectedWorkflowId(workflowId); - receiver.setExpectedWorkflowName(workflowName); + receiver.setExpectedWorkflowName(wfNameStr); receiver.setExpectedAuthor(workflowOwner); bytes memory metadata = _encodeMetadata(workflowId, workflowName, workflowOwner); From f4cdbf83745ac52b2edeea8fac1d8d5399cf90e8 Mon Sep 17 00:00:00 2001 From: christian harrington Date: Thu, 22 Jan 2026 20:06:39 +0000 Subject: [PATCH 08/11] fix tests, event tweaks, setWorkflowName now as a string, author now mandatory --- .../mock/mockRules/MockCreReceiver.sol | 34 +++++++ .../test/foundry/rules/CreReceiver.t.sol | 88 ++++++++----------- .../test/foundry/rules/CreReceiver_Fuzz.t.sol | 62 ++++++++----- 3 files changed, 109 insertions(+), 75 deletions(-) diff --git a/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol b/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol index 9782047a..588b357f 100644 --- a/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol +++ b/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol @@ -16,4 +16,38 @@ contract MockCreReceiver is CreReceiver { processCalled = true; lastReport = report; } + + bytes private constant HEX_CHARS = "0123456789abcdef"; + + /// @notice Helper function to convert bytes to hex string + /// @param data The bytes to convert + /// @return The hex string representation + function _creReceiverBytesToHexString( + bytes memory data + ) private pure returns (bytes memory) { + bytes memory hexString = new bytes(data.length * 2); + + for (uint256 i = 0; i < data.length; i++) { + hexString[i * 2] = HEX_CHARS[uint8(data[i] >> 4)]; + hexString[i * 2 + 1] = HEX_CHARS[uint8(data[i] & 0x0f)]; + } + + return hexString; + } + + function encodeWorkflowName(string memory _name) external pure returns (bytes10) { + if (bytes(_name).length == 0) { + return bytes10(0); + } + + // Convert workflow name to bytes10: + // SHA256 hash → hex encode → take first 10 chars → hex encode those chars + bytes32 hash = sha256(bytes(_name)); + bytes memory hexString = _creReceiverBytesToHexString(abi.encodePacked(hash)); + bytes memory first10 = new bytes(10); + for (uint256 i = 0; i < 10; i++) { + first10[i] = hexString[i]; + } + return bytes10(first10); + } } diff --git a/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol b/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol index 91da41f8..e8162baf 100644 --- a/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol +++ b/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol @@ -14,10 +14,11 @@ contract CreReceiverTest is Test { MockCreReceiver internal receiver; // Re-declare events so we can use expectEmit - event ExpectedAuthorChanged(address indexed newAuthor, address indexed oldAuthor, address indexed changer); - event ExpectedWorkflowNameChanged(bytes10 indexed newName, bytes10 indexed oldName, address indexed changer); - event ExpectedWorkflowIdChanged(bytes32 indexed newId, bytes32 indexed oldId, address indexed changer); - event ForwarderAddressChanged(address indexed newForwarder, address indexed oldForwarder, address indexed changer); + event ForwarderAddressUpdated(address indexed previousForwarder, address indexed newForwarder); + event ExpectedAuthorUpdated(address indexed previousAuthor, address indexed newAuthor); + event ExpectedWorkflowNameUpdated(bytes10 indexed previousName, bytes10 indexed newName); + event ExpectedWorkflowIdUpdated(bytes32 indexed previousId, bytes32 indexed newId); + event SecurityWarning(string message); function setUp() public { receiver = new MockCreReceiver(address(this)); @@ -56,7 +57,7 @@ contract CreReceiverTest is Test { } function testInitialState() public view { - assertEq(receiver.getForwarderAddress(), address(0)); + assertEq(receiver.getForwarderAddress(), address(this)); assertEq(receiver.getExpectedAuthor(), address(0)); assertEq(receiver.getExpectedWorkflowName(), bytes10(0)); assertEq(receiver.getExpectedWorkflowId(), bytes32(0)); @@ -70,7 +71,7 @@ contract CreReceiverTest is Test { address oldForwarder = receiver.getForwarderAddress(); vm.expectEmit(true, true, true, false); - emit ForwarderAddressChanged(newForwarder, oldForwarder, address(this)); + emit ForwarderAddressUpdated(oldForwarder, newForwarder); receiver.setForwarderAddress(newForwarder); @@ -91,7 +92,7 @@ contract CreReceiverTest is Test { address oldAuthor = receiver.getExpectedAuthor(); vm.expectEmit(true, true, true, false); - emit ExpectedAuthorChanged(newAuthor, oldAuthor, address(this)); + emit ExpectedAuthorUpdated(oldAuthor, newAuthor); receiver.setExpectedAuthor(newAuthor); @@ -108,14 +109,13 @@ contract CreReceiverTest is Test { } function testSetExpectedWorkflowNameUpdatesStateAndEmitsEvent() public { - bytes10 newName = bytes10("WF_LOW_001"); + bytes10 newName = receiver.encodeWorkflowName("WF_LOW_001"); bytes10 oldName = receiver.getExpectedWorkflowName(); vm.expectEmit(true, true, true, false); - emit ExpectedWorkflowNameChanged(newName, oldName, address(this)); + emit ExpectedWorkflowNameUpdated(oldName, newName); - string memory newNameStr = string(abi.encodePacked(newName)); - receiver.setExpectedWorkflowName(newNameStr); + receiver.setExpectedWorkflowName("WF_LOW_001"); assertEq(receiver.getExpectedWorkflowName(), newName); } @@ -135,7 +135,7 @@ contract CreReceiverTest is Test { bytes32 oldId = receiver.getExpectedWorkflowId(); vm.expectEmit(true, true, true, false); - emit ExpectedWorkflowIdChanged(newId, oldId, address(this)); + emit ExpectedWorkflowIdUpdated(oldId, newId); receiver.setExpectedWorkflowId(newId); @@ -255,9 +255,7 @@ contract CreReceiverTest is Test { } function testOnReportRevertsForInvalidWorkflowName() public { - bytes10 expectedName = bytes10("WF_EXPECT"); - string memory expectedNameStr = string(abi.encodePacked(expectedName)); - receiver.setExpectedWorkflowName(expectedNameStr); + receiver.setExpectedWorkflowName("WF_EXPECT"); bytes32 workflowId = keccak256("wf-id"); bytes10 wrongName = bytes10("WF_WRONG"); @@ -266,18 +264,19 @@ contract CreReceiverTest is Test { bytes memory metadata = _encodeMetadata(workflowId, wrongName, workflowOwner); bytes memory report = abi.encodePacked(uint256(123)); - vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), wrongName, expectedName)); + vm.expectRevert( + abi.encodeWithSelector(bytes4(keccak256("WorkflowNameRequiresAuthorValidation()"))) + ); receiver.onReport(metadata, report); } function testOnReportSucceedsWithCorrectWorkflowName() public { - bytes10 expectedName = bytes10("WF_EXPECT"); - string memory expectedNameStr = string(abi.encodePacked(expectedName)); - receiver.setExpectedWorkflowName(expectedNameStr); + bytes10 expectedName = receiver.encodeWorkflowName("WF_EXPECT"); + receiver.setExpectedWorkflowName("WF_EXPECT"); bytes32 workflowId = keccak256("wf-id"); address workflowOwner = address(this); - + receiver.setExpectedAuthor(workflowOwner); bytes memory metadata = _encodeMetadata(workflowId, expectedName, workflowOwner); bytes memory report = abi.encodePacked(uint256(123)); @@ -292,14 +291,12 @@ contract CreReceiverTest is Test { receiver.setForwarderAddress(trustedForwarder); bytes32 expectedId = keccak256("expected-id"); - bytes10 expectedName = bytes10("WF_EXPECT"); + bytes10 expectedName = receiver.encodeWorkflowName("WF_EXPECT"); address expectedAuthor = address(0xA1); receiver.setExpectedWorkflowId(expectedId); - - string memory expectedNameStr = string(abi.encodePacked(expectedName)); - receiver.setExpectedWorkflowName(expectedNameStr); - + receiver.setExpectedWorkflowName("WF_EXPECT"); + vm.assertEq(receiver.getExpectedWorkflowName(), expectedName); receiver.setExpectedAuthor(expectedAuthor); bytes memory metadata = _encodeMetadata(expectedId, expectedName, expectedAuthor); @@ -322,7 +319,6 @@ contract CreReceiverTest is Test { vm.prank(nonForwarder); vm.expectRevert(abi.encodeWithSelector(_invalidSenderSelector(), nonForwarder, trustedForwarder)); - receiver.onReport(metadata, report); receiver.setForwarderAddress(address(0)); @@ -375,10 +371,8 @@ contract CreReceiverTest is Test { assertEq(receiver.lastReport(), report); } - function testDisableExpectedWorkflowNameAllowsAnyWorkflowName() public { - bytes10 expectedName = bytes10("WF_NAME_OK"); - string memory expectedNameStr = string(abi.encodePacked(expectedName)); - receiver.setExpectedWorkflowName(expectedNameStr); + function testNameOnlyFailsValidation() public { + receiver.setExpectedWorkflowName("WF_EXPECT"); bytes32 workflowId = keccak256("wf-id"); bytes10 wrongName = bytes10("BAD_NAME"); @@ -387,15 +381,10 @@ contract CreReceiverTest is Test { bytes memory badMetadata = _encodeMetadata(workflowId, wrongName, workflowOwner); bytes memory report = abi.encodePacked(uint256(444)); - vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), wrongName, expectedName)); - receiver.onReport(badMetadata, report); - - string memory zeroNameStr = string(abi.encodePacked(bytes10(0))); - receiver.setExpectedWorkflowName(zeroNameStr); - + vm.expectRevert( + abi.encodeWithSelector(bytes4(keccak256("WorkflowNameRequiresAuthorValidation()"))) + ); receiver.onReport(badMetadata, report); - assertTrue(receiver.processCalled()); - assertEq(receiver.lastReport(), report); } function testOnReportIgnoresAuthorWhenExpectationZero() public { @@ -451,9 +440,7 @@ contract CreReceiverTest is Test { address expectedAuthor = address(0xA1); receiver.setExpectedAuthor(expectedAuthor); - bytes10 expectedName = bytes10("WF_EXPECT"); - string memory expectedNameStr = string(abi.encodePacked(expectedName)); - receiver.setExpectedWorkflowName(expectedNameStr); + receiver.setExpectedWorkflowName("WF_EXPECT"); // All three are wrong: id, author, name bytes32 wrongId = keccak256("wrong-id"); @@ -471,9 +458,7 @@ contract CreReceiverTest is Test { address expectedAuthor = address(0xA1); receiver.setExpectedAuthor(expectedAuthor); - bytes10 expectedName = bytes10("WF_EXPECT"); - string memory expectedNameStr = string(abi.encodePacked(expectedName)); - receiver.setExpectedWorkflowName(expectedNameStr); + receiver.setExpectedWorkflowName("WF_EXPECT"); bytes32 workflowId = keccak256("wf-id"); address wrongAuthor = address(0xDEAD); @@ -516,7 +501,7 @@ contract CreReceiverTest is Test { receiver.setForwarderAddress(forwarder); vm.expectEmit(true, true, true, false); - emit ForwarderAddressChanged(forwarder, forwarder, address(this)); + emit ForwarderAddressUpdated(forwarder, forwarder); receiver.setForwarderAddress(forwarder); } @@ -526,20 +511,19 @@ contract CreReceiverTest is Test { receiver.setExpectedAuthor(author); vm.expectEmit(true, true, true, false); - emit ExpectedAuthorChanged(author, author, address(this)); + emit ExpectedAuthorUpdated(author, author); receiver.setExpectedAuthor(author); } function testSetExpectedWorkflowNameEmitsWhenSettingSameValue() public { - bytes10 name = bytes10("WF_REPEAT"); - string memory nameStr = string(abi.encodePacked(name)); - receiver.setExpectedWorkflowName(nameStr); + bytes10 name = receiver.encodeWorkflowName("WF_EXPECT"); + receiver.setExpectedWorkflowName("WF_EXPECT"); vm.expectEmit(true, true, true, false); - emit ExpectedWorkflowNameChanged(name, name, address(this)); + emit ExpectedWorkflowNameUpdated(name, name); - receiver.setExpectedWorkflowName(nameStr); + receiver.setExpectedWorkflowName("WF_EXPECT"); } function testSetExpectedWorkflowIdEmitsWhenSettingSameValue() public { @@ -547,7 +531,7 @@ contract CreReceiverTest is Test { receiver.setExpectedWorkflowId(id); vm.expectEmit(true, true, true, false); - emit ExpectedWorkflowIdChanged(id, id, address(this)); + emit ExpectedWorkflowIdUpdated(id, id); receiver.setExpectedWorkflowId(id); } diff --git a/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol b/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol index 22d785c5..713dc13b 100644 --- a/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol +++ b/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol @@ -14,10 +14,11 @@ contract CreReceiverFuzzTest is Test { MockCreReceiver internal receiver; // Re-declare events so we can use expectEmit - event ExpectedAuthorChanged(address indexed newAuthor, address indexed oldAuthor, address indexed changer); - event ExpectedWorkflowNameChanged(bytes10 indexed newName, bytes10 indexed oldName, address indexed changer); - event ExpectedWorkflowIdChanged(bytes32 indexed newId, bytes32 indexed oldId, address indexed changer); - event ForwarderAddressChanged(address indexed newForwarder, address indexed oldForwarder, address indexed changer); + event ForwarderAddressUpdated(address indexed previousForwarder, address indexed newForwarder); + event ExpectedAuthorUpdated(address indexed previousAuthor, address indexed newAuthor); + event ExpectedWorkflowNameUpdated(bytes10 indexed previousName, bytes10 indexed newName); + event ExpectedWorkflowIdUpdated(bytes32 indexed previousId, bytes32 indexed newId); + event SecurityWarning(string message); function setUp() public { receiver = new MockCreReceiver(address(this)); @@ -63,7 +64,7 @@ contract CreReceiverFuzzTest is Test { address oldForwarder = receiver.getForwarderAddress(); vm.expectEmit(true, true, true, false); - emit ForwarderAddressChanged(newForwarder, oldForwarder, address(this)); + emit ForwarderAddressUpdated(oldForwarder, newForwarder); receiver.setForwarderAddress(newForwarder); @@ -82,7 +83,7 @@ contract CreReceiverFuzzTest is Test { address oldAuthor = receiver.getExpectedAuthor(); vm.expectEmit(true, true, true, false); - emit ExpectedAuthorChanged(newAuthor, oldAuthor, address(this)); + emit ExpectedAuthorUpdated(oldAuthor, newAuthor); receiver.setExpectedAuthor(newAuthor); @@ -100,13 +101,13 @@ contract CreReceiverFuzzTest is Test { function testFuzz_SetExpectedWorkflowNameUpdatesStateAndEmitsEvent(bytes10 newName) public { bytes10 oldName = receiver.getExpectedWorkflowName(); + string memory newNameStr = string(abi.encodePacked(newName)); vm.expectEmit(true, true, true, false); - emit ExpectedWorkflowNameChanged(newName, oldName, address(this)); + emit ExpectedWorkflowNameUpdated(oldName, receiver.encodeWorkflowName(newNameStr)); - string memory newNameStr = string(abi.encodePacked(newName)); receiver.setExpectedWorkflowName(newNameStr); - assertEq(receiver.getExpectedWorkflowName(), newName); + assertEq(receiver.getExpectedWorkflowName(), receiver.encodeWorkflowName(newNameStr)); } function testFuzz_SetExpectedWorkflowNameOnlyOwner(bytes10 newName, address nonOwner) public { @@ -123,7 +124,7 @@ contract CreReceiverFuzzTest is Test { bytes32 oldId = receiver.getExpectedWorkflowId(); vm.expectEmit(true, true, true, false); - emit ExpectedWorkflowIdChanged(newId, oldId, address(this)); + emit ExpectedWorkflowIdUpdated(oldId, newId); receiver.setExpectedWorkflowId(newId); @@ -142,7 +143,7 @@ contract CreReceiverFuzzTest is Test { receiver.setForwarderAddress(forwarder); vm.expectEmit(true, true, true, false); - emit ForwarderAddressChanged(forwarder, forwarder, address(this)); + emit ForwarderAddressUpdated(forwarder, forwarder); receiver.setForwarderAddress(forwarder); } @@ -151,7 +152,7 @@ contract CreReceiverFuzzTest is Test { receiver.setExpectedAuthor(author); vm.expectEmit(true, true, true, false); - emit ExpectedAuthorChanged(author, author, address(this)); + emit ExpectedAuthorUpdated(author, author); receiver.setExpectedAuthor(author); } @@ -159,9 +160,10 @@ contract CreReceiverFuzzTest is Test { function testFuzz_SetExpectedWorkflowNameEmitsWhenSettingSameValue(bytes10 name) public { string memory nameStr = string(abi.encodePacked(name)); receiver.setExpectedWorkflowName(nameStr); + bytes10 expectedName = receiver.encodeWorkflowName(nameStr); vm.expectEmit(true, true, true, false); - emit ExpectedWorkflowNameChanged(name, name, address(this)); + emit ExpectedWorkflowNameUpdated(expectedName, expectedName); receiver.setExpectedWorkflowName(nameStr); } @@ -170,7 +172,7 @@ contract CreReceiverFuzzTest is Test { receiver.setExpectedWorkflowId(id); vm.expectEmit(true, true, true, false); - emit ExpectedWorkflowIdChanged(id, id, address(this)); + emit ExpectedWorkflowIdUpdated(id, id); receiver.setExpectedWorkflowId(id); } @@ -366,13 +368,17 @@ contract CreReceiverFuzzTest is Test { ) public { vm.assume(expectedName != bytes10(0)); vm.assume(wrongName != expectedName); + vm.assume(workflowOwner != address(0)); string memory wfNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedAuthor(workflowOwner); receiver.setExpectedWorkflowName(wfNameStr); + bytes10 encodedExpectedName = receiver.encodeWorkflowName(wfNameStr); + bytes10 encodedWrongName = receiver.encodeWorkflowName(string(abi.encodePacked(wrongName))); - bytes memory metadata = _encodeMetadata(workflowId, wrongName, workflowOwner); + bytes memory metadata = _encodeMetadata(workflowId, encodedWrongName, workflowOwner); - vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), wrongName, expectedName)); + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), encodedWrongName, encodedExpectedName)); receiver.onReport(metadata, report); } @@ -383,11 +389,15 @@ contract CreReceiverFuzzTest is Test { bytes memory report ) public { vm.assume(expectedName != bytes10(0)); + vm.assume(workflowOwner != address(0)); + receiver.setExpectedAuthor(workflowOwner); string memory wfNameStr = string(abi.encodePacked(expectedName)); receiver.setExpectedWorkflowName(wfNameStr); - bytes memory metadata = _encodeMetadata(workflowId, expectedName, workflowOwner); + bytes10 encodedExpectedName = receiver.encodeWorkflowName(wfNameStr); + + bytes memory metadata = _encodeMetadata(workflowId, encodedExpectedName, workflowOwner); receiver.onReport(metadata, report); } @@ -401,16 +411,22 @@ contract CreReceiverFuzzTest is Test { ) public { vm.assume(expectedName != bytes10(0)); vm.assume(wrongName != expectedName); + vm.assume(workflowOwner != address(0)); string memory wfNameStr = string(abi.encodePacked(expectedName)); + + receiver.setExpectedAuthor(workflowOwner); receiver.setExpectedWorkflowName(wfNameStr); - bytes memory badMetadata = _encodeMetadata(workflowId, wrongName, workflowOwner); + bytes10 encodedExpectedName = receiver.encodeWorkflowName(wfNameStr); + bytes10 encodedWrongName = receiver.encodeWorkflowName(string(abi.encodePacked(wrongName))); + + bytes memory badMetadata = _encodeMetadata(workflowId, encodedWrongName, workflowOwner); - vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), wrongName, expectedName)); + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), encodedWrongName, encodedExpectedName)); receiver.onReport(badMetadata, report); - receiver.setExpectedWorkflowName(wfNameStr); + receiver.setExpectedWorkflowName(string(abi.encodePacked(wrongName))); receiver.onReport(badMetadata, report); } @@ -421,7 +437,7 @@ contract CreReceiverFuzzTest is Test { address workflowOwner, bytes memory report ) public { - assertEq(receiver.getForwarderAddress(), address(0)); + assertEq(receiver.getForwarderAddress(), address(this)); assertEq(receiver.getExpectedAuthor(), address(0)); assertEq(receiver.getExpectedWorkflowName(), bytes10(0)); assertEq(receiver.getExpectedWorkflowId(), bytes32(0)); @@ -518,13 +534,13 @@ contract CreReceiverFuzzTest is Test { vm.assume(workflowOwner != address(0)); string memory wfNameStr = string(abi.encodePacked(workflowName)); - + bytes10 expectedName = receiver.encodeWorkflowName(wfNameStr); receiver.setForwarderAddress(trustedForwarder); receiver.setExpectedWorkflowId(workflowId); receiver.setExpectedWorkflowName(wfNameStr); receiver.setExpectedAuthor(workflowOwner); - bytes memory metadata = _encodeMetadata(workflowId, workflowName, workflowOwner); + bytes memory metadata = _encodeMetadata(workflowId, expectedName, workflowOwner); vm.prank(trustedForwarder); receiver.onReport(metadata, report); From 16ffbf11e6f50f1210595b2dfcf2cd340adb8654 Mon Sep 17 00:00:00 2001 From: christian harrington Date: Fri, 23 Jan 2026 13:29:52 +0000 Subject: [PATCH 09/11] I-01 remove unused struct var --- pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol b/pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol index b10184e9..56747d1e 100644 --- a/pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol +++ b/pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol @@ -28,7 +28,6 @@ contract CreUpdateRule is QuantAMMMathGuard, CreReceiver, ITypeAndVersion, IUpda struct WorkflowMetadata { address poolAddress; uint256[] weights; - uint40 lastInterpolationTimePossible; } event TargetWeightsForwarded(address indexed pool, address sender); From 97d573dc0673a8bf121dc448eb4ffdbc96aeae63 Mon Sep 17 00:00:00 2001 From: christian harrington Date: Fri, 23 Jan 2026 13:35:49 +0000 Subject: [PATCH 10/11] bring tests inline --- .../test/foundry/rules/CreUpdateRule.t.sol | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/pkg/pool-quantamm/test/foundry/rules/CreUpdateRule.t.sol b/pkg/pool-quantamm/test/foundry/rules/CreUpdateRule.t.sol index 85808e1e..7dc31a9b 100644 --- a/pkg/pool-quantamm/test/foundry/rules/CreUpdateRule.t.sol +++ b/pkg/pool-quantamm/test/foundry/rules/CreUpdateRule.t.sol @@ -185,8 +185,7 @@ contract CreUpdateRuleTest is Test, QuantAMMTestUtils { bytes memory metaData = abi.encode( CreUpdateRule.WorkflowMetadata({ poolAddress: address(mockPool), - weights: weights, - lastInterpolationTimePossible: updateInterval + weights: weights }) ); @@ -223,8 +222,7 @@ contract CreUpdateRuleTest is Test, QuantAMMTestUtils { bytes memory metaData = abi.encode( CreUpdateRule.WorkflowMetadata({ poolAddress: address(0), - weights: weights, - lastInterpolationTimePossible: uint40(1) + weights: weights }) ); @@ -286,8 +284,7 @@ contract CreUpdateRuleTest is Test, QuantAMMTestUtils { bytes memory metaData = abi.encode( CreUpdateRule.WorkflowMetadata({ poolAddress: address(mockPool), - weights: weights, - lastInterpolationTimePossible: updateInterval + weights: weights }) ); @@ -319,8 +316,7 @@ contract CreUpdateRuleTest is Test, QuantAMMTestUtils { bytes memory metaData = abi.encode( CreUpdateRule.WorkflowMetadata({ poolAddress: address(unconfiguredPool), - weights: weights, - lastInterpolationTimePossible: uint40(1) + weights: weights }) ); @@ -387,8 +383,7 @@ contract CreUpdateRuleTest is Test, QuantAMMTestUtils { bytes memory metaData = abi.encode( CreUpdateRule.WorkflowMetadata({ poolAddress: address(mockPool), - weights: weights, - lastInterpolationTimePossible: updateInterval + weights: weights }) ); @@ -463,8 +458,7 @@ contract CreUpdateRuleTest is Test, QuantAMMTestUtils { bytes memory metaData = abi.encode( CreUpdateRule.WorkflowMetadata({ poolAddress: address(mockPool), - weights: weights, - lastInterpolationTimePossible: updateInterval + weights: weights }) ); From f504592253c2bc32539988dba216e7cf1195f4ad Mon Sep 17 00:00:00 2001 From: christian harrington Date: Wed, 28 Jan 2026 10:09:42 +0000 Subject: [PATCH 11/11] M-01 fix --- .../contracts/rules/CreReceiver.sol | 20 ++-- .../test/foundry/rules/CreReceiver.t.sol | 49 ++------- .../test/foundry/rules/CreReceiver_Fuzz.t.sol | 102 ++++++++---------- 3 files changed, 59 insertions(+), 112 deletions(-) diff --git a/pkg/pool-quantamm/contracts/rules/CreReceiver.sol b/pkg/pool-quantamm/contracts/rules/CreReceiver.sol index 9cf2ae31..28b0fe6b 100644 --- a/pkg/pool-quantamm/contracts/rules/CreReceiver.sol +++ b/pkg/pool-quantamm/contracts/rules/CreReceiver.sol @@ -123,17 +123,13 @@ abstract contract CreReceiver is IReceiver, Ownable { /// @notice Updates the forwarder address that is allowed to call onReport /// @param _forwarder The new forwarder address - /// @dev WARNING: Setting to address(0) disables forwarder validation. - /// This makes your contract INSECURE - anyone can call onReport() with arbitrary data. - /// Only use address(0) if you fully understand the security implications. function setForwarderAddress( address _forwarder ) external onlyOwner { address previousForwarder = s_forwarderAddress; - // Emit warning if disabling forwarder check if (_forwarder == address(0)) { - emit SecurityWarning("Forwarder address set to zero - contract is now INSECURE"); + revert InvalidForwarderAddress(); } s_forwarderAddress = _forwarder; @@ -141,10 +137,13 @@ abstract contract CreReceiver is IReceiver, Ownable { } /// @notice Updates the expected workflow owner address - /// @param _author The new expected author address (use address(0) to disable this check) + /// @param _author The new expected author address function setExpectedAuthor( address _author ) external onlyOwner { + if(_author == address(0)){ + revert InvalidAuthor(_author, s_expectedAuthor); + } address previousAuthor = s_expectedAuthor; s_expectedAuthor = _author; emit ExpectedAuthorUpdated(previousAuthor, _author); @@ -164,9 +163,7 @@ abstract contract CreReceiver is IReceiver, Ownable { bytes10 previousName = s_expectedWorkflowName; if (bytes(_name).length == 0) { - s_expectedWorkflowName = bytes10(0); - emit ExpectedWorkflowNameUpdated(previousName, bytes10(0)); - return; + revert InvalidWorkflowName(bytes10(0), s_expectedWorkflowName); } // Convert workflow name to bytes10: @@ -182,10 +179,13 @@ abstract contract CreReceiver is IReceiver, Ownable { } /// @notice Updates the expected workflow ID - /// @param _id The new expected workflow ID (use bytes32(0) to disable this check) + /// @param _id The new expected workflow ID function setExpectedWorkflowId( bytes32 _id ) external onlyOwner { + if(_id == bytes32(0)){ + revert InvalidWorkflowId(_id, s_expectedWorkflowId); + } bytes32 previousId = s_expectedWorkflowId; s_expectedWorkflowId = _id; emit ExpectedWorkflowIdUpdated(previousId, _id); diff --git a/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol b/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol index e8162baf..e416d816 100644 --- a/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol +++ b/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol @@ -40,6 +40,10 @@ contract CreReceiverTest is Test { return bytes4(keccak256("InvalidSender(address,address)")); } + function _invalidForwarderAddress() internal pure returns (bytes4) { + return bytes4(keccak256("InvalidForwarderAddress()")); + } + function _invalidAuthorSelector() internal pure returns (bytes4) { return bytes4(keccak256("InvalidAuthor(address,address)")); } @@ -312,63 +316,24 @@ contract CreReceiverTest is Test { function testDisableForwarderAllowsAnySender() public { address trustedForwarder = address(0xF0F0); receiver.setForwarderAddress(trustedForwarder); - - address nonForwarder = address(0xBEEF); - bytes memory metadata = ""; - bytes memory report = abi.encodePacked(uint256(111)); - - vm.prank(nonForwarder); - vm.expectRevert(abi.encodeWithSelector(_invalidSenderSelector(), nonForwarder, trustedForwarder)); - receiver.onReport(metadata, report); + vm.expectRevert(_invalidForwarderAddress()); receiver.setForwarderAddress(address(0)); - - vm.prank(nonForwarder); - receiver.onReport(metadata, report); - - assertTrue(receiver.processCalled()); - assertEq(receiver.lastReport(), report); } function testDisableExpectedAuthorAllowsAnyAuthor() public { address expectedAuthor = address(0xA1); receiver.setExpectedAuthor(expectedAuthor); - bytes32 workflowId = keccak256("wf-id"); - bytes10 workflowName = bytes10("WF_AUTH"); - address wrongOwner = address(0xDEAD); - - bytes memory metadata = _encodeMetadata(workflowId, workflowName, wrongOwner); - bytes memory report = abi.encodePacked(uint256(222)); - - vm.expectRevert(abi.encodeWithSelector(_invalidAuthorSelector(), wrongOwner, expectedAuthor)); - receiver.onReport(metadata, report); - + vm.expectRevert(abi.encodeWithSelector(_invalidAuthorSelector(), address(0), expectedAuthor)); receiver.setExpectedAuthor(address(0)); - receiver.onReport(metadata, report); - - assertTrue(receiver.processCalled()); - assertEq(receiver.lastReport(), report); } function testDisableExpectedWorkflowIdAllowsAnyWorkflowId() public { bytes32 expectedId = keccak256("expected-id"); receiver.setExpectedWorkflowId(expectedId); - bytes32 wrongId = keccak256("wrong-id"); - bytes10 workflowName = bytes10("WF_ID"); - address workflowOwner = address(this); - - bytes memory badMetadata = _encodeMetadata(wrongId, workflowName, workflowOwner); - bytes memory report = abi.encodePacked(uint256(333)); - - vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), wrongId, expectedId)); - receiver.onReport(badMetadata, report); - + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), bytes32(0), expectedId)); receiver.setExpectedWorkflowId(bytes32(0)); - - receiver.onReport(badMetadata, report); - assertTrue(receiver.processCalled()); - assertEq(receiver.lastReport(), report); } function testNameOnlyFailsValidation() public { diff --git a/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol b/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol index 713dc13b..e91f9b2d 100644 --- a/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol +++ b/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol @@ -36,6 +36,11 @@ contract CreReceiverFuzzTest is Test { return abi.encodePacked(workflowId, workflowName, workflowOwner); } + function _invalidForwarderAddress() internal pure returns (bytes4) { + return bytes4(keccak256("InvalidForwarderAddress()")); + } + + function _invalidSenderSelector() internal pure returns (bytes4) { return bytes4(keccak256("InvalidSender(address,address)")); } @@ -61,6 +66,8 @@ contract CreReceiverFuzzTest is Test { // ----------------------- function testFuzz_SetForwarderAddressUpdatesStateAndEmitsEvent(address newForwarder) public { + vm.assume(newForwarder != address(0)); + address oldForwarder = receiver.getForwarderAddress(); vm.expectEmit(true, true, true, false); @@ -80,6 +87,8 @@ contract CreReceiverFuzzTest is Test { } function testFuzz_SetExpectedAuthorUpdatesStateAndEmitsEvent(address newAuthor) public { + vm.assume(newAuthor != address(0)); + address oldAuthor = receiver.getExpectedAuthor(); vm.expectEmit(true, true, true, false); @@ -121,6 +130,8 @@ contract CreReceiverFuzzTest is Test { } function testFuzz_SetExpectedWorkflowIdUpdatesStateAndEmitsEvent(bytes32 newId) public { + vm.assume(newId != bytes32(0)); + bytes32 oldId = receiver.getExpectedWorkflowId(); vm.expectEmit(true, true, true, false); @@ -140,6 +151,8 @@ contract CreReceiverFuzzTest is Test { } function testFuzz_SetForwarderAddressEmitsWhenSettingSameValue(address forwarder) public { + vm.assume(forwarder != address(0)); + receiver.setForwarderAddress(forwarder); vm.expectEmit(true, true, true, false); @@ -149,6 +162,8 @@ contract CreReceiverFuzzTest is Test { } function testFuzz_SetExpectedAuthorEmitsWhenSettingSameValue(address author) public { + vm.assume(author != address(0)); + receiver.setExpectedAuthor(author); vm.expectEmit(true, true, true, false); @@ -169,6 +184,8 @@ contract CreReceiverFuzzTest is Test { } function testFuzz_SetExpectedWorkflowIdEmitsWhenSettingSameValue(bytes32 id) public { + vm.assume(id != bytes32(0)); + receiver.setExpectedWorkflowId(id); vm.expectEmit(true, true, true, false); @@ -225,30 +242,6 @@ contract CreReceiverFuzzTest is Test { receiver.onReport(metadata, report); } - function testFuzz_DisableForwarderAllowsAnySender( - address trustedForwarder, - address nonForwarder, - bytes32 workflowId, - bytes10 workflowName, - address workflowOwner, - bytes memory report - ) public { - vm.assume(trustedForwarder != address(0)); - vm.assume(nonForwarder != trustedForwarder); - - receiver.setForwarderAddress(trustedForwarder); - bytes memory metadata = _encodeMetadata(workflowId, workflowName, workflowOwner); - - vm.prank(nonForwarder); - vm.expectRevert(abi.encodeWithSelector(_invalidSenderSelector(), nonForwarder, trustedForwarder)); - receiver.onReport(metadata, report); - - receiver.setForwarderAddress(address(0)); - - vm.prank(nonForwarder); - receiver.onReport(metadata, report); - } - function testFuzz_OnReportRevertsForInvalidWorkflowId( bytes32 expectedId, bytes32 wrongId, @@ -282,28 +275,6 @@ contract CreReceiverFuzzTest is Test { receiver.onReport(metadata, report); } - function testFuzz_DisableExpectedWorkflowIdAllowsAnyWorkflowId( - bytes32 expectedId, - bytes32 wrongId, - bytes10 workflowName, - address workflowOwner, - bytes memory report - ) public { - vm.assume(expectedId != bytes32(0)); - vm.assume(wrongId != expectedId); - - receiver.setExpectedWorkflowId(expectedId); - - bytes memory badMetadata = _encodeMetadata(wrongId, workflowName, workflowOwner); - - vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), wrongId, expectedId)); - receiver.onReport(badMetadata, report); - - receiver.setExpectedWorkflowId(bytes32(0)); - - receiver.onReport(badMetadata, report); - } - function testFuzz_OnReportRevertsForInvalidAuthor( address expectedAuthor, address wrongOwner, @@ -337,26 +308,37 @@ contract CreReceiverFuzzTest is Test { receiver.onReport(metadata, report); } - function testFuzz_DisableExpectedAuthorAllowsAnyAuthor( - address expectedAuthor, - address wrongOwner, - bytes32 workflowId, - bytes10 workflowName, - bytes memory report + + function testFuzz_DisableForwarderNotAllowed( + address trustedForwarder ) public { - vm.assume(expectedAuthor != address(0)); - vm.assume(wrongOwner != expectedAuthor); + vm.assume(trustedForwarder != address(0)); - receiver.setExpectedAuthor(expectedAuthor); + receiver.setForwarderAddress(trustedForwarder); + + vm.expectRevert(abi.encodeWithSelector(_invalidForwarderAddress(), address(0), trustedForwarder)); + receiver.setForwarderAddress(address(0)); + } - bytes memory metadata = _encodeMetadata(workflowId, workflowName, wrongOwner); + function testFuzz_DisableExpectedWorkflowIdNotAllowed( + bytes32 expectedId + ) public { + vm.assume(expectedId != bytes32(0)); - vm.expectRevert(abi.encodeWithSelector(_invalidAuthorSelector(), wrongOwner, expectedAuthor)); - receiver.onReport(metadata, report); + receiver.setExpectedWorkflowId(expectedId); - receiver.setExpectedAuthor(address(0)); + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), bytes32(0), expectedId)); + receiver.setExpectedWorkflowId(bytes32(0)); + } - receiver.onReport(metadata, report); + function testFuzz_DisableExpectedAuthorNotAllowed( + address expectedAuthor + ) public { + vm.assume(expectedAuthor != address(0)); + + receiver.setExpectedAuthor(expectedAuthor); + vm.expectRevert(abi.encodeWithSelector(_invalidAuthorSelector(), address(0), expectedAuthor)); + receiver.setExpectedAuthor(address(0)); } function testFuzz_OnReportRevertsForInvalidWorkflowName(