From e646828bfc9876e7f1bcc481cf0d7e58f1bb2714 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 14 Aug 2026 11:37:02 -0500 Subject: [PATCH 1/2] fix: prevent native protocol fee payout DoS --- .gas-snapshot.runtime | 88 +++++++++++++++---------------- src/DeepstateV1.sol | 19 +++++-- test/DeepstateV1NativeETH.t.sol | 92 ++++++++++++++++++++++++++++++++- 3 files changed, 149 insertions(+), 50 deletions(-) diff --git a/.gas-snapshot.runtime b/.gas-snapshot.runtime index 0e6960b..60b193f 100644 --- a/.gas-snapshot.runtime +++ b/.gas-snapshot.runtime @@ -1,80 +1,80 @@ -DeepstateV1IntegratorFeeGasTest:testGas_IntegratorAndProtocolFeeFill() (gas: 129374) -DeepstateV1IntegratorFeeGasTest:testGas_IntegratorFillAskFullyMatchesSingleBid() (gas: 103768) -DeepstateV1IntegratorFeeGasTest:testGas_IntegratorFillBidFullyMatchesSingleAsk() (gas: 103888) +DeepstateV1IntegratorFeeGasTest:testGas_IntegratorAndProtocolFeeFill() (gas: 129346) +DeepstateV1IntegratorFeeGasTest:testGas_IntegratorFillAskFullyMatchesSingleBid() (gas: 103757) +DeepstateV1IntegratorFeeGasTest:testGas_IntegratorFillBidFullyMatchesSingleAsk() (gas: 103866) DeepstateV1IntegratorFeeGasTest:testGas_IntegratorFillRestBidEmptyBook() (gas: 151809) -DeepstateV1IntegratorFeeGasTest:testGas_IntegratorRouteNetsRepeatedFeeToken() (gas: 122504) -RadixMatchingEngineFeeGasTest:testGas_CancelAskSkipsPathologicalBidTree() (gas: 39664) -RadixMatchingEngineFeeGasTest:testGas_CancelFilledBidClaim() (gas: 36750) +DeepstateV1IntegratorFeeGasTest:testGas_IntegratorRouteNetsRepeatedFeeToken() (gas: 122482) +RadixMatchingEngineFeeGasTest:testGas_CancelAskSkipsPathologicalBidTree() (gas: 39659) +RadixMatchingEngineFeeGasTest:testGas_CancelFilledBidClaim() (gas: 36739) RadixMatchingEngineFeeGasTest:testGas_CancelFullDepthBidCombRightmost() (gas: 423078) -RadixMatchingEngineFeeGasTest:testGas_CancelMaxValidDepthAskCombRightmost() (gas: 424771) -RadixMatchingEngineFeeGasTest:testGas_CancelPartialBid() (gas: 48804) -RadixMatchingEngineFeeGasTest:testGas_CancelUnfilledAsk() (gas: 39881) +RadixMatchingEngineFeeGasTest:testGas_CancelMaxValidDepthAskCombRightmost() (gas: 424760) +RadixMatchingEngineFeeGasTest:testGas_CancelPartialBid() (gas: 48797) +RadixMatchingEngineFeeGasTest:testGas_CancelUnfilledAsk() (gas: 39876) RadixMatchingEngineFeeGasTest:testGas_CancelUnfilledBid() (gas: 39717) RadixMatchingEngineFeeGasTest:testGas_FillAskConsumesDirtySamePriceBidSubtree() (gas: 87846) -RadixMatchingEngineFeeGasTest:testGas_FillAskConsumesFullDepthBidComb() (gas: 352999) +RadixMatchingEngineFeeGasTest:testGas_FillAskConsumesFullDepthBidComb() (gas: 352993) RadixMatchingEngineFeeGasTest:testGas_FillAskFullyMatchesSingleBid() (gas: 78197) RadixMatchingEngineFeeGasTest:testGas_FillAskPartiallyMatchesSingleBid() (gas: 97892) -RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesAskAndRestsRemainder() (gas: 130838) -RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesDirtySamePriceAskSubtree() (gas: 87970) -RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesMaxValidDepthAskComb() (gas: 324094) -RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesSamePriceAskSubtree() (gas: 77962) -RadixMatchingEngineFeeGasTest:testGas_FillBidFullyMatchesSingleAsk() (gas: 77862) -RadixMatchingEngineFeeGasTest:testGas_FillBidPartiallyMatchesSingleAsk() (gas: 97776) +RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesAskAndRestsRemainder() (gas: 130827) +RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesDirtySamePriceAskSubtree() (gas: 87959) +RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesMaxValidDepthAskComb() (gas: 324083) +RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesSamePriceAskSubtree() (gas: 77951) +RadixMatchingEngineFeeGasTest:testGas_FillBidFullyMatchesSingleAsk() (gas: 77851) +RadixMatchingEngineFeeGasTest:testGas_FillBidPartiallyMatchesSingleAsk() (gas: 97765) RadixMatchingEngineFeeGasTest:testGas_FillRestAskEmptyBook() (gas: 150894) RadixMatchingEngineFeeGasTest:testGas_FillRestBidEmptyBook() (gas: 152651) RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookAskMatchesOneBid() (gas: 122101) -RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookBidMatchesOneAsk() (gas: 137302) -RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookPartialFillsAsk() (gas: 138440) +RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookBidMatchesOneAsk() (gas: 137291) +RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookPartialFillsAsk() (gas: 138429) RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookPartialFillsBid() (gas: 123225) RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookRestsAsk() (gas: 163968) RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookRestsBid() (gas: 173286) -RadixMatchingEngineGasTest:testGas_CancelAskSkipsPathologicalBidTree() (gas: 39664) -RadixMatchingEngineGasTest:testGas_CancelFilledBidClaim() (gas: 36750) +RadixMatchingEngineGasTest:testGas_CancelAskSkipsPathologicalBidTree() (gas: 39659) +RadixMatchingEngineGasTest:testGas_CancelFilledBidClaim() (gas: 36739) RadixMatchingEngineGasTest:testGas_CancelFullDepthBidCombRightmost() (gas: 423078) -RadixMatchingEngineGasTest:testGas_CancelMaxValidDepthAskCombRightmost() (gas: 424751) -RadixMatchingEngineGasTest:testGas_CancelPartialBid() (gas: 48804) -RadixMatchingEngineGasTest:testGas_CancelUnfilledAsk() (gas: 39881) +RadixMatchingEngineGasTest:testGas_CancelMaxValidDepthAskCombRightmost() (gas: 424740) +RadixMatchingEngineGasTest:testGas_CancelPartialBid() (gas: 48797) +RadixMatchingEngineGasTest:testGas_CancelUnfilledAsk() (gas: 39876) RadixMatchingEngineGasTest:testGas_CancelUnfilledBid() (gas: 39717) RadixMatchingEngineGasTest:testGas_FillAskConsumesDirtySamePriceBidSubtree() (gas: 87242) RadixMatchingEngineGasTest:testGas_FillAskConsumesFullDepthBidComb() (gas: 327303) RadixMatchingEngineGasTest:testGas_FillAskFullyMatchesSingleBid() (gas: 77593) RadixMatchingEngineGasTest:testGas_FillAskPartiallyMatchesSingleBid() (gas: 97288) -RadixMatchingEngineGasTest:testGas_FillBidConsumesAskAndRestsRemainder() (gas: 130241) -RadixMatchingEngineGasTest:testGas_FillBidConsumesDirtySamePriceAskSubtree() (gas: 87376) -RadixMatchingEngineGasTest:testGas_FillBidConsumesMaxValidDepthAskComb() (gas: 323500) -RadixMatchingEngineGasTest:testGas_FillBidConsumesSamePriceAskSubtree() (gas: 77368) -RadixMatchingEngineGasTest:testGas_FillBidFullyMatchesSingleAsk() (gas: 77268) -RadixMatchingEngineGasTest:testGas_FillBidPartiallyMatchesSingleAsk() (gas: 97182) +RadixMatchingEngineGasTest:testGas_FillBidConsumesAskAndRestsRemainder() (gas: 130230) +RadixMatchingEngineGasTest:testGas_FillBidConsumesDirtySamePriceAskSubtree() (gas: 87365) +RadixMatchingEngineGasTest:testGas_FillBidConsumesMaxValidDepthAskComb() (gas: 323489) +RadixMatchingEngineGasTest:testGas_FillBidConsumesSamePriceAskSubtree() (gas: 77357) +RadixMatchingEngineGasTest:testGas_FillBidFullyMatchesSingleAsk() (gas: 77257) +RadixMatchingEngineGasTest:testGas_FillBidPartiallyMatchesSingleAsk() (gas: 97171) RadixMatchingEngineGasTest:testGas_FillRestAskEmptyBook() (gas: 150668) RadixMatchingEngineGasTest:testGas_FillRestBidEmptyBook() (gas: 152432) RadixMatchingEngineGasTest:testGas_LargeRandomBookAskMatchesOneBid() (gas: 121497) -RadixMatchingEngineGasTest:testGas_LargeRandomBookBidMatchesOneAsk() (gas: 136708) -RadixMatchingEngineGasTest:testGas_LargeRandomBookPartialFillsAsk() (gas: 137846) +RadixMatchingEngineGasTest:testGas_LargeRandomBookBidMatchesOneAsk() (gas: 136697) +RadixMatchingEngineGasTest:testGas_LargeRandomBookPartialFillsAsk() (gas: 137835) RadixMatchingEngineGasTest:testGas_LargeRandomBookPartialFillsBid() (gas: 122621) RadixMatchingEngineGasTest:testGas_LargeRandomBookRestsAsk() (gas: 163742) RadixMatchingEngineGasTest:testGas_LargeRandomBookRestsBid() (gas: 173067) -RadixMatchingEngineHookGasTest:testGas_CancelAskSkipsPathologicalBidTree() (gas: 43705) -RadixMatchingEngineHookGasTest:testGas_CancelFilledBidClaim() (gas: 37497) +RadixMatchingEngineHookGasTest:testGas_CancelAskSkipsPathologicalBidTree() (gas: 43698) +RadixMatchingEngineHookGasTest:testGas_CancelFilledBidClaim() (gas: 37486) RadixMatchingEngineHookGasTest:testGas_CancelFullDepthBidCombRightmost() (gas: 446433) -RadixMatchingEngineHookGasTest:testGas_CancelMaxValidDepthAskCombRightmost() (gas: 447267) -RadixMatchingEngineHookGasTest:testGas_CancelPartialBid() (gas: 53592) -RadixMatchingEngineHookGasTest:testGas_CancelUnfilledAsk() (gas: 43922) +RadixMatchingEngineHookGasTest:testGas_CancelMaxValidDepthAskCombRightmost() (gas: 447256) +RadixMatchingEngineHookGasTest:testGas_CancelPartialBid() (gas: 53587) +RadixMatchingEngineHookGasTest:testGas_CancelUnfilledAsk() (gas: 43915) RadixMatchingEngineHookGasTest:testGas_CancelUnfilledBid() (gas: 44505) RadixMatchingEngineHookGasTest:testGas_FillAskConsumesDirtySamePriceBidSubtree() (gas: 95252) RadixMatchingEngineHookGasTest:testGas_FillAskConsumesFullDepthBidComb() (gas: 414778) RadixMatchingEngineHookGasTest:testGas_FillAskFullyMatchesSingleBid() (gas: 85769) RadixMatchingEngineHookGasTest:testGas_FillAskPartiallyMatchesSingleBid() (gas: 105472) -RadixMatchingEngineHookGasTest:testGas_FillBidConsumesAskAndRestsRemainder() (gas: 139218) -RadixMatchingEngineHookGasTest:testGas_FillBidConsumesDirtySamePriceAskSubtree() (gas: 94243) -RadixMatchingEngineHookGasTest:testGas_FillBidConsumesMaxValidDepthAskComb() (gas: 410116) -RadixMatchingEngineHookGasTest:testGas_FillBidConsumesSamePriceAskSubtree() (gas: 93348) -RadixMatchingEngineHookGasTest:testGas_FillBidFullyMatchesSingleAsk() (gas: 84195) -RadixMatchingEngineHookGasTest:testGas_FillBidPartiallyMatchesSingleAsk() (gas: 104117) +RadixMatchingEngineHookGasTest:testGas_FillBidConsumesAskAndRestsRemainder() (gas: 139207) +RadixMatchingEngineHookGasTest:testGas_FillBidConsumesDirtySamePriceAskSubtree() (gas: 94232) +RadixMatchingEngineHookGasTest:testGas_FillBidConsumesMaxValidDepthAskComb() (gas: 410105) +RadixMatchingEngineHookGasTest:testGas_FillBidConsumesSamePriceAskSubtree() (gas: 93337) +RadixMatchingEngineHookGasTest:testGas_FillBidFullyMatchesSingleAsk() (gas: 84184) +RadixMatchingEngineHookGasTest:testGas_FillBidPartiallyMatchesSingleAsk() (gas: 104106) RadixMatchingEngineHookGasTest:testGas_FillRestAskEmptyBook() (gas: 140357) RadixMatchingEngineHookGasTest:testGas_FillRestBidEmptyBook() (gas: 142118) RadixMatchingEngineHookGasTest:testGas_LargeRandomBookAskMatchesOneBid() (gas: 138973) -RadixMatchingEngineHookGasTest:testGas_LargeRandomBookBidMatchesOneAsk() (gas: 154786) -RadixMatchingEngineHookGasTest:testGas_LargeRandomBookPartialFillsAsk() (gas: 146871) +RadixMatchingEngineHookGasTest:testGas_LargeRandomBookBidMatchesOneAsk() (gas: 154775) +RadixMatchingEngineHookGasTest:testGas_LargeRandomBookPartialFillsAsk() (gas: 146860) RadixMatchingEngineHookGasTest:testGas_LargeRandomBookPartialFillsBid() (gas: 133256) RadixMatchingEngineHookGasTest:testGas_LargeRandomBookRestsAsk() (gas: 193723) RadixMatchingEngineHookGasTest:testGas_LargeRandomBookRestsBid() (gas: 198650) diff --git a/src/DeepstateV1.sol b/src/DeepstateV1.sol index fed699c..fab44fb 100644 --- a/src/DeepstateV1.sol +++ b/src/DeepstateV1.sol @@ -2237,7 +2237,7 @@ contract DeepstateV1 is Ownable { } /// @notice Configure protocol fees taken from taker output on matched quantity. - /// @param recipient Fee recipient. Zero disables fees and requires `bps == 0`. + /// @param recipient Fee recipient. Zero disables fees; native ETH fees cannot be rejected. /// @param bps Fee in basis points, capped at 100. function setFeeConfig(address recipient, uint16 bps) external onlyOwner { if (bps > _MAX_FEE_BPS || (recipient == address(0) && bps != 0)) revert InvalidFeeConfig(); @@ -2315,7 +2315,7 @@ contract DeepstateV1 is Ownable { assembly { feeRecipient := config } - _safeTransferOut(params.isBid ? params.token0 : params.token1, feeRecipient, feeAmount); + _safeTransferProtocolFeeOut(params.isBid ? params.token0 : params.token1, feeRecipient, feeAmount); } _refundNativeValue(nativeRefund); } @@ -2347,7 +2347,7 @@ contract DeepstateV1 is Ownable { _settleDeltas(params.token0, settlement.token0Delta, params.token1, settlement.token1Delta); address outputToken = params.isBid ? params.token0 : params.token1; if (settlement.protocolFeeAmount != 0) { - _safeTransferOut(outputToken, _configRecipient(protocolConfig), settlement.protocolFeeAmount); + _safeTransferProtocolFeeOut(outputToken, _configRecipient(protocolConfig), settlement.protocolFeeAmount); } if (settlement.integratorFeeAmount != 0) { _safeTransferOut(outputToken, integratorFee.recipient, settlement.integratorFeeAmount); @@ -3154,7 +3154,7 @@ contract DeepstateV1 is Ownable { tstore(slot, 0) } - if (amount != 0) _safeTransferOut(token, recipient, amount); + if (amount != 0) _safeTransferProtocolFeeOut(token, recipient, amount); unchecked { ++i; @@ -3311,6 +3311,17 @@ contract DeepstateV1 is Ownable { if (amount != 0) _safeTransferOut(address(0), msg.sender, amount); } + /// @notice Pay an owner-configured protocol fee without letting an ETH receiver block fills. + /// @dev ERC20 settlement retains standard safe-transfer behavior. For native ETH, Solady first + /// attempts a bounded call and force-sends through an ephemeral contract if the recipient rejects. + function _safeTransferProtocolFeeOut(address token, address to, uint256 amount) private { + if (token == address(0)) { + to.forceSafeTransferETH(amount); + } else { + token.safeTransfer(to, amount); + } + } + /// @notice Pay either native ETH (`token == address(0)`) or an ERC20. function _safeTransferOut(address token, address to, uint256 amount) private { if (token == address(0)) { diff --git a/test/DeepstateV1NativeETH.t.sol b/test/DeepstateV1NativeETH.t.sol index 02a627b..f9a3972 100644 --- a/test/DeepstateV1NativeETH.t.sol +++ b/test/DeepstateV1NativeETH.t.sol @@ -299,7 +299,8 @@ contract DeepstateV1NativeETHTest is Test { function test_RouteTransfersNativeFeeAfterUserOutput() public { uint160 quantity = 10_000; - engine.setFeeConfig(feeRecipient, 100); + NativeFeeReceiver receiver = new NativeFeeReceiver(); + engine.setFeeConfig(address(receiver), 100); vm.prank(alice); engine.fill{value: quantity}(_fill(quoteA, _order(0, quantity), false, false, false)); @@ -312,7 +313,8 @@ contract DeepstateV1NativeETHTest is Test { engine.fillRoute(route); assertEq(bob.balance, bobEthBefore + 9_900); - assertEq(feeRecipient.balance, 100); + assertEq(address(receiver).balance, 100); + assertEq(receiver.receiveCalls(), 1); assertEq(address(engine).balance, 0); } @@ -382,6 +384,79 @@ contract DeepstateV1NativeETHTest is Test { assertEq(address(engine).balance, 0); } + function test_RevertingNativeProtocolFeeRecipientCannotBlockFill() public { + uint160 quantity = 10_000; + NativeRejectingFeeRecipient rejector = new NativeRejectingFeeRecipient(); + engine.setFeeConfig(address(rejector), 100); + bytes32 restingAsk = _restNativeAsk(quantity); + + uint256 bobEthBefore = bob.balance; + uint256 bobQuoteBefore = quoteA.balanceOf(bob); + vm.prank(bob); + bytes32 remainder = engine.fill(_fill(quoteA, _order(0, quantity), true, true, true)); + + assertEq(remainder, bytes32(0)); + assertEq(bob.balance, bobEthBefore + 9_900); + assertEq(quoteA.balanceOf(bob), bobQuoteBefore - quantity); + assertEq(address(rejector).balance, 100); + _assertNativeAskConsumed(restingAsk); + } + + function test_RevertingNativeProtocolFeeRecipientCannotBlockIntegratorFill() public { + uint160 quantity = 10_000; + NativeRejectingFeeRecipient rejector = new NativeRejectingFeeRecipient(); + engine.setFeeConfig(address(rejector), 100); + bytes32 restingAsk = _restNativeAsk(quantity); + + uint256 bobEthBefore = bob.balance; + vm.prank(bob); + bytes32 remainder = engine.fillWithIntegratorFee( + _fill(quoteA, _order(0, quantity), true, true, true), + DeepstateV1.IntegratorFee({recipient: integratorRecipient, bps: 50}) + ); + + assertEq(remainder, bytes32(0)); + assertEq(bob.balance, bobEthBefore + 9_850); + assertEq(address(rejector).balance, 100); + assertEq(integratorRecipient.balance, 50); + _assertNativeAskConsumed(restingAsk); + } + + function test_RevertingNativeProtocolFeeRecipientCannotBlockRoute() public { + uint160 quantity = 10_000; + NativeRejectingFeeRecipient rejector = new NativeRejectingFeeRecipient(); + engine.setFeeConfig(address(rejector), 100); + bytes32 restingAsk = _restNativeAsk(quantity); + + DeepstateV1.FillParams[] memory route = new DeepstateV1.FillParams[](1); + route[0] = _fill(quoteA, _order(0, quantity), true, true, true); + uint256 bobEthBefore = bob.balance; + vm.prank(bob); + engine.fillRoute(route); + + assertEq(bob.balance, bobEthBefore + 9_900); + assertEq(address(rejector).balance, 100); + _assertNativeAskConsumed(restingAsk); + } + + function test_RevertingNativeProtocolFeeRecipientCannotBlockIntegratorRoute() public { + uint160 quantity = 10_000; + NativeRejectingFeeRecipient rejector = new NativeRejectingFeeRecipient(); + engine.setFeeConfig(address(rejector), 100); + bytes32 restingAsk = _restNativeAsk(quantity); + + DeepstateV1.FillParams[] memory route = new DeepstateV1.FillParams[](1); + route[0] = _fill(quoteA, _order(0, quantity), true, true, true); + uint256 bobEthBefore = bob.balance; + vm.prank(bob); + engine.fillRouteWithIntegratorFee(route, DeepstateV1.IntegratorFee({recipient: integratorRecipient, bps: 50})); + + assertEq(bob.balance, bobEthBefore + 9_850); + assertEq(address(rejector).balance, 100); + assertEq(integratorRecipient.balance, 50); + _assertNativeAskConsumed(restingAsk); + } + function test_RevertingNativeIntegratorPayoutRevertsFillAtomically() public { uint160 quantity = 10_000; NativeRejectingFeeRecipient rejector = new NativeRejectingFeeRecipient(); @@ -462,6 +537,19 @@ contract DeepstateV1NativeETHTest is Test { assertEq(address(engine).balance, forcedSurplus); } + function _restNativeAsk(uint160 quantity) internal returns (bytes32 restingAsk) { + vm.prank(alice); + restingAsk = engine.fill{value: quantity}(_fill(quoteA, _order(0, quantity), false, false, false)); + } + + function _assertNativeAskConsumed(bytes32 restingAsk) internal view { + bytes32 id = engine.bookId(address(0), address(quoteA), 0); + (bytes32 askRoot,) = engine.roots(address(0), address(quoteA), 0); + assertEq(askRoot, bytes32(0)); + assertEq(engine.ownerOfOrder(engine.orderId(id, restingAsk)), alice); + assertEq(address(engine).balance, 0); + } + function _fill(NativeTestERC20 quote, bytes32 order, bool isBid, bool noRest, bool fillOrKill) internal pure From ccf22f2ba06eaf5d963fa1ba5bce2e6738fc59fb Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 14 Aug 2026 12:06:31 -0500 Subject: [PATCH 2/2] test: update formal bindings for forced fee payouts --- script/prove_protocol.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/script/prove_protocol.py b/script/prove_protocol.py index 1da2c82..ba6ba7e 100644 --- a/script/prove_protocol.py +++ b/script/prove_protocol.py @@ -118,15 +118,17 @@ def bind_model_to_source(): "refund = msg.value - required;", "_refundNativeValue(nativeRefund);", "if (amount != 0) _safeTransferOut(address(0), msg.sender, amount);", - "_safeTransferOut(params.isBid ? params.token0 : params.token1, feeRecipient, feeAmount);", - "_safeTransferOut(outputToken, _configRecipient(protocolConfig), settlement.protocolFeeAmount);", + "_safeTransferProtocolFeeOut(params.isBid ? params.token0 : params.token1, feeRecipient, feeAmount);", + "_safeTransferProtocolFeeOut(outputToken, _configRecipient(protocolConfig), settlement.protocolFeeAmount);", "_safeTransferOut(outputToken, integratorFee.recipient, settlement.integratorFeeAmount);", "if (baseAmount != 0) _safeTransferOut(token0, owner, baseAmount);", "_safeTransferOut(token, msg.sender, uint256(amount));", "_safeTransferOut(token0, msg.sender, uint256(amount0));", + "if (amount != 0) _safeTransferProtocolFeeOut(token, recipient, amount);", "if (amount != 0) _safeTransferOut(token, recipient, amount);", "if iszero(lt(token0, token1)) {", "if (token == address(0)) {", + "to.forceSafeTransferETH(amount);", "to.safeTransferETH(amount);", "if tload(_REENTRANCY_GUARD_SLOT) {", "try IHook(hook).execute{gas: _HOOK_GAS_LIMIT}", @@ -135,12 +137,16 @@ def bind_model_to_source(): if missing: raise ProofFailure(f"production/model binding is stale; missing source fragment: {missing[0]}") - # Every native outflow must pass through the nine modelled call sites and the single transfer - # helper. A future direct ETH transfer or additional helper call must extend the proof first. - if source.count("_safeTransferOut(") != 10: - raise ProofFailure("production/model binding is stale; unexpected native-capable outflow site count") + # Every native outflow must pass through the nine modelled call sites and exactly one of the two + # transfer helpers. A future direct ETH transfer or additional helper call must extend the proof. + if source.count("_safeTransferOut(") != 7: + raise ProofFailure("production/model binding is stale; unexpected general outflow site count") + if source.count("_safeTransferProtocolFeeOut(") != 4: + raise ProofFailure("production/model binding is stale; unexpected protocol-fee outflow site count") if source.count("safeTransferETH(") != 1: raise ProofFailure("production/model binding is stale; unexpected direct native transfer count") + if source.count("forceSafeTransferETH(") != 1: + raise ProofFailure("production/model binding is stale; unexpected forced native transfer count") def prove_tick_decomposition(p):