├── test ├── Tribunal.t.sol ├── mocks │ ├── MockTheCompact.sol │ ├── MockERC20.sol │ ├── FillerContract.sol │ ├── MockPriceCurveLib.sol │ ├── ReentrantReceiver.sol │ ├── MockDispatchTarget.sol │ └── MockAllocator.sol ├── helpers │ └── PriceCurveTestHelper.sol ├── TribunalTypeHashesTest.t.sol ├── TribunalFilledTest.t.sol ├── TribunalBasicTest.t.sol ├── TribunalFinalCoverageGapsTest.t.sol ├── MultipleZeroDurationTest.t.sol ├── TribunalClaimAndFillInvalidAdjusterTest.t.sol ├── TribunalSettleOrRegisterTest.t.sol ├── TribunalReentrancyTest.t.sol ├── PriceCurveDocumentationTests.t.sol ├── TribunalViewFunctionsTest.t.sol ├── TribunalClaimReductionScalingFactorTest.t.sol ├── TribunalDeriveAmountsTest.t.sol ├── TribunalCoverageGapsTest.t.sol └── TribunalFillRevertsTest.t.sol ├── .gitignore ├── .gitmodules ├── foundry.toml ├── src ├── interfaces │ ├── IArbSys.sol │ ├── IDestinationSettler.sol │ ├── ITribunalCallback.sol │ ├── IRecipientCallback.sol │ └── IDispatchCallback.sol ├── BlockNumberish.sol ├── lib │ └── DomainLib.sol ├── ERC7683Tribunal.sol └── types │ ├── TribunalStructs.sol │ └── TribunalTypeHashes.sol ├── .github └── workflows │ └── test.yml ├── .gas-snapshot └── LICENSE /test/Tribunal.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | 6 | // This file has been split into multiple test files. See test/Tribunal*.t.sol for individual tests. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | lcov.info 6 | coverage/ 7 | 8 | # Ignores development broadcast logs 9 | !/broadcast 10 | /broadcast/*/31337/ 11 | /broadcast/**/dry-run/ 12 | 13 | # Dotenv file 14 | .env 15 | 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/the-compact"] 5 | path = lib/the-compact 6 | url = https://github.com/uniswap/the-compact 7 | branch = v1 8 | [submodule "lib/solady"] 9 | path = lib/solady 10 | url = https://github.com/vectorized/solady 11 | -------------------------------------------------------------------------------- /test/mocks/MockTheCompact.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {BatchClaim as CompactBatchClaim} from "the-compact/src/types/BatchClaims.sol"; 5 | 6 | contract MockTheCompact { 7 | function batchClaim(CompactBatchClaim calldata) external pure returns (bytes32) { 8 | return bytes32(uint256(0x5ab5d4a8ba29d5317682f2808ad60826cc75eb191581bea9f13d498a6f8e6311)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | evm_version = "cancun" 6 | optimizer_runs = 4_294_967_295 7 | via_ir = true 8 | bytecode_hash = "none" 9 | 10 | [profile.coverage] 11 | src = "src" 12 | out = "out" 13 | libs = ["lib"] 14 | evm_version = "cancun" 15 | optimizer_runs = 4_294_967_295 16 | via_ir = false 17 | bytecode_hash = "none" 18 | 19 | [fmt] 20 | line_length = 100 21 | 22 | [profile.remappings] 23 | solady = "lib/solady/src" 24 | -------------------------------------------------------------------------------- /test/helpers/PriceCurveTestHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {PriceCurveLib} from "../../src/lib/PriceCurveLib.sol"; 5 | 6 | contract PriceCurveTestHelper { 7 | using PriceCurveLib for uint256[]; 8 | 9 | function getCalculatedValues(uint256[] memory priceCurve, uint256 blocksPassed) 10 | external 11 | pure 12 | returns (uint256) 13 | { 14 | return priceCurve.getCalculatedValues(blocksPassed); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/interfaces/IArbSys.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @notice Minimal interface for interacting with Arbitrum system contracts 6 | * @custom:security-contact security@uniswap.org 7 | */ 8 | interface IArbSys { 9 | /** 10 | * @notice Get Arbitrum block number (distinct from L1 block number; Arbitrum genesis block has block number 0) 11 | * @return block number as int 12 | */ 13 | function arbBlockNumber() external view returns (uint256); 14 | } 15 | -------------------------------------------------------------------------------- /test/mocks/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {ERC20} from "solady/tokens/ERC20.sol"; 5 | 6 | contract MockERC20 is ERC20 { 7 | constructor() { 8 | _mint(msg.sender, 1000000e18); 9 | } 10 | 11 | function name() public pure override returns (string memory) { 12 | return "Mock Token"; 13 | } 14 | 15 | function symbol() public pure override returns (string memory) { 16 | return "MOCK"; 17 | } 18 | 19 | function mint(address to, uint256 amount) external { 20 | _mint(to, amount); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/mocks/FillerContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {ITribunalCallback} from "../../src/interfaces/ITribunalCallback.sol"; 5 | import {Lock} from "the-compact/src/types/EIP712Types.sol"; 6 | import {FillRequirement} from "../../src/types/TribunalStructs.sol"; 7 | 8 | contract FillerContract is ITribunalCallback { 9 | receive() external payable {} 10 | 11 | // Implement ITribunalCallback 12 | function tribunalCallback( 13 | bytes32, 14 | Lock[] calldata, 15 | uint256[] calldata, 16 | FillRequirement[] calldata 17 | ) external { 18 | // Empty implementation - just need to be callable 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/IDestinationSettler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | /// @title IDestinationSettler 5 | /// @custom:security-contact security@uniswap.org 6 | /// @notice Standard interface for settlement contracts on the destination chain 7 | interface IDestinationSettler { 8 | /// @notice Fills a single leg of a particular order on the destination chain 9 | /// @param orderId Unique order identifier for this order 10 | /// @param originData Data emitted on the origin to parameterize the fill 11 | /// @param fillerData Data provided by the filler to inform the fill or express their preferences 12 | function fill(bytes32 orderId, bytes calldata originData, bytes calldata fillerData) 13 | external 14 | payable; 15 | } 16 | -------------------------------------------------------------------------------- /src/BlockNumberish.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {IArbSys} from "./interfaces/IArbSys.sol"; 5 | 6 | /// @title BlockNumberish 7 | /// @custom:security-contact security@uniswap.org 8 | /// A helper contract to get the current block number on different chains 9 | contract BlockNumberish { 10 | uint256 private constant ARB_CHAIN_ID = 42161; 11 | address private constant ARB_SYS_ADDRESS = 0x0000000000000000000000000000000000000064; 12 | 13 | function _getBlockNumberish() internal view returns (uint256) { 14 | // Set the function to use based on chainid 15 | if (block.chainid == ARB_CHAIN_ID) { 16 | return IArbSys(ARB_SYS_ADDRESS).arbBlockNumber(); 17 | } else { 18 | return block.number; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | env: 9 | FOUNDRY_PROFILE: ci 10 | 11 | jobs: 12 | check: 13 | strategy: 14 | fail-fast: true 15 | 16 | name: Foundry project 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | submodules: recursive 22 | 23 | - name: Install Foundry 24 | uses: foundry-rs/foundry-toolchain@v1 25 | with: 26 | version: v1.4.0 27 | 28 | - name: Show Forge version 29 | run: | 30 | forge --version 31 | 32 | - name: Run Forge fmt 33 | run: | 34 | forge fmt --check 35 | id: fmt 36 | 37 | - name: Run Forge build 38 | run: | 39 | forge build --sizes 40 | id: build 41 | 42 | - name: Run Forge tests 43 | run: | 44 | forge test -vvv 45 | id: test 46 | -------------------------------------------------------------------------------- /.gas-snapshot: -------------------------------------------------------------------------------- 1 | TribunalTest:test_DeriveAmounts_ExactIn() (gas: 11053) 2 | TribunalTest:test_DeriveAmounts_ExactOut() (gas: 11391) 3 | TribunalTest:test_DeriveAmounts_ExtremePriorityFee() (gas: 10788) 4 | TribunalTest:test_DeriveAmounts_NoPriorityFee() (gas: 10644) 5 | TribunalTest:test_DeriveAmounts_RealisticExactIn() (gas: 11206) 6 | TribunalTest:test_DeriveAmounts_RealisticExactOut() (gas: 11083) 7 | TribunalTest:test_DeriveClaimHash() (gas: 14010) 8 | TribunalTest:test_DeriveMandateHash() (gas: 10748) 9 | TribunalTest:test_DeriveMandateHash_DifferentSalt() (gas: 10830) 10 | TribunalTest:test_DispositionReturnsTrueForUsedClaim() (gas: 44305) 11 | TribunalTest:test_GetCompactWitnessDetails() (gas: 10727) 12 | TribunalTest:test_Name() (gas: 9641) 13 | TribunalTest:test_PetitionRevertsOnExpiredMandate() (gas: 12702) 14 | TribunalTest:test_PetitionRevertsOnInvalidGasPrice() (gas: 36521) 15 | TribunalTest:test_PetitionRevertsOnReusedClaim() (gas: 42448) 16 | TribunalTest:test_PetitionSettlesERC20Token() (gas: 99903) 17 | TribunalTest:test_PetitionSettlesNativeToken() (gas: 85008) 18 | TribunalTest:test_Quote() (gas: 12175) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Uniswap Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/interfaces/ITribunalCallback.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.27; 3 | 4 | import {Lock} from "the-compact/src/types/EIP712Types.sol"; 5 | import {FillRequirement} from "../types/TribunalStructs.sol"; 6 | 7 | /** 8 | * @title ITribunalCallback 9 | * @custom:security-contact security@uniswap.org 10 | * @notice Interface for filler contracts that can receive callbacks from Tribunal during single-chain fills. 11 | * @dev Called after claiming tokens from The Compact but before transferring fill tokens to the recipient and 12 | * potentially triggering a recipient callback. 13 | */ 14 | interface ITribunalCallback { 15 | /** 16 | * @notice Callback function to be called by the Tribunal contract to the filler. 17 | * @dev At this point, the filler has already received the claimed tokens and must 18 | * supply the actual fill amounts of each fill token to Tribunal before exiting the callback. 19 | * @param claimHash The claim hash of the associated compact that has been claimed. 20 | * @param commitments The resource lock commitments provided by the sponsor. 21 | * @param claimedAmounts The actual amounts claimed to complete the fill. 22 | * @param fillRequirements Array of fill requirements specifying tokens, minimum amounts, and realized amounts. 23 | */ 24 | function tribunalCallback( 25 | bytes32 claimHash, 26 | Lock[] calldata commitments, 27 | uint256[] calldata claimedAmounts, 28 | FillRequirement[] calldata fillRequirements 29 | ) external; 30 | } 31 | -------------------------------------------------------------------------------- /src/interfaces/IRecipientCallback.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.27; 3 | 4 | import {BatchCompact} from "../types/TribunalStructs.sol"; 5 | 6 | /** 7 | * @title IRecipientCallback 8 | * @custom:security-contact security@uniswap.org 9 | * @notice Interface for contracts that can receive callbacks from Tribunal after same-chain fills. 10 | * @dev Implementers must return the correct function selector to confirm successful execution. 11 | */ 12 | interface IRecipientCallback { 13 | /** 14 | * @notice Callback function to be called by the Tribunal contract to the recipient. 15 | * @param chainId The target chain ID communicated as part of the callback. 16 | * @param sourceClaimHash The claim hash of the compact that has been claimed. 17 | * @param sourceMandateHash The mandate hash of the compact that has been claimed. 18 | * @param fillToken The reward token of the fill. 19 | * @param fillAmount The actual fill amount that is provided. 20 | * @param targetCompact The new compact associated with the callback. 21 | * @param targetMandateHash The mandate hash associated with the new compact. 22 | * @param context Arbitrary context associated with the callback. 23 | * @return This function selector to confirm successful execution. 24 | */ 25 | function tribunalCallback( 26 | uint256 chainId, 27 | bytes32 sourceClaimHash, 28 | bytes32 sourceMandateHash, 29 | address fillToken, 30 | uint256 fillAmount, 31 | BatchCompact calldata targetCompact, 32 | bytes32 targetMandateHash, 33 | bytes calldata context 34 | ) external returns (bytes4); 35 | } 36 | -------------------------------------------------------------------------------- /src/interfaces/IDispatchCallback.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.27; 3 | 4 | import {BatchCompact} from "the-compact/src/types/EIP712Types.sol"; 5 | 6 | /** 7 | * @title IDispatchCallback 8 | * @custom:security-contact security@uniswap.org 9 | * @notice Interface for contracts that can receive dispatch callbacks from Tribunal after fills. 10 | * @dev Implementers must return the correct function selector to confirm successful execution. 11 | */ 12 | interface IDispatchCallback { 13 | /** 14 | * @notice Callback function to be called by the Tribunal contract after a fill is completed. 15 | * @param chainId The chain ID the dispatch callback is intended to interact with. 16 | * @param compact The compact parameters from the fill. 17 | * @param mandateHash The mandate hash from the fill. 18 | * @param claimHash The claim hash of the compact that can be claimed on performing the fill. 19 | * @param claimant The bytes32 value representing the claimant (lock tag ++ address). 20 | * @param claimReductionScalingFactor The scaling factor applied to claim amounts (1e18 if no reduction). 21 | * @param claimAmounts The actual amounts of tokens claimed (after any reductions). 22 | * @param context Arbitrary context data provided by the filler. 23 | * @return This function selector to confirm successful execution. 24 | */ 25 | function dispatchCallback( 26 | uint256 chainId, 27 | BatchCompact calldata compact, 28 | bytes32 mandateHash, 29 | bytes32 claimHash, 30 | bytes32 claimant, 31 | uint256 claimReductionScalingFactor, 32 | uint256[] calldata claimAmounts, 33 | bytes calldata context 34 | ) external payable returns (bytes4); 35 | } 36 | -------------------------------------------------------------------------------- /test/mocks/MockPriceCurveLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {PriceCurveLib, PriceCurveElement} from "../../src/lib/PriceCurveLib.sol"; 5 | 6 | /** 7 | * @title MockPriceCurveLib 8 | * @dev Mock contract to expose internal PriceCurveLib functions for testing, 9 | * particularly the memory version of applySupplementalPriceCurve 10 | */ 11 | contract MockPriceCurveLib { 12 | using PriceCurveLib for uint256[]; 13 | 14 | /** 15 | * @dev Expose the memory version of applySupplementalPriceCurve for testing 16 | */ 17 | function applyMemorySupplementalPriceCurve( 18 | uint256[] memory parameters, 19 | uint256[] memory supplementalParameters 20 | ) external pure returns (uint256[] memory) { 21 | return parameters.applyMemorySupplementalPriceCurve(supplementalParameters); 22 | } 23 | 24 | /** 25 | * @dev Expose the calldata version of applySupplementalPriceCurve for testing 26 | */ 27 | function applySupplementalPriceCurve( 28 | uint256[] calldata parameters, 29 | uint256[] calldata supplementalParameters 30 | ) external pure returns (uint256[] memory) { 31 | return parameters.applySupplementalPriceCurve(supplementalParameters); 32 | } 33 | 34 | /** 35 | * @dev Expose getCalculatedValues for testing 36 | */ 37 | function getCalculatedValues(uint256[] memory parameters, uint256 blocksPassed) 38 | external 39 | pure 40 | returns (uint256) 41 | { 42 | return parameters.getCalculatedValues(blocksPassed); 43 | } 44 | 45 | /** 46 | * @dev Expose sharesScalingDirection for testing 47 | */ 48 | function sharesScalingDirection(uint256 a, uint256 b) external pure returns (bool) { 49 | return PriceCurveLib.sharesScalingDirection(a, b); 50 | } 51 | 52 | /** 53 | * @dev Expose create function for testing 54 | */ 55 | function create(uint16 blockDuration, uint240 scalingFactor) 56 | external 57 | pure 58 | returns (PriceCurveElement) 59 | { 60 | return PriceCurveLib.create(blockDuration, scalingFactor); 61 | } 62 | 63 | /** 64 | * @dev Expose getComponents for testing 65 | */ 66 | function getComponents(PriceCurveElement element) 67 | external 68 | pure 69 | returns (uint256 blockDuration, uint256 scalingFactor) 70 | { 71 | return PriceCurveLib.getComponents(element); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/mocks/ReentrantReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Tribunal} from "../../src/Tribunal.sol"; 5 | import {ITribunal} from "../../src/interfaces/ITribunal.sol"; 6 | import { 7 | Mandate, 8 | FillParameters, 9 | FillComponent, 10 | RecipientCallback, 11 | Adjustment, 12 | BatchClaim 13 | } from "../../src/types/TribunalStructs.sol"; 14 | import {BatchCompact, Lock} from "the-compact/src/types/EIP712Types.sol"; 15 | 16 | contract ReentrantReceiver { 17 | error NoProfit(uint256 balanceBefore, uint256 balanceAfter); 18 | 19 | Tribunal private immutable _TRIBUNAL; 20 | BatchClaim private _claim; 21 | FillParameters private _fillParams; 22 | 23 | constructor(Tribunal _tribunal) payable { 24 | _TRIBUNAL = _tribunal; 25 | 26 | // Initialize storage variables field by field to avoid memory-to-storage copy issues 27 | _claim.compact.arbiter = address(this); 28 | _claim.compact.sponsor = address(this); 29 | _claim.compact.nonce = 0; 30 | _claim.compact.expires = type(uint32).max; 31 | // commitments array is already empty by default 32 | 33 | // Initialize _fillParams fields directly 34 | _fillParams.chainId = block.chainid; 35 | _fillParams.tribunal = address(_TRIBUNAL); 36 | _fillParams.expires = type(uint32).max; 37 | _fillParams.baselinePriorityFee = 0; 38 | _fillParams.scalingFactor = 1e18; 39 | _fillParams.salt = bytes32(uint256(1)); 40 | 41 | // Initialize components array by pushing to it 42 | _fillParams.components 43 | .push( 44 | FillComponent({ 45 | fillToken: address(0), 46 | minimumFillAmount: 0, 47 | recipient: address(this), 48 | applyScaling: true 49 | }) 50 | ); 51 | } 52 | 53 | receive() external payable { 54 | Adjustment memory adjustment = Adjustment({ 55 | adjuster: address(this), 56 | fillIndex: 0, 57 | targetBlock: block.number, 58 | supplementalPriceCurve: new uint256[](0), 59 | validityConditions: bytes32(0), 60 | adjustmentAuthorization: new bytes(0) 61 | }); 62 | 63 | bytes32[] memory fillHashes = new bytes32[](1); 64 | fillHashes[0] = _TRIBUNAL.deriveFillHash(_fillParams); 65 | 66 | uint256 balanceBefore = address(this).balance; 67 | try _TRIBUNAL.fill( 68 | _claim.compact, 69 | _fillParams, 70 | adjustment, 71 | fillHashes, 72 | bytes32(uint256(uint160(address(this)))), 73 | 0 74 | ) { 75 | if (address(this).balance < balanceBefore) { 76 | revert NoProfit(balanceBefore, address(this).balance); 77 | } 78 | _claim.compact.nonce++; 79 | } catch {} 80 | } 81 | 82 | function getMandate() public view returns (FillParameters memory) { 83 | return _fillParams; 84 | } 85 | 86 | function getClaim() public view returns (BatchClaim memory) { 87 | return _claim; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/lib/DomainLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.27; 3 | 4 | /** 5 | * @title DomainLib 6 | * @custom:security-contact security@uniswap.org 7 | * @notice Library contract implementing logic for deriving domain hashes. 8 | */ 9 | library DomainLib { 10 | /// @dev `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`. 11 | bytes32 internal constant _DOMAIN_TYPEHASH = 12 | 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; 13 | 14 | /// @dev `keccak256(bytes("Tribunal"))`. 15 | bytes32 internal constant _NAME_HASH = 16 | 0x0e2a7404936dd29a4a3b49dad6c2f86f8e2da9cf7cf60ef9518bb049b4cb9b44; 17 | 18 | /// @dev `keccak256("1")`. 19 | bytes32 internal constant _VERSION_HASH = 20 | 0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6; 21 | 22 | /** 23 | * @notice Internal view function that returns the current domain separator, deriving a new one 24 | * if the chain ID has changed from the initial chain ID. 25 | * @param initialDomainSeparator The domain separator derived at deployment time. 26 | * @param initialChainId The chain ID at the time of deployment. 27 | * @return domainSeparator The current domain separator. 28 | */ 29 | function toLatest(bytes32 initialDomainSeparator, uint256 initialChainId) 30 | internal 31 | view 32 | returns (bytes32 domainSeparator) 33 | { 34 | // Set the initial domain separator as the default domain separator. 35 | domainSeparator = initialDomainSeparator; 36 | 37 | assembly ("memory-safe") { 38 | // Derive domain separator again if initial chain ID differs from current one. 39 | if xor(chainid(), initialChainId) { 40 | // Retrieve the free memory pointer. 41 | let m := mload(0x40) 42 | 43 | // Prepare domain data: EIP-712 typehash, name hash, version hash, chain ID, & verifying contract. 44 | mstore(m, _DOMAIN_TYPEHASH) 45 | mstore(add(m, 0x20), _NAME_HASH) 46 | mstore(add(m, 0x40), _VERSION_HASH) 47 | mstore(add(m, 0x60), chainid()) 48 | mstore(add(m, 0x80), address()) 49 | 50 | // Derive the domain separator. 51 | domainSeparator := keccak256(m, 0xa0) 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * @notice Internal view function that derives a domain separator for the current chain ID. 58 | * @return domainSeparator The current domain separator. 59 | */ 60 | function toCurrentDomainSeparator() internal view returns (bytes32 domainSeparator) { 61 | assembly ("memory-safe") { 62 | // Retrieve the free memory pointer. 63 | let m := mload(0x40) 64 | 65 | // Prepare domain data: EIP-712 typehash, name hash, version hash, chain ID, and verifying contract. 66 | mstore(m, _DOMAIN_TYPEHASH) 67 | mstore(add(m, 0x20), _NAME_HASH) 68 | mstore(add(m, 0x40), _VERSION_HASH) 69 | mstore(add(m, 0x60), chainid()) 70 | mstore(add(m, 0x80), address()) 71 | 72 | // Derive the domain separator. 73 | domainSeparator := keccak256(m, 0xa0) 74 | } 75 | } 76 | 77 | /** 78 | * @notice Internal pure function that combines a message hash with a domain separator 79 | * to create a domain-specific hash according to EIP-712. 80 | * @param messageHash The EIP-712 hash of the message data. 81 | * @param domainSeparator The domain separator to combine with the message hash. 82 | * @return domainHash The domain-specific hash. 83 | */ 84 | function withDomain(bytes32 messageHash, bytes32 domainSeparator) 85 | internal 86 | pure 87 | returns (bytes32 domainHash) 88 | { 89 | assembly ("memory-safe") { 90 | // Retrieve and cache the free memory pointer. 91 | let m := mload(0x40) 92 | 93 | // Prepare the 712 prefix. 94 | mstore(0, 0x1901) 95 | 96 | // Prepare the domain separator. 97 | mstore(0x20, domainSeparator) 98 | 99 | // Prepare the message hash and compute the domain hash. 100 | mstore(0x40, messageHash) 101 | domainHash := keccak256(0x1e, 0x42) 102 | 103 | // Restore the free memory pointer. 104 | mstore(0x40, m) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/TribunalTypeHashesTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import "../src/types/TribunalTypeHashes.sol"; 6 | 7 | contract TribunalTypeHashesTest is Test { 8 | function test_MandateTypeHashMatchesKeccak() public pure { 9 | bytes32 computed = keccak256(bytes(MANDATE_TYPESTRING)); 10 | assertEq( 11 | computed, 12 | MANDATE_TYPEHASH, 13 | "MANDATE_TYPEHASH constant should match keccak256 of MANDATE_TYPESTRING" 14 | ); 15 | } 16 | 17 | function test_MandateFillTypeHashMatchesKeccak() public pure { 18 | bytes32 computed = keccak256(bytes(MANDATE_FILL_TYPESTRING)); 19 | assertEq( 20 | computed, 21 | MANDATE_FILL_TYPEHASH, 22 | "MANDATE_FILL_TYPEHASH constant should match keccak256 of MANDATE_FILL_TYPESTRING" 23 | ); 24 | } 25 | 26 | function test_MandateFillComponentTypeHashMatchesKeccak() public pure { 27 | bytes32 computed = keccak256(bytes(MANDATE_FILL_COMPONENT_TYPESTRING)); 28 | assertEq( 29 | computed, 30 | MANDATE_FILL_COMPONENT_TYPEHASH, 31 | "MANDATE_FILL_COMPONENT_TYPEHASH constant should match keccak256 of MANDATE_FILL_COMPONENT_TYPESTRING" 32 | ); 33 | } 34 | 35 | function test_MandateRecipientCallbackTypeHashMatchesKeccak() public pure { 36 | bytes32 computed = keccak256(bytes(MANDATE_RECIPIENT_CALLBACK_TYPESTRING)); 37 | assertEq( 38 | computed, 39 | MANDATE_RECIPIENT_CALLBACK_TYPEHASH, 40 | "MANDATE_RECIPIENT_CALLBACK_TYPEHASH constant should match keccak256 of MANDATE_RECIPIENT_CALLBACK_TYPESTRING" 41 | ); 42 | } 43 | 44 | function test_MandateBatchCompactTypeHashMatchesKeccak() public pure { 45 | bytes32 computed = keccak256(bytes(MANDATE_BATCH_COMPACT_TYPESTRING)); 46 | assertEq( 47 | computed, 48 | MANDATE_BATCH_COMPACT_TYPEHASH, 49 | "MANDATE_BATCH_COMPACT_TYPEHASH constant should match keccak256 of MANDATE_BATCH_COMPACT_TYPESTRING" 50 | ); 51 | } 52 | 53 | function test_MandateLockTypeHashMatchesKeccak() public pure { 54 | bytes32 computed = keccak256(bytes(MANDATE_LOCK_TYPESTRING)); 55 | assertEq( 56 | computed, 57 | MANDATE_LOCK_TYPEHASH, 58 | "MANDATE_LOCK_TYPEHASH constant should match keccak256 of MANDATE_LOCK_TYPESTRING" 59 | ); 60 | } 61 | 62 | function test_CompactWithMandateTypeHashMatchesKeccak() public pure { 63 | bytes32 computed = keccak256(bytes(COMPACT_WITH_MANDATE_TYPESTRING)); 64 | assertEq( 65 | computed, 66 | COMPACT_TYPEHASH_WITH_MANDATE, 67 | "COMPACT_TYPEHASH_WITH_MANDATE constant should match keccak256 of COMPACT_WITH_MANDATE_TYPESTRING" 68 | ); 69 | } 70 | 71 | function test_AdjustmentTypeHashMatchesKeccak() public pure { 72 | bytes32 computed = keccak256(bytes(ADJUSTMENT_TYPESTRING)); 73 | assertEq( 74 | computed, 75 | ADJUSTMENT_TYPEHASH, 76 | "ADJUSTMENT_TYPEHASH constant should match keccak256 of ADJUSTMENT_TYPESTRING" 77 | ); 78 | } 79 | 80 | function test_TypeStringFormats() public pure { 81 | // Verify type strings are non-empty 82 | assertTrue(bytes(MANDATE_TYPESTRING).length > 0, "MANDATE_TYPESTRING should not be empty"); 83 | assertTrue( 84 | bytes(MANDATE_FILL_TYPESTRING).length > 0, "MANDATE_FILL_TYPESTRING should not be empty" 85 | ); 86 | assertTrue( 87 | bytes(MANDATE_RECIPIENT_CALLBACK_TYPESTRING).length > 0, 88 | "MANDATE_RECIPIENT_CALLBACK_TYPESTRING should not be empty" 89 | ); 90 | assertTrue( 91 | bytes(MANDATE_BATCH_COMPACT_TYPESTRING).length > 0, 92 | "MANDATE_BATCH_COMPACT_TYPESTRING should not be empty" 93 | ); 94 | assertTrue( 95 | bytes(MANDATE_LOCK_TYPESTRING).length > 0, "MANDATE_LOCK_TYPESTRING should not be empty" 96 | ); 97 | assertTrue( 98 | bytes(COMPACT_WITH_MANDATE_TYPESTRING).length > 0, 99 | "COMPACT_WITH_MANDATE_TYPESTRING should not be empty" 100 | ); 101 | assertTrue( 102 | bytes(ADJUSTMENT_TYPESTRING).length > 0, "ADJUSTMENT_TYPESTRING should not be empty" 103 | ); 104 | assertTrue(bytes(WITNESS_TYPESTRING).length > 0, "WITNESS_TYPESTRING should not be empty"); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/mocks/MockDispatchTarget.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.27; 3 | 4 | import {BatchCompact} from "the-compact/src/types/EIP712Types.sol"; 5 | import {IDispatchCallback} from "../../src/interfaces/IDispatchCallback.sol"; 6 | 7 | contract MockDispatchTarget is IDispatchCallback { 8 | enum Mode { 9 | Success, 10 | Revert, 11 | WrongSelector 12 | } 13 | 14 | Mode public mode; 15 | 16 | // Expected values for validation 17 | uint256 public expectedChainId; 18 | BatchCompact public expectedCompact; 19 | bytes32 public expectedMandateHash; 20 | bytes32 public expectedClaimHash; 21 | bytes32 public expectedClaimant; 22 | uint256 public expectedClaimReductionScalingFactor; 23 | uint256[] public expectedClaimAmounts; 24 | bytes public expectedContext; 25 | 26 | // Received values 27 | uint256 public receivedChainId; 28 | BatchCompact public receivedCompact; 29 | bytes32 public receivedMandateHash; 30 | bytes32 public receivedClaimHash; 31 | bytes32 public receivedClaimant; 32 | uint256 public receivedClaimReductionScalingFactor; 33 | uint256[] public receivedClaimAmounts; 34 | bytes public receivedContext; 35 | uint256 public receivedValue; 36 | 37 | // Track if callback was called 38 | bool public callbackCalled; 39 | 40 | function setMode(Mode _mode) external { 41 | mode = _mode; 42 | } 43 | 44 | function setExpectedValues( 45 | uint256 _chainId, 46 | BatchCompact calldata _compact, 47 | bytes32 _mandateHash, 48 | bytes32 _claimHash, 49 | bytes32 _claimant, 50 | uint256 _claimReductionScalingFactor, 51 | uint256[] calldata _claimAmounts, 52 | bytes calldata _context 53 | ) external { 54 | expectedChainId = _chainId; 55 | expectedCompact = _compact; 56 | expectedMandateHash = _mandateHash; 57 | expectedClaimHash = _claimHash; 58 | expectedClaimant = _claimant; 59 | expectedClaimReductionScalingFactor = _claimReductionScalingFactor; 60 | delete expectedClaimAmounts; 61 | for (uint256 i = 0; i < _claimAmounts.length; i++) { 62 | expectedClaimAmounts.push(_claimAmounts[i]); 63 | } 64 | expectedContext = _context; 65 | } 66 | 67 | function dispatchCallback( 68 | uint256 chainId, 69 | BatchCompact calldata compact, 70 | bytes32 mandateHash, 71 | bytes32 claimHash, 72 | bytes32 claimant, 73 | uint256 claimReductionScalingFactor, 74 | uint256[] calldata claimAmounts, 75 | bytes calldata context 76 | ) external payable returns (bytes4) { 77 | callbackCalled = true; 78 | receivedValue = msg.value; 79 | 80 | // Store received values 81 | receivedChainId = chainId; 82 | receivedCompact = compact; 83 | receivedMandateHash = mandateHash; 84 | receivedClaimHash = claimHash; 85 | receivedClaimant = claimant; 86 | receivedClaimReductionScalingFactor = claimReductionScalingFactor; 87 | delete receivedClaimAmounts; 88 | for (uint256 i = 0; i < claimAmounts.length; i++) { 89 | receivedClaimAmounts.push(claimAmounts[i]); 90 | } 91 | receivedContext = context; 92 | 93 | // Validate received values match expected (only if expected values were set) 94 | if (expectedChainId != 0) { 95 | require(chainId == expectedChainId, "ChainId mismatch"); 96 | require(compact.sponsor == expectedCompact.sponsor, "Sponsor mismatch"); 97 | require(compact.nonce == expectedCompact.nonce, "Nonce mismatch"); 98 | require(compact.expires == expectedCompact.expires, "Expires mismatch"); 99 | require(compact.arbiter == expectedCompact.arbiter, "Arbiter mismatch"); 100 | require( 101 | compact.commitments.length == expectedCompact.commitments.length, 102 | "Commitments length mismatch" 103 | ); 104 | require(mandateHash == expectedMandateHash, "MandateHash mismatch"); 105 | require(claimHash == expectedClaimHash, "ClaimHash mismatch"); 106 | require(claimant == expectedClaimant, "Claimant mismatch"); 107 | require( 108 | claimReductionScalingFactor == expectedClaimReductionScalingFactor, 109 | "ClaimReductionScalingFactor mismatch" 110 | ); 111 | require( 112 | claimAmounts.length == expectedClaimAmounts.length, "ClaimAmounts length mismatch" 113 | ); 114 | for (uint256 i = 0; i < claimAmounts.length; i++) { 115 | require(claimAmounts[i] == expectedClaimAmounts[i], "ClaimAmount mismatch"); 116 | } 117 | require(keccak256(context) == keccak256(expectedContext), "Context mismatch"); 118 | } 119 | 120 | if (mode == Mode.Revert) { 121 | revert("MockDispatchTarget: forced revert"); 122 | } else if (mode == Mode.WrongSelector) { 123 | return bytes4(0xdeadbeef); 124 | } 125 | 126 | return IDispatchCallback.dispatchCallback.selector; 127 | } 128 | 129 | function reset() external { 130 | callbackCalled = false; 131 | receivedValue = 0; 132 | delete receivedClaimAmounts; 133 | delete expectedClaimAmounts; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/TribunalFilledTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Tribunal} from "../src/Tribunal.sol"; 6 | import {ITribunal} from "../src/interfaces/ITribunal.sol"; 7 | import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; 8 | import { 9 | Mandate, 10 | FillParameters, 11 | FillComponent, 12 | Adjustment, 13 | RecipientCallback, 14 | FillRecipient, 15 | BatchClaim 16 | } from "../src/types/TribunalStructs.sol"; 17 | import {BatchCompact, Lock} from "the-compact/src/types/EIP712Types.sol"; 18 | import {MANDATE_TYPEHASH} from "../src/types/TribunalTypeHashes.sol"; 19 | 20 | contract TribunalFilledTest is Test { 21 | using FixedPointMathLib for uint256; 22 | 23 | struct ClaimAndFillArgs { 24 | BatchCompact compact; 25 | FillParameters fill; 26 | Adjustment adjustment; 27 | bytes32[] fillHashes; 28 | bytes32 claimant; 29 | uint256 value; 30 | } 31 | 32 | Tribunal public tribunal; 33 | address theCompact; 34 | address sponsor; 35 | address adjuster; 36 | uint256 adjusterPrivateKey; 37 | 38 | uint256[] public emptyPriceCurve; 39 | 40 | receive() external payable {} 41 | 42 | function setUp() public { 43 | theCompact = address(0xC0); 44 | tribunal = new Tribunal(); 45 | (sponsor,) = makeAddrAndKey("sponsor"); 46 | (adjuster, adjusterPrivateKey) = makeAddrAndKey("adjuster"); 47 | 48 | emptyPriceCurve = new uint256[](0); 49 | } 50 | 51 | function test_FilledReturnsTrueForUsedClaim() public { 52 | ClaimAndFillArgs memory args; 53 | args.claimant = bytes32(uint256(uint160(address(this)))); 54 | args.value = 0; 55 | 56 | bytes32 actualClaimHash; 57 | uint256[] memory claimAmounts = new uint256[](1); 58 | 59 | // Block 1: Create fill parameters 60 | { 61 | FillComponent[] memory components = new FillComponent[](1); 62 | components[0] = FillComponent({ 63 | fillToken: address(0), 64 | minimumFillAmount: 1 ether, 65 | recipient: address(0xCAFE), 66 | applyScaling: true 67 | }); 68 | 69 | args.fill = FillParameters({ 70 | chainId: block.chainid, 71 | tribunal: address(tribunal), 72 | expires: 1703116800, 73 | components: components, 74 | baselinePriorityFee: 100 wei, 75 | scalingFactor: 1e18, 76 | priceCurve: emptyPriceCurve, 77 | recipientCallback: new RecipientCallback[](0), 78 | salt: bytes32(uint256(1)) 79 | }); 80 | } 81 | 82 | // Block 2: Create claim and fillHashes 83 | { 84 | Lock[] memory commitments = new Lock[](1); 85 | commitments[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 86 | 87 | args.compact = BatchCompact({ 88 | arbiter: address(this), 89 | sponsor: sponsor, 90 | nonce: 0, 91 | expires: block.timestamp + 1 hours, 92 | commitments: commitments 93 | }); 94 | 95 | args.fillHashes = new bytes32[](1); 96 | args.fillHashes[0] = tribunal.deriveFillHash(args.fill); 97 | 98 | // Build a Mandate struct to compute the hash properly 99 | Mandate memory mandateStruct = 100 | Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 101 | mandateStruct.fills[0] = args.fill; 102 | 103 | bytes32 mandateHash = tribunal.deriveMandateHash(mandateStruct); 104 | bytes32 claimHash = tribunal.deriveClaimHash(args.compact, mandateHash); 105 | assertEq(tribunal.filled(claimHash), bytes32(0)); 106 | 107 | claimAmounts[0] = commitments[0].amount; 108 | 109 | // The actual claimHash will be computed in _fill using the mandateHash from _deriveMandateHash 110 | bytes32 actualMandateHash = keccak256( 111 | abi.encode(MANDATE_TYPEHASH, adjuster, keccak256(abi.encodePacked(args.fillHashes))) 112 | ); 113 | actualClaimHash = tribunal.deriveClaimHash(args.compact, actualMandateHash); 114 | } 115 | 116 | // Block 3: Create and sign adjustment 117 | { 118 | args.adjustment = Adjustment({ 119 | adjuster: adjuster, 120 | fillIndex: 0, 121 | targetBlock: vm.getBlockNumber(), 122 | supplementalPriceCurve: new uint256[](0), 123 | validityConditions: bytes32(0), 124 | adjustmentAuthorization: "" 125 | }); 126 | 127 | bytes32 adjustmentHash = keccak256( 128 | abi.encode( 129 | keccak256( 130 | "Adjustment(bytes32 claimHash,uint256 fillIndex,uint256 targetBlock,uint256[] supplementalPriceCurve,bytes32 validityConditions)" 131 | ), 132 | actualClaimHash, 133 | args.adjustment.fillIndex, 134 | args.adjustment.targetBlock, 135 | keccak256(abi.encodePacked(args.adjustment.supplementalPriceCurve)), 136 | args.adjustment.validityConditions 137 | ) 138 | ); 139 | 140 | bytes32 domainSeparator = keccak256( 141 | abi.encode( 142 | keccak256( 143 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 144 | ), 145 | keccak256("Tribunal"), 146 | keccak256("1"), 147 | block.chainid, 148 | address(tribunal) 149 | ) 150 | ); 151 | 152 | bytes32 digest = 153 | keccak256(abi.encodePacked("\x19\x01", domainSeparator, adjustmentHash)); 154 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(adjusterPrivateKey, digest); 155 | args.adjustment.adjustmentAuthorization = abi.encodePacked(r, s, v); 156 | } 157 | 158 | FillRecipient[] memory fillRecipients = new FillRecipient[](1); 159 | fillRecipients[0] = FillRecipient({fillAmount: 1 ether, recipient: address(0xCAFE)}); 160 | 161 | // Expect Fill event for cross-chain fills 162 | vm.expectEmit(true, true, true, true, address(tribunal)); 163 | emit ITribunal.Fill( 164 | sponsor, 165 | args.claimant, 166 | actualClaimHash, 167 | fillRecipients, 168 | claimAmounts, 169 | args.adjustment.targetBlock 170 | ); 171 | 172 | tribunal.fill{ 173 | value: 1 ether 174 | }(args.compact, args.fill, args.adjustment, args.fillHashes, args.claimant, args.value); 175 | assertEq(tribunal.filled(actualClaimHash), args.claimant); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/ERC7683Tribunal.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {LibBytes} from "solady/utils/LibBytes.sol"; 5 | 6 | import {IDestinationSettler} from "./interfaces/IDestinationSettler.sol"; 7 | import {Tribunal} from "./Tribunal.sol"; 8 | import {FillParameters, Adjustment, BatchClaim} from "./types/TribunalStructs.sol"; 9 | import {BatchCompact} from "the-compact/src/types/EIP712Types.sol"; 10 | 11 | /// @title ERC7683Tribunal 12 | /// @custom:security-contact security@uniswap.org 13 | /// @notice A contract that enables the tribunal compatibility with the ERC7683 destination settler interface. 14 | /// @dev IMPORTANT NOTE: this contract (specifically the low-level decoding) is probably broken at the moment! 15 | contract ERC7683Tribunal is Tribunal, IDestinationSettler { 16 | // ======== Constructor ======== 17 | constructor() Tribunal() {} 18 | 19 | // ======== External Functions ======== 20 | /** 21 | * @notice Attempt to fill a cross-chain swap using ERC7683 interface. 22 | * @dev Unused initial parameter included for EIP7683 interface compatibility. 23 | * @param originData The encoded Claim and Mandate data. 24 | * @param fillerData The encoded claimant address. 25 | */ 26 | function fill(bytes32, bytes calldata originData, bytes calldata fillerData) 27 | external 28 | payable 29 | nonReentrant 30 | { 31 | ( 32 | BatchClaim calldata claim, 33 | FillParameters calldata mandate, 34 | bytes32[] calldata fillHashes, 35 | Adjustment calldata adjustment, 36 | bytes32 claimant, 37 | uint256 fillBlock 38 | ) = _parseCalldata(originData, fillerData); 39 | 40 | _fill( 41 | claim.compact, mandate, adjustment, claimant, _validateFillBlock(fillBlock), fillHashes 42 | ); 43 | } 44 | 45 | /** 46 | * @notice Encode the filler data for the fill function. 47 | * @param adjustment The adjustment struct, including adjuster, adjustments, and authorization. 48 | * @param claimant The claimant address that will receive the reward tokens. The first 12 bytes will be as the Lock tag for retrieval instructions. 49 | * @param fillBlock The fill block at which the filler should be executed. 50 | * @return fillerData The filler data. 51 | */ 52 | function getFillerData(Adjustment calldata adjustment, bytes32 claimant, uint256 fillBlock) 53 | external 54 | pure 55 | returns (bytes memory fillerData) 56 | { 57 | fillerData = abi.encode(adjustment, claimant, fillBlock); 58 | } 59 | 60 | /** 61 | * @notice Parses the calldata to extract the necessary parameters without copying to memory. 62 | * @param originData The encoded Claim and Mandate data. 63 | * @param fillerData The encoded claimant address. 64 | * @return claim The Claim struct. 65 | * @return mandate The Mandate struct. 66 | * @return fillHashes The fillHashes array. 67 | * @return adjustment The Adjustment struct (includes adjuster and adjustmentAuthorization). 68 | * @return claimant The claimant address. 69 | * @return fillBlock The fillBlock. 70 | */ 71 | function _parseCalldata(bytes calldata originData, bytes calldata fillerData) 72 | internal 73 | pure 74 | returns ( 75 | BatchClaim calldata claim, 76 | FillParameters calldata mandate, 77 | bytes32[] calldata fillHashes, 78 | Adjustment calldata adjustment, 79 | bytes32 claimant, 80 | uint256 fillBlock 81 | ) 82 | { 83 | /* 84 | * Need 27 words in originData at minimum: 85 | * - 1 word for offset to claim (dynamic struct). 86 | * - 1 word for offset to the main fill (dynamic struct). 87 | * - 1 word for offset to fillHashes. 88 | * - 4 words for fixed claim fields (BatchCompact.arbiter, BatchCompact.sponsor, BatchCompact.nonce, BatchCompact.expires). 89 | * - 9 words for fixed mandate fields. 90 | * - 1 word for offset to claim.BatchCompact 91 | * - 5 words for dynamic offsets (BatchCompact.commitments, sponsorSignature, allocatorSignature, Fill.priceCurve and Fill.recipientCallback). 92 | * - 5 words for lengths of dynamics (assuming empty). 93 | * - 2 words for fillHashes length & at least a single word for fill hash. 94 | * Also ensure no funny business with the claim pointer (should be 0x60). 95 | * 96 | * Need 7 words in fillerData at minimum: 97 | * - 1 word for offset to adjustment (dynamic struct). 98 | * - 1 word for claimant. 99 | * - 1 word for fillBlock. 100 | * - 4 words for fixed adjustment fields in the struct (adjuster, fillIndex, targetBlock, validityConditions). 101 | * - 2 words for dynamic offsets (supplementalPriceCurve, adjustmentAuthorization). 102 | * - 2 words for supplementalPriceCurve and adjustmentAuthorization length (assuming empty). 103 | * Also ensure no funny business with the adjustment pointer (should be 0x60). 104 | */ 105 | assembly ("memory-safe") { 106 | if or( 107 | or(lt(originData.length, 0x360), xor(calldataload(originData.offset), 0x60)), 108 | or(lt(fillerData.length, 0x100), xor(calldataload(fillerData.offset), 0x60)) 109 | ) { revert(0, 0) } 110 | } 111 | 112 | // Get the claim, fill, and fillHashes encoded as bytes arrays with bounds checks from the originData. 113 | { 114 | bytes calldata encodedClaim = LibBytes.dynamicStructInCalldata(originData, 0x00); 115 | bytes calldata encodedFill = LibBytes.dynamicStructInCalldata(originData, 0x20); 116 | assembly ("memory-safe") { 117 | claim := encodedClaim.offset 118 | mandate := encodedFill.offset 119 | } 120 | } 121 | 122 | { 123 | bytes calldata encodedFillHashes = LibBytes.bytesInCalldata(originData, 0x40); 124 | assembly ("memory-safe") { 125 | // originData 126 | fillHashes.offset := encodedFillHashes.offset 127 | fillHashes.length := encodedFillHashes.length 128 | } 129 | } 130 | 131 | // Get the adjustment, claimant and fillBlock encoded as bytes arrays with bounds checks from the fillerData. 132 | bytes calldata encodedAdjustment = LibBytes.dynamicStructInCalldata(fillerData, 0x00); 133 | bytes32 encodedClaimant = LibBytes.loadCalldata(fillerData, 0x20); 134 | bytes32 encodedFillBlock = LibBytes.loadCalldata(fillerData, 0x40); 135 | 136 | // Extract static structs and other static variables directly. 137 | // Note: This doesn't sanitize struct elements; that should happen downstream. 138 | assembly ("memory-safe") { 139 | // fillerData 140 | adjustment := encodedAdjustment.offset 141 | claimant := encodedClaimant 142 | fillBlock := encodedFillBlock 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /test/TribunalBasicTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Tribunal} from "../src/Tribunal.sol"; 6 | import {ITribunal} from "../src/interfaces/ITribunal.sol"; 7 | import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; 8 | import { 9 | Mandate, 10 | FillParameters, 11 | FillComponent, 12 | Adjustment, 13 | RecipientCallback, 14 | ArgDetail 15 | } from "../src/types/TribunalStructs.sol"; 16 | import {BatchCompact, Lock} from "the-compact/src/types/EIP712Types.sol"; 17 | import {WITNESS_TYPESTRING} from "../src/types/TribunalTypeHashes.sol"; 18 | 19 | contract TribunalBasicTest is Test { 20 | using FixedPointMathLib for uint256; 21 | 22 | Tribunal public tribunal; 23 | address theCompact; 24 | address sponsor; 25 | address adjuster; 26 | 27 | uint256[] public emptyPriceCurve; 28 | 29 | bytes32 constant MANDATE_TYPEHASH = 30 | 0xd98eceb6e5c7770b3b664a99c269855402fe5255294a30970d25376caea662c6; 31 | 32 | bytes32 constant COMPACT_TYPEHASH_WITH_MANDATE = 33 | 0xdbbdcf42471b4a26f7824df9f33f0a4f9bb4e7a66be6a31be8868a6cbbec0a7d; 34 | 35 | bytes32 constant MANDATE_LOCK_TYPEHASH = 36 | keccak256("Mandate_Lock(bytes12 lockTag,address token,uint256 amount)"); 37 | 38 | bytes32 constant LOCK_TYPEHASH = 39 | keccak256("Lock(bytes12 lockTag,address token,uint256 amount)"); 40 | 41 | receive() external payable {} 42 | 43 | function setUp() public { 44 | theCompact = address(0xC0); 45 | tribunal = new Tribunal(); 46 | (sponsor,) = makeAddrAndKey("sponsor"); 47 | (adjuster,) = makeAddrAndKey("adjuster"); 48 | 49 | emptyPriceCurve = new uint256[](0); 50 | } 51 | 52 | function test_Name() public view { 53 | assertEq(tribunal.name(), "Tribunal"); 54 | } 55 | 56 | function test_DeriveMandateHash() public view { 57 | FillComponent[] memory components = new FillComponent[](1); 58 | components[0] = FillComponent({ 59 | fillToken: address(0xDEAD), 60 | minimumFillAmount: 1 ether, 61 | recipient: address(0xCAFE), 62 | applyScaling: true 63 | }); 64 | 65 | FillParameters memory fill = FillParameters({ 66 | chainId: block.chainid, 67 | tribunal: address(tribunal), 68 | expires: 1703116800, 69 | components: components, 70 | baselinePriorityFee: 100 wei, 71 | scalingFactor: 1e18, 72 | priceCurve: emptyPriceCurve, 73 | recipientCallback: new RecipientCallback[](0), 74 | salt: bytes32(uint256(1)) 75 | }); 76 | 77 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 78 | mandate.fills[0] = fill; 79 | 80 | bytes32 fillsHash = keccak256(abi.encodePacked(tribunal.deriveFillHash(fill))); 81 | 82 | bytes32 expectedHash = keccak256(abi.encode(MANDATE_TYPEHASH, mandate.adjuster, fillsHash)); 83 | 84 | assertEq(tribunal.deriveMandateHash(mandate), expectedHash); 85 | } 86 | 87 | function test_DeriveMandateHash_DifferentSalt() public view { 88 | FillComponent[] memory components = new FillComponent[](1); 89 | components[0] = FillComponent({ 90 | fillToken: address(0xDEAD), 91 | minimumFillAmount: 1 ether, 92 | recipient: address(0xCAFE), 93 | applyScaling: true 94 | }); 95 | 96 | FillParameters memory fill = FillParameters({ 97 | chainId: block.chainid, 98 | tribunal: address(tribunal), 99 | expires: 1703116800, 100 | components: components, 101 | baselinePriorityFee: 100 wei, 102 | scalingFactor: 1e18, 103 | priceCurve: emptyPriceCurve, 104 | recipientCallback: new RecipientCallback[](0), 105 | salt: bytes32(uint256(2)) 106 | }); 107 | 108 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 109 | mandate.fills[0] = fill; 110 | 111 | bytes32 fillsHash = keccak256(abi.encodePacked(tribunal.deriveFillHash(fill))); 112 | 113 | bytes32 expectedHash = keccak256(abi.encode(MANDATE_TYPEHASH, mandate.adjuster, fillsHash)); 114 | 115 | assertEq(tribunal.deriveMandateHash(mandate), expectedHash); 116 | } 117 | 118 | function test_DeriveClaimHash() public view { 119 | FillComponent[] memory components = new FillComponent[](1); 120 | components[0] = FillComponent({ 121 | fillToken: address(0xDEAD), 122 | minimumFillAmount: 1 ether, 123 | recipient: address(0xCAFE), 124 | applyScaling: true 125 | }); 126 | 127 | FillParameters memory fill = FillParameters({ 128 | chainId: block.chainid, 129 | tribunal: address(tribunal), 130 | expires: 1703116800, 131 | components: components, 132 | baselinePriorityFee: 100 wei, 133 | scalingFactor: 1e18, 134 | priceCurve: emptyPriceCurve, 135 | recipientCallback: new RecipientCallback[](0), 136 | salt: bytes32(uint256(1)) 137 | }); 138 | 139 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 140 | mandate.fills[0] = fill; 141 | 142 | Lock[] memory commitments = new Lock[](1); 143 | commitments[0] = Lock({lockTag: bytes12(0), token: address(0xDEAD), amount: 1 ether}); 144 | 145 | BatchCompact memory compact = BatchCompact({ 146 | arbiter: address(this), 147 | sponsor: sponsor, 148 | nonce: 0, 149 | expires: block.timestamp + 1 hours, 150 | commitments: commitments 151 | }); 152 | 153 | bytes32 mandateHash = tribunal.deriveMandateHash(mandate); 154 | 155 | bytes32 commitmentsHash = keccak256( 156 | abi.encodePacked( 157 | keccak256( 158 | abi.encode( 159 | LOCK_TYPEHASH, 160 | compact.commitments[0].lockTag, 161 | compact.commitments[0].token, 162 | compact.commitments[0].amount 163 | ) 164 | ) 165 | ) 166 | ); 167 | 168 | bytes32 expectedHash = keccak256( 169 | abi.encode( 170 | COMPACT_TYPEHASH_WITH_MANDATE, 171 | compact.arbiter, 172 | compact.sponsor, 173 | compact.nonce, 174 | compact.expires, 175 | commitmentsHash, 176 | mandateHash 177 | ) 178 | ); 179 | 180 | assertEq(tribunal.deriveClaimHash(compact, mandateHash), expectedHash); 181 | } 182 | 183 | function test_GetCompactWitnessDetails() public view { 184 | (string memory witnessTypeString, ArgDetail[] memory details) = 185 | tribunal.getCompactWitnessDetails(); 186 | 187 | assertEq(witnessTypeString, string.concat("Mandate(", WITNESS_TYPESTRING, ")")); 188 | assertEq(details.length, 1); 189 | assertEq(details[0].tokenPath, "fills[].components[].fillToken"); 190 | assertEq(details[0].argPath, "fills[].components[].minimumFillAmount"); 191 | assertEq( 192 | details[0].description, 193 | "Output token and minimum amount for each fill component in the Fills array" 194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/types/TribunalStructs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.27; 3 | 4 | import {BatchCompact} from "the-compact/src/types/EIP712Types.sol"; 5 | 6 | // Overview of the sequence of steps: 7 | // 1) sponsor deposits and registers a compact on the source chain assigning an adjuster, a cross-chain fill, and a source-chain fallback action that will trigger a deposit and registration of a target-chain compact. Allocator ensures that deposited tokens are allocated. 8 | // 2) adjuster cosigns an adjustment for the cross-chain fill (if they do not cosign, sponsor signs a new compact or withdraws) 9 | // 3) cross-chain fill is revealed and active (source chain and target chain actions remain hidden) 10 | // 3a) filler provides tokens on target chain, then claims tokens on source chain 11 | // 3b) sponsor cancels early by submitting a transaction on the target chain before the fill occurs 12 | // 3c) no filler takes the cross-chain order in time, proceed to step 4 13 | // 4) adjuster cosigns an adjustment for the source chain fill (if they do not cosign, sponsor signs a new compact or withdraws) 14 | // 5) source chain fill is revealed and active (target chain action remains hidden, though input token and expiration are revealed) 15 | // 5a) filler takes input tokens and provides intermediate output tokens on source chain, then triggers bridge to tribunal on target chain that in turn triggers desposit + register (+ allocate if needed) on target chain (mandate remains hidden), proceed to step 6 16 | // 5b) sponsor cancels early by submitting a transaction on the target chain — note that step 5a still may occur 17 | // 5c) no filler takes the source chain order in time, sponsor signs a new compact or withdraws tokens 18 | // 6) bridge lands and bridged tokens are deposited into the compact on target chain with accompanying compact registration — note that if a filler completed step 3a then tokens are sent to them instead, and if sponsor completed step 3b then tokens are sent to them directly 19 | // 7a) sponsor claims deposited tokens directly, cancelling early 20 | // 7b) adjuster cosigns an adjustment for the target chain fill (if they do not cosign, sponsor signs a new compact or withdraws) 21 | // Note: important to handle the case where the bridge transaction is not completed and funds need to be withdrawn from the bridge contract on the source chain and returned to the sponsor 22 | // 8) target chain fill is revealed and active 23 | // 8a) filler claims deposited tokens in exchange for providing output tokens to recipient 24 | // 8b) sponsor claims deposited tokens directly, cancelling 25 | // 8b) no filler takes the target chain order in time, proceed to step 9 26 | // 9) tokens are deallocated and available in The Compact on target chain 27 | // 9a) sponsor signs a new compact to perform a modified target chain swap 28 | // 9b) sponsor signs a new compact to perform a cross-chain swap back to the original source chain (basically return to step 1 with target and source swapped) — note that this could also be part of the originally registered target chain compact and performed automatically 29 | // 9c) sponsor manually withdraws tokens on target chain 30 | 31 | // Parent mandate signed by the sponsor on source chain. Note that the EIP-712 payload differs slightly from the structs declared here (mainly around utilizing full mandates rather than mandate hashes). 32 | struct Mandate { 33 | address adjuster; 34 | FillParameters[] fills; // Arbitrary-length array; note that in EIP-712 payload this is Mandate_Fill 35 | } 36 | 37 | // Mandate_Fill in EIP-712 payload 38 | struct FillParameters { 39 | uint256 chainId; // Same-chain if value matches chainId(), otherwise cross-chain 40 | address tribunal; // Contract where the fill is performed. 41 | uint256 expires; // Fill expiration timestamp. 42 | FillComponent[] components; // Fill components. 43 | uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in. 44 | uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline). 45 | uint256[] priceCurve; // Block durations and uint240 additional scaling factors per each duration. 46 | RecipientCallback[] recipientCallback; // Array of length 0 or 1; note that in EIP-712 payload this is Mandate_RecipientCallback[] 47 | bytes32 salt; 48 | } 49 | 50 | // Mandate_FillComponent in EIP-712 payload 51 | struct FillComponent { 52 | address fillToken; // Token to be provided (address(0) for native). 53 | uint256 minimumFillAmount; // Minimum fill amount. 54 | address recipient; // Recipient of the tokens — address(0) or tribunal indicate that funds will be pulled by the directive. 55 | bool applyScaling; // Whether or not to apply scaling factor to the minimum amount. 56 | } 57 | 58 | // If a callback is specified, tribunal will follow up with a call to the first recipient with fill details (including realized fill amount), a new compact and hash of an accompanying mandate, a target chainId, and context 59 | // Note that this does not directly map to the EIP-712 payload (which contains a Mandate_BatchCompact containing the full `Mandate mandate` rather than BatchCompact + mandateHash) 60 | // Mandate_RecipientCallback in EIP-712 payload 61 | struct RecipientCallback { 62 | uint256 chainId; 63 | BatchCompact compact; 64 | bytes32 mandateHash; 65 | bytes context; 66 | } 67 | 68 | // Arguments signed for by adjuster. 69 | struct Adjustment { 70 | address adjuster; // Assigned adjuster for the fill (not included in EIP-712 payload). 71 | // bytes32 claimHash included in EIP-712 payload but not provided as an argument. 72 | uint256 fillIndex; 73 | uint256 targetBlock; 74 | uint256[] supplementalPriceCurve; // Additional scaling factor specified duration on price curve. 75 | bytes32 validityConditions; // Optional value consisting of a number of blocks past the target and a exclusive filler address. 76 | bytes adjustmentAuthorization; // Authorization from the adjuster (not included in EIP-712 payload). 77 | } 78 | 79 | // Struct for event emissions that pairs fill amounts with recipients. 80 | struct FillRecipient { 81 | uint256 fillAmount; 82 | address recipient; 83 | } 84 | 85 | // Struct for filler callback that contains all fill component details. 86 | struct FillRequirement { 87 | address fillToken; // Token to be provided (address(0) for native). 88 | uint256 minimumFillAmount; // Minimum specified fill amount. 89 | uint256 realizedFillAmount; // Actual fill amount that must be provided. 90 | } 91 | 92 | // Struct for dispatch callback parameters. 93 | struct DispatchParameters { 94 | uint256 chainId; // Chain ID the dispatch callback is intended to interact with. 95 | address target; // Address that will receive the dispatch callback. 96 | uint256 value; // Amount of native tokens to send with the callback. 97 | bytes context; // Arbitrary context data to pass to the callback. 98 | } 99 | 100 | // A disposition refers to a successful fill. Tribunal allows the filler to assign 101 | // an indicated claimant and scaling factor that should be applied to a subsequent 102 | // claim, though the arbiter is ultimately responsible for their application. 103 | struct DispositionDetails { 104 | bytes32 claimant; 105 | uint256 scalingFactor; 106 | } 107 | 108 | struct BatchClaim { 109 | BatchCompact compact; 110 | bytes sponsorSignature; // Authorization from the sponsor 111 | bytes allocatorSignature; // Authorization from the allocator 112 | } 113 | 114 | struct ArgDetail { 115 | string tokenPath; 116 | string argPath; 117 | string description; 118 | } 119 | -------------------------------------------------------------------------------- /test/TribunalFinalCoverageGapsTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Tribunal} from "../src/Tribunal.sol"; 6 | import {MockERC20} from "./mocks/MockERC20.sol"; 7 | import {MockAllocator} from "./mocks/MockAllocator.sol"; 8 | import {DeployTheCompact} from "./helpers/DeployTheCompact.sol"; 9 | import {TheCompact} from "../lib/the-compact/src/TheCompact.sol"; 10 | import { 11 | Mandate, 12 | FillParameters, 13 | FillComponent, 14 | Adjustment, 15 | RecipientCallback, 16 | BatchClaim 17 | } from "../src/types/TribunalStructs.sol"; 18 | import {ADJUSTMENT_TYPEHASH, WITNESS_TYPESTRING} from "../src/types/TribunalTypeHashes.sol"; 19 | import {BatchCompact, Lock} from "the-compact/src/types/EIP712Types.sol"; 20 | import {ITheCompact} from "the-compact/src/interfaces/ITheCompact.sol"; 21 | 22 | /** 23 | * @title TribunalFinalCoverageGapsTest 24 | * @notice Targeted tests for specific remaining coverage gaps using proper integration with THE_COMPACT 25 | */ 26 | contract TribunalFinalCoverageGapsTest is Test, DeployTheCompact { 27 | Tribunal public tribunal; 28 | MockERC20 public token; 29 | MockAllocator public allocator; 30 | TheCompact public theCompact; 31 | 32 | address public sponsor; 33 | address public filler; 34 | address public adjuster; 35 | uint256 public adjusterPrivateKey; 36 | address public arbiter; 37 | address public recipient; 38 | 39 | bytes12 public lockTag; 40 | uint256 public lockId; 41 | 42 | function setUp() public { 43 | // Deploy THE_COMPACT using the helper 44 | theCompact = deployTheCompact(); 45 | 46 | tribunal = new Tribunal(); 47 | token = new MockERC20(); 48 | allocator = new MockAllocator(); 49 | 50 | sponsor = makeAddr("Sponsor"); 51 | filler = makeAddr("Filler"); 52 | (adjuster, adjusterPrivateKey) = makeAddrAndKey("Adjuster"); 53 | arbiter = makeAddr("Arbiter"); 54 | recipient = makeAddr("Recipient"); 55 | 56 | // Setup token balances 57 | token.mint(sponsor, 100 ether); 58 | token.mint(filler, 1000 ether); 59 | 60 | // Configure allocator 61 | allocator.setNonceToReturn(123); 62 | 63 | // Register the MockAllocator contract as an allocator 64 | vm.prank(address(allocator)); 65 | uint96 allocatorId = theCompact.__registerAllocator(address(allocator), ""); 66 | lockTag = bytes12(allocatorId); 67 | lockId = uint256(bytes32(lockTag)) | uint256(uint160(address(token))); 68 | 69 | // Sponsor makes a deposit to establish the lock 70 | vm.startPrank(sponsor); 71 | token.approve(address(theCompact), 20 ether); 72 | theCompact.depositERC20(address(token), lockTag, 10 ether, sponsor); 73 | vm.stopPrank(); 74 | 75 | // Setup filler approval 76 | vm.prank(filler); 77 | token.approve(address(tribunal), type(uint256).max); 78 | } 79 | 80 | // ============ Coverage Gap: Lines 289-290 - settleOrRegister with mandateHash == 0 ============ 81 | /** 82 | * @notice Test settleOrRegister deposit path without registration (mandateHash = 0) 83 | * @dev Covers lines 289-290 - batchDeposit without registration 84 | */ 85 | function test_SettleOrRegister_DepositWithoutRegistration() public { 86 | uint256 depositAmount = 5 ether; 87 | 88 | // Create compact with existing lockTag 89 | Lock[] memory commitments = new Lock[](1); 90 | commitments[0] = Lock({ 91 | lockTag: lockTag, // Use registered lock 92 | token: address(token), 93 | amount: depositAmount 94 | }); 95 | 96 | BatchCompact memory compact = BatchCompact({ 97 | arbiter: arbiter, 98 | sponsor: sponsor, 99 | nonce: 1, 100 | expires: uint256(block.timestamp + 1 days), 101 | commitments: commitments 102 | }); 103 | 104 | bytes32 sourceClaimHash = bytes32(0); // No source claim 105 | bytes32 mandateHash = bytes32(0); // No registration - triggers deposit path (lines 289-290) 106 | 107 | // Fund tribunal with tokens and approve 108 | token.mint(address(tribunal), depositAmount); 109 | vm.prank(address(tribunal)); 110 | token.approve(address(theCompact), type(uint256).max); 111 | 112 | // Check recipient balance before 113 | uint256 balanceBefore = theCompact.balanceOf(recipient, lockId); 114 | 115 | // Should deposit without registration (lines 289-290) 116 | bytes32 result = 117 | tribunal.settleOrRegister(sourceClaimHash, compact, mandateHash, recipient, ""); 118 | 119 | // Verify deposit occurred without registration 120 | assertEq(result, bytes32(0), "Should return 0 for deposit without registration"); 121 | assertGt( 122 | theCompact.balanceOf(recipient, lockId), 123 | balanceBefore, 124 | "Recipient should receive deposit" 125 | ); 126 | } 127 | 128 | // ============ Coverage Gap: Line 1085 - Token balance path in _prepareIdsAndAmounts ============ 129 | /** 130 | * @notice Test _prepareIdsAndAmounts with sufficient allowance 131 | * @dev Covers line 1085 - the balanceOf path when allowance is already sufficient 132 | */ 133 | function test_PrepareIdsAndAmounts_SufficientAllowance() public { 134 | uint256 depositAmount = 7 ether; 135 | 136 | // Setup: tribunal has tokens and pre-existing allowance 137 | token.mint(address(tribunal), depositAmount); 138 | 139 | // Give tribunal pre-existing sufficient allowance to THE_COMPACT 140 | // This triggers the path where line 1085 checks balance without re-approving 141 | vm.prank(address(tribunal)); 142 | token.approve(address(theCompact), type(uint256).max); 143 | 144 | Lock[] memory commitments = new Lock[](1); 145 | commitments[0] = Lock({ 146 | lockTag: lockTag, // Use registered lock 147 | token: address(token), 148 | amount: depositAmount 149 | }); 150 | 151 | BatchCompact memory compact = BatchCompact({ 152 | arbiter: arbiter, 153 | sponsor: sponsor, 154 | nonce: 2, // Non-zero nonce triggers direct registration path 155 | expires: uint256(block.timestamp + 1 days), 156 | commitments: commitments 157 | }); 158 | 159 | bytes32 sourceClaimHash = bytes32(0); 160 | bytes32 mandateHash = keccak256("test"); 161 | 162 | // This executes _prepareIdsAndAmounts where line 1085 checks the balance 163 | // Since allowance is already sufficient, it skips the approval 164 | bytes32 result = 165 | tribunal.settleOrRegister(sourceClaimHash, compact, mandateHash, recipient, ""); 166 | 167 | // Should return a claim hash for registration 168 | assertTrue(result != bytes32(0), "Should return claim hash for registration"); 169 | } 170 | 171 | // ============ Helper Functions ============ 172 | function _getBatchCompact(uint256 amount) internal view returns (BatchCompact memory) { 173 | Lock[] memory commitments = new Lock[](1); 174 | commitments[0] = Lock({lockTag: lockTag, token: address(token), amount: amount}); 175 | 176 | return BatchCompact({ 177 | arbiter: arbiter, 178 | sponsor: sponsor, 179 | nonce: 1, 180 | expires: uint256(block.timestamp + 1 days), 181 | commitments: commitments 182 | }); 183 | } 184 | 185 | function _getFillParameters(uint256 minimumFillAmount) 186 | internal 187 | view 188 | returns (FillParameters memory) 189 | { 190 | FillComponent[] memory components = new FillComponent[](1); 191 | components[0] = FillComponent({ 192 | fillToken: address(token), 193 | minimumFillAmount: minimumFillAmount, 194 | recipient: sponsor, 195 | applyScaling: true 196 | }); 197 | 198 | return FillParameters({ 199 | chainId: block.chainid, 200 | tribunal: address(tribunal), 201 | expires: uint256(block.timestamp + 1 days), 202 | components: components, 203 | baselinePriorityFee: 100 wei, 204 | scalingFactor: 1e18, 205 | priceCurve: new uint256[](0), 206 | recipientCallback: new RecipientCallback[](0), 207 | salt: bytes32(uint256(1)) 208 | }); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /test/MultipleZeroDurationTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {PriceCurveLib, PriceCurveElement} from "../src/lib/PriceCurveLib.sol"; 6 | import {PriceCurveTestHelper} from "./helpers/PriceCurveTestHelper.sol"; 7 | 8 | /** 9 | * @title MultipleZeroDurationTest 10 | * @notice Comprehensive test to verify the behavior of multiple consecutive zero-duration elements 11 | * @dev Confirms that: 12 | * 1. At the exact block, the FIRST zero-duration element is used 13 | * 2. For subsequent interpolation, the LAST zero-duration element is used as starting point 14 | */ 15 | contract MultipleZeroDurationTest is Test { 16 | using PriceCurveLib for uint256[]; 17 | 18 | PriceCurveTestHelper public helper; 19 | 20 | function setUp() public { 21 | helper = new PriceCurveTestHelper(); 22 | } 23 | 24 | /** 25 | * @notice Test the actual behavior of multiple consecutive zero-duration elements 26 | * @dev This test verifies the documentation update showing that: 27 | * - At block 10: returns the first zero-duration (1.5e18) 28 | * - At blocks 11-20: interpolates from the last zero-duration (1.3e18) to 1.0e18 29 | */ 30 | function test_MultipleConsecutiveZeroDuration_DetailedBehavior() public pure { 31 | uint256[] memory priceCurve = new uint256[](4); 32 | priceCurve[0] = (10 << 240) | uint256(1.2e18); // 10 blocks at 1.2x 33 | priceCurve[1] = (0 << 240) | uint256(1.5e18); // First zero-duration at block 10 34 | priceCurve[2] = (0 << 240) | uint256(1.3e18); // Second zero-duration at block 10 35 | priceCurve[3] = (10 << 240) | uint256(1e18); // 10 blocks ending at 1x 36 | 37 | // Test interpolation during first segment (blocks 0-9) 38 | // Should interpolate from 1.2x towards 1.5x (the next element) 39 | uint256 scalingAtBlock0 = priceCurve.getCalculatedValues(0); 40 | assertEq(scalingAtBlock0, 1.2e18, "Block 0: should be 1.2x"); 41 | 42 | uint256 scalingAtBlock5 = priceCurve.getCalculatedValues(5); 43 | // Expected: 1.2 + (1.5 - 1.2) * (5/10) = 1.35 44 | assertEq(scalingAtBlock5, 1.35e18, "Block 5: should interpolate to 1.35x"); 45 | 46 | uint256 scalingAtBlock9 = priceCurve.getCalculatedValues(9); 47 | // Expected: 1.2 + (1.5 - 1.2) * (9/10) = 1.47 48 | assertEq(scalingAtBlock9, 1.47e18, "Block 9: should be close to 1.5x"); 49 | 50 | // CRITICAL TEST 1: At block 10, should return FIRST zero-duration element 51 | uint256 scalingAtBlock10 = priceCurve.getCalculatedValues(10); 52 | assertEq(scalingAtBlock10, 1.5e18, "Block 10: should use FIRST zero-duration (1.5x)"); 53 | 54 | // CRITICAL TEST 2: After block 10, should interpolate from LAST zero-duration element 55 | // The implementation uses parameters[i-1] when hasPassedZeroDuration is true 56 | // This means it uses priceCurve[2] (1.3e18) as the starting point 57 | 58 | uint256 scalingAtBlock11 = priceCurve.getCalculatedValues(11); 59 | // Expected: 1.3 - (1.3 - 1.0) * (1/10) = 1.27 60 | assertEq( 61 | scalingAtBlock11, 1.27e18, "Block 11: should interpolate from LAST zero-duration (1.3x)" 62 | ); 63 | 64 | uint256 scalingAtBlock15 = priceCurve.getCalculatedValues(15); 65 | // Expected: 1.3 - (1.3 - 1.0) * (5/10) = 1.15 66 | assertEq(scalingAtBlock15, 1.15e18, "Block 15: midway from 1.3x to 1.0x"); 67 | 68 | uint256 scalingAtBlock19 = priceCurve.getCalculatedValues(19); 69 | // Expected: 1.3 - (1.3 - 1.0) * (9/10) = 1.03 70 | assertEq(scalingAtBlock19, 1.03e18, "Block 19: close to 1.0x"); 71 | } 72 | 73 | /** 74 | * @notice Test with three consecutive zero-duration elements 75 | * @dev Verifies that with 3 zero-duration elements: 76 | * - At the exact block: first is used 77 | * - For interpolation: last (third) is used 78 | */ 79 | function test_ThreeConsecutiveZeroDuration() public pure { 80 | uint256[] memory priceCurve = new uint256[](5); 81 | priceCurve[0] = (10 << 240) | uint256(1.1e18); // 10 blocks at 1.1x 82 | priceCurve[1] = (0 << 240) | uint256(1.6e18); // First zero-duration 83 | priceCurve[2] = (0 << 240) | uint256(1.4e18); // Second zero-duration 84 | priceCurve[3] = (0 << 240) | uint256(1.2e18); // Third zero-duration (last) 85 | priceCurve[4] = (10 << 240) | uint256(1e18); // 10 blocks to 1x 86 | 87 | // At block 10: should return FIRST zero-duration 88 | uint256 scalingAtBlock10 = priceCurve.getCalculatedValues(10); 89 | assertEq(scalingAtBlock10, 1.6e18, "Should use first zero-duration (1.6x)"); 90 | 91 | // After block 10: should interpolate from LAST (third) zero-duration 92 | uint256 scalingAtBlock11 = priceCurve.getCalculatedValues(11); 93 | // Expected: 1.2 - (1.2 - 1.0) * (1/10) = 1.18 94 | assertEq(scalingAtBlock11, 1.18e18, "Should interpolate from third zero-duration (1.2x)"); 95 | 96 | uint256 scalingAtBlock15 = priceCurve.getCalculatedValues(15); 97 | // Expected: 1.2 - (1.2 - 1.0) * (5/10) = 1.1 98 | assertEq(scalingAtBlock15, 1.1e18, "Midway from 1.2x to 1.0x"); 99 | } 100 | 101 | /** 102 | * @notice Test zero-duration elements at different positions 103 | * @dev Verifies that multiple zero-duration elements only affect the same block position 104 | */ 105 | function test_ZeroDurationAtDifferentPositions() public pure { 106 | uint256[] memory priceCurve = new uint256[](5); 107 | priceCurve[0] = (10 << 240) | uint256(1.2e18); // 10 blocks 108 | priceCurve[1] = (0 << 240) | uint256(1.5e18); // Zero-duration at block 10 109 | priceCurve[2] = (10 << 240) | uint256(1.3e18); // 10 blocks 110 | priceCurve[3] = (0 << 240) | uint256(1.4e18); // Zero-duration at block 20 111 | priceCurve[4] = (10 << 240) | uint256(1e18); // 10 blocks 112 | 113 | // At block 10: first zero-duration 114 | assertEq(priceCurve.getCalculatedValues(10), 1.5e18, "Block 10 zero-duration"); 115 | 116 | // At block 15: interpolating in segment 2 117 | uint256 scalingAtBlock15 = priceCurve.getCalculatedValues(15); 118 | // After a zero-duration element, the implementation interpolates from the zero-duration value 119 | // to the current segment's scaling factor (NOT to the next element) 120 | // Segment 2 has scaling factor 1.3, so we interpolate from 1.5 (zero-duration) to 1.3 121 | // At block 15 (5 blocks into 10-block segment): 1.5 - (1.5 - 1.3) * (5/10) = 1.4 122 | assertEq(scalingAtBlock15, 1.4e18, "Block 15 interpolation"); 123 | 124 | // At block 20: second zero-duration 125 | assertEq(priceCurve.getCalculatedValues(20), 1.4e18, "Block 20 zero-duration"); 126 | 127 | // At block 25: interpolating in final segment 128 | uint256 scalingAtBlock25 = priceCurve.getCalculatedValues(25); 129 | // Interpolates from 1.4 (zero-duration at block 20) to 1.0 (segment 4's end value) 130 | // Expected: 1.4 - (1.4 - 1.0) * (5/10) = 1.2 131 | assertEq(scalingAtBlock25, 1.2e18, "Block 25 interpolation"); 132 | } 133 | 134 | /** 135 | * @notice Test edge case with all zero-duration elements 136 | * @dev Verifies behavior when curve consists only of zero-duration elements 137 | */ 138 | function test_OnlyZeroDurationElements() public { 139 | uint256[] memory priceCurve = new uint256[](3); 140 | priceCurve[0] = (0 << 240) | uint256(1.5e18); // First zero-duration at block 0 141 | priceCurve[1] = (0 << 240) | uint256(1.3e18); // Second zero-duration at block 0 142 | priceCurve[2] = (0 << 240) | uint256(1.1e18); // Third zero-duration at block 0 143 | 144 | // At block 0: should return first zero-duration 145 | uint256 scalingAtBlock0 = priceCurve.getCalculatedValues(0); 146 | assertEq(scalingAtBlock0, 1.5e18, "Block 0: should use first zero-duration"); 147 | 148 | // Any block after 0 should revert (no duration to interpolate) 149 | // The curve has no actual duration, so accessing beyond block 0 exceeds it 150 | // Using helper to properly capture the revert 151 | vm.expectRevert(PriceCurveLib.PriceCurveBlocksExceeded.selector); 152 | helper.getCalculatedValues(priceCurve, 1); 153 | } 154 | 155 | /** 156 | * @notice Test practical use case: step function with instant price change 157 | * @dev Shows how multiple zero-duration elements can create complex price dynamics 158 | */ 159 | function test_PracticalUseCase_StepWithInstantChange() public pure { 160 | uint256[] memory priceCurve = new uint256[](4); 161 | priceCurve[0] = (50 << 240) | uint256(2e18); // Start high at 2x for 50 blocks 162 | priceCurve[1] = (0 << 240) | uint256(1.2e18); // Instant drop to 1.2x for display 163 | priceCurve[2] = (0 << 240) | uint256(1.5e18); // But actually start next phase at 1.5x 164 | priceCurve[3] = (50 << 240) | uint256(1e18); // Decay to 1x over 50 blocks 165 | 166 | // During first segment 167 | uint256 scalingAtBlock25 = priceCurve.getCalculatedValues(25); 168 | // Expected: 2.0 - (2.0 - 1.2) * (25/50) = 1.6 169 | assertEq(scalingAtBlock25, 1.6e18, "Block 25: interpolating to instant drop point"); 170 | 171 | // At block 50: shows the instant drop price 172 | uint256 scalingAtBlock50 = priceCurve.getCalculatedValues(50); 173 | assertEq(scalingAtBlock50, 1.2e18, "Block 50: instant drop to 1.2x (first zero-duration)"); 174 | 175 | // After block 50: interpolates from different starting point 176 | uint256 scalingAtBlock51 = priceCurve.getCalculatedValues(51); 177 | // Expected: 1.5 - (1.5 - 1.0) * (1/50) = 1.49 178 | assertEq( 179 | scalingAtBlock51, 1.49e18, "Block 51: interpolating from 1.5x (last zero-duration)" 180 | ); 181 | 182 | uint256 scalingAtBlock75 = priceCurve.getCalculatedValues(75); 183 | // Expected: 1.5 - (1.5 - 1.0) * (25/50) = 1.25 184 | assertEq(scalingAtBlock75, 1.25e18, "Block 75: midway from 1.5x to 1.0x"); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/types/TribunalTypeHashes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | /* Example payload signed by swapper for a cross-chain swap with a decomposed same-chain swap on source chain + bridge + register same-chain swap on target chain: 5 | { 6 | arbiter: SOURCE_CHAIN_TRIBUNAL_CONTRACT, 7 | sponsor: SWAPPER, 8 | nonce: SOURCE_CLAIM_NONCE, 9 | expires: SOURCE_CLAIM_EXPIRATION, 10 | commitments: [ 11 | { 12 | lockTag: ONCHAIN_ALLOCATOR_LOCK_TAG_WITH_DEFAULT_PARAMS, 13 | token: SOURCE_CHAIN_UNI, 14 | amount: 1_000_000_000_000_000_000 15 | } 16 | ], 17 | mandate: { 18 | adjuster: UNISWAP_TEE, 19 | fills: [ 20 | { 21 | chainId: TARGET_CHAIN_ID, 22 | tribunal: TARGET_CHAIN_TRIBUNAL_CONTRACT, 23 | expires; CROSS_CHAIN_FILL_EXPIRATION, 24 | fillToken: TARGET_CHAIN_USDC, 25 | minimumFillAmount: 10_017_500, 26 | baselinePriorityFee; 1_000_000_000, 27 | scalingFactor; 0x00000000000000000000000000000000000000000000000000000de0b6b3a7640005, 28 | priceCurve: [0x0002000000000000000000000000000000000000000000000de0b6b3a7640002, 0x0003000000000000000000000000000000000000000000000de0b6b3a7640001] 29 | recipient: SWAPPER, 30 | recipientCallback: [], 31 | salt: 0x1234567890123456789012345678901234567890123456789012345678901234 32 | }, 33 | { 34 | chainId: SOURCE_CHAIN_ID, 35 | tribunal: SOURCE_CHAIN_TRIBUNAL_CONTRACT, 36 | expires; SAME_CHAIN_FILL_EXPIRATION, 37 | fillToken: ETH, 38 | minimumFillAmount: 2_404_400_000_000_000, 39 | baselinePriorityFee: 0, 40 | scalingFactor: 0x00000000000000000000000000000000000000000000000000000de0b6b3a7640000, 41 | priceCurve: [0x0002000000000000000000000000000000000000000000000de0b6b3a7640005, 0x0003000000000000000000000000000000000000000000000de0b6b3a7640003] 42 | recipient: ACROSS_ADAPTER, 43 | recipientCallback: [ 44 | { 45 | chainId: TARGET_CHAIN_ID, 46 | compact: { 47 | arbiter: TARGET_CHAIN_TRIBUNAL_CONTRACT, 48 | sponsor: SWAPPER, 49 | nonce: TARGET_CLAIM_NONCE, 50 | expires: TARGET_CLAIM_EXPIRATION, 51 | commitments: [ 52 | { 53 | lockTag: ONCHAIN_ALLOCATOR_LOCK_TAG_WITH_DEFAULT_PARAMS, 54 | token: ETH, 55 | amount: PLACEHOLDER_VALUE_POPULATED_BY_ADAPTER 56 | } 57 | ], 58 | mandate: { 59 | adjuster: UNISWAP_TEE, 60 | fills: [ 61 | { 62 | chainId: TARGET_CHAIN_ID, 63 | tribunal: TARGET_CHAIN_TRIBUNAL_CONTRACT, 64 | expires; TARGET_CHAIN_FILL_EXPIRATION, 65 | fillToken: TARGET_CHAIN_USDC, 66 | minimumFillAmount: 10_010_000, 67 | baselinePriorityFee; 1_000_000_000, 68 | scalingFactor; 0x00000000000000000000000000000000000000000000000000000de0b6b3a7640005, 69 | priceCurve: [0x0002000000000000000000000000000000000000000000000de0b6b3a7640002, 0x0003000000000000000000000000000000000000000000000de0b6b3a7640001] 70 | recipient: SWAPPER, 71 | recipientCallback: [], 72 | salt: 0x1234567890123456789012345678901234567890123456789012345678901234 73 | } 74 | ] 75 | } 76 | }, 77 | context: "0x" 78 | } 79 | ], 80 | salt: 0x1234567890123456789012345678901234567890123456789012345678901234 81 | }, 82 | ] 83 | } 84 | } 85 | */ 86 | 87 | // Type string constants extracted from Tribunal.sol 88 | string constant MANDATE_TYPESTRING = 89 | "Mandate(address adjuster,Mandate_Fill[] fills)Mandate_BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Mandate_Lock[] commitments,Mandate mandate)Mandate_Fill(uint256 chainId,address tribunal,uint256 expires,Mandate_FillComponent[] components,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] priceCurve,Mandate_RecipientCallback[] recipientCallback,bytes32 salt)Mandate_FillComponent(address fillToken,uint256 minimumFillAmount,address recipient,bool applyScaling)Mandate_Lock(bytes12 lockTag,address token,uint256 amount)Mandate_RecipientCallback(uint256 chainId,Mandate_BatchCompact compact,bytes context)"; 90 | 91 | string constant MANDATE_FILL_TYPESTRING = 92 | "Mandate_Fill(uint256 chainId,address tribunal,uint256 expires,Mandate_FillComponent[] components,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] priceCurve,Mandate_RecipientCallback[] recipientCallback,bytes32 salt)Mandate(address adjuster,Mandate_Fill[] fills)Mandate_BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Mandate_Lock[] commitments,Mandate mandate)Mandate_FillComponent(address fillToken,uint256 minimumFillAmount,address recipient,bool applyScaling)Mandate_Lock(bytes12 lockTag,address token,uint256 amount)Mandate_RecipientCallback(uint256 chainId,Mandate_BatchCompact compact,bytes context)"; 93 | 94 | string constant MANDATE_FILL_COMPONENT_TYPESTRING = 95 | "Mandate_FillComponent(address fillToken,uint256 minimumFillAmount,address recipient,bool applyScaling)"; 96 | 97 | string constant MANDATE_RECIPIENT_CALLBACK_TYPESTRING = 98 | "Mandate_RecipientCallback(uint256 chainId,Mandate_BatchCompact compact,bytes context)Mandate(address adjuster,Mandate_Fill[] fills)Mandate_BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Mandate_Lock[] commitments,Mandate mandate)Mandate_Fill(uint256 chainId,address tribunal,uint256 expires,Mandate_FillComponent[] components,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] priceCurve,Mandate_RecipientCallback[] recipientCallback,bytes32 salt)Mandate_FillComponent(address fillToken,uint256 minimumFillAmount,address recipient,bool applyScaling)Mandate_Lock(bytes12 lockTag,address token,uint256 amount)"; 99 | 100 | string constant MANDATE_BATCH_COMPACT_TYPESTRING = 101 | "Mandate_BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Mandate_Lock[] commitments,Mandate mandate)Mandate(address adjuster,Mandate_Fill[] fills)Mandate_Fill(uint256 chainId,address tribunal,uint256 expires,Mandate_FillComponent[] components,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] priceCurve,Mandate_RecipientCallback[] recipientCallback,bytes32 salt)Mandate_FillComponent(address fillToken,uint256 minimumFillAmount,address recipient,bool applyScaling)Mandate_Lock(bytes12 lockTag,address token,uint256 amount)Mandate_RecipientCallback(uint256 chainId,Mandate_BatchCompact compact,bytes context)"; 102 | 103 | string constant MANDATE_LOCK_TYPESTRING = 104 | "Mandate_Lock(bytes12 lockTag,address token,uint256 amount)"; 105 | 106 | string constant COMPACT_WITH_MANDATE_TYPESTRING = 107 | "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(address adjuster,Mandate_Fill[] fills)Mandate_BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Mandate_Lock[] commitments,Mandate mandate)Mandate_Fill(uint256 chainId,address tribunal,uint256 expires,Mandate_FillComponent[] components,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] priceCurve,Mandate_RecipientCallback[] recipientCallback,bytes32 salt)Mandate_FillComponent(address fillToken,uint256 minimumFillAmount,address recipient,bool applyScaling)Mandate_Lock(bytes12 lockTag,address token,uint256 amount)Mandate_RecipientCallback(uint256 chainId,Mandate_BatchCompact compact,bytes context)"; 108 | 109 | string constant ADJUSTMENT_TYPESTRING = 110 | "Adjustment(bytes32 claimHash,uint256 fillIndex,uint256 targetBlock,uint256[] supplementalPriceCurve,bytes32 validityConditions)"; 111 | 112 | // Typehash constants (hardcoded to reduce init code size) 113 | bytes32 constant MANDATE_TYPEHASH = 114 | 0xd98eceb6e5c7770b3b664a99c269855402fe5255294a30970d25376caea662c6; 115 | 116 | bytes32 constant MANDATE_FILL_TYPEHASH = 117 | 0x1d0ee69a7bc1ac54d9a6b38f32ab156fbfe09a9098843d54f89e7b1033533d33; 118 | 119 | bytes32 constant MANDATE_FILL_COMPONENT_TYPEHASH = 120 | 0x97a135285706d21a6b74ac159b77b16cea827acc358fc6c33e430ce0a85fe9d6; 121 | 122 | bytes32 constant MANDATE_RECIPIENT_CALLBACK_TYPEHASH = 123 | 0xb60a17eb6828a433f2f2fcbeb119166fa25e1fb6ae3866e33952bb74f5055031; 124 | 125 | bytes32 constant MANDATE_BATCH_COMPACT_TYPEHASH = 126 | 0x75d7205b7ec9e9b203d9161387d95a46c8440f4530dceab1bb28d4194a586227; 127 | 128 | bytes32 constant MANDATE_LOCK_TYPEHASH = 129 | 0xce4f0854d9091f37d9dfb64592eee0de534c6680a5444fd55739b61228a6e0b0; 130 | 131 | bytes32 constant COMPACT_TYPEHASH_WITH_MANDATE = 132 | 0xdbbdcf42471b4a26f7824df9f33f0a4f9bb4e7a66be6a31be8868a6cbbec0a7d; 133 | 134 | bytes32 constant ADJUSTMENT_TYPEHASH = 135 | 0xe829b2a82439f37ac7578a226e337d334e0ee0da2f05ab63891c19cb84714414; 136 | 137 | // Witness typestring (partial string that is provided to The Compact by Tribunal to process claims) 138 | string constant WITNESS_TYPESTRING = 139 | "address adjuster,Mandate_Fill[] fills)Mandate_BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Mandate_Lock[] commitments,Mandate mandate)Mandate_Fill(uint256 chainId,address tribunal,uint256 expires,Mandate_FillComponent[] components,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] priceCurve,Mandate_RecipientCallback[] recipientCallback,bytes32 salt)Mandate_FillComponent(address fillToken,uint256 minimumFillAmount,address recipient,bool applyScaling)Mandate_Lock(bytes12 lockTag,address token,uint256 amount)Mandate_RecipientCallback(uint256 chainId,Mandate_BatchCompact compact,bytes context"; 140 | -------------------------------------------------------------------------------- /test/mocks/MockAllocator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {IOnChainAllocation} from "the-compact/src/interfaces/IOnChainAllocation.sol"; 5 | 6 | /** 7 | * @title MockAllocator 8 | * @notice Mock allocator contract for testing _handleOnChainAllocation 9 | * @dev Validates that expected calldata is received in prepareAllocation and executeAllocation 10 | */ 11 | contract MockAllocator is IOnChainAllocation { 12 | // Storage for validation 13 | struct PrepareAllocationCall { 14 | address recipient; 15 | uint256[2][] idsAndAmounts; 16 | address arbiter; 17 | uint256 expires; 18 | bytes32 typehash; 19 | bytes32 witness; 20 | bytes orderData; 21 | bool wasCalled; 22 | } 23 | 24 | struct ExecuteAllocationCall { 25 | address recipient; 26 | uint256[2][] idsAndAmounts; 27 | address arbiter; 28 | uint256 expires; 29 | bytes32 typehash; 30 | bytes32 witness; 31 | bytes orderData; 32 | bool wasCalled; 33 | } 34 | 35 | PrepareAllocationCall public lastPrepareCall; 36 | ExecuteAllocationCall public lastExecuteCall; 37 | 38 | uint256 public nonceToReturn; 39 | bool public shouldRevertOnPrepare; 40 | bool public shouldRevertOnExecute; 41 | 42 | constructor() { 43 | nonceToReturn = 1; // Default nonce 44 | } 45 | 46 | /** 47 | * @notice Sets the nonce to return from prepareAllocation 48 | */ 49 | function setNonceToReturn(uint256 _nonce) external { 50 | nonceToReturn = _nonce; 51 | } 52 | 53 | /** 54 | * @notice Configure revert behavior 55 | */ 56 | function setShouldRevert(bool _shouldRevertOnPrepare, bool _shouldRevertOnExecute) external { 57 | shouldRevertOnPrepare = _shouldRevertOnPrepare; 58 | shouldRevertOnExecute = _shouldRevertOnExecute; 59 | } 60 | 61 | /** 62 | * @notice Reset call tracking 63 | */ 64 | function reset() external { 65 | delete lastPrepareCall; 66 | delete lastExecuteCall; 67 | } 68 | 69 | /** 70 | * @notice Get prepare call data 71 | */ 72 | function getPrepareCall() 73 | external 74 | view 75 | returns ( 76 | address recipient, 77 | address arbiter, 78 | uint256 expires, 79 | bytes32 typehash, 80 | bytes32 witness, 81 | bytes memory orderData, 82 | bool wasCalled 83 | ) 84 | { 85 | return ( 86 | lastPrepareCall.recipient, 87 | lastPrepareCall.arbiter, 88 | lastPrepareCall.expires, 89 | lastPrepareCall.typehash, 90 | lastPrepareCall.witness, 91 | lastPrepareCall.orderData, 92 | lastPrepareCall.wasCalled 93 | ); 94 | } 95 | 96 | /** 97 | * @notice Get execute call data 98 | */ 99 | function getExecuteCall() 100 | external 101 | view 102 | returns ( 103 | address recipient, 104 | address arbiter, 105 | uint256 expires, 106 | bytes32 typehash, 107 | bytes32 witness, 108 | bytes memory orderData, 109 | bool wasCalled 110 | ) 111 | { 112 | return ( 113 | lastExecuteCall.recipient, 114 | lastExecuteCall.arbiter, 115 | lastExecuteCall.expires, 116 | lastExecuteCall.typehash, 117 | lastExecuteCall.witness, 118 | lastExecuteCall.orderData, 119 | lastExecuteCall.wasCalled 120 | ); 121 | } 122 | 123 | /** 124 | * @notice Get prepare call idsAndAmounts 125 | */ 126 | function getPrepareIdsAndAmounts() external view returns (uint256[2][] memory) { 127 | return lastPrepareCall.idsAndAmounts; 128 | } 129 | 130 | /** 131 | * @notice Get execute call idsAndAmounts 132 | */ 133 | function getExecuteIdsAndAmounts() external view returns (uint256[2][] memory) { 134 | return lastExecuteCall.idsAndAmounts; 135 | } 136 | 137 | /** 138 | * @inheritdoc IOnChainAllocation 139 | */ 140 | function prepareAllocation( 141 | address recipient, 142 | uint256[2][] calldata idsAndAmounts, 143 | address arbiter, 144 | uint256 expires, 145 | bytes32 typehash, 146 | bytes32 witness, 147 | bytes calldata orderData 148 | ) external override returns (uint256 nonce) { 149 | if (shouldRevertOnPrepare) { 150 | revert InvalidPreparation(); 151 | } 152 | 153 | // Store the call data for validation 154 | lastPrepareCall.recipient = recipient; 155 | lastPrepareCall.arbiter = arbiter; 156 | lastPrepareCall.expires = expires; 157 | lastPrepareCall.typehash = typehash; 158 | lastPrepareCall.witness = witness; 159 | lastPrepareCall.orderData = orderData; 160 | lastPrepareCall.wasCalled = true; 161 | 162 | // Copy idsAndAmounts array 163 | delete lastPrepareCall.idsAndAmounts; 164 | for (uint256 i = 0; i < idsAndAmounts.length; i++) { 165 | lastPrepareCall.idsAndAmounts.push(idsAndAmounts[i]); 166 | } 167 | 168 | return nonceToReturn; 169 | } 170 | 171 | /** 172 | * @inheritdoc IOnChainAllocation 173 | */ 174 | function executeAllocation( 175 | address recipient, 176 | uint256[2][] calldata idsAndAmounts, 177 | address arbiter, 178 | uint256 expires, 179 | bytes32 typehash, 180 | bytes32 witness, 181 | bytes calldata orderData 182 | ) external override { 183 | if (shouldRevertOnExecute) { 184 | revert("MockAllocator: executeAllocation reverted"); 185 | } 186 | 187 | // Store the call data for validation 188 | lastExecuteCall.recipient = recipient; 189 | lastExecuteCall.arbiter = arbiter; 190 | lastExecuteCall.expires = expires; 191 | lastExecuteCall.typehash = typehash; 192 | lastExecuteCall.witness = witness; 193 | lastExecuteCall.orderData = orderData; 194 | lastExecuteCall.wasCalled = true; 195 | 196 | // Copy idsAndAmounts array 197 | delete lastExecuteCall.idsAndAmounts; 198 | for (uint256 i = 0; i < idsAndAmounts.length; i++) { 199 | lastExecuteCall.idsAndAmounts.push(idsAndAmounts[i]); 200 | } 201 | 202 | // Emit event 203 | emit Allocated(recipient, new Lock[](0), nonceToReturn, expires, witness); 204 | } 205 | 206 | /** 207 | * @notice Helper to verify prepareAllocation was called with expected params 208 | */ 209 | function verifyPrepareAllocationCall( 210 | address expectedRecipient, 211 | uint256[2][] memory expectedIdsAndAmounts, 212 | address expectedArbiter, 213 | uint256 expectedExpires, 214 | bytes32 expectedTypehash, 215 | bytes32 expectedWitness, 216 | bytes memory expectedOrderData 217 | ) external view returns (bool) { 218 | if (!lastPrepareCall.wasCalled) return false; 219 | if (lastPrepareCall.recipient != expectedRecipient) return false; 220 | if (lastPrepareCall.arbiter != expectedArbiter) return false; 221 | if (lastPrepareCall.expires != expectedExpires) return false; 222 | if (lastPrepareCall.typehash != expectedTypehash) return false; 223 | if (lastPrepareCall.witness != expectedWitness) return false; 224 | if (keccak256(lastPrepareCall.orderData) != keccak256(expectedOrderData)) return false; 225 | if (lastPrepareCall.idsAndAmounts.length != expectedIdsAndAmounts.length) return false; 226 | 227 | for (uint256 i = 0; i < expectedIdsAndAmounts.length; i++) { 228 | if (lastPrepareCall.idsAndAmounts[i][0] != expectedIdsAndAmounts[i][0]) return false; 229 | if (lastPrepareCall.idsAndAmounts[i][1] != expectedIdsAndAmounts[i][1]) return false; 230 | } 231 | 232 | return true; 233 | } 234 | 235 | /** 236 | * @notice Helper to verify executeAllocation was called with expected params 237 | */ 238 | function verifyExecuteAllocationCall( 239 | address expectedRecipient, 240 | uint256[2][] memory expectedIdsAndAmounts, 241 | address expectedArbiter, 242 | uint256 expectedExpires, 243 | bytes32 expectedTypehash, 244 | bytes32 expectedWitness, 245 | bytes memory expectedOrderData 246 | ) external view returns (bool) { 247 | if (!lastExecuteCall.wasCalled) return false; 248 | if (lastExecuteCall.recipient != expectedRecipient) return false; 249 | if (lastExecuteCall.arbiter != expectedArbiter) return false; 250 | if (lastExecuteCall.expires != expectedExpires) return false; 251 | if (lastExecuteCall.typehash != expectedTypehash) return false; 252 | if (lastExecuteCall.witness != expectedWitness) return false; 253 | if (keccak256(lastExecuteCall.orderData) != keccak256(expectedOrderData)) return false; 254 | if (lastExecuteCall.idsAndAmounts.length != expectedIdsAndAmounts.length) return false; 255 | 256 | for (uint256 i = 0; i < expectedIdsAndAmounts.length; i++) { 257 | if (lastExecuteCall.idsAndAmounts[i][0] != expectedIdsAndAmounts[i][0]) return false; 258 | if (lastExecuteCall.idsAndAmounts[i][1] != expectedIdsAndAmounts[i][1]) return false; 259 | } 260 | 261 | return true; 262 | } 263 | 264 | // ============ IAllocator Functions ============ 265 | // These are required by the interface but not used in our tests 266 | 267 | function attest(address, address, address, uint256, uint256) 268 | external 269 | pure 270 | override 271 | returns (bytes4) 272 | { 273 | return this.attest.selector; 274 | } 275 | 276 | function authorizeClaim( 277 | bytes32, 278 | address, 279 | address, 280 | uint256, 281 | uint256, 282 | uint256[2][] calldata, 283 | bytes calldata 284 | ) external pure override returns (bytes4) { 285 | return this.authorizeClaim.selector; 286 | } 287 | 288 | function isClaimAuthorized( 289 | bytes32, 290 | address, 291 | address, 292 | uint256, 293 | uint256, 294 | uint256[2][] calldata, 295 | bytes calldata 296 | ) external pure override returns (bool) { 297 | return true; 298 | } 299 | } 300 | 301 | // Import Lock type for the event 302 | import {Lock} from "the-compact/src/types/EIP712Types.sol"; 303 | -------------------------------------------------------------------------------- /test/TribunalClaimAndFillInvalidAdjusterTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Tribunal} from "../src/Tribunal.sol"; 6 | import {ITribunal} from "../src/interfaces/ITribunal.sol"; 7 | import {DeployTheCompact} from "./helpers/DeployTheCompact.sol"; 8 | import {TheCompact} from "the-compact/src/TheCompact.sol"; 9 | import {MockERC20} from "./mocks/MockERC20.sol"; 10 | import {ITribunalCallback} from "../src/interfaces/ITribunalCallback.sol"; 11 | import { 12 | Mandate, 13 | FillParameters, 14 | FillComponent, 15 | FillRequirement, 16 | Adjustment, 17 | RecipientCallback, 18 | BatchClaim 19 | } from "../src/types/TribunalStructs.sol"; 20 | import {BatchCompact, Lock, LOCK_TYPEHASH} from "the-compact/src/types/EIP712Types.sol"; 21 | import {ADJUSTMENT_TYPEHASH} from "../src/types/TribunalTypeHashes.sol"; 22 | 23 | /** 24 | * @title TribunalClaimAndFillInvalidAdjusterTest 25 | * @notice Test coverage for invalid adjuster authorization during claimAndFill execution 26 | * @dev Covers lines 841-847 in Tribunal.sol (_claimAndFill function) 27 | */ 28 | contract TribunalClaimAndFillInvalidAdjusterTest is DeployTheCompact, ITribunalCallback { 29 | Tribunal public tribunal; 30 | TheCompact public compactContract; 31 | MockERC20 public token; 32 | address sponsor; 33 | uint256 sponsorPrivateKey; 34 | address adjuster; 35 | uint256 adjusterPrivateKey; 36 | uint96 allocatorId; 37 | 38 | uint256[] public emptyPriceCurve; 39 | 40 | receive() external payable {} 41 | 42 | function setUp() public { 43 | compactContract = deployTheCompact(); 44 | 45 | // Register an allocator for same-chain fills 46 | vm.prank(address(this)); 47 | allocatorId = compactContract.__registerAllocator(address(this), ""); 48 | 49 | tribunal = new Tribunal(); 50 | token = new MockERC20(); 51 | (sponsor, sponsorPrivateKey) = makeAddrAndKey("sponsor"); 52 | (adjuster, adjusterPrivateKey) = makeAddrAndKey("adjuster"); 53 | 54 | emptyPriceCurve = new uint256[](0); 55 | 56 | // Fund accounts 57 | vm.deal(sponsor, 100 ether); 58 | vm.deal(address(this), 100 ether); 59 | 60 | // Transfer tokens to sponsor and test contract (which will be the filler) 61 | token.transfer(sponsor, 200e18); 62 | token.transfer(address(this), 100e18); 63 | 64 | // Sponsor deposits tokens 65 | vm.startPrank(sponsor); 66 | token.approve(address(compactContract), 200e18); 67 | compactContract.depositERC20(address(token), bytes12(uint96(allocatorId)), 200e18, sponsor); 68 | vm.stopPrank(); 69 | 70 | // Test contract (filler) approves tribunal 71 | token.approve(address(tribunal), type(uint256).max); 72 | } 73 | 74 | // Implement ITribunalCallback 75 | function tribunalCallback( 76 | bytes32, 77 | Lock[] calldata, 78 | uint256[] calldata, 79 | FillRequirement[] calldata 80 | ) external { 81 | // Empty implementation for testing 82 | } 83 | 84 | // Implement allocator interface for TheCompact 85 | function authorizeClaim( 86 | bytes32, 87 | address, 88 | address, 89 | uint256, 90 | uint256, 91 | uint256[2][] calldata, 92 | bytes calldata 93 | ) external pure returns (bytes32) { 94 | return this.authorizeClaim.selector; 95 | } 96 | 97 | /** 98 | * @notice Test that claimAndFill reverts with InvalidAdjustment when adjustment authorization is invalid 99 | * @dev This covers the uncovered lines 841-847 in the _claimAndFill function 100 | */ 101 | function test_ClaimAndFill_InvalidAdjusterAuthorization() public { 102 | BatchCompact memory compact = _createCompact(); 103 | FillParameters memory fillParams = _createFillParams(); 104 | bytes32 mandateHash = _deriveMandateHash(fillParams); 105 | 106 | BatchClaim memory claim = _createBatchClaim(compact, mandateHash); 107 | bytes32[] memory fillHashes = _createFillHashes(fillParams); 108 | Adjustment memory adjustment = _createInvalidAdjustment(compact, mandateHash); 109 | 110 | // Attempt claimAndFill - should revert with InvalidAdjustment 111 | vm.expectRevert(ITribunal.InvalidAdjustment.selector); 112 | tribunal.claimAndFill( 113 | claim, fillParams, adjustment, fillHashes, bytes32(uint256(uint160(address(this)))), 0 114 | ); 115 | } 116 | 117 | function _createCompact() internal view returns (BatchCompact memory) { 118 | Lock[] memory commitments = new Lock[](1); 119 | commitments[0] = 120 | Lock({lockTag: bytes12(uint96(allocatorId)), token: address(token), amount: 100e18}); 121 | 122 | return BatchCompact({ 123 | arbiter: address(tribunal), 124 | sponsor: sponsor, 125 | nonce: 0, 126 | expires: block.timestamp + 1 hours, 127 | commitments: commitments 128 | }); 129 | } 130 | 131 | function _createFillParams() internal view returns (FillParameters memory) { 132 | FillComponent[] memory components = new FillComponent[](1); 133 | components[0] = FillComponent({ 134 | fillToken: address(token), 135 | minimumFillAmount: 100e18, 136 | recipient: address(0xBEEF), 137 | applyScaling: false 138 | }); 139 | 140 | return FillParameters({ 141 | chainId: block.chainid, 142 | tribunal: address(tribunal), 143 | expires: uint256(block.timestamp + 1), 144 | components: components, 145 | baselinePriorityFee: 0, 146 | scalingFactor: 1e18, 147 | priceCurve: emptyPriceCurve, 148 | recipientCallback: new RecipientCallback[](0), 149 | salt: bytes32(uint256(1)) 150 | }); 151 | } 152 | 153 | function _deriveMandateHash(FillParameters memory fillParams) internal view returns (bytes32) { 154 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 155 | mandate.fills[0] = fillParams; 156 | return tribunal.deriveMandateHash(mandate); 157 | } 158 | 159 | function _createBatchClaim(BatchCompact memory compact, bytes32 mandateHash) 160 | internal 161 | view 162 | returns (BatchClaim memory) 163 | { 164 | bytes memory sponsorSig = _generateSponsorSignature(compact, mandateHash); 165 | return BatchClaim({ 166 | compact: compact, sponsorSignature: sponsorSig, allocatorSignature: new bytes(0) 167 | }); 168 | } 169 | 170 | function _createFillHashes(FillParameters memory fillParams) 171 | internal 172 | view 173 | returns (bytes32[] memory) 174 | { 175 | bytes32[] memory fillHashes = new bytes32[](1); 176 | fillHashes[0] = tribunal.deriveFillHash(fillParams); 177 | return fillHashes; 178 | } 179 | 180 | function _createInvalidAdjustment(BatchCompact memory compact, bytes32 mandateHash) 181 | internal 182 | returns (Adjustment memory) 183 | { 184 | Adjustment memory adjustment = Adjustment({ 185 | adjuster: adjuster, 186 | fillIndex: 0, 187 | targetBlock: vm.getBlockNumber(), 188 | supplementalPriceCurve: new uint256[](0), 189 | validityConditions: bytes32(0), 190 | adjustmentAuthorization: "" 191 | }); 192 | 193 | // Sign with wrong private key to create invalid signature 194 | (, uint256 wrongPrivateKey) = makeAddrAndKey("wrongAdjuster"); 195 | bytes32 claimHash = tribunal.deriveClaimHash(compact, mandateHash); 196 | adjustment.adjustmentAuthorization = _signAdjustment(adjustment, claimHash, wrongPrivateKey); 197 | 198 | return adjustment; 199 | } 200 | 201 | function _signAdjustment(Adjustment memory adjustment, bytes32 claimHash, uint256 privateKey) 202 | internal 203 | view 204 | returns (bytes memory) 205 | { 206 | bytes32 adjustmentHash = keccak256( 207 | abi.encode( 208 | ADJUSTMENT_TYPEHASH, 209 | claimHash, 210 | adjustment.fillIndex, 211 | adjustment.targetBlock, 212 | keccak256(abi.encodePacked(adjustment.supplementalPriceCurve)), 213 | adjustment.validityConditions 214 | ) 215 | ); 216 | 217 | bytes32 domainSeparator = keccak256( 218 | abi.encode( 219 | keccak256( 220 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 221 | ), 222 | keccak256("Tribunal"), 223 | keccak256("1"), 224 | block.chainid, 225 | address(tribunal) 226 | ) 227 | ); 228 | 229 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, adjustmentHash)); 230 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); 231 | return abi.encodePacked(r, s, v); 232 | } 233 | 234 | function _generateSponsorSignature(BatchCompact memory compact, bytes32 mandateHash) 235 | internal 236 | view 237 | returns (bytes memory) 238 | { 239 | string memory witnessTypestring = 240 | "address adjuster,Mandate_Fill[] fills)Mandate_BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Mandate_Lock[] commitments,Mandate mandate)Mandate_Fill(uint256 chainId,address tribunal,uint256 expires,Mandate_FillComponent[] components,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] priceCurve,Mandate_RecipientCallback[] recipientCallback,bytes32 salt)Mandate_FillComponent(address fillToken,uint256 minimumFillAmount,address recipient,bool applyScaling)Mandate_Lock(bytes12 lockTag,address token,uint256 amount)Mandate_RecipientCallback(uint256 chainId,Mandate_BatchCompact compact,bytes context"; 241 | 242 | string memory fullTypestring = string.concat( 243 | "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(", 244 | witnessTypestring, 245 | ")" 246 | ); 247 | 248 | bytes32 computedTypehash = keccak256(bytes(fullTypestring)); 249 | bytes32 commitmentsHash = _deriveCommitmentsHash(compact.commitments); 250 | 251 | bytes32 structHash = keccak256( 252 | abi.encode( 253 | computedTypehash, 254 | compact.arbiter, 255 | compact.sponsor, 256 | compact.nonce, 257 | compact.expires, 258 | commitmentsHash, 259 | mandateHash 260 | ) 261 | ); 262 | 263 | bytes32 domainSeparator = compactContract.DOMAIN_SEPARATOR(); 264 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); 265 | 266 | (bytes32 r, bytes32 vs) = vm.signCompact(sponsorPrivateKey, digest); 267 | return abi.encodePacked(r, vs); 268 | } 269 | 270 | function _deriveCommitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { 271 | bytes32[] memory lockHashes = new bytes32[](commitments.length); 272 | for (uint256 i = 0; i < commitments.length; i++) { 273 | lockHashes[i] = keccak256( 274 | abi.encode( 275 | LOCK_TYPEHASH, 276 | commitments[i].lockTag, 277 | commitments[i].token, 278 | commitments[i].amount 279 | ) 280 | ); 281 | } 282 | return keccak256(abi.encodePacked(lockHashes)); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /test/TribunalSettleOrRegisterTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Tribunal} from "../src/Tribunal.sol"; 6 | import {ERC7683Tribunal} from "../src/ERC7683Tribunal.sol"; 7 | import {MockERC20} from "./mocks/MockERC20.sol"; 8 | import {MockTheCompact} from "./mocks/MockTheCompact.sol"; 9 | import {ITribunal} from "../src/interfaces/ITribunal.sol"; 10 | import { 11 | Mandate, 12 | FillParameters, 13 | FillComponent, 14 | Adjustment, 15 | BatchClaim, 16 | RecipientCallback 17 | } from "../src/types/TribunalStructs.sol"; 18 | import {ADJUSTMENT_TYPEHASH} from "../src/types/TribunalTypeHashes.sol"; 19 | import {BatchCompact, Lock} from "the-compact/src/types/EIP712Types.sol"; 20 | 21 | /** 22 | * @title TribunalSettleOrRegisterTest 23 | * @notice Comprehensive tests for settleOrRegister function paths to improve coverage 24 | */ 25 | contract TribunalSettleOrRegisterTest is Test { 26 | ERC7683Tribunal public tribunal; 27 | MockERC20 public token; 28 | MockTheCompact public mockCompact; 29 | address public sponsor; 30 | address public filler; 31 | address public adjuster; 32 | uint256 public adjusterPrivateKey; 33 | address public arbiter; 34 | address public recipient; 35 | 36 | function setUp() public { 37 | tribunal = new ERC7683Tribunal(); 38 | token = new MockERC20(); 39 | sponsor = makeAddr("Sponsor"); 40 | filler = makeAddr("Filler"); 41 | (adjuster, adjusterPrivateKey) = makeAddrAndKey("Adjuster"); 42 | arbiter = makeAddr("Arbiter"); 43 | recipient = makeAddr("Recipient"); 44 | 45 | // Setup tokens 46 | token.mint(address(tribunal), 10 ether); 47 | vm.deal(address(tribunal), 10 ether); 48 | token.mint(filler, 100 ether); 49 | } 50 | 51 | // ============ Test _handleClaimantTransfer Coverage ============ 52 | 53 | /** 54 | * @notice Test settleOrRegister with existing claimant (ERC20 transfer) 55 | * @dev Covers lines 252-269 in Tribunal.sol (_handleClaimantTransfer ERC20 path) 56 | */ 57 | function test_SettleOrRegister_ExistingClaimant_ERC20() public { 58 | // First, create a fill to establish a claimant 59 | BatchCompact memory compact = _getBatchCompact(10 ether, address(token)); 60 | FillParameters memory fill = _getFillParameters(1 ether); 61 | Mandate memory mandate = _getMandate(fill); 62 | bytes32 mandateHash = tribunal.deriveMandateHash(mandate); 63 | bytes32 claimHash = tribunal.deriveClaimHash(compact, mandateHash); 64 | 65 | // Mock a fill by setting the disposition directly via storage manipulation 66 | bytes32 claimantBytes = bytes32(uint256(uint160(filler))); 67 | bytes32 dispositionSlot = keccak256(abi.encodePacked(claimHash, uint256(0))); 68 | vm.store(address(tribunal), dispositionSlot, claimantBytes); 69 | 70 | // Now call settleOrRegister - should transfer to claimant 71 | uint256 balanceBefore = token.balanceOf(filler); 72 | 73 | vm.prank(sponsor); 74 | bytes32 result = tribunal.settleOrRegister(claimHash, compact, mandateHash, recipient, ""); 75 | 76 | assertEq(result, bytes32(0), "Should return bytes32(0) for claimant transfer"); 77 | assertGt(token.balanceOf(filler), balanceBefore, "Claimant should receive tokens"); 78 | } 79 | 80 | /** 81 | * @notice Test settleOrRegister with existing claimant (native token transfer) 82 | * @dev Covers lines 252-269 in Tribunal.sol (_handleClaimantTransfer native path) 83 | */ 84 | function test_SettleOrRegister_ExistingClaimant_Native() public { 85 | // Create compact with native token (address(0)) 86 | BatchCompact memory compact = _getBatchCompact(5 ether, address(0)); 87 | FillParameters memory fill = _getFillParameters(1 ether); 88 | Mandate memory mandate = _getMandate(fill); 89 | bytes32 mandateHash = tribunal.deriveMandateHash(mandate); 90 | bytes32 claimHash = tribunal.deriveClaimHash(compact, mandateHash); 91 | 92 | // Mock a fill by setting the disposition 93 | bytes32 claimantBytes = bytes32(uint256(uint160(filler))); 94 | bytes32 dispositionSlot = keccak256(abi.encodePacked(claimHash, uint256(0))); 95 | vm.store(address(tribunal), dispositionSlot, claimantBytes); 96 | 97 | // Call settleOrRegister with native tokens 98 | uint256 balanceBefore = filler.balance; 99 | 100 | vm.prank(sponsor); 101 | bytes32 result = tribunal.settleOrRegister(claimHash, compact, mandateHash, recipient, ""); 102 | 103 | assertEq(result, bytes32(0), "Should return bytes32(0) for claimant transfer"); 104 | assertGt(filler.balance, balanceBefore, "Claimant should receive native tokens"); 105 | } 106 | 107 | // ============ Test _handleDirectTransfer Coverage ============ 108 | 109 | /** 110 | * @notice Test settleOrRegister with empty lockTag (direct ERC20 transfer) 111 | * @dev Covers lines 273-281 in Tribunal.sol (_handleDirectTransfer ERC20 path) 112 | */ 113 | function test_SettleOrRegister_DirectTransfer_ERC20() public { 114 | // Create compact with empty lockTag (direct transfer) 115 | Lock[] memory commitments = new Lock[](1); 116 | commitments[0] = Lock({ 117 | lockTag: bytes12(0), // Empty lockTag for direct transfer 118 | token: address(token), 119 | amount: 5 ether 120 | }); 121 | 122 | BatchCompact memory compact = BatchCompact({ 123 | arbiter: arbiter, 124 | sponsor: sponsor, 125 | nonce: 0, 126 | expires: uint256(block.timestamp + 1 days), 127 | commitments: commitments 128 | }); 129 | 130 | bytes32 sourceClaimHash = bytes32(uint256(1)); 131 | bytes32 mandateHash = bytes32(0); // No mandate hash needed for direct transfer 132 | 133 | vm.prank(sponsor); 134 | bytes32 result = 135 | tribunal.settleOrRegister(sourceClaimHash, compact, mandateHash, recipient, ""); 136 | 137 | assertEq(result, bytes32(0), "Should return bytes32(0) for direct transfer"); 138 | assertGt(token.balanceOf(recipient), 0, "Recipient should receive tokens"); 139 | } 140 | 141 | /** 142 | * @notice Test settleOrRegister with empty lockTag (direct native transfer) 143 | * @dev Covers lines 273-281 in Tribunal.sol (_handleDirectTransfer native path) 144 | */ 145 | function test_SettleOrRegister_DirectTransfer_Native() public { 146 | // Create compact with empty lockTag and native token 147 | Lock[] memory commitments = new Lock[](1); 148 | commitments[0] = Lock({ 149 | lockTag: bytes12(0), // Empty lockTag for direct transfer 150 | token: address(0), // Native token 151 | amount: 3 ether 152 | }); 153 | 154 | BatchCompact memory compact = BatchCompact({ 155 | arbiter: arbiter, 156 | sponsor: sponsor, 157 | nonce: 0, 158 | expires: uint256(block.timestamp + 1 days), 159 | commitments: commitments 160 | }); 161 | 162 | bytes32 sourceClaimHash = bytes32(uint256(1)); 163 | bytes32 mandateHash = bytes32(0); 164 | 165 | uint256 balanceBefore = recipient.balance; 166 | 167 | vm.prank(sponsor); 168 | bytes32 result = 169 | tribunal.settleOrRegister(sourceClaimHash, compact, mandateHash, recipient, ""); 170 | 171 | assertEq(result, bytes32(0), "Should return bytes32(0) for direct transfer"); 172 | assertGt(recipient.balance, balanceBefore, "Recipient should receive native tokens"); 173 | } 174 | 175 | /** 176 | * @notice Test settleOrRegister with empty lockTag and no recipient (defaults to sponsor) 177 | * @dev Covers line 268-269 assembly block for recipient default 178 | */ 179 | function test_SettleOrRegister_DirectTransfer_DefaultRecipient() public { 180 | Lock[] memory commitments = new Lock[](1); 181 | commitments[0] = Lock({lockTag: bytes12(0), token: address(token), amount: 2 ether}); 182 | 183 | BatchCompact memory compact = BatchCompact({ 184 | arbiter: arbiter, 185 | sponsor: sponsor, 186 | nonce: 0, 187 | expires: uint256(block.timestamp + 1 days), 188 | commitments: commitments 189 | }); 190 | 191 | bytes32 sourceClaimHash = bytes32(uint256(1)); 192 | bytes32 mandateHash = bytes32(0); 193 | 194 | uint256 balanceBefore = token.balanceOf(sponsor); 195 | 196 | vm.prank(sponsor); 197 | bytes32 result = tribunal.settleOrRegister( 198 | sourceClaimHash, 199 | compact, 200 | mandateHash, 201 | address(0), 202 | "" // No recipient specified 203 | ); 204 | 205 | assertEq(result, bytes32(0), "Should return bytes32(0)"); 206 | assertGt( 207 | token.balanceOf(sponsor), balanceBefore, "Sponsor should receive tokens as default" 208 | ); 209 | } 210 | 211 | // ============ Test InvalidCommitmentsArray Error ============ 212 | 213 | /** 214 | * @notice Test settleOrRegister reverts with multiple commitments 215 | * @dev Covers line 259-260 in Tribunal.sol 216 | */ 217 | function test_SettleOrRegister_Revert_MultipleCommitments() public { 218 | Lock[] memory commitments = new Lock[](2); 219 | commitments[0] = Lock({lockTag: bytes12(0), token: address(token), amount: 1 ether}); 220 | commitments[1] = Lock({lockTag: bytes12(0), token: address(token), amount: 1 ether}); 221 | 222 | BatchCompact memory compact = BatchCompact({ 223 | arbiter: arbiter, 224 | sponsor: sponsor, 225 | nonce: 0, 226 | expires: uint256(block.timestamp + 1 days), 227 | commitments: commitments 228 | }); 229 | 230 | bytes32 sourceClaimHash = bytes32(uint256(1)); 231 | bytes32 mandateHash = bytes32(0); 232 | 233 | vm.expectRevert(ITribunal.InvalidCommitmentsArray.selector); 234 | vm.prank(sponsor); 235 | tribunal.settleOrRegister(sourceClaimHash, compact, mandateHash, recipient, ""); 236 | } 237 | 238 | // ============ Helper Functions ============ 239 | 240 | function _getBatchCompact(uint256 amount, address tokenAddress) 241 | internal 242 | view 243 | returns (BatchCompact memory) 244 | { 245 | Lock[] memory commitments = new Lock[](1); 246 | commitments[0] = Lock({ 247 | lockTag: bytes12(uint96(1)), // Non-zero lockTag 248 | token: tokenAddress, 249 | amount: amount 250 | }); 251 | 252 | return BatchCompact({ 253 | arbiter: arbiter, 254 | sponsor: sponsor, 255 | nonce: 1, 256 | expires: uint256(block.timestamp + 1 days), 257 | commitments: commitments 258 | }); 259 | } 260 | 261 | function _getFillParameters(uint256 minimumFillAmount) 262 | internal 263 | view 264 | returns (FillParameters memory) 265 | { 266 | FillComponent[] memory components = new FillComponent[](1); 267 | components[0] = FillComponent({ 268 | fillToken: address(token), 269 | minimumFillAmount: minimumFillAmount, 270 | recipient: sponsor, 271 | applyScaling: true 272 | }); 273 | 274 | return FillParameters({ 275 | chainId: block.chainid, 276 | tribunal: address(tribunal), 277 | expires: uint256(block.timestamp + 1 days), 278 | components: components, 279 | baselinePriorityFee: 100 wei, 280 | scalingFactor: 1e18, 281 | priceCurve: new uint256[](0), 282 | recipientCallback: new RecipientCallback[](0), 283 | salt: bytes32(uint256(1)) 284 | }); 285 | } 286 | 287 | function _getMandate(FillParameters memory fill) internal pure returns (Mandate memory) { 288 | FillParameters[] memory fills = new FillParameters[](1); 289 | fills[0] = fill; 290 | 291 | return Mandate({adjuster: address(0), fills: fills}); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /test/TribunalReentrancyTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Tribunal} from "../src/Tribunal.sol"; 6 | import {ITribunal} from "../src/interfaces/ITribunal.sol"; 7 | import {DeployTheCompact} from "./helpers/DeployTheCompact.sol"; 8 | import {TheCompact} from "the-compact/src/TheCompact.sol"; 9 | import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; 10 | import {ReentrantReceiver} from "./mocks/ReentrantReceiver.sol"; 11 | import {FillerContract} from "./mocks/FillerContract.sol"; 12 | import {MockTheCompact} from "./mocks/MockTheCompact.sol"; 13 | import {ITribunalCallback} from "../src/interfaces/ITribunalCallback.sol"; 14 | import { 15 | Mandate, 16 | FillParameters, 17 | FillComponent, 18 | FillRequirement, 19 | Adjustment, 20 | RecipientCallback, 21 | BatchClaim 22 | } from "../src/types/TribunalStructs.sol"; 23 | import {BatchCompact, Lock, LOCK_TYPEHASH} from "the-compact/src/types/EIP712Types.sol"; 24 | 25 | contract TribunalReentrancyTest is DeployTheCompact, ITribunalCallback { 26 | using FixedPointMathLib for uint256; 27 | 28 | struct ClaimAndFillArgs { 29 | BatchClaim claim; 30 | FillParameters fill; 31 | Adjustment adjustment; 32 | bytes32[] fillHashes; 33 | bytes32 claimant; 34 | uint256 value; 35 | } 36 | 37 | Tribunal public tribunal; 38 | TheCompact public compactContract; 39 | address sponsor; 40 | uint256 sponsorPrivateKey; 41 | address adjuster; 42 | uint256 adjusterPrivateKey; 43 | FillerContract public filler; 44 | uint96 allocatorId; 45 | 46 | uint256[] public emptyPriceCurve; 47 | 48 | receive() external payable {} 49 | 50 | function setUp() public { 51 | compactContract = deployTheCompact(); 52 | 53 | // Register an allocator for same-chain fills 54 | vm.prank(address(this)); 55 | allocatorId = compactContract.__registerAllocator(address(this), ""); 56 | 57 | tribunal = new Tribunal(); 58 | (sponsor, sponsorPrivateKey) = makeAddrAndKey("sponsor"); 59 | (adjuster, adjusterPrivateKey) = makeAddrAndKey("adjuster"); 60 | filler = new FillerContract(); 61 | 62 | emptyPriceCurve = new uint256[](0); 63 | 64 | // Fund the sponsor and filler 65 | vm.deal(sponsor, 10 ether); 66 | vm.deal(address(filler), 10 ether); 67 | } 68 | 69 | // Implement ITribunalCallback 70 | function tribunalCallback( 71 | bytes32, 72 | Lock[] calldata, 73 | uint256[] calldata, 74 | FillRequirement[] calldata 75 | ) external { 76 | // Empty implementation for testing 77 | } 78 | 79 | // Implement allocator interface for TheCompact 80 | function authorizeClaim( 81 | bytes32, 82 | address, 83 | address, 84 | uint256, 85 | uint256, 86 | uint256[2][] calldata, 87 | bytes calldata 88 | ) external pure returns (bytes32) { 89 | // Simply approve the claim 90 | return this.authorizeClaim.selector; 91 | } 92 | 93 | function _generateSponsorSignature(BatchCompact memory compact, bytes32 mandateHash) 94 | internal 95 | view 96 | returns (bytes memory) 97 | { 98 | // TheCompact constructs the full typestring by combining: 99 | // 1. BatchCompact prefix and "Mandate(" 100 | // 2. The witness typestring from Tribunal (which includes ) after arguments but no final )) 101 | // 3. A closing parenthesis at the very end 102 | 103 | // Import the actual witness typestring that Tribunal sends 104 | string memory witnessTypestring = 105 | "address adjuster,Mandate_Fill[] fills)Mandate_BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Mandate_Lock[] commitments,Mandate mandate)Mandate_Fill(uint256 chainId,address tribunal,uint256 expires,Mandate_FillComponent[] components,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] priceCurve,Mandate_RecipientCallback[] recipientCallback,bytes32 salt)Mandate_FillComponent(address fillToken,uint256 minimumFillAmount,address recipient,bool applyScaling)Mandate_Lock(bytes12 lockTag,address token,uint256 amount)Mandate_RecipientCallback(uint256 chainId,Mandate_BatchCompact compact,bytes context"; 106 | 107 | // Construct the full typestring as TheCompact would 108 | string memory fullTypestring = string.concat( 109 | "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(", 110 | witnessTypestring, 111 | ")" 112 | ); 113 | 114 | // Compute the typehash from the full typestring 115 | bytes32 computedTypehash = keccak256(bytes(fullTypestring)); 116 | 117 | // Generate the struct hash for the batch compact with mandate 118 | bytes32[] memory lockHashes = new bytes32[](compact.commitments.length); 119 | for (uint256 i = 0; i < compact.commitments.length; i++) { 120 | lockHashes[i] = keccak256( 121 | abi.encode( 122 | LOCK_TYPEHASH, 123 | compact.commitments[i].lockTag, 124 | compact.commitments[i].token, 125 | compact.commitments[i].amount 126 | ) 127 | ); 128 | } 129 | bytes32 commitmentsHash = keccak256(abi.encodePacked(lockHashes)); 130 | 131 | bytes32 structHash = keccak256( 132 | abi.encode( 133 | computedTypehash, 134 | compact.arbiter, 135 | compact.sponsor, 136 | compact.nonce, 137 | compact.expires, 138 | commitmentsHash, 139 | mandateHash 140 | ) 141 | ); 142 | 143 | // Get TheCompact's domain separator 144 | bytes32 domainSeparator = compactContract.DOMAIN_SEPARATOR(); 145 | 146 | // Create the EIP-712 digest 147 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); 148 | 149 | // Sign with compact format (r, vs) 150 | bytes32 r; 151 | bytes32 vs; 152 | (r, vs) = vm.signCompact(sponsorPrivateKey, digest); 153 | 154 | return abi.encodePacked(r, vs); 155 | } 156 | 157 | function test_FillWithReentrancyAttack() public { 158 | // Deposit native tokens to TheCompact for the sponsor 159 | vm.prank(sponsor); 160 | compactContract.depositNative{value: 2 ether}(bytes12(uint96(allocatorId)), sponsor); 161 | 162 | ReentrantReceiver reentrantReceiver = new ReentrantReceiver{value: 10 ether}(tribunal); 163 | 164 | ClaimAndFillArgs memory args; 165 | args.claimant = bytes32(uint256(uint160(address(filler)))); 166 | args.value = 0; 167 | 168 | uint256 minimumFillAmount; 169 | uint256 initialRecipientBalance = address(reentrantReceiver).balance; 170 | uint256 initialFillerBalance = address(filler).balance; 171 | 172 | // Block 1: Create fill parameters 173 | { 174 | FillComponent[] memory components = new FillComponent[](1); 175 | components[0] = FillComponent({ 176 | fillToken: address(0), 177 | minimumFillAmount: 1 ether, 178 | recipient: address(reentrantReceiver), 179 | applyScaling: false 180 | }); 181 | minimumFillAmount = components[0].minimumFillAmount; 182 | 183 | args.fill = FillParameters({ 184 | chainId: block.chainid, 185 | tribunal: address(tribunal), 186 | expires: uint256(block.timestamp + 1), 187 | components: components, 188 | baselinePriorityFee: 0, 189 | scalingFactor: 1e18, 190 | priceCurve: emptyPriceCurve, 191 | recipientCallback: new RecipientCallback[](0), 192 | salt: bytes32(uint256(1)) 193 | }); 194 | } 195 | 196 | // Block 2: Create claim and fillHashes 197 | { 198 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 199 | mandate.fills[0] = args.fill; 200 | 201 | Lock[] memory commitments = new Lock[](1); 202 | commitments[0] = 203 | Lock({lockTag: bytes12(uint96(allocatorId)), token: address(0), amount: 1 ether}); 204 | 205 | bytes32 mandateHash = tribunal.deriveMandateHash(mandate); 206 | 207 | bytes memory sponsorSig = _generateSponsorSignature( 208 | BatchCompact({ 209 | arbiter: address(tribunal), 210 | sponsor: sponsor, 211 | nonce: 0, 212 | expires: block.timestamp + 1 hours, 213 | commitments: commitments 214 | }), 215 | mandateHash 216 | ); 217 | 218 | args.claim = BatchClaim({ 219 | compact: BatchCompact({ 220 | arbiter: address(tribunal), 221 | sponsor: sponsor, 222 | nonce: 0, 223 | expires: block.timestamp + 1 hours, 224 | commitments: commitments 225 | }), 226 | sponsorSignature: sponsorSig, 227 | allocatorSignature: new bytes(0) 228 | }); 229 | 230 | args.fillHashes = new bytes32[](1); 231 | args.fillHashes[0] = tribunal.deriveFillHash(args.fill); 232 | } 233 | 234 | // Block 3: Create and sign adjustment 235 | { 236 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 237 | mandate.fills[0] = args.fill; 238 | bytes32 mandateHash = tribunal.deriveMandateHash(mandate); 239 | bytes32 claimHash = tribunal.deriveClaimHash(args.claim.compact, mandateHash); 240 | 241 | args.adjustment = Adjustment({ 242 | adjuster: adjuster, 243 | fillIndex: 0, 244 | targetBlock: vm.getBlockNumber(), 245 | supplementalPriceCurve: new uint256[](0), 246 | validityConditions: bytes32(0), 247 | adjustmentAuthorization: "" 248 | }); 249 | 250 | bytes32 adjustmentHash = keccak256( 251 | abi.encode( 252 | keccak256( 253 | "Adjustment(bytes32 claimHash,uint256 fillIndex,uint256 targetBlock,uint256[] supplementalPriceCurve,bytes32 validityConditions)" 254 | ), 255 | claimHash, 256 | args.adjustment.fillIndex, 257 | args.adjustment.targetBlock, 258 | keccak256(abi.encodePacked(args.adjustment.supplementalPriceCurve)), 259 | args.adjustment.validityConditions 260 | ) 261 | ); 262 | 263 | bytes32 domainSeparator = keccak256( 264 | abi.encode( 265 | keccak256( 266 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 267 | ), 268 | keccak256("Tribunal"), 269 | keccak256("1"), 270 | block.chainid, 271 | address(tribunal) 272 | ) 273 | ); 274 | 275 | bytes32 digest = 276 | keccak256(abi.encodePacked("\x19\x01", domainSeparator, adjustmentHash)); 277 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(adjusterPrivateKey, digest); 278 | args.adjustment.adjustmentAuthorization = abi.encodePacked(r, s, v); 279 | } 280 | 281 | // The first fill should succeed despite the reentrancy attempt 282 | // The ReentrantReceiver will try to reenter but will be blocked by the reentrancy guard 283 | vm.prank(address(filler)); 284 | tribunal.claimAndFill{ 285 | value: 1 ether 286 | }(args.claim, args.fill, args.adjustment, args.fillHashes, args.claimant, args.value); 287 | 288 | // Verify that the first fill succeeded and the recipient received the funds 289 | assertEq(address(reentrantReceiver).balance, initialRecipientBalance + minimumFillAmount); 290 | assertEq(address(filler).balance, initialFillerBalance); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /test/PriceCurveDocumentationTests.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Tribunal} from "../src/Tribunal.sol"; 6 | import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; 7 | import {PriceCurveLib, PriceCurveElement} from "../src/lib/PriceCurveLib.sol"; 8 | import { 9 | Mandate, 10 | FillParameters, 11 | Adjustment, 12 | RecipientCallback 13 | } from "../src/types/TribunalStructs.sol"; 14 | import {BatchCompact, Lock} from "the-compact/src/types/EIP712Types.sol"; 15 | import {PriceCurveTestHelper} from "./helpers/PriceCurveTestHelper.sol"; 16 | 17 | /** 18 | * @title PriceCurveDocumentationTests 19 | * @notice Tests that validate all examples from PRICE_CURVE_DOCUMENTATION.md 20 | * @dev Each test corresponds to a specific example in the documentation 21 | */ 22 | contract PriceCurveDocumentationTests is Test { 23 | using FixedPointMathLib for uint256; 24 | using PriceCurveLib for uint256[]; 25 | 26 | Tribunal public tribunal; 27 | address theCompact; 28 | PriceCurveTestHelper public helper; 29 | 30 | function setUp() public { 31 | theCompact = address(0xC0); 32 | tribunal = new Tribunal(); 33 | helper = new PriceCurveTestHelper(); 34 | } 35 | 36 | // ============ Tests for Documentation Examples ============ 37 | 38 | /** 39 | * @notice Test the Step Function with Plateaus example from documentation 40 | * @dev Documentation shows a 4-element curve with a zero-duration drop 41 | */ 42 | function test_Doc_StepFunctionWithPlateaus() public pure { 43 | uint256[] memory priceCurve = new uint256[](4); 44 | priceCurve[0] = (50 << 240) | uint256(1.5e18); // High price for 50 blocks 45 | priceCurve[1] = (0 << 240) | uint256(1.2e18); // Drop to 1.2x (zero-duration) 46 | priceCurve[2] = (50 << 240) | uint256(1.2e18); // Hold at 1.2x for 50 blocks 47 | priceCurve[3] = (50 << 240) | uint256(1e18); // Final decay to 1.0x 48 | 49 | // At block 25: should interpolate from 1.5 towards 1.2 (next zero-duration element) 50 | uint256 scalingAtBlock25 = priceCurve.getCalculatedValues(25); 51 | // Expected: 1.5 - (1.5 - 1.2) * (25/50) = 1.35 52 | assertEq(scalingAtBlock25, 1.35e18, "Block 25 scaling"); 53 | 54 | // At block 50: should be exactly 1.2x (zero-duration element) 55 | uint256 scalingAtBlock50 = priceCurve.getCalculatedValues(50); 56 | assertEq(scalingAtBlock50, 1.2e18, "Block 50 scaling"); 57 | 58 | // During plateau (block 75): ACTUAL BEHAVIOR - stays at 1.2 59 | // When zero-duration element has same value as next segment, it creates a true plateau 60 | uint256 scalingAtBlock75 = priceCurve.getCalculatedValues(75); 61 | // The implementation interpolates from segment 2's value (1.2) to segment 3's value (1.0) 62 | // But since segment 2 itself is 1.2, and the zero-duration is 1.2, it stays at 1.2 63 | assertEq(scalingAtBlock75, 1.2e18, "Block 75 holds at plateau"); 64 | 65 | // Documentation Note: This creates a true step function with a plateau at 1.2x 66 | // from blocks 50-100, then drops to segment 3 67 | } 68 | 69 | /** 70 | * @notice Test the Aggressive Initial Discount example from documentation 71 | */ 72 | function test_Doc_AggressiveInitialDiscount() public pure { 73 | uint256[] memory priceCurve = new uint256[](2); 74 | priceCurve[0] = (10 << 240) | uint256(5e17); // Start at 0.5x for 10 blocks 75 | priceCurve[1] = (90 << 240) | uint256(9e17); // Then 0.9x for 90 blocks 76 | // Total duration: 10 + 90 = 100 blocks 77 | 78 | // At block 0: should be 0.5x 79 | assertEq(priceCurve.getCalculatedValues(0), 0.5e18, "Initial discount"); 80 | 81 | // At block 5: midway through first segment 82 | uint256 scalingAtBlock5 = priceCurve.getCalculatedValues(5); 83 | // Expected: 0.5 + (0.9 - 0.5) * (5/10) = 0.7 84 | assertEq(scalingAtBlock5, 0.7e18, "Block 5 scaling"); 85 | 86 | // At block 10: start of second segment at 0.9x 87 | assertEq(priceCurve.getCalculatedValues(10), 0.9e18, "Start of gradual rise"); 88 | 89 | // At block 55: midway through second segment 90 | uint256 scalingAtBlock55 = priceCurve.getCalculatedValues(55); 91 | // Expected: 0.9 + (1.0 - 0.9) * (45/90) = 0.95 92 | assertEq(scalingAtBlock55, 0.95e18, "Block 55 scaling"); 93 | 94 | // At block 99: last valid block, should be close to 1.0x 95 | uint256 scalingAtBlock99 = priceCurve.getCalculatedValues(99); 96 | // Expected: 0.9 + (1.0 - 0.9) * (89/90) ≈ 0.9989 97 | assertApproxEqRel(scalingAtBlock99, 0.9989e18, 0.001e18, "Near end scaling"); 98 | } 99 | 100 | function test_Doc_AggressiveInitialDiscount_ExceedsDuration() public { 101 | uint256[] memory priceCurve = new uint256[](2); 102 | priceCurve[0] = (10 << 240) | uint256(5e17); 103 | priceCurve[1] = (90 << 240) | uint256(9e17); 104 | 105 | vm.expectRevert(PriceCurveLib.PriceCurveBlocksExceeded.selector); 106 | helper.getCalculatedValues(priceCurve, 100); 107 | } 108 | 109 | /** 110 | * @notice Test the Reverse Dutch Auction example from documentation 111 | */ 112 | function test_Doc_ReverseDutchAuction() public pure { 113 | uint256[] memory priceCurve = new uint256[](1); 114 | priceCurve[0] = (200 << 240) | uint256(2e18); // Start at 2x for 200 blocks 115 | 116 | // At block 0: should be 2x 117 | assertEq(priceCurve.getCalculatedValues(0), 2e18, "Start at 2x"); 118 | 119 | // At block 100: midway, should be 1.5x 120 | uint256 scalingAtBlock100 = priceCurve.getCalculatedValues(100); 121 | // Expected: 2.0 - (2.0 - 1.0) * (100/200) = 1.5 122 | assertEq(scalingAtBlock100, 1.5e18, "Midway at 1.5x"); 123 | 124 | // At block 199: last valid block, should be close to 1x 125 | uint256 scalingAtBlock199 = priceCurve.getCalculatedValues(199); 126 | // Expected: 2.0 - (2.0 - 1.0) * (199/200) = 1.005 127 | assertEq(scalingAtBlock199, 1.005e18, "Near end at ~1x"); 128 | } 129 | 130 | function test_Doc_ReverseDutchAuction_ExceedsDuration() public { 131 | uint256[] memory priceCurve = new uint256[](1); 132 | priceCurve[0] = (200 << 240) | uint256(2e18); 133 | 134 | vm.expectRevert(PriceCurveLib.PriceCurveBlocksExceeded.selector); 135 | helper.getCalculatedValues(priceCurve, 200); 136 | } 137 | 138 | /** 139 | * @notice Test the Complete Dutch Auction Example from documentation 140 | */ 141 | function test_Doc_CompleteDutchAuctionExample() public view { 142 | // Create the exact curve from documentation 143 | uint256[] memory curve = new uint256[](3); 144 | 145 | // First 30 blocks: Start at 1.5x, decay to 1.2x 146 | curve[0] = (30 << 240) | uint256(15e17); 147 | 148 | // Next 40 blocks: Continue from 1.2x to 1.0x 149 | curve[1] = (40 << 240) | uint256(12e17); 150 | 151 | // Final 30 blocks: Remain at minimum price (1.0x) 152 | curve[2] = (30 << 240) | uint256(1e18); 153 | // Total duration: 30 + 40 + 30 = 100 blocks 154 | 155 | // Test key points from the documentation timeline 156 | // Block 0: Auction starts at 1.5x scaling 157 | assertEq(curve.getCalculatedValues(0), 1.5e18, "Start at 1.5x"); 158 | 159 | // Block 30: Price at 1.2x (end of first segment) 160 | assertEq(curve.getCalculatedValues(30), 1.2e18, "Block 30 at 1.2x"); 161 | 162 | // Block 70: Price at 1.0x (end of second segment) 163 | assertEq(curve.getCalculatedValues(70), 1e18, "Block 70 at 1.0x"); 164 | 165 | // Block 99: Last valid fill block at 1.0x 166 | assertEq(curve.getCalculatedValues(99), 1e18, "Block 99 still at 1.0x"); 167 | 168 | // Test with Tribunal's deriveAmounts to verify integration 169 | Lock[] memory maximumClaimAmounts = new Lock[](1); 170 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 171 | 172 | // At block 15 (midway through first segment) 173 | (uint256 fillAmount15,) = tribunal.deriveAmounts( 174 | maximumClaimAmounts, 175 | curve, 176 | 1000000, // targetBlock as in example 177 | 1000015, // 15 blocks after target 178 | 1000e6, // minimumFillAmount as in example 179 | 1 gwei, // baselinePriorityFee as in example 180 | 15e17 // scalingFactor as in example (1.5x) 181 | ); 182 | 183 | // Expected scaling: 1.5 - (1.5-1.2) * (15/30) = 1.35 184 | // With exact-in mode and no priority fee above baseline 185 | uint256 expectedFill15 = uint256(1000e6).mulWadUp(1.35e18); 186 | assertEq(fillAmount15, expectedFill15, "Fill amount at block 15"); 187 | } 188 | 189 | function test_Doc_CompleteDutchAuctionExample_ExceedsDuration() public { 190 | uint256[] memory curve = new uint256[](3); 191 | curve[0] = (30 << 240) | uint256(15e17); 192 | curve[1] = (40 << 240) | uint256(12e17); 193 | curve[2] = (30 << 240) | uint256(1e18); 194 | 195 | vm.expectRevert(PriceCurveLib.PriceCurveBlocksExceeded.selector); 196 | helper.getCalculatedValues(curve, 100); 197 | } 198 | 199 | /** 200 | * @notice Test that validates multiple consecutive zero-duration behavior 201 | * @dev Confirms that only the first zero-duration element at a given block is used 202 | */ 203 | function test_Doc_MultipleConsecutiveZeroDurationVerification() public pure { 204 | uint256[] memory priceCurve = new uint256[](4); 205 | priceCurve[0] = (10 << 240) | uint256(1.2e18); 206 | priceCurve[1] = (0 << 240) | uint256(1.5e18); // First zero-duration at block 10 207 | priceCurve[2] = (0 << 240) | uint256(1.3e18); // Second zero-duration at block 10 (ignored) 208 | priceCurve[3] = (10 << 240) | uint256(1e18); 209 | 210 | // Test what actually happens at block 10 211 | uint256 scalingAtBlock10 = priceCurve.getCalculatedValues(10); 212 | 213 | // Confirms it uses the FIRST zero-duration element (1.5x), not subsequent ones 214 | assertEq(scalingAtBlock10, 1.5e18, "Uses first zero-duration element"); 215 | } 216 | 217 | /** 218 | * @notice Test Complex Multi-Phase Curve from documentation 219 | * @dev Tests a valid multi-phase curve where all segments stay on the same side of 1e18 220 | */ 221 | function test_Doc_ComplexMultiPhaseCurve_Corrected() public pure { 222 | // All segments must stay on the same side of 1e18 (cannot mix exact-in and exact-out) 223 | uint256[] memory priceCurve = new uint256[](3); 224 | priceCurve[0] = (30 << 240) | uint256(0.5e18); // Start at 0.5x 225 | priceCurve[1] = (40 << 240) | uint256(0.7e18); // Rise to 0.7x at block 30 226 | priceCurve[2] = (30 << 240) | uint256(0.8e18); // Rise to 0.8x at block 70 227 | 228 | // Block 15: interpolating from 0.5 to 0.7 229 | uint256 scalingAtBlock15 = priceCurve.getCalculatedValues(15); 230 | // Expected: 0.5 + (0.7 - 0.5) * (15/30) = 0.6 231 | assertEq(scalingAtBlock15, 0.6e18, "Block 15"); 232 | 233 | // Block 50: interpolating from 0.7 to 0.8 234 | uint256 scalingAtBlock50 = priceCurve.getCalculatedValues(50); 235 | // Expected: 0.7 + (0.8 - 0.7) * (20/40) = 0.75 236 | assertEq(scalingAtBlock50, 0.75e18, "Block 50"); 237 | 238 | // Block 85: interpolating from 0.8 to 1.0 239 | uint256 scalingAtBlock85 = priceCurve.getCalculatedValues(85); 240 | // Expected: 0.8 + (1.0 - 0.8) * (15/30) = 0.9 241 | assertEq(scalingAtBlock85, 0.9e18, "Block 85"); 242 | 243 | // Block 99: last valid block 244 | uint256 scalingAtBlock99 = priceCurve.getCalculatedValues(99); 245 | // Expected: 0.8 + (1.0 - 0.8) * (29/30) ≈ 0.9933 246 | assertApproxEqRel(scalingAtBlock99, 0.9933e18, 0.001e18, "Block 99"); 247 | } 248 | 249 | function test_Doc_ComplexMultiPhaseCurve_ExceedsDuration() public { 250 | uint256[] memory priceCurve = new uint256[](3); 251 | priceCurve[0] = (30 << 240) | uint256(0.5e18); 252 | priceCurve[1] = (40 << 240) | uint256(0.7e18); 253 | priceCurve[2] = (30 << 240) | uint256(0.8e18); 254 | 255 | vm.expectRevert(PriceCurveLib.PriceCurveBlocksExceeded.selector); 256 | helper.getCalculatedValues(priceCurve, 100); 257 | } 258 | 259 | /** 260 | * @notice Test that empty price curve with targetBlock returns neutral scaling 261 | * @dev Confirms empty price curve always returns 1e18 regardless of targetBlock 262 | */ 263 | function test_Doc_EmptyPriceCurveWithTargetBlock_ActualBehavior() public view { 264 | Lock[] memory maximumClaimAmounts = new Lock[](1); 265 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 266 | 267 | uint256[] memory priceCurve = new uint256[](0); 268 | 269 | // Empty price curve returns neutral scaling (1e18) even with non-zero targetBlock 270 | (uint256 fillAmount, uint256[] memory claimAmounts) = tribunal.deriveAmounts( 271 | maximumClaimAmounts, 272 | priceCurve, 273 | 100, // targetBlock != 0 274 | 200, // fillBlock 275 | 1 ether, 276 | 0, 277 | 1e18 278 | ); 279 | 280 | // Confirms neutral scaling behavior 281 | assertEq(fillAmount, 1 ether, "Neutral fill amount"); 282 | assertEq(claimAmounts[0], 1 ether, "Neutral claim amount"); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /test/TribunalViewFunctionsTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Tribunal} from "../src/Tribunal.sol"; 6 | import {ITribunal} from "../src/interfaces/ITribunal.sol"; 7 | import { 8 | Mandate, 9 | FillParameters, 10 | FillComponent, 11 | RecipientCallback, 12 | DispositionDetails 13 | } from "../src/types/TribunalStructs.sol"; 14 | import {BatchCompact, Lock} from "the-compact/src/types/EIP712Types.sol"; 15 | 16 | contract TribunalViewFunctionsTest is Test { 17 | Tribunal public tribunal; 18 | 19 | address public adjuster = address(0x3333); 20 | uint256[] public emptyPriceCurve; 21 | 22 | function setUp() public { 23 | tribunal = new Tribunal(); 24 | emptyPriceCurve = new uint256[](0); 25 | } 26 | 27 | function test_getDispositionDetails() public view { 28 | // Test with array of claim hashes 29 | bytes32[] memory claimHashes = new bytes32[](2); 30 | claimHashes[0] = bytes32(uint256(1)); 31 | claimHashes[1] = bytes32(uint256(2)); 32 | 33 | DispositionDetails[] memory details = tribunal.getDispositionDetails(claimHashes); 34 | 35 | assertEq(details.length, 2, "Should return two disposition details"); 36 | // Unfilled claims should have zero claimant and 1e18 scaling factor 37 | assertEq(details[0].claimant, bytes32(0), "Unfilled claim should have zero claimant"); 38 | assertEq(details[0].scalingFactor, 1e18, "Unfilled claim should have 1e18 scaling factor"); 39 | } 40 | 41 | function test_extsload_single() public view { 42 | // Test single slot reading 43 | bytes32[] memory slots = new bytes32[](1); 44 | slots[0] = bytes32(uint256(0)); // Read from slot 0 45 | 46 | bytes32[] memory values = tribunal.extsload(slots); 47 | assertEq(values.length, 1, "Should return one value"); 48 | } 49 | 50 | function test_extsload_multiple() public view { 51 | // Test multiple slot reading 52 | bytes32[] memory slots = new bytes32[](5); 53 | for (uint256 i = 0; i < 5; i++) { 54 | slots[i] = bytes32(i); 55 | } 56 | 57 | bytes32[] memory values = tribunal.extsload(slots); 58 | assertEq(values.length, 5, "Should return five values"); 59 | } 60 | 61 | function test_reentrancyGuardStatus() public view { 62 | address caller = tribunal.reentrancyGuardStatus(); 63 | assertEq(caller, address(0), "Reentrancy guard should be in unlocked state"); 64 | } 65 | 66 | function test_deriveFillsHash() public view { 67 | FillComponent[] memory components1 = new FillComponent[](1); 68 | components1[0] = FillComponent({ 69 | fillToken: address(0x1234), 70 | minimumFillAmount: 1 ether, 71 | recipient: address(0x5678), 72 | applyScaling: true 73 | }); 74 | 75 | FillComponent[] memory components2 = new FillComponent[](1); 76 | components2[0] = FillComponent({ 77 | fillToken: address(0x9ABC), 78 | minimumFillAmount: 2 ether, 79 | recipient: address(0xDEF0), 80 | applyScaling: false 81 | }); 82 | 83 | FillParameters[] memory fills = new FillParameters[](2); 84 | fills[0] = FillParameters({ 85 | chainId: block.chainid, 86 | tribunal: address(tribunal), 87 | expires: block.timestamp + 1 days, 88 | components: components1, 89 | baselinePriorityFee: 0, 90 | scalingFactor: 1e18, 91 | priceCurve: emptyPriceCurve, 92 | recipientCallback: new RecipientCallback[](0), 93 | salt: bytes32(0) 94 | }); 95 | 96 | fills[1] = FillParameters({ 97 | chainId: block.chainid, 98 | tribunal: address(tribunal), 99 | expires: block.timestamp + 1 days, 100 | components: components2, 101 | baselinePriorityFee: 0, 102 | scalingFactor: 1e18, 103 | priceCurve: emptyPriceCurve, 104 | recipientCallback: new RecipientCallback[](0), 105 | salt: bytes32(uint256(1)) 106 | }); 107 | 108 | bytes32 fillsHash = tribunal.deriveFillsHash(fills); 109 | assertTrue(fillsHash != bytes32(0), "Fills hash should not be zero"); 110 | 111 | // Verify consistency 112 | bytes32 fillsHash2 = tribunal.deriveFillsHash(fills); 113 | assertEq(fillsHash, fillsHash2, "Hash should be consistent"); 114 | } 115 | 116 | function test_deriveFillComponentHash() public view { 117 | FillComponent memory component = FillComponent({ 118 | fillToken: address(0x1234), 119 | minimumFillAmount: 1 ether, 120 | recipient: address(0x5678), 121 | applyScaling: true 122 | }); 123 | 124 | bytes32 componentHash = tribunal.deriveFillComponentHash(component); 125 | assertTrue(componentHash != bytes32(0), "Component hash should not be zero"); 126 | } 127 | 128 | function test_deriveRecipientCallbackHash_empty() public view { 129 | // Test with empty array 130 | RecipientCallback[] memory emptyCallbacks = new RecipientCallback[](0); 131 | bytes32 emptyHash = tribunal.deriveRecipientCallbackHash(emptyCallbacks); 132 | assertEq( 133 | emptyHash, 134 | bytes32(0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470), 135 | "Empty callback should have zero hash" 136 | ); 137 | } 138 | 139 | function test_deriveRecipientCallbackHash_withCallback() public view { 140 | // Test with actual callback 141 | RecipientCallback[] memory callbacks = new RecipientCallback[](1); 142 | 143 | Lock[] memory commitments = new Lock[](1); 144 | commitments[0] = Lock({lockTag: bytes12(0), token: address(0x1234), amount: 1 ether}); 145 | 146 | BatchCompact memory compact = BatchCompact({ 147 | arbiter: address(this), 148 | sponsor: address(0x5678), 149 | nonce: 0, 150 | expires: uint256(block.timestamp + 1 days), 151 | commitments: commitments 152 | }); 153 | 154 | callbacks[0] = RecipientCallback({ 155 | chainId: block.chainid, 156 | compact: compact, 157 | mandateHash: bytes32(uint256(1)), 158 | context: abi.encode("test") 159 | }); 160 | 161 | bytes32 callbackHash = tribunal.deriveRecipientCallbackHash(callbacks); 162 | assertTrue(callbackHash != bytes32(0), "Callback hash should not be zero"); 163 | } 164 | 165 | function test_deriveMandateHash() public view { 166 | FillComponent[] memory components = new FillComponent[](1); 167 | components[0] = FillComponent({ 168 | fillToken: address(0xDEAD), 169 | minimumFillAmount: 1 ether, 170 | recipient: address(0xCAFE), 171 | applyScaling: true 172 | }); 173 | 174 | FillParameters[] memory fills = new FillParameters[](1); 175 | fills[0] = FillParameters({ 176 | chainId: block.chainid, 177 | tribunal: address(tribunal), 178 | expires: block.timestamp + 1 days, 179 | components: components, 180 | baselinePriorityFee: 100 wei, 181 | scalingFactor: 1e18, 182 | priceCurve: emptyPriceCurve, 183 | recipientCallback: new RecipientCallback[](0), 184 | salt: bytes32(uint256(1)) 185 | }); 186 | 187 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: fills}); 188 | 189 | bytes32 mandateHash = tribunal.deriveMandateHash(mandate); 190 | assertTrue(mandateHash != bytes32(0), "Mandate hash should not be zero"); 191 | } 192 | 193 | function test_deriveClaimHash() public view { 194 | FillComponent[] memory components = new FillComponent[](1); 195 | components[0] = FillComponent({ 196 | fillToken: address(0xDEAD), 197 | minimumFillAmount: 1 ether, 198 | recipient: address(0xCAFE), 199 | applyScaling: true 200 | }); 201 | 202 | FillParameters[] memory fills = new FillParameters[](1); 203 | fills[0] = FillParameters({ 204 | chainId: block.chainid, 205 | tribunal: address(tribunal), 206 | expires: block.timestamp + 1 days, 207 | components: components, 208 | baselinePriorityFee: 100 wei, 209 | scalingFactor: 1e18, 210 | priceCurve: emptyPriceCurve, 211 | recipientCallback: new RecipientCallback[](0), 212 | salt: bytes32(uint256(1)) 213 | }); 214 | 215 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: fills}); 216 | 217 | Lock[] memory commitments = new Lock[](1); 218 | commitments[0] = Lock({lockTag: bytes12(0), token: address(0xDEAD), amount: 1 ether}); 219 | 220 | BatchCompact memory compact = BatchCompact({ 221 | arbiter: address(this), 222 | sponsor: address(0x1234), 223 | nonce: 0, 224 | expires: block.timestamp + 1 hours, 225 | commitments: commitments 226 | }); 227 | 228 | bytes32 mandateHash = tribunal.deriveMandateHash(mandate); 229 | bytes32 claimHash = tribunal.deriveClaimHash(compact, mandateHash); 230 | 231 | assertTrue(claimHash != bytes32(0), "Claim hash should not be zero"); 232 | } 233 | 234 | function test_deriveAmountsFromComponents_revertsOnOppositeScalingDirections() public { 235 | // Set up proper gas environment to avoid underflow in priority fee calculations 236 | vm.fee(1 gwei); 237 | vm.txGasPrice(2 gwei); 238 | 239 | // Create a price curve that will produce a currentScalingFactor < 1e18 240 | // Using PriceCurveLib to create a proper price curve element 241 | uint256[] memory priceCurve = new uint256[](1); 242 | // blockDuration = 10, scalingFactor = 0.7e18 (below 1e18, indicating decreasing claim amounts) 243 | priceCurve[0] = (uint256(10) << 240) | uint256(0.7e18); 244 | 245 | FillComponent[] memory components = new FillComponent[](1); 246 | components[0] = FillComponent({ 247 | fillToken: address(0x1234), 248 | minimumFillAmount: 1 ether, 249 | recipient: address(0x5678), 250 | applyScaling: true 251 | }); 252 | 253 | Lock[] memory maximumClaimAmounts = new Lock[](1); 254 | maximumClaimAmounts[0] = 255 | Lock({lockTag: bytes12(0), token: address(0x1234), amount: 2 ether}); 256 | 257 | // Set scalingFactor to > 1e18 (exact-in mode, increasing fills) 258 | // but currentScalingFactor will be < 1e18 from price curve (decreasing claims) 259 | // At targetBlock (blocksPassed=0), currentScalingFactor will be 0.7e18 260 | // This creates opposite scaling directions and should revert 261 | uint256 scalingFactor = 1.5e18; // > 1e18 (exact-in) 262 | uint256 targetBlock = 1000; 263 | uint256 fillBlock = 1000; // At start of curve 264 | uint256 baselinePriorityFee = 0; 265 | 266 | // Expect revert with InvalidPriceCurveParameters 267 | vm.expectRevert(abi.encodeWithSignature("InvalidPriceCurveParameters()")); 268 | tribunal.deriveAmountsFromComponents( 269 | maximumClaimAmounts, 270 | components, 271 | priceCurve, 272 | targetBlock, 273 | fillBlock, 274 | baselinePriorityFee, 275 | scalingFactor 276 | ); 277 | } 278 | 279 | function test_deriveAmountsFromComponents_revertsOnOppositeScalingDirections_exactOut() public { 280 | // Set up proper gas environment to avoid underflow in priority fee calculations 281 | vm.fee(1 gwei); 282 | vm.txGasPrice(2 gwei); 283 | 284 | // Create a price curve that will produce a currentScalingFactor > 1e18 285 | // Using PriceCurveLib to create a proper price curve element 286 | uint256[] memory priceCurve = new uint256[](1); 287 | // blockDuration = 10, scalingFactor = 1.5e18 (above 1e18, indicating increasing fill amounts) 288 | priceCurve[0] = (uint256(10) << 240) | uint256(1.5e18); 289 | 290 | FillComponent[] memory components = new FillComponent[](1); 291 | components[0] = FillComponent({ 292 | fillToken: address(0x1234), 293 | minimumFillAmount: 1 ether, 294 | recipient: address(0x5678), 295 | applyScaling: true 296 | }); 297 | 298 | Lock[] memory maximumClaimAmounts = new Lock[](1); 299 | maximumClaimAmounts[0] = 300 | Lock({lockTag: bytes12(0), token: address(0x1234), amount: 2 ether}); 301 | 302 | // Set scalingFactor to < 1e18 (exact-out mode, decreasing claims) 303 | // but currentScalingFactor will be > 1e18 from price curve (increasing fills) 304 | // At targetBlock (blocksPassed=0), currentScalingFactor will be 1.5e18 305 | // This creates opposite scaling directions and should revert 306 | uint256 scalingFactor = 0.7e18; // < 1e18 (exact-out) 307 | uint256 targetBlock = 1000; 308 | uint256 fillBlock = 1000; // At start of curve 309 | uint256 baselinePriorityFee = 0; 310 | 311 | // Expect revert with InvalidPriceCurveParameters 312 | vm.expectRevert(abi.encodeWithSignature("InvalidPriceCurveParameters()")); 313 | tribunal.deriveAmountsFromComponents( 314 | maximumClaimAmounts, 315 | components, 316 | priceCurve, 317 | targetBlock, 318 | fillBlock, 319 | baselinePriorityFee, 320 | scalingFactor 321 | ); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /test/TribunalClaimReductionScalingFactorTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Tribunal} from "../src/Tribunal.sol"; 6 | import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; 7 | import { 8 | Mandate, 9 | FillParameters, 10 | FillComponent, 11 | Adjustment, 12 | RecipientCallback, 13 | BatchClaim 14 | } from "../src/types/TribunalStructs.sol"; 15 | import {BatchCompact, Lock} from "the-compact/src/types/EIP712Types.sol"; 16 | import {ADJUSTMENT_TYPEHASH} from "../src/types/TribunalTypeHashes.sol"; 17 | import {MockTheCompact} from "./mocks/MockTheCompact.sol"; 18 | import {MockERC20} from "./mocks/MockERC20.sol"; 19 | 20 | contract TribunalClaimReductionScalingFactorTest is Test { 21 | using FixedPointMathLib for uint256; 22 | 23 | Tribunal public tribunal; 24 | MockTheCompact public theCompact; 25 | MockERC20 public token; 26 | 27 | address public sponsor = address(0x1); 28 | address public filler = address(0x3); 29 | 30 | // Use actual private keys for signing 31 | uint256 public adjusterPrivateKey = 0xA11CE; 32 | address public adjuster; 33 | 34 | uint256 public constant BASE_SCALING_FACTOR = 1e18; 35 | 36 | receive() external payable {} 37 | 38 | function setUp() public { 39 | theCompact = new MockTheCompact(); 40 | tribunal = new Tribunal(); 41 | token = new MockERC20(); 42 | 43 | adjuster = vm.addr(adjusterPrivateKey); 44 | 45 | vm.label(sponsor, "sponsor"); 46 | vm.label(adjuster, "adjuster"); 47 | vm.label(filler, "filler"); 48 | vm.label(address(tribunal), "tribunal"); 49 | vm.label(address(theCompact), "theCompact"); 50 | } 51 | 52 | // Helper function to sign adjustment with EIP-712 53 | function _toAdjustmentSignature( 54 | Adjustment memory adjustment, 55 | BatchCompact memory compact, 56 | Mandate memory mandate 57 | ) internal view returns (bytes memory) { 58 | bytes32 mandateHash = tribunal.deriveMandateHash(mandate); 59 | bytes32 claimHash = tribunal.deriveClaimHash(compact, mandateHash); 60 | 61 | bytes32 adjustmentHash = keccak256( 62 | abi.encode( 63 | ADJUSTMENT_TYPEHASH, 64 | claimHash, 65 | adjustment.fillIndex, 66 | adjustment.targetBlock, 67 | keccak256(abi.encodePacked(adjustment.supplementalPriceCurve)), 68 | adjustment.validityConditions 69 | ) 70 | ); 71 | 72 | bytes32 domainSeparator = keccak256( 73 | abi.encode( 74 | keccak256( 75 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 76 | ), 77 | keccak256("Tribunal"), 78 | keccak256("1"), 79 | block.chainid, 80 | address(tribunal) 81 | ) 82 | ); 83 | 84 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, adjustmentHash)); 85 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(adjusterPrivateKey, digest); 86 | return abi.encodePacked(r, s, v); 87 | } 88 | 89 | function test_ClaimReductionScalingFactor_ReturnsBaseWhenNotSet() public view { 90 | bytes32 claimHash = keccak256("test"); 91 | uint256 scalingFactor = tribunal.claimReductionScalingFactor(claimHash); 92 | assertEq(scalingFactor, BASE_SCALING_FACTOR, "Should return 1e18 when not set"); 93 | } 94 | 95 | function test_ClaimReductionScalingFactor_StoredWhenReduced() public { 96 | // Setup a fill that will reduce claim amounts (exact-out mode with scaling < 1e18) 97 | Lock[] memory commitments = new Lock[](1); 98 | commitments[0] = Lock({lockTag: bytes12(uint96(1)), token: address(token), amount: 1 ether}); 99 | 100 | FillComponent[] memory components = new FillComponent[](1); 101 | components[0] = FillComponent({ 102 | fillToken: address(token), 103 | minimumFillAmount: 0.95 ether, 104 | recipient: filler, 105 | applyScaling: true 106 | }); 107 | 108 | FillParameters memory fillData = FillParameters({ 109 | chainId: block.chainid, 110 | tribunal: address(tribunal), 111 | expires: block.timestamp + 1 days, 112 | components: components, 113 | baselinePriorityFee: 100 gwei, 114 | scalingFactor: 8e17, // 0.8 - exact-out mode 115 | priceCurve: new uint256[](0), 116 | recipientCallback: new RecipientCallback[](0), 117 | salt: 0 118 | }); 119 | 120 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 121 | mandate.fills[0] = fillData; 122 | 123 | BatchCompact memory compact = BatchCompact({ 124 | arbiter: address(0), 125 | sponsor: sponsor, 126 | nonce: 1, 127 | expires: block.timestamp + 1 days, 128 | commitments: commitments 129 | }); 130 | 131 | Adjustment memory adjustmentForSig = Adjustment({ 132 | adjuster: adjuster, 133 | fillIndex: 0, 134 | targetBlock: 0, 135 | supplementalPriceCurve: new uint256[](0), 136 | validityConditions: bytes32(uint256(uint160(filler))), 137 | adjustmentAuthorization: "" 138 | }); 139 | 140 | BatchClaim memory claim = BatchClaim({ 141 | compact: compact, sponsorSignature: new bytes(0), allocatorSignature: new bytes(0) 142 | }); 143 | 144 | bytes32[] memory fillHashes = new bytes32[](1); 145 | fillHashes[0] = tribunal.deriveFillHash(fillData); 146 | 147 | // Set up gas price to have priority fee above baseline 148 | vm.fee(1 gwei); 149 | vm.txGasPrice(1 gwei + 100 gwei + 1 wei); // 1 wei above baseline 150 | 151 | // Mock token balance and approvals 152 | token.mint(filler, 100 ether); 153 | vm.prank(filler); 154 | token.approve(address(tribunal), type(uint256).max); 155 | 156 | // Sign the adjustment properly using EIP-712 157 | bytes memory adjustmentAuth = _toAdjustmentSignature(adjustmentForSig, compact, mandate); 158 | 159 | Adjustment memory adjustment = Adjustment({ 160 | adjuster: adjuster, 161 | fillIndex: 0, 162 | targetBlock: 0, 163 | supplementalPriceCurve: new uint256[](0), 164 | validityConditions: bytes32(uint256(uint160(filler))), 165 | adjustmentAuthorization: adjustmentAuth 166 | }); 167 | 168 | // Execute the fill 169 | vm.prank(filler); 170 | (bytes32 claimHash,,,) = tribunal.fill( 171 | claim.compact, 172 | fillData, 173 | adjustment, 174 | fillHashes, 175 | bytes32(uint256(uint160(filler))), 176 | block.number 177 | ); 178 | 179 | // Check that scaling factor was stored 180 | uint256 storedScalingFactor = tribunal.claimReductionScalingFactor(claimHash); 181 | 182 | // Calculate expected scaling multiplier: 1e18 - ((1e18 - 8e17) * 1) = 1e18 - 2e17 = 8e17 183 | uint256 expectedScalingMultiplier = BASE_SCALING_FACTOR - ((BASE_SCALING_FACTOR - 8e17) * 1); 184 | 185 | assertLt( 186 | storedScalingFactor, BASE_SCALING_FACTOR, "Scaling factor should be less than 1e18" 187 | ); 188 | assertEq( 189 | storedScalingFactor, 190 | expectedScalingMultiplier, 191 | "Stored scaling factor should match calculated value" 192 | ); 193 | } 194 | 195 | function test_ClaimReductionScalingFactor_NotStoredWhenNotReduced() public { 196 | // Setup a fill that will NOT reduce claim amounts (exact-in mode with scaling > 1e18) 197 | Lock[] memory commitments = new Lock[](1); 198 | commitments[0] = Lock({lockTag: bytes12(uint96(1)), token: address(token), amount: 1 ether}); 199 | 200 | FillComponent[] memory components = new FillComponent[](1); 201 | components[0] = FillComponent({ 202 | fillToken: address(token), 203 | minimumFillAmount: 0.95 ether, 204 | recipient: filler, 205 | applyScaling: true 206 | }); 207 | 208 | FillParameters memory fillData = FillParameters({ 209 | chainId: block.chainid, 210 | tribunal: address(tribunal), 211 | expires: block.timestamp + 1 days, 212 | components: components, 213 | baselinePriorityFee: 100 gwei, 214 | scalingFactor: 15e17, // 1.5 - exact-in mode 215 | priceCurve: new uint256[](0), 216 | recipientCallback: new RecipientCallback[](0), 217 | salt: 0 218 | }); 219 | 220 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 221 | mandate.fills[0] = fillData; 222 | 223 | BatchCompact memory compact = BatchCompact({ 224 | arbiter: address(0), 225 | sponsor: sponsor, 226 | nonce: 1, 227 | expires: block.timestamp + 1 days, 228 | commitments: commitments 229 | }); 230 | 231 | Adjustment memory adjustmentForSig = Adjustment({ 232 | adjuster: adjuster, 233 | fillIndex: 0, 234 | targetBlock: 0, 235 | supplementalPriceCurve: new uint256[](0), 236 | validityConditions: bytes32(uint256(uint160(filler))), 237 | adjustmentAuthorization: "" 238 | }); 239 | 240 | BatchClaim memory claim = BatchClaim({ 241 | compact: compact, sponsorSignature: new bytes(0), allocatorSignature: new bytes(0) 242 | }); 243 | 244 | bytes32[] memory fillHashes = new bytes32[](1); 245 | fillHashes[0] = tribunal.deriveFillHash(fillData); 246 | 247 | // Set up gas price 248 | vm.fee(1 gwei); 249 | vm.txGasPrice(1 gwei + 100 gwei + 2 wei); 250 | 251 | // Mock token balance and approvals 252 | token.mint(filler, 100 ether); 253 | vm.prank(filler); 254 | token.approve(address(tribunal), type(uint256).max); 255 | 256 | // Sign the adjustment properly using EIP-712 257 | bytes memory adjustmentAuth = _toAdjustmentSignature(adjustmentForSig, compact, mandate); 258 | 259 | Adjustment memory adjustment = Adjustment({ 260 | adjuster: adjuster, 261 | fillIndex: 0, 262 | targetBlock: 0, 263 | supplementalPriceCurve: new uint256[](0), 264 | validityConditions: bytes32(uint256(uint160(filler))), 265 | adjustmentAuthorization: adjustmentAuth 266 | }); 267 | 268 | // Execute the fill 269 | vm.prank(filler); 270 | (bytes32 claimHash,,,) = tribunal.fill( 271 | claim.compact, 272 | fillData, 273 | adjustment, 274 | fillHashes, 275 | bytes32(uint256(uint160(filler))), 276 | block.number 277 | ); 278 | 279 | // Check that scaling factor returns base value (not stored) 280 | uint256 storedScalingFactor = tribunal.claimReductionScalingFactor(claimHash); 281 | assertEq(storedScalingFactor, BASE_SCALING_FACTOR, "Should return 1e18 when not reduced"); 282 | } 283 | 284 | function test_ClaimReductionScalingFactor_RevertsOnZeroScalingFactor() public { 285 | // Setup conditions that would result in a zero scaling factor 286 | Lock[] memory commitments = new Lock[](1); 287 | commitments[0] = Lock({lockTag: bytes12(uint96(1)), token: address(token), amount: 1 ether}); 288 | 289 | FillComponent[] memory components = new FillComponent[](1); 290 | components[0] = FillComponent({ 291 | fillToken: address(token), 292 | minimumFillAmount: 0.95 ether, 293 | recipient: filler, 294 | applyScaling: true 295 | }); 296 | 297 | // Use a very aggressive scaling factor and high priority fee to drive scaling to 0 298 | // scalingFactor = 0 means we want to scale down from 1e18 299 | // With high priority fee, this could reach 0 300 | FillParameters memory fillData = FillParameters({ 301 | chainId: block.chainid, 302 | tribunal: address(tribunal), 303 | expires: block.timestamp + 1 days, 304 | components: components, 305 | baselinePriorityFee: 0, // No baseline, so all priority fee counts 306 | scalingFactor: 1, // Extremely low scaling factor (almost 0) 307 | priceCurve: new uint256[](0), 308 | recipientCallback: new RecipientCallback[](0), 309 | salt: 0 310 | }); 311 | 312 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 313 | mandate.fills[0] = fillData; 314 | 315 | BatchCompact memory compact = BatchCompact({ 316 | arbiter: address(0), 317 | sponsor: sponsor, 318 | nonce: 1, 319 | expires: block.timestamp + 1 days, 320 | commitments: commitments 321 | }); 322 | 323 | Adjustment memory adjustment = Adjustment({ 324 | adjuster: adjuster, 325 | fillIndex: 0, 326 | targetBlock: 0, 327 | supplementalPriceCurve: new uint256[](0), 328 | validityConditions: bytes32(uint256(uint160(filler))), 329 | adjustmentAuthorization: "" 330 | }); 331 | 332 | BatchClaim memory claim = BatchClaim({ 333 | compact: compact, sponsorSignature: new bytes(0), allocatorSignature: new bytes(0) 334 | }); 335 | 336 | bytes32[] memory fillHashes = new bytes32[](1); 337 | fillHashes[0] = tribunal.deriveFillHash(fillData); 338 | 339 | // Set priority fee high enough to drive scaling to 0 340 | // scalingMultiplier = currentScalingFactor - ((1e18 - scalingFactor) * priorityFeeAboveBaseline) 341 | // With scalingFactor = 1, currentScalingFactor = 1e18: 342 | // scalingMultiplier = 1e18 - ((1e18 - 1) * priorityFeeAboveBaseline) 343 | // To get 0: priorityFeeAboveBaseline >= 1e18 / (1e18 - 1) ≈ 1 344 | vm.fee(0); 345 | vm.txGasPrice(2); // Priority fee of 2 wei, which is > 1 346 | 347 | // Mock token balance and approvals 348 | token.mint(filler, 100 ether); 349 | vm.prank(filler); 350 | token.approve(address(tribunal), type(uint256).max); 351 | 352 | // Expect revert due to zero scaling factor 353 | vm.expectRevert(); 354 | vm.prank(filler); 355 | tribunal.fill( 356 | claim.compact, 357 | fillData, 358 | adjustment, 359 | fillHashes, 360 | bytes32(uint256(uint160(filler))), 361 | block.number 362 | ); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /test/TribunalDeriveAmountsTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Tribunal} from "../src/Tribunal.sol"; 6 | import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; 7 | import { 8 | Mandate, 9 | FillParameters, 10 | Adjustment, 11 | RecipientCallback 12 | } from "../src/types/TribunalStructs.sol"; 13 | import {BatchCompact, Lock} from "the-compact/src/types/EIP712Types.sol"; 14 | 15 | contract TribunalDeriveAmountsTest is Test { 16 | using FixedPointMathLib for uint256; 17 | 18 | Tribunal public tribunal; 19 | address theCompact; 20 | 21 | uint256[] public emptyPriceCurve; 22 | 23 | receive() external payable {} 24 | 25 | function setUp() public { 26 | theCompact = address(0xC0); 27 | tribunal = new Tribunal(); 28 | 29 | emptyPriceCurve = new uint256[](0); 30 | } 31 | 32 | function test_DeriveAmounts_NoPriorityFee() public { 33 | Lock[] memory maximumClaimAmounts = new Lock[](1); 34 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 100 ether}); 35 | 36 | uint256 minimumFillAmount = 95 ether; 37 | uint256 baselinePriorityFee = 100 gwei; 38 | uint256 scalingFactor = 1e18; 39 | 40 | vm.fee(baselinePriorityFee); 41 | vm.txGasPrice(baselinePriorityFee + 1 wei); 42 | 43 | (uint256 fillAmount, uint256[] memory claimAmounts) = tribunal.deriveAmounts( 44 | maximumClaimAmounts, 45 | emptyPriceCurve, 46 | 0, 47 | vm.getBlockNumber(), 48 | minimumFillAmount, 49 | baselinePriorityFee, 50 | scalingFactor 51 | ); 52 | 53 | assertEq(fillAmount, minimumFillAmount); 54 | assertEq(claimAmounts[0], maximumClaimAmounts[0].amount); 55 | } 56 | 57 | function test_DeriveAmounts_ExactOut() public { 58 | Lock[] memory maximumClaimAmounts = new Lock[](1); 59 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 60 | 61 | uint256 minimumFillAmount = 0.95 ether; 62 | uint256 baselinePriorityFee = 100 gwei; 63 | uint256 scalingFactor = 5e17; 64 | 65 | vm.fee(1 gwei); 66 | vm.txGasPrice(1 gwei + baselinePriorityFee + 2 wei); 67 | 68 | (uint256 fillAmount, uint256[] memory claimAmounts) = tribunal.deriveAmounts( 69 | maximumClaimAmounts, 70 | emptyPriceCurve, 71 | 0, 72 | vm.getBlockNumber(), 73 | minimumFillAmount, 74 | baselinePriorityFee, 75 | scalingFactor 76 | ); 77 | 78 | assertEq(fillAmount, minimumFillAmount); 79 | 80 | uint256 scalingMultiplier = 1e18 - ((1e18 - scalingFactor) * 2); 81 | uint256 expectedClaimAmount = maximumClaimAmounts[0].amount.mulWad(scalingMultiplier); 82 | assertEq(claimAmounts[0], expectedClaimAmount); 83 | } 84 | 85 | function test_DeriveAmounts_ExactIn() public { 86 | Lock[] memory maximumClaimAmounts = new Lock[](1); 87 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 88 | 89 | uint256 minimumFillAmount = 0.95 ether; 90 | uint256 baselinePriorityFee = 100 gwei; 91 | uint256 scalingFactor = 15e17; 92 | 93 | vm.fee(1 gwei); 94 | vm.txGasPrice(1 gwei + baselinePriorityFee + 2 wei); 95 | 96 | (uint256 fillAmount, uint256[] memory claimAmounts) = tribunal.deriveAmounts( 97 | maximumClaimAmounts, 98 | emptyPriceCurve, 99 | 0, 100 | vm.getBlockNumber(), 101 | minimumFillAmount, 102 | baselinePriorityFee, 103 | scalingFactor 104 | ); 105 | 106 | assertEq(claimAmounts[0], maximumClaimAmounts[0].amount); 107 | 108 | uint256 scalingMultiplier = 1e18 + ((scalingFactor - 1e18) * 2); 109 | uint256 expectedFillAmount = minimumFillAmount.mulWadUp(scalingMultiplier); 110 | assertEq(fillAmount, expectedFillAmount); 111 | } 112 | 113 | function test_DeriveAmounts_ExtremePriorityFee() public { 114 | Lock[] memory maximumClaimAmounts = new Lock[](1); 115 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 116 | 117 | uint256 minimumFillAmount = 0.95 ether; 118 | uint256 baselinePriorityFee = 100 gwei; 119 | uint256 scalingFactor = 15e17; 120 | 121 | uint256 baseFee = 1 gwei; 122 | vm.fee(baseFee); 123 | vm.txGasPrice(baseFee + baselinePriorityFee + 10 wei); 124 | 125 | (uint256 fillAmount, uint256[] memory claimAmounts) = tribunal.deriveAmounts( 126 | maximumClaimAmounts, 127 | emptyPriceCurve, 128 | 0, 129 | vm.getBlockNumber(), 130 | minimumFillAmount, 131 | baselinePriorityFee, 132 | scalingFactor 133 | ); 134 | 135 | assertEq(claimAmounts[0], maximumClaimAmounts[0].amount); 136 | 137 | uint256 scalingMultiplier = 1e18 + ((scalingFactor - 1e18) * 10); 138 | uint256 expectedFillAmount = minimumFillAmount.mulWadUp(scalingMultiplier); 139 | assertEq(fillAmount, expectedFillAmount); 140 | } 141 | 142 | function test_DeriveAmounts_RealisticExactIn() public { 143 | Lock[] memory maximumClaimAmounts = new Lock[](1); 144 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 145 | 146 | uint256 minimumFillAmount = 0.95 ether; 147 | uint256 baselinePriorityFee = 100 gwei; 148 | uint256 scalingFactor = 1000000000100000000; 149 | 150 | vm.fee(1 gwei); 151 | vm.txGasPrice(1 gwei + baselinePriorityFee + 5 gwei); 152 | 153 | (uint256 fillAmount, uint256[] memory claimAmounts) = tribunal.deriveAmounts( 154 | maximumClaimAmounts, 155 | emptyPriceCurve, 156 | 0, 157 | vm.getBlockNumber(), 158 | minimumFillAmount, 159 | baselinePriorityFee, 160 | scalingFactor 161 | ); 162 | 163 | assertEq(claimAmounts[0], maximumClaimAmounts[0].amount); 164 | 165 | uint256 scalingMultiplier = 1e18 + ((scalingFactor - 1e18) * 5 gwei); 166 | uint256 expectedFillAmount = minimumFillAmount.mulWadUp(scalingMultiplier); 167 | assertEq(fillAmount, expectedFillAmount); 168 | } 169 | 170 | function test_DeriveAmounts_RealisticExactOut() public { 171 | Lock[] memory maximumClaimAmounts = new Lock[](1); 172 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 173 | 174 | uint256 minimumFillAmount = 0.95 ether; 175 | uint256 baselinePriorityFee = 100 gwei; 176 | uint256 scalingFactor = 999999999900000000; 177 | 178 | vm.fee(1 gwei); 179 | vm.txGasPrice(1 gwei + baselinePriorityFee + 5 gwei); 180 | 181 | (uint256 fillAmount, uint256[] memory claimAmounts) = tribunal.deriveAmounts( 182 | maximumClaimAmounts, 183 | emptyPriceCurve, 184 | 0, 185 | vm.getBlockNumber(), 186 | minimumFillAmount, 187 | baselinePriorityFee, 188 | scalingFactor 189 | ); 190 | 191 | assertEq(fillAmount, minimumFillAmount); 192 | 193 | uint256 scalingMultiplier = 1e18 - ((1e18 - scalingFactor) * 5 gwei); 194 | uint256 expectedClaimAmount = maximumClaimAmounts[0].amount.mulWad(scalingMultiplier); 195 | assertEq(claimAmounts[0], expectedClaimAmount); 196 | } 197 | 198 | function test_DeriveAmounts_WithPriceCurve() public view { 199 | Lock[] memory maximumClaimAmounts = new Lock[](1); 200 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 201 | 202 | uint256 minimumFillAmount = 0.95 ether; 203 | uint256 baselinePriorityFee = 0; 204 | uint256 scalingFactor = 1e18; 205 | 206 | uint256 targetBlock = vm.getBlockNumber(); 207 | uint256 fillBlock = targetBlock + 5; 208 | 209 | uint256[] memory priceCurve = new uint256[](3); 210 | priceCurve[0] = (3 << 240) | uint256(8e17); // 0.8 * 10^18 (scaling down) 211 | priceCurve[1] = (10 << 240) | uint256(6e17); // 0.6 * 10^18 (scaling down more) 212 | priceCurve[2] = (10 << 240) | uint256(0); // 0 * 10^18 (scaling down to 0) 213 | 214 | (uint256 fillAmount, uint256[] memory claimAmounts) = tribunal.deriveAmounts( 215 | maximumClaimAmounts, 216 | priceCurve, 217 | targetBlock, 218 | fillBlock, 219 | minimumFillAmount, 220 | baselinePriorityFee, 221 | scalingFactor 222 | ); 223 | 224 | // With exact-out mode and price curve scaling down 225 | assertEq(fillAmount, minimumFillAmount); // Fill amount stays the same 226 | 227 | // Calculate expected claim amount based on interpolation at block 5 228 | // We're 5 blocks in, with segment ending at block 3+10=13 229 | // So we're 5-3=2 blocks into a 10-block segment 230 | // Interpolating from 0.6 to 0 (last segment ends at 0) 231 | // scalingMultiplier = 0.6 - (0.6 * 2/10) = 0.6 * 0.8 = 0.48 232 | uint256 expectedScaling = 48e16; // 0.48 * 10^18 233 | uint256 expectedClaimAmount = maximumClaimAmounts[0].amount.mulWad(expectedScaling); 234 | assertEq(claimAmounts[0], expectedClaimAmount); 235 | } 236 | 237 | function test_DeriveAmounts_WithPriceCurve_Dutch() public view { 238 | Lock[] memory maximumClaimAmounts = new Lock[](1); 239 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 240 | 241 | uint256 minimumFillAmount = 0.95 ether; 242 | uint256 baselinePriorityFee = 0; 243 | uint256 scalingFactor = 1e18; 244 | 245 | uint256 targetBlock = vm.getBlockNumber(); 246 | uint256 fillBlock = targetBlock + 5; 247 | 248 | uint256[] memory priceCurve = new uint256[](1); 249 | priceCurve[0] = (10 << 240) | uint256(1.2e18); 250 | 251 | (uint256 fillAmount, uint256[] memory claimAmounts) = tribunal.deriveAmounts( 252 | maximumClaimAmounts, 253 | priceCurve, 254 | targetBlock, 255 | fillBlock, 256 | minimumFillAmount, 257 | baselinePriorityFee, 258 | scalingFactor 259 | ); 260 | 261 | // With exact-in mode and price curve scaling down 262 | // 5 blocks in, 10 blocks in segment 263 | // Interpolating from 1.2 to 1 (last segment ends at 1e18) 264 | // scalingMultiplier = 1.2 - (0.2 * 5/10) = 1.1 265 | uint256 expectedScaling = 1.1e18; 266 | assertEq(fillAmount, minimumFillAmount.mulWadUp(expectedScaling)); 267 | 268 | assertEq(claimAmounts[0], maximumClaimAmounts[0].amount); 269 | } 270 | 271 | function test_DeriveAmounts_WithPriceCurve_Dutch_nonNeutralEndScalingFactor() public view { 272 | Lock[] memory maximumClaimAmounts = new Lock[](1); 273 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 274 | 275 | uint256 minimumFillAmount = 0.95 ether; 276 | uint256 baselinePriorityFee = 0; 277 | uint256 scalingFactor = 1e18; 278 | 279 | uint256 targetBlock = vm.getBlockNumber(); 280 | uint256 fillBlock = targetBlock + 5; 281 | 282 | uint256[] memory priceCurve = new uint256[](2); 283 | priceCurve[0] = (10 << 240) | uint256(1.2e18); 284 | priceCurve[1] = (0 << 240) | uint256(1.1e18); 285 | 286 | (uint256 fillAmount, uint256[] memory claimAmounts) = tribunal.deriveAmounts( 287 | maximumClaimAmounts, 288 | priceCurve, 289 | targetBlock, 290 | fillBlock, 291 | minimumFillAmount, 292 | baselinePriorityFee, 293 | scalingFactor 294 | ); 295 | 296 | // With exact-in mode and price curve scaling down 297 | // 5 blocks in, 10 blocks in segment 298 | // Interpolating from 1.2 to 1.1 (last segment ends at 1e18) 299 | // scalingMultiplier = 1.2 - (0.1 * 5/10) = 1.05 300 | uint256 expectedScaling = 1.15e18; 301 | assertEq(fillAmount, minimumFillAmount.mulWadUp(expectedScaling)); 302 | 303 | assertEq(claimAmounts[0], maximumClaimAmounts[0].amount); 304 | } 305 | 306 | function test_DeriveAmounts_WithPriceCurve_ReverseDutch() public view { 307 | Lock[] memory maximumClaimAmounts = new Lock[](1); 308 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 309 | 310 | uint256 minimumFillAmount = 0.95 ether; 311 | uint256 baselinePriorityFee = 0; 312 | uint256 scalingFactor = 1e18; 313 | 314 | uint256 targetBlock = vm.getBlockNumber(); 315 | 316 | uint256[] memory priceCurve = new uint256[](2); 317 | priceCurve[0] = (10 << 240) | uint256(8e17); 318 | priceCurve[1] = (10 << 240) | uint256(1e18); 319 | 320 | uint256 fillAmount; 321 | uint256[] memory claimAmounts; 322 | 323 | (fillAmount, claimAmounts) = tribunal.deriveAmounts( 324 | maximumClaimAmounts, 325 | priceCurve, 326 | targetBlock, 327 | targetBlock + 5, 328 | minimumFillAmount, 329 | baselinePriorityFee, 330 | scalingFactor 331 | ); 332 | 333 | // With exact-out mode and price curve scaling down 334 | assertEq(fillAmount, minimumFillAmount); // Fill amount stays the same 335 | 336 | // Calculate expected claim amount based on interpolation at block 5 337 | // We're 5 blocks in, with segment ending at block 10 338 | // Interpolating from 0.8 to 1 339 | // scalingMultiplier = 0.8 + (0.2 * 5/10) = 0.9 340 | uint256 expectedScaling = 9e17; 341 | uint256 expectedClaimAmount = maximumClaimAmounts[0].amount.mulWad(expectedScaling); 342 | assertEq(claimAmounts[0], expectedClaimAmount); 343 | 344 | (fillAmount, claimAmounts) = tribunal.deriveAmounts( 345 | maximumClaimAmounts, 346 | priceCurve, 347 | targetBlock, 348 | targetBlock + 15, 349 | minimumFillAmount, 350 | baselinePriorityFee, 351 | scalingFactor 352 | ); 353 | 354 | assertEq(fillAmount, minimumFillAmount); 355 | assertEq(claimAmounts[0], maximumClaimAmounts[0].amount); 356 | } 357 | 358 | function test_DeriveAmounts_InvalidTargetBlockDesignation() public { 359 | Lock[] memory maximumClaimAmounts = new Lock[](1); 360 | maximumClaimAmounts[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 361 | 362 | uint256[] memory priceCurve = new uint256[](1); 363 | priceCurve[0] = 1e18; 364 | 365 | vm.expectRevert(abi.encodeWithSignature("InvalidTargetBlockDesignation()")); 366 | tribunal.deriveAmounts( 367 | maximumClaimAmounts, priceCurve, 0, vm.getBlockNumber(), 1 ether, 0, 1e18 368 | ); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /test/TribunalCoverageGapsTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Tribunal} from "../src/Tribunal.sol"; 6 | import {ERC7683Tribunal} from "../src/ERC7683Tribunal.sol"; 7 | import {MockERC20} from "./mocks/MockERC20.sol"; 8 | import {ITribunal} from "../src/interfaces/ITribunal.sol"; 9 | import { 10 | Mandate, 11 | FillParameters, 12 | FillComponent, 13 | Adjustment, 14 | RecipientCallback, 15 | BatchClaim, 16 | DispositionDetails, 17 | ArgDetail 18 | } from "../src/types/TribunalStructs.sol"; 19 | import {ADJUSTMENT_TYPEHASH} from "../src/types/TribunalTypeHashes.sol"; 20 | import {BatchCompact, Lock} from "the-compact/src/types/EIP712Types.sol"; 21 | 22 | /** 23 | * @title TribunalCoverageGapsTest 24 | * @notice Tests to improve code coverage for identified gaps 25 | */ 26 | contract TribunalCoverageGapsTest is Test { 27 | ERC7683Tribunal public tribunal; 28 | MockERC20 public token; 29 | address public sponsor; 30 | address public filler; 31 | address public adjuster; 32 | uint256 public adjusterPrivateKey; 33 | address public arbiter; 34 | 35 | function setUp() public { 36 | tribunal = new ERC7683Tribunal(); 37 | token = new MockERC20(); 38 | sponsor = makeAddr("Sponsor"); 39 | filler = makeAddr("Filler"); 40 | (adjuster, adjusterPrivateKey) = makeAddrAndKey("Adjuster"); 41 | arbiter = makeAddr("Arbiter"); 42 | } 43 | 44 | // ============ BlockNumberish Coverage ============ 45 | 46 | /** 47 | * @notice Test Arbitrum block number retrieval 48 | * @dev Covers line 16 in BlockNumberish.sol (arbBlockNumber path) 49 | */ 50 | function test_ArbitrumBlockNumber() public { 51 | // Fork Arbitrum mainnet to test Arbitrum-specific logic 52 | vm.createSelectFork("https://arb1.arbitrum.io/rpc"); 53 | 54 | // Deploy tribunal on Arbitrum to trigger the arbBlockNumber path 55 | new ERC7683Tribunal(); 56 | 57 | // The block number should come from ArbSys 58 | // We can't easily test the exact value, but we can verify it doesn't revert 59 | // and that the contract was deployed successfully on Arbitrum 60 | assertEq(block.chainid, 42161, "Should be on Arbitrum"); 61 | } 62 | 63 | // ============ ERC7683Tribunal.getFillerData Coverage ============ 64 | 65 | /** 66 | * @notice Test getFillerData function 67 | * @dev Covers lines 52-57 in ERC7683Tribunal.sol 68 | */ 69 | function test_GetFillerData() public view { 70 | uint256 targetBlock = 100; 71 | bytes32 claimantAddress = bytes32(uint256(uint160(filler))); 72 | 73 | Adjustment memory adjustment = Adjustment({ 74 | adjuster: adjuster, 75 | fillIndex: 0, 76 | targetBlock: targetBlock, 77 | supplementalPriceCurve: new uint256[](0), 78 | validityConditions: bytes32(0), 79 | adjustmentAuthorization: hex"1234567890" 80 | }); 81 | 82 | bytes memory fillerData = tribunal.getFillerData(adjustment, claimantAddress, targetBlock); 83 | 84 | // Verify the encoded data 85 | (Adjustment memory decodedAdjustment, bytes32 decodedClaimant, uint256 decodedFillBlock) = 86 | abi.decode(fillerData, (Adjustment, bytes32, uint256)); 87 | 88 | assertEq(decodedAdjustment.adjuster, adjuster); 89 | assertEq(decodedAdjustment.fillIndex, 0); 90 | assertEq(decodedAdjustment.targetBlock, targetBlock); 91 | assertEq(decodedClaimant, claimantAddress); 92 | assertEq(decodedFillBlock, targetBlock); 93 | } 94 | 95 | // ============ Tribunal.nonReentrant Revert Path Coverage ============ 96 | 97 | /** 98 | * @notice Test nonReentrant modifier revert 99 | * @dev Covers lines 105-108 in Tribunal.sol (reentrancy guard revert) 100 | */ 101 | function test_ReentrancyGuard_Revert() public view { 102 | // We need to create a scenario where reentrancy is attempted 103 | // This will be tested via the ReentrantReceiver mock 104 | // The TribunalReentrancyTest.t.sol should already cover this, 105 | // but let's verify the status check works 106 | 107 | address status = tribunal.reentrancyGuardStatus(); 108 | assertEq(status, address(0), "Initial status should be address(0) (unlocked)"); 109 | } 110 | 111 | // ============ Tribunal.deriveFillComponentsHash Coverage ============ 112 | 113 | /** 114 | * @notice Test deriveFillComponentsHash function 115 | * @dev Covers lines 509-518 in Tribunal.sol 116 | */ 117 | function test_DeriveFillComponentsHash() public view { 118 | FillComponent[] memory components = new FillComponent[](2); 119 | components[0] = FillComponent({ 120 | fillToken: address(token), 121 | minimumFillAmount: 1 ether, 122 | recipient: sponsor, 123 | applyScaling: true 124 | }); 125 | components[1] = FillComponent({ 126 | fillToken: address(token), 127 | minimumFillAmount: 2 ether, 128 | recipient: filler, 129 | applyScaling: false 130 | }); 131 | 132 | bytes32 componentsHash = tribunal.deriveFillComponentsHash(components); 133 | 134 | // Verify the hash is non-zero 135 | assertTrue(componentsHash != bytes32(0), "Components hash should not be zero"); 136 | 137 | // Verify deterministic hashing (same input produces same output) 138 | bytes32 componentsHash2 = tribunal.deriveFillComponentsHash(components); 139 | assertEq(componentsHash, componentsHash2, "Hash should be deterministic"); 140 | } 141 | 142 | // ============ Tribunal.deriveAmountsFromComponents Coverage ============ 143 | 144 | /** 145 | * @notice Test deriveAmountsFromComponents function 146 | * @dev Covers lines 600-628 in Tribunal.sol 147 | */ 148 | function test_DeriveAmountsFromComponents() public view { 149 | Lock[] memory maximumClaimAmounts = new Lock[](2); 150 | maximumClaimAmounts[0] = 151 | Lock({lockTag: bytes12(0), token: address(token), amount: 10 ether}); 152 | maximumClaimAmounts[1] = 153 | Lock({lockTag: bytes12(0), token: address(token), amount: 20 ether}); 154 | 155 | FillComponent[] memory components = new FillComponent[](2); 156 | components[0] = FillComponent({ 157 | fillToken: address(token), 158 | minimumFillAmount: 1 ether, 159 | recipient: sponsor, 160 | applyScaling: true 161 | }); 162 | components[1] = FillComponent({ 163 | fillToken: address(token), 164 | minimumFillAmount: 2 ether, 165 | recipient: filler, 166 | applyScaling: false 167 | }); 168 | 169 | uint256[] memory priceCurve = new uint256[](0); 170 | uint256 targetBlock = 100; 171 | uint256 fillBlock = 100; 172 | uint256 baselinePriorityFee = 100 wei; 173 | uint256 scalingFactor = 1e18; 174 | 175 | (uint256[] memory fillAmounts, uint256[] memory claimAmounts) = tribunal.deriveAmountsFromComponents( 176 | maximumClaimAmounts, 177 | components, 178 | priceCurve, 179 | targetBlock, 180 | fillBlock, 181 | baselinePriorityFee, 182 | scalingFactor 183 | ); 184 | 185 | // Verify amounts were calculated 186 | assertEq(claimAmounts.length, 2, "Should have 2 claim amounts"); 187 | assertEq(fillAmounts.length, 2, "Should have 2 fill amounts"); 188 | 189 | // First component applies scaling, second doesn't 190 | assertTrue(claimAmounts[0] > 0, "First claim amount should be positive"); 191 | assertTrue(claimAmounts[1] > 0, "Second claim amount should be positive"); 192 | assertTrue(fillAmounts[0] > 0, "First fill amount should be positive"); 193 | assertTrue(fillAmounts[1] > 0, "Second fill amount should be positive"); 194 | } 195 | 196 | // ============ Tribunal.extsload Variants Coverage ============ 197 | 198 | /** 199 | * @notice Test extsload with single slot 200 | * @dev Covers lines 378-381 in Tribunal.sol (first extsload variant) 201 | */ 202 | function test_Extsload_SingleSlot() public view { 203 | bytes32 slot = bytes32(uint256(0)); // dispositions slot 204 | 205 | bytes32 value = Tribunal(payable(address(tribunal))).extsload(slot); 206 | 207 | // Value should be bytes32(0) for uninitialized slot 208 | assertEq(value, bytes32(0), "Should return zero for uninitialized slot"); 209 | } 210 | 211 | /** 212 | * @notice Test extsload with bytes32[] array 213 | * @dev Covers lines 390-409 in Tribunal.sol (second extsload variant) 214 | */ 215 | function test_Extsload_Bytes32Array() public view { 216 | bytes32[] memory slots = new bytes32[](2); 217 | slots[0] = bytes32(uint256(0)); // dispositions slot 218 | slots[1] = bytes32(uint256(1)); // another slot 219 | 220 | bytes32[] memory values = Tribunal(payable(address(tribunal))).extsload(slots); 221 | 222 | assertEq(values.length, 2, "Should return 2 values"); 223 | } 224 | 225 | // ============ Helper Functions ============ 226 | 227 | function _getBatchCompact(uint256 amount) internal view returns (BatchCompact memory) { 228 | Lock[] memory commitments = new Lock[](1); 229 | commitments[0] = Lock({lockTag: bytes12(0), token: address(token), amount: amount}); 230 | 231 | return BatchCompact({ 232 | arbiter: arbiter, 233 | sponsor: sponsor, 234 | nonce: 1, 235 | expires: uint256(block.timestamp + 1 days), 236 | commitments: commitments 237 | }); 238 | } 239 | 240 | function _getFillParameters(uint256 minimumFillAmount) 241 | internal 242 | view 243 | returns (FillParameters memory) 244 | { 245 | FillComponent[] memory components = new FillComponent[](1); 246 | components[0] = FillComponent({ 247 | fillToken: address(token), 248 | minimumFillAmount: minimumFillAmount, 249 | recipient: sponsor, 250 | applyScaling: true 251 | }); 252 | 253 | return FillParameters({ 254 | chainId: block.chainid, 255 | tribunal: address(tribunal), 256 | expires: uint256(block.timestamp + 1 days), 257 | components: components, 258 | baselinePriorityFee: 100 wei, 259 | scalingFactor: 1e18, 260 | priceCurve: new uint256[](0), 261 | recipientCallback: new RecipientCallback[](0), 262 | salt: bytes32(uint256(1)) 263 | }); 264 | } 265 | 266 | function _getMandate(FillParameters memory fill) internal view returns (Mandate memory) { 267 | FillParameters[] memory fills = new FillParameters[](1); 268 | fills[0] = fill; 269 | 270 | return Mandate({adjuster: adjuster, fills: fills}); 271 | } 272 | 273 | function _toAdjustmentSignature( 274 | Adjustment memory adjustment, 275 | BatchCompact memory compact, 276 | Mandate memory mandate 277 | ) internal view returns (bytes memory) { 278 | bytes32 mandateHash = tribunal.deriveMandateHash(mandate); 279 | bytes32 claimHash = tribunal.deriveClaimHash(compact, mandateHash); 280 | 281 | bytes32 adjustmentHash = keccak256( 282 | abi.encode( 283 | ADJUSTMENT_TYPEHASH, 284 | claimHash, 285 | adjustment.fillIndex, 286 | adjustment.targetBlock, 287 | keccak256(abi.encodePacked(adjustment.supplementalPriceCurve)), 288 | adjustment.validityConditions 289 | ) 290 | ); 291 | 292 | bytes32 domainSeparator = keccak256( 293 | abi.encode( 294 | keccak256( 295 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 296 | ), 297 | keccak256("Tribunal"), 298 | keccak256("1"), 299 | block.chainid, 300 | address(tribunal) 301 | ) 302 | ); 303 | 304 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, adjustmentHash)); 305 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(adjusterPrivateKey, digest); 306 | 307 | return abi.encodePacked(r, s, v); 308 | } 309 | 310 | // ============ Additional View Function Coverage ============ 311 | 312 | /** 313 | * @notice Test getDispositionDetails 314 | * @dev Improves coverage for view functions 315 | */ 316 | function test_GetDispositionDetails() public view { 317 | BatchCompact memory compact = _getBatchCompact(10 ether); 318 | FillParameters memory fill = _getFillParameters(1 ether); 319 | Mandate memory mandate = _getMandate(fill); 320 | 321 | bytes32 mandateHash = tribunal.deriveMandateHash(mandate); 322 | bytes32 claimHash = tribunal.deriveClaimHash(compact, mandateHash); 323 | 324 | bytes32[] memory claimHashes = new bytes32[](1); 325 | claimHashes[0] = claimHash; 326 | 327 | DispositionDetails[] memory details = tribunal.getDispositionDetails(claimHashes); 328 | 329 | // For unfilled order, claimant should be bytes32(0) and scalingFactor should be BASE_SCALING_FACTOR 330 | assertEq(details[0].claimant, bytes32(0), "Unfilled order should have claimant bytes32(0)"); 331 | assertEq(details[0].scalingFactor, 1e18, "Unfilled order should have scalingFactor 1e18"); 332 | } 333 | 334 | /** 335 | * @notice Test filled view function 336 | * @dev Improves coverage for filled status check 337 | */ 338 | function test_Filled_ViewFunction() public view { 339 | BatchCompact memory compact = _getBatchCompact(10 ether); 340 | FillParameters memory fill = _getFillParameters(1 ether); 341 | Mandate memory mandate = _getMandate(fill); 342 | 343 | bytes32 mandateHash = tribunal.deriveMandateHash(mandate); 344 | bytes32 claimHash = tribunal.deriveClaimHash(compact, mandateHash); 345 | 346 | bytes32 filledStatus = tribunal.filled(claimHash); 347 | 348 | assertEq(filledStatus, bytes32(0), "Order should not be filled initially"); 349 | } 350 | 351 | /** 352 | * @notice Test getCompactWitnessDetails 353 | * @dev Improves coverage for compact witness view function 354 | */ 355 | function test_GetCompactWitnessDetails() public view { 356 | (string memory witnessTypeString, ArgDetail[] memory details) = 357 | tribunal.getCompactWitnessDetails(); 358 | 359 | assertTrue(bytes(witnessTypeString).length > 0, "Witness type string should not be empty"); 360 | assertTrue(details.length > 0, "Should have at least one detail"); 361 | } 362 | 363 | /** 364 | * @notice Test name function 365 | * @dev Improves coverage for name view function 366 | */ 367 | function test_Name() public view { 368 | string memory name = tribunal.name(); 369 | assertEq(name, "Tribunal", "Contract name should be Tribunal"); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /test/TribunalFillRevertsTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Tribunal} from "../src/Tribunal.sol"; 6 | import {ITribunal} from "../src/interfaces/ITribunal.sol"; 7 | import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; 8 | import { 9 | Mandate, 10 | FillParameters, 11 | FillComponent, 12 | Adjustment, 13 | RecipientCallback, 14 | BatchClaim 15 | } from "../src/types/TribunalStructs.sol"; 16 | import {BatchCompact, Lock} from "the-compact/src/types/EIP712Types.sol"; 17 | import {MANDATE_TYPEHASH, ADJUSTMENT_TYPEHASH} from "../src/types/TribunalTypeHashes.sol"; 18 | 19 | contract TribunalFillRevertsTest is Test { 20 | using FixedPointMathLib for uint256; 21 | 22 | Tribunal public tribunal; 23 | address theCompact; 24 | address sponsor; 25 | address adjuster; 26 | uint256 adjusterPrivateKey; 27 | 28 | uint256[] public emptyPriceCurve; 29 | 30 | receive() external payable {} 31 | 32 | function signAdjustment(Adjustment memory adjustment, bytes32 claimHash, uint256 privateKey) 33 | internal 34 | view 35 | returns (bytes memory) 36 | { 37 | bytes32 adjustmentHash = keccak256( 38 | abi.encode( 39 | ADJUSTMENT_TYPEHASH, 40 | claimHash, 41 | adjustment.fillIndex, 42 | adjustment.targetBlock, 43 | keccak256(abi.encodePacked(adjustment.supplementalPriceCurve)), 44 | adjustment.validityConditions 45 | ) 46 | ); 47 | 48 | bytes32 domainSeparator = keccak256( 49 | abi.encode( 50 | keccak256( 51 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 52 | ), 53 | keccak256("Tribunal"), 54 | keccak256("1"), 55 | block.chainid, 56 | address(tribunal) 57 | ) 58 | ); 59 | 60 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, adjustmentHash)); 61 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); 62 | return abi.encodePacked(r, s, v); 63 | } 64 | 65 | function setUp() public { 66 | theCompact = address(0xC0); 67 | tribunal = new Tribunal(); 68 | (sponsor,) = makeAddrAndKey("sponsor"); 69 | (adjuster, adjusterPrivateKey) = makeAddrAndKey("adjuster"); 70 | 71 | emptyPriceCurve = new uint256[](0); 72 | } 73 | 74 | function test_fillRevertsOnInvalidTargetBlock() public { 75 | FillComponent[] memory components = new FillComponent[](1); 76 | components[0] = FillComponent({ 77 | fillToken: address(0), 78 | minimumFillAmount: 1 ether, 79 | recipient: address(0xBEEF), 80 | applyScaling: true 81 | }); 82 | 83 | FillParameters memory fill = FillParameters({ 84 | chainId: block.chainid, 85 | tribunal: address(tribunal), 86 | expires: uint256(block.timestamp + 1), 87 | components: components, 88 | baselinePriorityFee: 0, 89 | scalingFactor: 0, 90 | priceCurve: emptyPriceCurve, 91 | recipientCallback: new RecipientCallback[](0), 92 | salt: bytes32(uint256(1)) 93 | }); 94 | 95 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 96 | mandate.fills[0] = fill; 97 | 98 | Lock[] memory commitments = new Lock[](1); 99 | commitments[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 100 | 101 | BatchClaim memory claim = BatchClaim({ 102 | compact: BatchCompact({ 103 | arbiter: address(this), 104 | sponsor: sponsor, 105 | nonce: 0, 106 | expires: block.timestamp + 1 hours, 107 | commitments: commitments 108 | }), 109 | sponsorSignature: new bytes(0), 110 | allocatorSignature: new bytes(0) 111 | }); 112 | 113 | Adjustment memory adjustment = Adjustment({ 114 | adjuster: adjuster, 115 | fillIndex: 0, 116 | targetBlock: vm.getBlockNumber() + 100, 117 | supplementalPriceCurve: new uint256[](0), 118 | validityConditions: bytes32(0), 119 | adjustmentAuthorization: new bytes(0) 120 | }); 121 | 122 | bytes32[] memory fillHashes = new bytes32[](1); 123 | fillHashes[0] = tribunal.deriveFillHash(fill); 124 | 125 | vm.expectRevert( 126 | abi.encodeWithSignature( 127 | "InvalidTargetBlock(uint256,uint256)", 128 | vm.getBlockNumber(), 129 | vm.getBlockNumber() + 100 130 | ) 131 | ); 132 | tribunal.fill{ 133 | value: 1 ether 134 | }(claim.compact, fill, adjustment, fillHashes, bytes32(uint256(uint160(address(this)))), 0); 135 | } 136 | 137 | function test_FillRevertsOnExpiredMandate() public { 138 | FillComponent[] memory components = new FillComponent[](1); 139 | components[0] = FillComponent({ 140 | fillToken: address(0xDEAD), 141 | minimumFillAmount: 1 ether, 142 | recipient: address(0xCAFE), 143 | applyScaling: true 144 | }); 145 | 146 | FillParameters memory fill = FillParameters({ 147 | chainId: block.chainid, 148 | tribunal: address(tribunal), 149 | expires: 1703116800, 150 | components: components, 151 | baselinePriorityFee: 100 wei, 152 | scalingFactor: 1e18, 153 | priceCurve: emptyPriceCurve, 154 | recipientCallback: new RecipientCallback[](0), 155 | salt: bytes32(uint256(1)) 156 | }); 157 | 158 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 159 | mandate.fills[0] = fill; 160 | 161 | Lock[] memory commitments = new Lock[](1); 162 | commitments[0] = Lock({lockTag: bytes12(0), token: address(0xDEAD), amount: 1 ether}); 163 | 164 | BatchClaim memory claim = BatchClaim({ 165 | compact: BatchCompact({ 166 | arbiter: address(this), 167 | sponsor: sponsor, 168 | nonce: 0, 169 | expires: block.timestamp + 1 hours, 170 | commitments: commitments 171 | }), 172 | sponsorSignature: new bytes(0), 173 | allocatorSignature: new bytes(0) 174 | }); 175 | 176 | Adjustment memory adjustment = Adjustment({ 177 | adjuster: adjuster, 178 | fillIndex: 0, 179 | targetBlock: vm.getBlockNumber(), 180 | supplementalPriceCurve: new uint256[](0), 181 | validityConditions: bytes32(0), 182 | adjustmentAuthorization: new bytes(0) 183 | }); 184 | 185 | bytes32[] memory fillHashes = new bytes32[](1); 186 | fillHashes[0] = tribunal.deriveFillHash(fill); 187 | 188 | vm.warp(fill.expires + 1); 189 | 190 | vm.expectRevert(abi.encodeWithSignature("Expired(uint256)", fill.expires)); 191 | tribunal.fill( 192 | claim.compact, fill, adjustment, fillHashes, bytes32(uint256(uint160(address(this)))), 0 193 | ); 194 | } 195 | 196 | function test_FillRevertsOnReusedClaim() public { 197 | FillComponent[] memory components = new FillComponent[](1); 198 | components[0] = FillComponent({ 199 | fillToken: address(0), 200 | minimumFillAmount: 1 ether, 201 | recipient: address(0xCAFE), 202 | applyScaling: true 203 | }); 204 | 205 | FillParameters memory fill = FillParameters({ 206 | chainId: block.chainid, 207 | tribunal: address(tribunal), 208 | expires: 1703116800, 209 | components: components, 210 | baselinePriorityFee: 100 wei, 211 | scalingFactor: 1e18, 212 | priceCurve: emptyPriceCurve, 213 | recipientCallback: new RecipientCallback[](0), 214 | salt: bytes32(uint256(1)) 215 | }); 216 | 217 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 218 | mandate.fills[0] = fill; 219 | 220 | Lock[] memory commitments = new Lock[](1); 221 | commitments[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 1 ether}); 222 | 223 | // Use a different chainId to make it a cross-chain fill 224 | BatchClaim memory claim = BatchClaim({ 225 | compact: BatchCompact({ 226 | arbiter: address(this), 227 | sponsor: sponsor, 228 | nonce: 0, 229 | expires: block.timestamp + 1 hours, 230 | commitments: commitments 231 | }), 232 | sponsorSignature: new bytes(0), 233 | allocatorSignature: new bytes(0) 234 | }); 235 | 236 | bytes32[] memory fillHashes = new bytes32[](1); 237 | fillHashes[0] = tribunal.deriveFillHash(fill); 238 | 239 | // Calculate mandateHash and claimHash for signature 240 | // Note: The fill function uses _deriveMandateHash internally with fillHashes 241 | bytes32 mandateHash = keccak256( 242 | abi.encode(MANDATE_TYPEHASH, adjuster, keccak256(abi.encodePacked(fillHashes))) 243 | ); 244 | bytes32 claimHash = tribunal.deriveClaimHash(claim.compact, mandateHash); 245 | 246 | // Create adjustment for signing 247 | Adjustment memory adjustmentForSig = Adjustment({ 248 | adjuster: adjuster, 249 | fillIndex: 0, 250 | targetBlock: vm.getBlockNumber(), 251 | supplementalPriceCurve: new uint256[](0), 252 | validityConditions: bytes32(0), 253 | adjustmentAuthorization: new bytes(0) 254 | }); 255 | 256 | Adjustment memory adjustment = Adjustment({ 257 | adjuster: adjuster, 258 | fillIndex: 0, 259 | targetBlock: vm.getBlockNumber(), 260 | supplementalPriceCurve: new uint256[](0), 261 | validityConditions: bytes32(0), 262 | adjustmentAuthorization: signAdjustment(adjustmentForSig, claimHash, adjusterPrivateKey) 263 | }); 264 | 265 | // Send ETH with the first fill 266 | tribunal.fill{ 267 | value: 1 ether 268 | }(claim.compact, fill, adjustment, fillHashes, bytes32(uint256(uint160(address(this)))), 0); 269 | 270 | vm.expectRevert(abi.encodeWithSignature("AlreadyFilled()")); 271 | tribunal.fill{ 272 | value: 1 ether 273 | }(claim.compact, fill, adjustment, fillHashes, bytes32(uint256(uint160(address(this)))), 0); 274 | } 275 | 276 | function test_FillRevertsOnInvalidGasPrice() public { 277 | FillComponent[] memory components = new FillComponent[](1); 278 | components[0] = FillComponent({ 279 | fillToken: address(0xDEAD), 280 | minimumFillAmount: 1 ether, 281 | recipient: address(0xCAFE), 282 | applyScaling: true 283 | }); 284 | 285 | FillParameters memory fill = FillParameters({ 286 | chainId: block.chainid, 287 | tribunal: address(tribunal), 288 | expires: 1703116800, 289 | components: components, 290 | baselinePriorityFee: 100 wei, 291 | scalingFactor: 1e18, 292 | priceCurve: emptyPriceCurve, 293 | recipientCallback: new RecipientCallback[](0), 294 | salt: bytes32(uint256(1)) 295 | }); 296 | 297 | Mandate memory mandate = Mandate({adjuster: adjuster, fills: new FillParameters[](1)}); 298 | mandate.fills[0] = fill; 299 | 300 | Lock[] memory commitments = new Lock[](1); 301 | commitments[0] = Lock({lockTag: bytes12(0), token: address(0xDEAD), amount: 1 ether}); 302 | 303 | BatchClaim memory claim = BatchClaim({ 304 | compact: BatchCompact({ 305 | arbiter: address(this), 306 | sponsor: sponsor, 307 | nonce: 0, 308 | expires: block.timestamp + 1 hours, 309 | commitments: commitments 310 | }), 311 | sponsorSignature: new bytes(0), 312 | allocatorSignature: new bytes(0) 313 | }); 314 | 315 | Adjustment memory adjustment = Adjustment({ 316 | adjuster: adjuster, 317 | fillIndex: 0, 318 | targetBlock: vm.getBlockNumber(), 319 | supplementalPriceCurve: new uint256[](0), 320 | validityConditions: bytes32(0), 321 | adjustmentAuthorization: new bytes(0) 322 | }); 323 | 324 | bytes32[] memory fillHashes = new bytes32[](1); 325 | fillHashes[0] = tribunal.deriveFillHash(fill); 326 | 327 | vm.fee(2 gwei); 328 | vm.txGasPrice(1 gwei); 329 | 330 | vm.expectRevert(abi.encodeWithSignature("InvalidGasPrice()")); 331 | tribunal.fill( 332 | claim.compact, fill, adjustment, fillHashes, bytes32(uint256(uint160(address(this)))), 0 333 | ); 334 | } 335 | 336 | function test_FillRevertsOnInvalidPriceCurveParameters_OppositeScalingDirections() public { 337 | // Set proper gas environment 338 | vm.fee(1 gwei); 339 | vm.txGasPrice(2 gwei); 340 | 341 | // Create a price curve that produces currentScalingFactor < 1e18 (exact-out, decreasing claims) 342 | uint256[] memory priceCurve = new uint256[](1); 343 | // blockDuration = 10, scalingFactor = 0.7e18 (below 1e18, indicating decreasing claim amounts) 344 | priceCurve[0] = (uint256(10) << 240) | uint256(0.7e18); 345 | 346 | FillComponent[] memory components = new FillComponent[](1); 347 | components[0] = FillComponent({ 348 | fillToken: address(0), 349 | minimumFillAmount: 1 ether, 350 | recipient: address(0xCAFE), 351 | applyScaling: true 352 | }); 353 | 354 | // Use scalingFactor > 1e18 (exact-in mode, increasing fills) 355 | // This conflicts with price curve < 1e18 (decreasing claims) 356 | FillParameters memory fill = FillParameters({ 357 | chainId: block.chainid, 358 | tribunal: address(tribunal), 359 | expires: block.timestamp + 1 days, 360 | components: components, 361 | baselinePriorityFee: 0, 362 | scalingFactor: 1.5e18, // > 1e18 (exact-in) 363 | priceCurve: priceCurve, 364 | recipientCallback: new RecipientCallback[](0), 365 | salt: bytes32(uint256(1)) 366 | }); 367 | 368 | Lock[] memory commitments = new Lock[](1); 369 | commitments[0] = Lock({lockTag: bytes12(0), token: address(0), amount: 2 ether}); 370 | 371 | BatchCompact memory compact = BatchCompact({ 372 | arbiter: address(this), 373 | sponsor: sponsor, 374 | nonce: 0, 375 | expires: block.timestamp + 1 hours, 376 | commitments: commitments 377 | }); 378 | 379 | Adjustment memory adjustment = Adjustment({ 380 | adjuster: adjuster, 381 | fillIndex: 0, 382 | targetBlock: vm.getBlockNumber(), 383 | supplementalPriceCurve: new uint256[](0), 384 | validityConditions: bytes32(0), 385 | adjustmentAuthorization: new bytes(0) 386 | }); 387 | 388 | bytes32[] memory fillHashes = new bytes32[](1); 389 | fillHashes[0] = tribunal.deriveFillHash(fill); 390 | 391 | // Expect revert with InvalidPriceCurveParameters 392 | vm.expectRevert(abi.encodeWithSignature("InvalidPriceCurveParameters()")); 393 | tribunal.fill{ 394 | value: 1 ether 395 | }(compact, fill, adjustment, fillHashes, bytes32(uint256(uint160(address(this)))), 0); 396 | } 397 | } 398 | --------------------------------------------------------------------------------