Skip to content

Conversation

@tamir-cow
Copy link
Collaborator

@tamir-cow tamir-cow commented Oct 27, 2025

Hook Validation Logic & Data Structure Refactoring

Summary

This PR implements the validation logic for hooks through the check_hooks function

Related PR: link - Contains the data fetching layer and extraction logic


Step 1: Fill Status Determination
offchain_trade = _find_offchain_trade(offchain_data, order_uid)
is_first_fill = offchain_trade.already_executed_amount == 0

Step 2: Hook Candidate Presence Check
has_no_hook_candidates = (
not onchain_data.hook_candidates.pre_hooks and
not onchain_data.hook_candidates.post_hooks
)
if has_no_hook_candidates and is_first_fill:
return False # Expected hooks but none executed
Rationale: If the order has hooks defined and it's the first
fill, onchain data must contain hook candidates.

Step 3: Pre-Hook Validation (Rule 3a)

Case A: First Fill
if is_first_fill:
for pre_hook in hooks.pre_hooks:
if not _check_hook_execution(onchain_data, order_uid,
pre_hook, "pre"):
return False

  • Requirement: All declared pre-hooks must be executed
  • Validation: Delegates to _check_hook_execution() for each
    pre-hook

Case B: Subsequent Fill
else: # Not first fill
if hooks.pre_hooks and
onchain_data.hook_candidates.pre_hooks:
for pre_hook in hooks.pre_hooks:
for executed_hook in
onchain_data.hook_candidates.pre_hooks:
if (executed_hook.target == pre_hook.target and
executed_hook.calldata ==
pre_hook.calldata):
logger.error("Pre-hook incorrectly executed
for subsequent fill")
return False

  • Requirement: Pre-hooks must NOT be executed on subsequent
    fills
  • Validation: Checks for presence of any declared pre-hooks in
    executed hooks

Step 4: Post-Hook Validation (Rule 3b)
for post_hook in hooks.post_hooks:
if not _check_hook_execution(onchain_data, order_uid,
post_hook, "post"):
return False

  • Requirement: All declared post-hooks must be executed on
    EVERY fill
  • No conditional logic: Always validated regardless of fill
    status

Rule Mapping

Rule 1: Pre-Hook Timing

Requirement: Pre-hooks must execute before the settlement
contract pulls user funds. The rule is enforced in the other pr when creating the hook candidates.


Rule 2: Post-Hook Timing

Requirement: Post-hooks must execute after the settlement
contract pushes proceeds to users.

Parallel to Rule 1: Same guarantee mechanism, applied to
post-hooks.


Rule 3a: Pre-Hooks on First Fill Only

Requirement: Partially fillable orders must execute pre-hooks
only on the first fill.

Dual Enforcement:

  1. Positive check: First fill must have all pre-hooks
  2. Negative check: Subsequent fills must not have any pre-hooks
  • Fill status determined by already_executed_amount == 0

Rule 3b: Post-Hooks on Every Fill

Requirement: Partially fillable orders must execute post-hooks
on every fill (first and subsequent).


Rule 4a: Matching Triplet (target, calldata, gas_limit)

Requirement: Executed hook must match expected hook on target
address, calldata, and gas limit.

Target Address and Calldata - regular equality check

Gas Limit:

  • Comparison: actual >= expected (inequality, not equality)
  • Special Case: gas_limit == 0 means unlimited (skip check)

Rule 4b: Hook Attempt Verification

Requirement: Hook must be attempted even if it reverts (revert
does not violate rules).

Not implemented yet


Rule 4c: No Upstream Reverts

Requirement: Intermediate calls between settle() and hook
execution must not revert.

Not implemented yet


Rule 4d: Gas Sufficiency

Requirement: Available gas forwarded to hook must be ≥
specified gas limit. Handled as part of rule 4a.

@tamir-cow tamir-cow marked this pull request as ready for review November 18, 2025 17:13
Copy link
Contributor

@fhenneke fhenneke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made one pass over the code. The general approach of adding hook attributes to onchain and offchain data seems good.

I had some issues matching the rules as described in the docstring of check_hooks() to the actual code. Maybe one changes to comments are required here. Will have a look at the fetching logic next.

There should be dedicated unit tests for check_hooks().

@tamir-cow
Copy link
Collaborator Author

Made one pass over the code. The general approach of adding hook attributes to onchain and offchain data seems good.

I had some issues matching the rules as described in the docstring of check_hooks() to the actual code. Maybe one changes to comments are required here. Will have a look at the fetching logic next.

There should be dedicated unit tests for check_hooks().

You need to look at both prs, this pr only has second phase rules applied and the other has the first phase

@fhenneke
Copy link
Contributor

I had a look at both this PR and at the PR related to data fetching and populating the objects used here.

I still think that every rule should either be checked explicitly or it should be explained (in docstrings of the function and model and in the PR description) how the data models must already make sure a test is not required.

Going through the different rules:

  1. This is guaranteed by OnchainSettlementData.hook_candidates being populated in a certain way -> docstring of check_hooks and OnchainSettlementData.
  2. Same as 1.
  3. This is currently not checked. One could put that into the data fetching (and document that) or one would need to change some models here and handle that explicitly here.
  4. a) I think one should check for matching (target, calldata, gas_limi) here even if the data fetching already makes sure it is satisfied. It is easy to check that as well and clearer how the code relates to the rule.
    b, c, d) This is currently not implemented. I think it is reasonable to extend the Hook data structure (e.g. an attribute enough_gas_available and no_upstream_revert) and test it explicitly once we check this. Until then, the docstring should mention that this is not yet implemented.

3. Partially fillable orders:
       a. Should execute the pre-hooks on the first fill only
       b. Should execute the post-hooks on every fill

by checking the executed field to determine if first fill or not
Copy link
Contributor

@fhenneke fhenneke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a few comment.

Note that the comment here as well as some of the points raise in the last review are largely unaddressed.

Copy link
Contributor

@fhenneke fhenneke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a few comment.

The most important ones are about checking for gas and checking for not executing hooks.

The design of the hook test also used some strong assumptions on data fetching. These should be removed or documented.

}
offchain_data.trades = [offchain_trade]

elif scenario == "insufficient_gas":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This is probably wrong, but lets wait for comments by Federico.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new changes I would rename that to "wrong gas limit" or something like that. A test on insufficient gas will only be implemented later.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree on renaming this to something like "incorrect gas limit," but overall the test looks reasonable. Maybe:

-             gas_limit=1000,  # Less than required 1030000
+             gas_limit=1000,  # Different from the required 1030000

Comment on lines +739 to +748
pre_hook_2 = Hook(
target=HexBytes("0xC02De8aB58f29E4d9fa9e274eC7c05a04e397313"),
calldata=HexBytes("0xc3985579"),
gas_limit=1040000,
)
post_hook_2 = Hook(
target=HexBytes("0xD03De8aB58f29E4d9fa9e274eC7c05a04e397314"),
calldata=HexBytes("0xd4985580"),
gas_limit=2050000,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use examples which are immediately identifiable as different from pre_hook and post_hook.

Copy link
Contributor

@fhenneke fhenneke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think the logic of checking hooks would benefit from a few changes. If we stick to the current implementation, which I do not recommend, there needs to be more explicit docs and also explicit checks for data consisentency.

ensuring Rule 1 (pre-hooks execute before pulling user funds)
- Post-hooks appear after the corresponding trade execution in the transaction trace,
ensuring Rule 2 (post-hooks execute after pushing proceeds)
- The ordering in each list reflects the actual execution order in the transaction
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this relevant? For now, we do not use it. In the future it is probably not enough to implement other checks.

}
offchain_data.trades = [offchain_trade]

elif scenario == "insufficient_gas":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new changes I would rename that to "wrong gas limit" or something like that. A test on insufficient gas will only be implemented later.

offchain_data.trades = [offchain_trade]

elif scenario == "subsequent_fill_with_pre_hooks":
# Subsequent fill - pre-hooks should NOT be executed, but they are
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That comment should probably more descriptive since the expected behavior is that the test succeeds.

Copy link

@fedgiac fedgiac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall logic and the code looks good. I only have small comments and suggestions.

Comment on lines +195 to +207
# If there are hooks in the offchain data and it's the first fill,
# but there aren't hook candidates in onchain data, return False
has_no_hook_candidates = (
not hook_candidates.pre_hooks and not hook_candidates.post_hooks
)
if has_no_hook_candidates and is_first_fill:
logger.error(
f"Transaction hash {tx_hash!r}: "
f"Hooks defined for order {trade.order_uid!r} "
f"but no hook candidates found in transaction"
)
return False

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this code really needed? The following code checks if the pre-hooks are in the candidate pre-hooks (if first fill) and if post-hooks are in the candidate post-hooks. This then seems redundant?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe so, it is added after encountering this case: 4814655aacfd3db9ebbaf16c428f23372aaf66cd39456c0c5fc15c1759d35588 where we have a mismatch in hooks because of subsequent fills, and based on logic we should skip them.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transaction you link to has order 0xce7aa6aa17e2e5f0e6cf94ff437de07add4b2852d9098c66237210fabd93dfb0fb632268c37856c6efd48738ef60a74680c4fb626b04786c in it, which has a hook, so it shouldn't match this block because has_no_hook_candidates is false.

But my point here is similar to this, with the extra note that it's particularly strange that if is_first_fill is false, not hook_candidates.pre_hooks is true, and not hook_candidates.post_hooks is true, then it skips this early exit condition and continues by using empty loop in the following checks.

Comment on lines +279 to +282
# If there are no trades, the rule for hooks is automatically satisfied
if not offchain_data.trades:
return True

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't needed, right? The loop should be empty and then the function returns true.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, this is to make this check explicit and clear

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind much, so feel free to keep as it is, but I was a bit confused here because I thought I was missing something. My vote always goes for less code when possible!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those early exits tend to confuse me more than they help as the case excluded is quite naturally treated with an empty for loop.

All other early exits in this PR seem to also not have an effect on the outcome of running the code but only serve the purpose to reduce mental load and, in the future, make bugs less likely.

For the example

    if not offchain_data.trades:
        return True

an can still see how it can help and it does not add too much noise (though more noise than I would like).

For the example

    # If there are hooks in the offchain data and it's the first fill,
    # but there aren't hook candidates in onchain data, return False
    has_no_hook_candidates = (
        not hook_candidates.pre_hooks and not hook_candidates.post_hooks
    )
    if has_no_hook_candidates and is_first_fill:
        logger.error(
            f"Transaction hash {tx_hash!r}: "
            f"Hooks defined for order {trade.order_uid!r} "
            f"but no hook candidates found in transaction"
        )
        return False

I would argue that the combination of three conditionals and including negations does not decrease mental load for me and adds quite a bit of noise. I can see this increasing the risk of bugs in the future.

}
offchain_data.trades = [offchain_trade]

elif scenario == "insufficient_gas":
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree on renaming this to something like "incorrect gas limit," but overall the test looks reasonable. Maybe:

-             gas_limit=1000,  # Less than required 1030000
+             gas_limit=1000,  # Different from the required 1030000

@fhenneke fhenneke self-requested a review December 5, 2025 09:40
@fhenneke
Copy link
Contributor

fhenneke commented Dec 5, 2025

Apart from some nitpicks the code looks good to me.

But I only want to approve it after checking how our code which depends on this repo handles the changes.

Copy link

@fedgiac fedgiac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks good, the only unaddressed comments are about coding style and can be disregarded. Not sure if I should approve since I don't know how the process work in this codebase, but you can consider this an approval to the best of my knowledge.

Comment on lines +229 to +230
# 0 means it's the first fill, any other value means it's not
already_executed_amount: int
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion, this came to mind while reviewing the other PR. It shouldn't be an issue by how we formulated the rules, but this may change in the future.

Suggested change
# 0 means it's the first fill, any other value means it's not
already_executed_amount: int
# This value represent how much the order was executed _before the settlement_.
# If the order is executed twice in the same settlement, this value will be the same for both orders.
# 0 means it's the first fill, any other value means it's not
already_executed_amount: int

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants