Objective
Close 3 functional gaps identified by independent line-by-line audit of the Phase 1+2 delivery (commit 55ab185) against the engineering brief dated 2026-05-10, plus 1 naming consistency fix identified by the Phase 3 audit (commit 8957d87):
- Fix #1 (E1): event-only discoverability of
conditionRefis broken. - Fix #2 (E2): global default payment token at
initializeis missing. - Fix #3 (E3):
reject()lifecycle coverage ofOpenandFundedstates is missing, with symmetric automatic refund onFunded → RejectedANDSubmitted → Rejected. - Fix #4 (P8):
ClearPactEvaluator.rejectResult()emits the wrong event name (DisputeResolvedinstead ofEvaluatorRejected).
Outcome expected: 3 surgical code changes to ClearPactJob.sol + 1 event rename in ClearPactEvaluator.sol, fully ABI-preserving for the 12 standard ERC-8183 selectors and 12 standard events on the Job contract. No regression on the existing 5/5 closed divergences from Phase 1+2 nor on the Phase 3 thin-wrapper architecture.
⚠️ Test suite impact : the existing Foundry test suite in contracts/test/ClearPactJob.t.sol (60 tests) was built against the Phase 2 non-corrected code (commit 55ab185). Phase 2bis WILL break ~85-90% of the tests mechanically (signature initialize changes from 4 args to 5 args, all setUp() and _createJob/_fundJob/_submitJob helpers ripple) and 1 test logically (test_state_cannotRejectBeforeSubmit becomes contre-pertinent). The agent MUST update the existing test suite as part of this PR, plus add new tests for the 4 fixes. See § « Test suite changes required » below for the full list.
Inputs provided
- Authoritative brief: the 2026-05-10 engineering brief — same source of truth as Phase 1+2 — owner can paste again if needed. Architectural Decisions 1-5 remain in force unchanged.
- Independent audit triage: line-by-line verdict on Phase 1+2 (
note-phase-1-2-erc-8183-triagein the owner wiki). Confirms 5/5 original divergences closed and exposes E1/E2/E3 as residual. - Source-of-truth code locations (in the working copy
/c/Users/renau/Documents/ClearPact/clearpact-2/contracts/src/):interfaces/IClearPactJob.sol— ERC-8183 ABI surface (no change expected)interfaces/IACPHook.sol— hook interface (no change expected)interfaces/IClearPactExtensions.sol— ClearPact extensions getters (may need 1 minor addition for E2, see below)ClearPactJob.sol— main implementation (3 targeted edits)
Pre-execution checkpoint
Before writing any code, the agent must confirm the following understanding back to the owner:
- Fix #1 (E1) modifies the emission point of
ClearPactJobMetadataonly. The event signature is unchanged:event ClearPactJobMetadata(uint256 indexed jobId, bytes32 conditionRef, string description);. The agent must NOT change the topic0 hash. - Fix #2 (E2) adds a parameter to
initialize(address paymentToken_) and adds storage (address public paymentToken). Becauseinitializeis called only once via the proxy at deployment, this is a breaking change to the initializer ABI — the owner must coordinate the deployment script accordingly. Storage layout safety: the new variable must be inserted before theuint256[50] private __gap;reserved slot at the end of the storage block, in a position that does NOT shift any existing variable’s storage slot. - Fix #3 (E3) does not change any function selector or event signature. It only expands the access control conditions inside the
reject()function body and adds symmetric automatic refund on bothFunded → RejectedandSubmitted → Rejectedpaths. - Fix #4 (P8) is a pure event rename in
ClearPactEvaluator.sol:DisputeResolved→EvaluatorRejectedwith the same parameter signature. The event topic0 hash WILL change for this event — this is acceptable because it is a ClearPact-specific custom event on the Evaluator contract, NOT a standard ERC-8183 event on the Job contract. No indexer compatibility impact. - None of the 4 fixes touches the 12 standard ERC-8183 selectors or the 12 standard event topic0 hashes on
ClearPactJobverified at Phase 2 (table inday-44-summary-ceo-briefing). - The existing 60-test Foundry suite in
contracts/test/ClearPactJob.t.solwill be modified as part of this PR:setUp()migrated to 5-arginitialize, 1 test reformulated, ~10 new tests added, 2 Evaluator tests enriched. See § « Test suite changes required ».
If any of these statements is unclear or contested, the agent must ask the owner BEFORE implementing.
Gap analysis and required fixes
Fix #1 — E1 — Re-emit ClearPactJobMetadata at setBudget when conditionRef is set
Problem statement
Current behavior in ClearPactJob.sol lines 167-172 (verbatim):
_jobCreatedAt[jobId] = block.timestamp;// Standard 6-param ERC-8183 event (no description in spec event).emit JobCreated(jobId, msg.sender, provider, evaluator, expiredAt, hook);// ClearPact-specific: description + conditionRef (zero until setBudget sets it).emit ClearPactJobMetadata(jobId, bytes32(0), description);
The conditionRef is later stored at setBudget (lines 214-219, verbatim):
if (optParams.length >= 64) {(, bytes32 conditionRef) = abi.decode(optParams[:64], (address, bytes32));if (conditionRef != bytes32(0)) {_jobConditionRefs[jobId] = conditionRef;}}
But no event is emitted at this point. Consequence: an event-only indexer subscribed to ClearPactJobMetadata will always see conditionRef = 0. The real conditionRef is only retrievable via the RPC call getJobConditionRef(jobId).
Why this matters
The 2026-05-10 brief explicitly justified ClearPactJobMetadata as:
« Émis à
createJob. Expose les deux champs ClearPact-significatifs en une seule lecture event-only :conditionRef(hash off-chain, intégrité) etdescription(texte human-readable pour discoverability). Permet aux indexers de récupérer ces deux champs sans faire d’appel RPC supplémentaire àgetJob(). »
The current implementation breaks this promise.
Recommended fix
Emit a second ClearPactJobMetadata event at setBudget when the optParams payload includes a non-zero conditionRef. The event signature stays exactly the same — only the emission frequency changes (1 at createJob with conditionRef=0, then 1 more at setBudget if conditionRef != 0).
Verbatim diff to ClearPactJob.sol:192-222 (replace the existing setBudget body):
function setBudget(uint256 jobId,uint256 amount,bytes calldata optParams) external override nonReentrant whenNotPaused {Job storage job = _jobs[jobId];if (job.client == address(0)) revert WrongState();if (msg.sender != job.client) revert OnlyClient();if (job.status != JobStatus.Open) revert WrongState();if (amount == 0) revert BudgetZero();job.budget = amount;// Decode optional params: (address token, bytes32 conditionRef).// Both are optional — partial decoding is allowed; missing fields default to zero.if (optParams.length >= 32) {address tokenOverride = abi.decode(optParams[:32], (address));if (tokenOverride != address(0)) {_jobTokens[jobId] = tokenOverride;emit ClearPactJobTokenSet(jobId, tokenOverride);}}if (optParams.length >= 64) {(, bytes32 conditionRef) = abi.decode(optParams[:64], (address, bytes32));if (conditionRef != bytes32(0)) {_jobConditionRefs[jobId] = conditionRef;// E1 fix: re-emit ClearPactJobMetadata with the real conditionRef so that// event-only indexers can pick it up without an RPC enrichment call.emit ClearPactJobMetadata(jobId, conditionRef, job.description);}}emit BudgetSet(jobId, amount);}
Notes:
- The second
ClearPactJobMetadatacarries the samedescriptionas the one emitted atcreateJob. Indexers that miss the first event can still reconstitute the full Metadata from the second one alone. - If
setBudgetis called multiple times with differentconditionRefvalues (unlikely but possible while job is Open), the event will be re-emitted each time. Acceptable — indexer keeps the latest.
Acceptance criteria
setBudget(jobId, amount, abi.encode(address(0), bytes32(0x123...)))emitsClearPactJobMetadata(jobId, bytes32(0x123...), <description>)in addition toBudgetSet.setBudget(jobId, amount, "")does NOT emit any additionalClearPactJobMetadata.setBudget(jobId, amount, abi.encode(USDC, bytes32(0)))does NOT emit any additionalClearPactJobMetadata(conditionRef is zero).- No regression on
ClearPactJobTokenSetemission logic. - Test coverage for this fix is out of scope of this ticket — it belongs to Phase 3 Foundry test suite (
#1552253). The agent must NOT add tests in this ticket. The Phase 3 ticket will be extended to cover the 3 new behaviors of Phase 2bis.
Fix #2 — E2 — Add global default paymentToken to initialize and fallback in _resolveToken
Problem statement
Current initialize signature in ClearPactJob.sol:104-109 (verbatim):
function initialize(address admin,address treasury_,uint256 platformFeeBP_,uint256 evaluatorFeeBP_) external initializer {
No paymentToken_ parameter. The helper _resolveToken at lines 427-430 (verbatim):
function _resolveToken(uint256 jobId) internal view returns (address token) {token = _jobTokens[jobId];require(token != address(0), "no token configured");}
Reverts if setBudget was not called with a token override. There is no fallback to a global default token.
Why this matters
The 2026-05-10 brief Decision 5 was explicit:
« Si
optParamsest vide, le contract utilise le default token global (USDC Base Sepolia, fixé àinitialize(paymentToken_, treasury_)). »
The current code requires the caller (typically the ClearPact backend SDK) to always pass an explicit token via setBudget optParams. This works as a backend workaround but contradicts the documented contract behavior. If a third party reads the contract docs and calls setBudget(jobId, amount, "") expecting USDC by default, they will revert.
Recommended fix
Three small changes:
Change 1 — Add paymentToken_ parameter to initialize and store it in a new state variable. Update ClearPactJob.sol:65-77 storage block and :104-125 initializer:
// ─── Storage ────────────────────────────────────────────────────────────// ... existing storage variables 1-9 unchanged ...mapping(address => bool) public whitelistedHooks;// E2 fix: global default payment token. Inserted BEFORE the __gap reserved slot// so existing storage slots are not shifted (UUPS safety).address public paymentToken;/// @dev UUPS upgrade safety gap. 50 slots reserved for future storage./// E2 fix: reduced from [50] to [49] to make room for paymentToken without/// breaking the upgrade story (one slot consumed by paymentToken).uint256[49] private __gap;
CRITICAL storage layout safety note: the new address public paymentToken variable consumes exactly one storage slot. The __gap array MUST be reduced from [50] to [49] to keep the total storage footprint identical. If the existing contract is already deployed before this fix lands (which is the case at commit 55ab185), the upgrade is binary-safe only if paymentToken is appended at the position immediately preceding __gap. The agent MUST verify with forge inspect ClearPactJob storage-layout before and after the change to confirm no slot of any other variable shifts.
Change 2 — Update initialize signature and grant logic at ClearPactJob.sol:104-125:
function initialize(address admin,address treasury_,address paymentToken_, // <-- NEW (E2)uint256 platformFeeBP_,uint256 evaluatorFeeBP_) external initializer {require(admin != address(0), "admin = zero");require(treasury_ != address(0), "treasury = zero");require(paymentToken_ != address(0), "paymentToken = zero"); // <-- NEW (E2)require(platformFeeBP_ + evaluatorFeeBP_ <= MAX_FEE_BP, "fees exceed 50%");__AccessControl_init();__Pausable_init();_grantRole(DEFAULT_ADMIN_ROLE, admin);_grantRole(OPERATOR_ROLE, admin);_grantRole(PAUSER_ROLE, admin);treasury = treasury_;paymentToken = paymentToken_; // <-- NEW (E2)platformFeeBP = platformFeeBP_;evaluatorFeeBP = evaluatorFeeBP_;}
Change 3 — Update _resolveToken at ClearPactJob.sol:427-430 to fall back to the global default:
/// @dev Resolve the token for a job: per-job override or global default./// Reverts only if neither the per-job override nor the global default is set.function _resolveToken(uint256 jobId) internal view returns (address token) {token = _jobTokens[jobId];if (token == address(0)) {token = paymentToken; // E2 fix: fallback to global default}require(token != address(0), "no token configured");}
Acceptance criteria
initializenow requiresaddress paymentToken_as the 3rd parameter; reverts onaddress(0).setBudget(jobId, amount, "")(emptyoptParams) →_resolveTokenreturnspaymentToken(default), no revert.setBudget(jobId, amount, abi.encode(customToken))→_resolveTokenreturnscustomToken(per-job override wins).forge inspect ClearPactJob storage-layoutbefore and after Fix #2: no slot of any existing variable shifts.paymentTokenoccupies the slot immediately preceding__gap,__gaplength is49.- Deploy script (Phase 4) updated to pass
paymentToken_= USDC Base Sepolia address. - Test coverage for this fix is out of scope of this ticket — it belongs to Phase 3 Foundry test suite (
#1552253). The agent must NOT add tests in this ticket.
Fix #3 — E3 — Expand reject() access control to cover Open (client) and Funded (evaluator)
Problem statement
Current reject() in ClearPactJob.sol:314-332 (verbatim):
function reject(uint256 jobId,bytes32 reason,bytes calldata optParams) external override nonReentrant whenNotPaused {Job storage job = _jobs[jobId];if (job.client == address(0)) revert WrongState();if (msg.sender != job.evaluator && msg.sender != job.client) revert OnlyEvaluator();if (job.status != JobStatus.Submitted) revert WrongState();// ... hook + transition + event ...}
The state check job.status != JobStatus.Submitted makes reject() callable only when the job is in Submitted state. The 2026-05-10 brief access control table specified three reject paths:
| reject from | Caller | Reason |
|---|---|---|
Open | client only | Client cancels before funding |
Funded | evaluator only | Evaluator pre-emptively rejects funded job (provider non-performance, etc.) |
Submitted | evaluator only (brief) — or client (current code, accepted as E5) | Evaluator rejects the submission |
Current code blocks the first two paths. Consequence for the Funded path: a job that is funded and whose provider never delivers can only be unblocked by claimRefund AFTER expiredAt. For long expiries (weeks/months), the client is stuck.
Why this matters
The brief was precise on the lifecycle coverage. The current implementation creates a UX hole: clients have no fast-path remedy when a provider goes dark on a funded job.
The Open path is less critical (no escrowed funds at stake), but still a divergence from the brief and a usability nicety.
Recommended fix
Rewrite the access control and state check in reject() to cover the 3 paths. Note: this fix is compatible with E5 (which we accept as documented — complete and reject are callable by client OR evaluator at Submitted). For the Funded path, we restrict to evaluator only (as per brief) to preserve the trust model.
Refund policy (owner decision 2026-05-14): symmetric automatic refund in both Funded → Rejected AND Submitted → Rejected paths. The spec EIP-8183 leaves Submitted → Rejected implementation-defined; ClearPact’s chosen policy is to always return escrowed funds to the client on rejection. This guarantees a fast on-chain recovery path without requiring claimRefund + expiredAt and without leaving funds in administrative limbo.
Verbatim diff to ClearPactJob.sol:314-332 (replace the existing reject body, hook positions preserved):
function reject(uint256 jobId,bytes32 reason,bytes calldata optParams) external override nonReentrant whenNotPaused {Job storage job = _jobs[jobId];if (job.client == address(0)) revert WrongState();JobStatus s = job.status;// E3 fix: 3 lifecycle paths supported.if (s == JobStatus.Open) {// Pre-funding cancel: client only.if (msg.sender != job.client) revert OnlyClient();} else if (s == JobStatus.Funded) {// Pre-submission reject: evaluator only (brief access control).if (msg.sender != job.evaluator) revert OnlyEvaluator();} else if (s == JobStatus.Submitted) {// Post-submission reject: evaluator OR client (E5 accepted decision).if (msg.sender != job.evaluator && msg.sender != job.client) revert OnlyEvaluator();} else {revert WrongState();}bytes4 sel = IClearPactJob.reject.selector;_callBeforeHook(jobId, sel, optParams, job.hook);job.status = JobStatus.Rejected;// E3 fix: symmetric automatic refund when funds are escrowed.// - Open state has no escrowed funds → no transfer.// - Funded and Submitted both have escrowed funds → transfer back to client.if (s == JobStatus.Funded || s == JobStatus.Submitted) {uint256 amount = job.budget;address token = _resolveToken(jobId);_jobSettledAt[jobId] = block.timestamp;IERC20(token).safeTransfer(job.client, amount);emit Refunded(jobId, job.client, amount);}emit JobRejected(jobId, msg.sender, reason);_callAfterHook(jobId, sel, optParams, job.hook);}
Notes on the symmetric refund policy
- Spec compliance: EIP-8183 does not mandate refund behavior on
Submitted → Rejected; it leaves the policy implementation-defined. ClearPact’s symmetric refund choice is explicitly compatible with the spec. - Off-chain dispute resolution: if a client and evaluator want to negotiate an alternative outcome (partial release, custom split) instead of a full refund, that negotiation must happen BEFORE calling
reject. Oncerejectis called, the refund is automatic and final. This is a deliberate design — it prevents funds from being stuck in administrative limbo while preserving an off-chain coordination window pre-call. _jobSettledAtis set on the refund path (Funded or Submitted reject), matching the behavior ofcompleteandclaimRefund. This keeps the timestamp accounting consistent across all terminal-state transitions.- Reentrancy: the existing
nonReentrantmodifier onrejectalready guards thesafeTransfercall. No additional reentrancy guard is needed. The refund happens AFTER the state transition (job.status = JobStatus.Rejected;) to prevent re-entry intorejectfor the same job — even if the token contract is malicious.
Acceptance criteria
reject(jobId, reason, optParams)fromOpenstate byclient→ state transitions toRejected, no token transfer,JobRejectedemitted, no revert.reject(...)fromOpenstate by non-client → reverts withOnlyClient.reject(...)fromFundedstate byevaluator→ state transitions toRejected, budget refunded to client in the resolved token,JobRejectedANDRefundedevents emitted,_jobSettledAtset.reject(...)fromFundedstate byclient(or anyone except evaluator) → reverts withOnlyEvaluator.reject(...)fromSubmittedstate byevaluatororclient→ state transitions toRejected, budget refunded to client (symmetric policy 2026-05-14),JobRejectedANDRefundedevents emitted,_jobSettledAtset.reject(...)fromCompleted,Rejected, orExpired→ reverts withWrongState.- Hook
beforeAction(jobId, reject.selector, optParams)/afterAction(...)called for ALL three paths (Open, Funded, Submitted) at the existing positions. - State transition (
job.status = JobStatus.Rejected;) happens BEFORE thesafeTransfercall (reentrancy safety). - Test coverage for this fix is mandated in this ticket (revised 2026-05-15) — see § « Test suite changes required » below. Update
test_state_cannotRejectBeforeSubmit(becomes contre-pertinent) and add new tests for the 3 lifecycle paths + symmetric refund.
Fix #4 — P8 — Rename event DisputeResolved → EvaluatorRejected in ClearPactEvaluator.sol
Problem statement
Current ClearPactEvaluator.sol:46-51, 118:
event DisputeResolved(address indexed jobContract,uint256 indexed jobId,address indexed evaluator,bytes32 reason);// ...function rejectResult(...) external {// ...emit DisputeResolved(jobContract, jobId, msg.sender, reason);}
The event DisputeResolved is emitted by rejectResult, which is the standard evaluator-side reject path (post-submission). The name suggests an arbitrage/admin override has occurred — incorrect semantics for the standard reject flow.
Why this matters
For audit trail symmetry with EvaluatorApproved, the event should be EvaluatorRejected. Off-chain indexers (8004scan.io, ClearPact internal) will display a more accurate semantic. No functional impact.
Recommended fix
Pure rename. Replace the event declaration and the emit site:
// Old:event DisputeResolved(address indexed jobContract, uint256 indexed jobId, address indexed evaluator, bytes32 reason);// New:event EvaluatorRejected(address indexed jobContract, uint256 indexed jobId, address indexed evaluator, bytes32 reason);// Old:emit DisputeResolved(jobContract, jobId, msg.sender, reason);// New:emit EvaluatorRejected(jobContract, jobId, msg.sender, reason);
Acceptance criteria
- Event
DisputeResolvedis removed fromClearPactEvaluator.sol. - Event
EvaluatorRejectedwith identical parameter signature is declared and emitted byrejectResult(). - Event
EvaluatorApprovedis unchanged. - The event topic0 hash changes for the rejection event (
DisputeResolvedtopic0 deprecated,EvaluatorRejectedtopic0 active). This is acceptable per Pre-execution checkpoint #4. - Update relevant test references (the existing test suite does NOT currently assert this event name — confirm via grep before edit).
Test suite changes required (revised 2026-05-15)
Owner decision 2026-05-15: Phase 2bis transmise comme v3 diff with extension of P3 tests. The Phase 3 Foundry suite at
contracts/test/ClearPactJob.t.sol(60 tests) was built against the Phase 2 non-corrected code (commit55ab185) and will be partially broken by Fix #2 and Fix #3. The agent MUST update the suite as part of this PR.
A. Mechanical migration
setUp() (l. 52-79) and test_zeroFee_providerGetsAll (l. 207-211) both call ClearPactJob.initialize(admin, treasury, FEE_BP, EVAL_BP) with the current 4-arg signature. Migrate to the new 5-arg signature:
// Old:bytes memory initData = abi.encodeCall(ClearPactJob.initialize,(admin, treasury, FEE_BP, EVAL_BP));// New (Fix #2):bytes memory initData = abi.encodeCall(ClearPactJob.initialize,(admin, treasury, address(token), FEE_BP, EVAL_BP));
This single migration fixes ~85-90% of the 60 tests that ripple through setUp() and helpers _createJob / _fundJob / _submitJob.
B. Logical test reformulation (1 test + 1 comment)
test_state_cannotRejectBeforeSubmit (l. 405-411) currently asserts that reject from Funded reverts:
function test_state_cannotRejectBeforeSubmit() public {uint256 jobId = _fundJob(address(0));vm.prank(evaluator);vm.expectRevert(IClearPactExtensions.WrongState.selector);job.reject(jobId, keccak256("no"), "");}
Post-Fix #3 (E3), reject from Funded by evaluator is allowed and triggers symmetric refund. This test becomes contre-pertinent. Replace it with test_reject_fromFunded_byEvaluator_refundsClient (see § C below).
Comment in test_revisionReverts_afterRejected (l. 166-170) currently documents the broken behavior:
« In our contract, reject sets Rejected (no re-submit possible from Rejected). Correct revision model: submit → reject → fund still held; provider can re-submit only if contract allows it. Our impl requires Funded state for submit, so: submit → complete is the acceptance path; reject leaves it Rejected. »
Reformulate post-Fix #3 to reflect the new policy: « reject from Submitted triggers symmetric automatic refund to client (per Phase 2bis Fix #3 owner decision 2026-05-14). Job ends in Rejected state, no re-submit possible, funds are returned. »
C. New tests to add (~10)
| Test name | Covers |
|---|---|
test_setBudget_emitsClearPactJobMetadata_whenConditionRefSet | Fix #1 E1 — re-emit event with non-zero conditionRef |
test_setBudget_doesNotEmit_whenConditionRefZero | Fix #1 E1 — idempotence (no spurious emit) |
test_initialize_revertsOnZeroPaymentToken | Fix #2 E2 — initialize validation |
test_resolveToken_fallbackToGlobal_whenSetBudgetEmpty | Fix #2 E2 — default token fallback |
test_resolveToken_overrideWins_whenSetBudgetWithToken | Fix #2 E2 — per-job override wins |
test_reject_fromOpen_byClient_success | Fix #3 E3 — Open path |
test_reject_fromOpen_byNonClient_reverts | Fix #3 E3 — ACL Open |
test_reject_fromFunded_byEvaluator_refundsClient_symmetric | Fix #3 E3 — Funded path + refund |
test_reject_fromFunded_byClient_reverts | Fix #3 E3 — ACL Funded (evaluator only) |
test_reject_fromSubmitted_refundsClient_symmetric | Fix #3 — Submitted path + refund symétrique |
test_reject_reentrancy_onFundedRefund | Fix #3 — reentrancy safety on new refund path |
D. Evaluator tests to enrich (2)
test_evaluator_rejectResult (l. 806-826) currently asserts only state transition to Rejected. Post-Fix #3, the symmetric refund applies. Enrich with:
// After existing assertions, add:assertEq(token.balanceOf(client), clientBalanceBefore + BUDGET, "client refunded symmetric");// Assert Refunded event was emitted on Job.// Assert _jobSettledAt set.
New test recommended: test_evaluator_rejectResult_fromFunded — verify the new Funded path via Evaluator. Requires job.evaluator == address(evaluatorContract) for msg.sender == job.evaluator check post-Fix #3.
Scope and exclusions
In-scope:
- Fix #1 (E1) —
setBudgetre-emitsClearPactJobMetadatawhen conditionRef ≠ 0. - Fix #2 (E2) — Add
paymentToken_toinitialize+ fallback in_resolveToken+ storage layout safe. - Fix #3 (E3) — Expand
reject()access control to cover Open (client) + Funded (evaluator) + Submitted (evaluator or client per E5), with symmetric automatic refund on Funded and Submitted paths. - Fix #4 (P8) — Rename event
DisputeResolved→EvaluatorRejectedinClearPactEvaluator.sol(revised 2026-05-15 post-audit Phase 3). - Test suite update: mechanical migration of
setUp()to 5-arginitialize, reformulation of 1 test, addition of ~10 new tests, enrichment of 2 Evaluator tests (revised 2026-05-15). forge inspect storage-layoutverification before/after Fix #2.- NatSpec updates for
setBudgetandreject(no signature change).
Out-of-scope (separate decisions or other phases):
PausableUpgradeableremoval or PAUSER_ROLE rework → owner-side governance decision, separate from this ticket.- 2 admin events (
PlatformFeeUpdated,EvaluatorFeeUpdated) hors brief → accepted documented (E4/E5), no change. - Decision 6 formalization (E5 souplesse complete/reject client) → owner-side, separate.
cancelfromOpen(E6) → covered by Fix #3’s new client-callable Open-reject; no separate cancel function.- P7 (terminology
approveResult / rejectResultvs briefresolveDispute) → owner decision archi 7ᵉ acted 2026-05-15 ; no code change required. - P9 (no ERC-165 check on
jobContractin Evaluator) → owner decision multi-tenant open acted 2026-05-15 ; no code change required. - P15 (
EVALUATOR_ROLEgranted to admin at init) → owner decision: document in Phase 4 deploy runbook (renounceRole(EVALUATOR_ROLE, admin)post-init + grant to dedicated evaluator EOAs). - Phase 4 deploy + verification → separate.
- Phase 5 API + SDK + Docs + Webhooks → separate.
- Sync
.solPolsia →github.com/clearpact/clearpactpublic repo → owner-side, separate (seeentities/clearpactActions Renaud).
Recommended phasing
| Step | Description |
|---|---|
| 1 | Pre-execution checkpoint: agent confirms understanding (6 statements above) to owner |
| 2 | Implement Fix #1 in setBudget (smallest change, no storage impact) |
| 3 | Implement Fix #2: initialize + storage + _resolveToken + storage-layout verification with forge inspect |
| 4 | Implement Fix #3: reject 3-path access control + symmetric refund (Funded and Submitted) |
| 5 | Implement Fix #4: rename event DisputeResolved → EvaluatorRejected in ClearPactEvaluator.sol (pure rename) |
| 6 | Test suite mechanical migration: setUp() + test_zeroFee_providerGetsAll to 5-arg initialize |
| 7 | Test suite logical changes: reformulate test_state_cannotRejectBeforeSubmit + update comment in test_revisionReverts_afterRejected + add ~10 new tests + enrich 2 Evaluator tests |
| 8 | Update IClearPactJob.sol and ClearPactEvaluator.sol NatSpec for new behaviors (no signature change on Job) |
| 9 | Deliver PR with diff + storage layout snapshot before/after + selectors confirmation + test results (60+ tests passing) |
Definition of Done
- Fix #1:
ClearPactJobMetadataemitted twice when conditionRef is set (once at createJob with zero, once at setBudget with real value); single emission when conditionRef is zero. - Fix #2:
initializeacceptspaymentToken_;_resolveTokenfalls back topaymentTokenwhen_jobTokens[jobId] == address(0); storage layout binary-identical except for the newpaymentTokenslot inserted before a shortened__gap[49]. - Fix #3:
reject()supports 3 lifecycle paths with correct access control per path; bothFunded → RejectedANDSubmitted → Rejectedtrigger automatic refund +Refundedevent (symmetric policy per owner decision 2026-05-14); state transition happens beforesafeTransfer(reentrancy safety). - Fix #4: event
DisputeResolvedrenamed toEvaluatorRejectedinClearPactEvaluator.sol(parameter signature unchanged). - Test suite:
setUp()migrated to 5-arginitialize;test_state_cannotRejectBeforeSubmitreplaced bytest_reject_fromFunded_byEvaluator_refundsClient_symmetric; ~10 new tests added (see § C); 2 Evaluator tests enriched (see § D); all tests pass (60+ tests total). - All 12 standard ERC-8183 function selectors unchanged on
ClearPactJob(re-verify withcast sigorforge inspect methods). - All 12 standard ERC-8183 event topic0 hashes unchanged on
ClearPactJob. forge buildclean (no new warnings beyond existingblock.timestamp).forge inspect ClearPactJob storage-layoutoutput included in PR description, before/after diff confirming onlypaymentTokenslot added and__gapshortened by 1.- NatSpec updated for
setBudgetandrejectinIClearPactJob.soland/orClearPactJob.sol(no signature change on Job). - NatSpec updated for
rejectResultinClearPactEvaluator.solto reference the new event name. - PR description references this ticket, the audit triages
note-phase-1-2-erc-8183-triageandnote-phase-3-erc-8183-triage.
Points of vigilance
- Storage layout (Fix #2): the new
paymentTokenMUST be inserted at exactly the position preceding__gapto avoid shifting any other variable’s slot. The__gaplength MUST be reduced from 50 to 49. If a UUPS upgrade is performed from55ab185to the post-Phase-2bis code, the storage layout must remain compatible. - Selector / topic0 stability: none of the 3 fixes should change any function selector or any event topic0 hash. Re-verify with
forge inspect methodsandcast sig-eventif needed. Any drift here breaks indexer compatibility (the whole point of Axe 1). - Refund logic in Fix #3 (Funded AND Submitted paths): introduces two code paths that move tokens. Both are protected by the existing
nonReentrantmodifier onrejectand usesafeTransfer. The state transition (job.status = JobStatus.Rejected;) MUST happen BEFORE thesafeTransfercall — this is the checks-effects-interactions pattern and a hard requirement for reentrancy safety. Adversarial scenarios will be covered in Phase 3 test suite. setBudgetre-emit (Fix #1) is idempotent and indexer-safe: emitting the sameClearPactJobMetadataevent multiple times for the samejobIdis acceptable; indexers should retain the latest payload.- Spec EIP-8183 still in draft: lock on the 2026-05-10 extraction (in
concepts/erc-8183of the owner wiki). Flag any amendment ASAP. - Hook gas cap (100 000) and
claimRefundnon-hookable: unchanged. The agent must NOT touch_callBeforeHook,_callAfterHook, orclaimRefundfor this ticket. - No test files: do NOT create or modify any file under
contracts/test/. Tests are Phase 3 scope.
Coordination with Renaud (owner)
- ✅ Pre-existing: brief 2026-05-10 + 5 architectural decisions are in force. No re-validation needed.
- Pre-execution checkpoint: agent confirms the 4 understanding statements (above) before writing code.
- Mid-execution check: storage-layout diff from
forge inspectmust be shown to owner before PR merge (Fix #2 only). - Post-execution: PR includes tests output + storage layout diff + selector/topic0 confirmation table.
- Deploy coordination: when Phase 4 follows, the deploy script must be updated to pass
paymentToken_ = USDC Base Sepolia(0x036CbD53842c5426634e7929541eC2318f3dCF7e).
Deliverable format
Pull Request on github.com/Polsia-Inc/clearpact (Polsia managed infra repo). PR title: Phase 2bis ERC-8183: fix E1/E2/E3/P8 residual gaps + test suite extension (setBudget event re-emit, default token, reject access control + symmetric refund, event rename).
PR content:
contracts/src/ClearPactJob.sol— 3 surgical edits (Fix #1, #2, #3)contracts/src/ClearPactEvaluator.sol— 1 event rename (Fix #4)contracts/src/interfaces/IClearPactJob.sol— NatSpec updates only (no signature change)contracts/test/ClearPactJob.t.sol— modified:setUp()migration + 1 test replaced + ~10 new tests + 2 Evaluator tests enrichedcontracts/storage-layout-before.txtandcontracts/storage-layout-after.txt— output offorge inspect ClearPactJob storage-layoutbefore and after Fix #2contracts/selectors-confirmation.txt— output offorge inspect methodsconfirming 12 selectors unchanged on Job- PR description: copy-paste the 4 acceptance criteria sections + test suite changes summary + reference this ticket