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/mock/mockRules/MockCreReceiver.sol b/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol new file mode 100644 index 00000000..588b357f --- /dev/null +++ b/pkg/pool-quantamm/contracts/mock/mockRules/MockCreReceiver.sol @@ -0,0 +1,53 @@ +// 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(address forwarderAddress) CreReceiver(forwarderAddress) {} + + function ProcessReport(bytes calldata metaData) external { + _processReport(metaData); + } + + function _processReport(bytes calldata report) internal override { + 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/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..28b0fe6b --- /dev/null +++ b/pkg/pool-quantamm/contracts/rules/CreReceiver.sol @@ -0,0 +1,245 @@ +// 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 ReceiverTemplate - Abstract receiver with optional permission controls +/// @notice Provides flexible, updatable security checks for receiving workflow reports +/// @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 { + // 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(); + } + 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); + } + + // 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); + } + } + } + + _processReport(report); + } + + /// @notice Updates the forwarder address that is allowed to call onReport + /// @param _forwarder The new forwarder address + function setForwarderAddress( + address _forwarder + ) external onlyOwner { + address previousForwarder = s_forwarderAddress; + + if (_forwarder == address(0)) { + revert InvalidForwarderAddress(); + } + + s_forwarderAddress = _forwarder; + emit ForwarderAddressUpdated(previousForwarder, _forwarder); + } + + /// @notice Updates the expected workflow owner address + /// @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); + } + + /// @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) { + revert InvalidWorkflowName(bytes10(0), s_expectedWorkflowName); + } + + // 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 + 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); + } + + /// @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)]; + } + + 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 new file mode 100644 index 00000000..56747d1e --- /dev/null +++ b/pkg/pool-quantamm/contracts/rules/CreUpdateRule.sol @@ -0,0 +1,129 @@ +// 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 { 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 { 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. +/// 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, CreReceiver, ITypeAndVersion, IUpdateRule { + constructor(address updateWeightRunnerAddress) CreReceiver(updateWeightRunnerAddress) { + updateWeightRunner = UpdateWeightRunner(updateWeightRunnerAddress); + } + + UpdateWeightRunner public updateWeightRunner; + + string public constant override typeAndVersion = "CreAuthWrapper 1.0.0"; + + struct WorkflowMetadata { + address poolAddress; + uint256[] weights; + } + + 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); + + /// @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 { + if (updateWeightRunnerAddress == address(0)) { + revert InvalidAddress(updateWeightRunnerAddress); + } + address oldAddress = address(updateWeightRunner); + + updateWeightRunner = UpdateWeightRunner(updateWeightRunnerAddress); + emit UpdateWeightRunnerChanged(updateWeightRunnerAddress, oldAddress, 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 _processReportMemory(bytes memory report) internal virtual { + UpdateWeightRunner.CalculateMuliplierAndSetWeightsLocal memory local; + + WorkflowMetadata memory meta = abi.decode(report, (WorkflowMetadata)); + + //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); + } + + 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(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 guardedWeights = _guardQuantAMMWeights( + updated, + local.currentWeights, + int128(uint128(poolSettings.epsilonMax)), + int128(uint128(poolSettings.absoluteWeightGuardRail)) + ); + + //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, + uint64[] calldata, + uint64, + uint64 + ) 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); + } + + 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 + //all parametisation should be within CRE workflow + } + + 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..e416d816 --- /dev/null +++ b/pkg/pool-quantamm/test/foundry/rules/CreReceiver.t.sol @@ -0,0 +1,503 @@ +// 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 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)); + } + + // ----------------------- + // 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 _invalidForwarderAddress() internal pure returns (bytes4) { + return bytes4(keccak256("InvalidForwarderAddress()")); + } + + 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.getForwarderAddress(), address(this)); + 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)); + } + + function testSetForwarderAddressUpdatesStateAndEmitsEvent() public { + address newForwarder = address(0xF0F0); + address oldForwarder = receiver.getForwarderAddress(); + + vm.expectEmit(true, true, true, false); + emit ForwarderAddressUpdated(oldForwarder, newForwarder); + + receiver.setForwarderAddress(newForwarder); + + assertEq(receiver.getForwarderAddress(), 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.getExpectedAuthor(); + + vm.expectEmit(true, true, true, false); + emit ExpectedAuthorUpdated(oldAuthor, newAuthor); + + receiver.setExpectedAuthor(newAuthor); + + assertEq(receiver.getExpectedAuthor(), 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 = receiver.encodeWorkflowName("WF_LOW_001"); + bytes10 oldName = receiver.getExpectedWorkflowName(); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowNameUpdated(oldName, newName); + + receiver.setExpectedWorkflowName("WF_LOW_001"); + + assertEq(receiver.getExpectedWorkflowName(), newName); + } + + function testSetExpectedWorkflowNameOnlyOwner() public { + address nonOwner = address(0xBEEF); + bytes10 newName = bytes10("WF_LOW_001"); + + vm.prank(nonOwner); + vm.expectRevert(abi.encodeWithSelector(_ownableUnauthorizedAccountSelector(), nonOwner)); + string memory newNameStr = string(abi.encodePacked(newName)); + receiver.setExpectedWorkflowName(newNameStr); + } + + function testSetExpectedWorkflowIdUpdatesStateAndEmitsEvent() public { + bytes32 newId = keccak256("new-workflow-id"); + bytes32 oldId = receiver.getExpectedWorkflowId(); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowIdUpdated(oldId, newId); + + receiver.setExpectedWorkflowId(newId); + + assertEq(receiver.getExpectedWorkflowId(), 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 { + receiver.setExpectedWorkflowName("WF_EXPECT"); + + 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(bytes4(keccak256("WorkflowNameRequiresAuthorValidation()"))) + ); + receiver.onReport(metadata, report); + } + + function testOnReportSucceedsWithCorrectWorkflowName() public { + 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)); + + 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 = receiver.encodeWorkflowName("WF_EXPECT"); + address expectedAuthor = address(0xA1); + + receiver.setExpectedWorkflowId(expectedId); + receiver.setExpectedWorkflowName("WF_EXPECT"); + vm.assertEq(receiver.getExpectedWorkflowName(), 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); + vm.expectRevert(_invalidForwarderAddress()); + receiver.setForwarderAddress(address(0)); + } + + function testDisableExpectedAuthorAllowsAnyAuthor() public { + address expectedAuthor = address(0xA1); + receiver.setExpectedAuthor(expectedAuthor); + + vm.expectRevert(abi.encodeWithSelector(_invalidAuthorSelector(), address(0), expectedAuthor)); + receiver.setExpectedAuthor(address(0)); + } + + function testDisableExpectedWorkflowIdAllowsAnyWorkflowId() public { + bytes32 expectedId = keccak256("expected-id"); + receiver.setExpectedWorkflowId(expectedId); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), bytes32(0), expectedId)); + receiver.setExpectedWorkflowId(bytes32(0)); + } + + function testNameOnlyFailsValidation() public { + receiver.setExpectedWorkflowName("WF_EXPECT"); + + 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(bytes4(keccak256("WorkflowNameRequiresAuthorValidation()"))) + ); + receiver.onReport(badMetadata, report); + } + + function testOnReportIgnoresAuthorWhenExpectationZero() public { + // Expectation is zero by default + assertEq(receiver.getExpectedAuthor(), 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.getExpectedWorkflowId(), 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.getExpectedWorkflowName(), 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); + + receiver.setExpectedWorkflowName("WF_EXPECT"); + + // 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); + + receiver.setExpectedWorkflowName("WF_EXPECT"); + + 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 ForwarderAddressUpdated(forwarder, forwarder); + + receiver.setForwarderAddress(forwarder); + } + + function testSetExpectedAuthorEmitsWhenSettingSameValue() public { + address author = address(0xA1); + receiver.setExpectedAuthor(author); + + vm.expectEmit(true, true, true, false); + emit ExpectedAuthorUpdated(author, author); + + receiver.setExpectedAuthor(author); + } + + function testSetExpectedWorkflowNameEmitsWhenSettingSameValue() public { + bytes10 name = receiver.encodeWorkflowName("WF_EXPECT"); + receiver.setExpectedWorkflowName("WF_EXPECT"); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowNameUpdated(name, name); + + receiver.setExpectedWorkflowName("WF_EXPECT"); + } + + function testSetExpectedWorkflowIdEmitsWhenSettingSameValue() public { + bytes32 id = keccak256("same-id"); + receiver.setExpectedWorkflowId(id); + + vm.expectEmit(true, true, true, false); + 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 new file mode 100644 index 00000000..e91f9b2d --- /dev/null +++ b/pkg/pool-quantamm/test/foundry/rules/CreReceiver_Fuzz.t.sol @@ -0,0 +1,536 @@ +// 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 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)); + } + + // ----------------------- + // Helpers + // ----------------------- + + function _encodeMetadata( + bytes32 workflowId, + bytes10 workflowName, + address workflowOwner + ) internal pure returns (bytes memory) { + 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)")); + } + + 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 { + vm.assume(newForwarder != address(0)); + + address oldForwarder = receiver.getForwarderAddress(); + + vm.expectEmit(true, true, true, false); + emit ForwarderAddressUpdated(oldForwarder, newForwarder); + + receiver.setForwarderAddress(newForwarder); + + assertEq(receiver.getForwarderAddress(), 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 { + vm.assume(newAuthor != address(0)); + + address oldAuthor = receiver.getExpectedAuthor(); + + vm.expectEmit(true, true, true, false); + emit ExpectedAuthorUpdated(oldAuthor, newAuthor); + + receiver.setExpectedAuthor(newAuthor); + + assertEq(receiver.getExpectedAuthor(), 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.getExpectedWorkflowName(); + + string memory newNameStr = string(abi.encodePacked(newName)); + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowNameUpdated(oldName, receiver.encodeWorkflowName(newNameStr)); + + receiver.setExpectedWorkflowName(newNameStr); + + assertEq(receiver.getExpectedWorkflowName(), receiver.encodeWorkflowName(newNameStr)); + } + + function testFuzz_SetExpectedWorkflowNameOnlyOwner(bytes10 newName, address nonOwner) public { + vm.assume(nonOwner != address(this)); + + vm.prank(nonOwner); + vm.expectRevert(abi.encodeWithSelector(_ownableUnauthorizedAccountSelector(), nonOwner)); + + string memory newNameStr = string(abi.encodePacked(newName)); + receiver.setExpectedWorkflowName(newNameStr); + } + + function testFuzz_SetExpectedWorkflowIdUpdatesStateAndEmitsEvent(bytes32 newId) public { + vm.assume(newId != bytes32(0)); + + bytes32 oldId = receiver.getExpectedWorkflowId(); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowIdUpdated(oldId, newId); + + receiver.setExpectedWorkflowId(newId); + + assertEq(receiver.getExpectedWorkflowId(), 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 { + vm.assume(forwarder != address(0)); + + receiver.setForwarderAddress(forwarder); + + vm.expectEmit(true, true, true, false); + emit ForwarderAddressUpdated(forwarder, forwarder); + + receiver.setForwarderAddress(forwarder); + } + + function testFuzz_SetExpectedAuthorEmitsWhenSettingSameValue(address author) public { + vm.assume(author != address(0)); + + receiver.setExpectedAuthor(author); + + vm.expectEmit(true, true, true, false); + emit ExpectedAuthorUpdated(author, author); + + receiver.setExpectedAuthor(author); + } + + 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 ExpectedWorkflowNameUpdated(expectedName, expectedName); + + receiver.setExpectedWorkflowName(nameStr); + } + + function testFuzz_SetExpectedWorkflowIdEmitsWhenSettingSameValue(bytes32 id) public { + vm.assume(id != bytes32(0)); + + receiver.setExpectedWorkflowId(id); + + vm.expectEmit(true, true, true, false); + emit ExpectedWorkflowIdUpdated(id, id); + + 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_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_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_DisableForwarderNotAllowed( + address trustedForwarder + ) public { + vm.assume(trustedForwarder != address(0)); + + receiver.setForwarderAddress(trustedForwarder); + + vm.expectRevert(abi.encodeWithSelector(_invalidForwarderAddress(), address(0), trustedForwarder)); + receiver.setForwarderAddress(address(0)); + } + + function testFuzz_DisableExpectedWorkflowIdNotAllowed( + bytes32 expectedId + ) public { + vm.assume(expectedId != bytes32(0)); + + receiver.setExpectedWorkflowId(expectedId); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowIdSelector(), bytes32(0), expectedId)); + receiver.setExpectedWorkflowId(bytes32(0)); + } + + 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( + bytes10 expectedName, + bytes10 wrongName, + bytes32 workflowId, + address workflowOwner, + bytes memory report + ) 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, encodedWrongName, workflowOwner); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), encodedWrongName, encodedExpectedName)); + receiver.onReport(metadata, report); + } + + function testFuzz_OnReportSucceedsWithCorrectWorkflowName( + bytes10 expectedName, + bytes32 workflowId, + address workflowOwner, + 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); + + bytes10 encodedExpectedName = receiver.encodeWorkflowName(wfNameStr); + + bytes memory metadata = _encodeMetadata(workflowId, encodedExpectedName, 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); + 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 badMetadata = _encodeMetadata(workflowId, encodedWrongName, workflowOwner); + + vm.expectRevert(abi.encodeWithSelector(_invalidWorkflowNameSelector(), encodedWrongName, encodedExpectedName)); + receiver.onReport(badMetadata, report); + + receiver.setExpectedWorkflowName(string(abi.encodePacked(wrongName))); + + receiver.onReport(badMetadata, report); + } + + function testFuzz_OnReportIgnoresAllExpectationsWhenZero( + bytes32 workflowId, + bytes10 workflowName, + address workflowOwner, + bytes memory report + ) public { + assertEq(receiver.getForwarderAddress(), address(this)); + assertEq(receiver.getExpectedAuthor(), address(0)); + assertEq(receiver.getExpectedWorkflowName(), bytes10(0)); + assertEq(receiver.getExpectedWorkflowId(), 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); + string memory wfNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(wfNameStr); + + 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); + string memory wfNameStr = string(abi.encodePacked(expectedName)); + receiver.setExpectedWorkflowName(wfNameStr); + + 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, + address workflowOwner, + bytes memory report, + bytes10 workflowName + ) public { + vm.assume(trustedForwarder != address(0)); + vm.assume(workflowId != bytes32(0)); + 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, expectedName, 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..7dc31a9b --- /dev/null +++ b/pkg/pool-quantamm/test/foundry/rules/CreUpdateRule.t.sol @@ -0,0 +1,480 @@ +// 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 + }) + ); + + 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 + }) + ); + + // 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 + }) + ); + + 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 + }) + ); + + // 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 + }) + ); + + 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 + }) + ); + + 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; + } +}