Skip to content

Add ace to pool hook#1593

Open
matYang wants to merge 33 commits intodevelopfrom
add-ace-to-pool-hook
Open

Add ace to pool hook#1593
matYang wants to merge 33 commits intodevelopfrom
add-ace-to-pool-hook

Conversation

@matYang
Copy link
Collaborator

@matYang matYang commented Jan 22, 2026

No description provided.

@matYang matYang changed the base branch from main to develop January 22, 2026 17:02
@matYang matYang requested review from 0xsuryansh and RensR January 28, 2026 09:49
/**
* @title IPolicyEngine
* @dev Interface for the policy engine.
*/
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment structure doesn't follow the style guide, but this is a completely internal project right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

latest version of IPolicyEngine.sol has almost finished audit and will be ported to a public repo and published in an npm package in the coming week, we'll eventually consume from there, let's keep the source as is for now

@@ -0,0 +1,378 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unusual pragma for something with no imports

Copy link
Collaborator Author

@matYang matYang Jan 30, 2026

Choose a reason for hiding this comment

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

ditto

Copy link
Collaborator

Choose a reason for hiding this comment

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

This isn't resolved? What do you mean ditto?

/// @param data The bytes to slice.
/// @param start The index starting from which to return the sub array.
/// @return Bytes sub array starting from start index.
function _slice(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is only used to cut off the first 4 bytes.

Can't you just add this function to AdvancedPoolHooksSetup, and maybe just make it external, make data calldata and use [4:]?


Pool.LockOrBurnInV1 memory lockOrBurnIn = _createLockOrBurnIn();

vm.stopPrank();
Copy link
Collaborator

Choose a reason for hiding this comment

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

When you're doing this in almost every test you can probably make it part of the setup and just do it in the few tests that need another prank

}
}

contract AdvancedPoolHooks_setPolicyEngineAllowFailedDetach is AdvancedPoolHooksSetup {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this a different contract?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

technically setPolicyEngineAllowFailedDetach is a standalone method so warranted its own test contract, but it was so simple I was thinking maybe just include in the same file; nvm, extracting it into its own

@matYang matYang marked this pull request as ready for review January 29, 2026 01:07
@matYang matYang requested a review from a team as a code owner January 29, 2026 01:07
});
bytes memory policyData =
abi.encodeWithSelector(CCIPPolicyEnginePayloads.POOL_HOOK_OUTBOUND_POLICY_DATA_V1_TAG, outboundData);
policyEngine.run(
Copy link

@masha256 masha256 Jan 30, 2026

Choose a reason for hiding this comment

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

I dont think we want to have this intermediate PoolHookOutboundPolicyDataV1. It would be cleaner if we just directly pass the calldata to the policy engine as the payload and have the extractor explicity decode these parameters into usable policy parameters. We also want to use tokenArgs as the "context" (ie, where something like the offchain permit can be passed to the policy engine). For example, the preflightCheck could be as simple as:

  function preflightCheck(
    Pool.LockOrBurnInV1 calldata lockOrBurnIn,
    uint16 blockConfirmationRequested,
    bytes calldata tokenArgs
  ) external {
    if (i_authorizedCallersEnabled) {
      _validateCaller();
    }
    checkAllowList(lockOrBurnIn.originalSender);

    if (address(s_policyEngine) != address(0)) {
      s_policyEngine.run(
        IPolicyEngine.Payload({selector: msg.sig, sender: msg.sender, data: msg.data[4:], context: tokenArgs})
      );
    }
  }

And postFlightCheck:

  function postFlightCheck(
    Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,
    uint256 localAmount,
    uint16 blockConfirmationRequested
  ) external {
    if (i_authorizedCallersEnabled) {
      _validateCaller();
    }

    if (address(s_policyEngine) != address(0)) {
      s_policyEngine.run(
        IPolicyEngine.Payload({selector: msg.sig, sender: msg.sender, data: msg.data[4:], context: releaseOrMintIn.offchainTokenData})
      );
    }
  }

/// @notice Sets a new policy engine while tolerating a pre-existing policy engine's detach reverting.
/// @dev Use this to force update an old policy engine whose detach() reverts.
/// @param newPolicyEngine The address of the new policy engine.
function setPolicyEngineAllowFailedDetach(
Copy link

Choose a reason for hiding this comment

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

Do you think we really need this? We have recently updated the ACE PolicyProtected to try/catch and not revert if the detatch reverts. A policy engine should not be able to hold hostage a contract. I think its fine to do the same here and not have the extra code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

with a defence in depth mentality, we should be more explicit about reverts, i.e. if detach reverts, it happens for a reason, the operator should properly investigate, then explicitly ack to allow detach reverts, this allows operators to reliably catch errors, instead of things failing silently and going unnoticed

Copy link

Choose a reason for hiding this comment

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

In PolicyProtected we do emit an event when it fails:

try s_policyEngine.detach() {
        // Detachment succeeded
      } catch (bytes memory reason) {
        emit PolicyEngineDetachFailed(address(s_policyEngine), reason);
      }

I would argue if its good enough for our client contracts (that inherit PolicyProtected) it should be good enough here. After all, we only did this approach because the pool was too big to inherit PolicyProtected, so logically they should implement the same pattern. But I will leave it to you guys - just feels like extra bytecode and complexity.

Copy link
Collaborator Author

@matYang matYang Feb 4, 2026

Choose a reason for hiding this comment

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

we probably operate with different trust assumptions, we have to assume the hook to be externally deployed and managed, and potentially using 3P policy engines, for example, if a 3P policy engine requires a prefund at time of attach, and auto-refunds at detach, letting detach revert non-blockingly can lead to loss of ability to receive refunds.

I've updated the hook to mirror PolicyProtected events, if user wants to replicate the same behavior as PolicyProtected, they can use the explicit AllowFailedDetach call, by default they are protected from this these kind of risks, which indeed are very unlikely to be real issues within BCM realm.

@github-actions
Copy link

github-actions bot commented Feb 4, 2026

Metric add-ace-to-pool-hook develop
Coverage 69.8% 69.5%

poolAddr := common.HexToAddress(tpDeployReport.Output.Address)
hooksAddr := common.HexToAddress(hooksDeployReport.Output.Address)

applyAuthorizedCallerUpdatesReport, err := cldf_ops.ExecuteOperation(b, advanced_pool_hooks.ApplyAuthorizedCallerUpdates, chain, evm_contract.FunctionInput[advanced_pool_hooks.AuthorizedCallerArgs]{
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should you not check if it's already configured?


function testFuzz_postflightCheck_WithPolicyEngine(
bytes memory sourcePoolData,
bytes memory offchainTokenData
Copy link
Collaborator

@RensR RensR Feb 5, 2026

Choose a reason for hiding this comment

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

nit: not sure this is worth fuzz testing as no edge cases are tested, just input passing. Fuzz tests take >100 times longer to run


function test_postflightCheck_RevertWhen_PolicyEngineRejects() public {
s_advancedPoolHooks.setPolicyEngine(address(s_mockPolicyEngine));
s_mockPolicyEngine.setShouldRevert(true, "Policy rejected");
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: cleaner as var instead of duplicating magic values

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.

3 participants