Skip to content

Conversation

@fedgiac
Copy link
Contributor

@fedgiac fedgiac commented Dec 3, 2025

Description

A proposal to simplify the current contracts by moving repeating code across different wrappers to the same contract.

This is achieved by creating a base abstract contract CowEvcBaseWrapper which is supposed to be inherited by all Euler wrappers.
It aggregates code related to three main features:

  • ERC712 hashing (in the form of constants, immutables, constructors and hashing)
  • EVC batch execution.
  • Errors.

The purpose is making it easier to spot what is different and what is the same between wrappers when reading the code.
Ideally, the remaining single-instance wrappers should have little remaining occurrences of remaining code. (One exception is _evcInternalSettle. I'm sure it can be simplified but I don't understand well enough the balance checks to make sure things are working after my changes, so I'm skipping it.)

The result is kind of nice imho: it makes clear(er) that what matters in the various instances is not how the batch is executed, but rather what's the content of the batches (i.e., the implementation of _encodeSignedBatchItems). It also makes it more visible that the settlement is executed before other operations when closing a position and after in the other two wrappers. I considered using a "itemsBefore/itemsAfter" model instead but I decided we can just switch to it if we ever find the need and just went for a flag.

The bad part of this PR is that Solidity doesn't have generics. This means that all external accessors like getSignedCalldata still need to be repeated on each individual wrapper instance, causing annoying code repetition. Because of that, I also had to play with raw memory pointers, which may be dangerous to do if we abuse assembly to generate structures in memory.

Note that I tried to make the minimal changes needed to the existing code base to show the difference, so some design choices I made may be questionable. I don't expect this PR to be merged, if anything because it would make a mess in the current PR stack.

Out of Scope

There should be no changes in the implementation, only in the code structure.

Testing Instructions

To be fair, I didn't go in depth when building the code, I'm trusting CI to tell me if something is off.

You may also want to compare the gas cost between the two runs. Here is the diff between PR base e8e9e6c and 0c98278.

forge snapshot --diff
Ran 10 test suites in 336.42ms (1.07s CPU time): 124 tests passed, 0 failed, 0 skipped (124 total tests)
━ CowWrapperTest::test_integration_ThreeWrappersChained() (gas: 99125 → 99125 | 0 0.000%)
━ CowWrapperTest::test_next_CallsWrapperAndThenNextSettlement() (gas: 39431 → 39431 | 0 0.000%)
━ CowWrapperTest::test_wrappedSettle_RevertsOnInvalidSettleSelector() (gas: 18822 → 18822 | 0 0.000%)
━ CowWrapperTest::test_wrappedSettle_RevertsWithNotASolver() (gas: 20222 → 20222 | 0 0.000%)
━ CowWrapperHelpersTest::test_immutableAuthenticators() (gas: 10869 → 10869 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_EmptyArrays() (gas: 10605 → 10605 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_EmptyWrapperData() (gas: 40352 → 40352 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_MixedWrapperDataSizes() (gas: 64717 → 64717 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_MultipleWrappers() (gas: 64380 → 64380 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnNotAWrapper() (gas: 22855 → 22855 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnNotAWrapper_SecondWrapper() (gas: 29118 → 29118 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnSettlementContractShouldNotBeSolver() (gas: 60187 → 60187 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnSettlementMismatch() (gas: 667039 → 667039 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnWrapperDataMalformed() (gas: 24869 → 24869 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnWrapperDataNotFullyConsumed() (gas: 29565 → 29565 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnWrapperDataTooLong_FirstWrapper() (gas: 565333 → 565333 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnWrapperDataTooLong_SecondWrapper() (gas: 580581 → 580581 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_SingleWrapper() (gas: 34243 → 34243 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_SucceedsWithMaxLengthData() (gas: 680581 → 680581 | 0 0.000%)
━ PreApprovedHashesUnitTest::testFuzz_ConsumePreApprovedHash(address,bytes32) (gas: 38101 → 38101 | 0 0.000%)
━ PreApprovedHashesUnitTest::testFuzz_MultipleUsersAndHashes(address,address,bytes32,bytes32) (gas: 69188 → 69188 | 0 0.000%)
━ PreApprovedHashesUnitTest::testFuzz_SetPreApprovedHash(address,bytes32) (gas: 27329 → 27329 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_ConsumePreApprovedHash_CannotConsumedTwice() (gas: 37254 → 37254 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_ConsumePreApprovedHash_EmitsEvent() (gas: 37959 → 37959 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_PreApprovedHashesStorage() (gas: 51111 → 51111 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_SetPreApprovedHash_CannotApproveConsumed() (gas: 38401 → 38401 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_SetPreApprovedHash_CannotRevokeConsumed() (gas: 38424 → 38424 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_SetPreApprovedHash_EmitsEvent() (gas: 28524 → 28524 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_SetPreApprovedHash_RevokeAndReapprove() (gas: 46036 → 46036 | 0 0.000%)
━ TestablePreApprovedHashes::testConsumeHash(address,bytes32) (gas: 2611 → 2611 | 0 0.000%)
↓ CowEvcCollateralSwapWrapperUnitTest::test_Constructor_SetsImmutables() (gas: 15749 → 15748 | -1 -0.006%)
↓ CowEvcCollateralSwapWrapperUnitTest::test_ParseWrapperData_EmptySignature() (gas: 13669 → 13668 | -1 -0.007%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_DifferentVaults() (gas: 1235312 → 1235840 | 528 0.043%)
↑ CowEvcClosePositionWrapperUnitTest::test_HelperRepay_RequiresCorrectOnBehalfOfAccount() (gas: 122723 → 122797 | 74 0.060%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_UnauthorizedInternalSettle() (gas: 13376 → 13386 | 10 0.075%)
↑ CowEvcClosePositionWrapperUnitTest::test_EvcInternalSettle_SubaccountMustBeControlledByOwner() (gas: 108564 → 108665 | 101 0.093%)
↑ CowEvcClosePositionWrapperUnitTest::test_EvcInternalSettle_OnlyEVC() (gas: 10313 → 10323 | 10 0.097%)
↑ CowEvcClosePositionWrapperUnitTest::test_Constructor_SetsDomainSeparator() (gas: 6495 → 6502 | 7 0.108%)
↓ CowEvcCollateralSwapWrapperUnitTest::test_Constructor_SetsDomainSeparator() (gas: 6487 → 6480 | -7 -0.108%)
↑ CowEvcOpenPositionWrapperUnitTest::test_Constructor_SetsDomainSeparator() (gas: 6297 → 6304 | 7 0.111%)
↑ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_WithLeveragedPosition() (gas: 1257541 → 1259196 | 1655 0.132%)
↓ CowEvcCollateralSwapWrapperUnitTest::test_ParseWrapperData_LongSignature() (gas: 13681 → 13659 | -22 -0.161%)
↑ CowEvcClosePositionWrapperUnitTest::test_HelperRepay_SuccessfulRepay() (gas: 180219 → 180533 | 314 0.174%)
↑ CowEvcClosePositionWrapperUnitTest::test_HelperRepay_WithDust() (gas: 179908 → 180222 | 314 0.175%)
↑ CowEvcClosePositionWrapperUnitTest::test_HelperRepay_RepayAll() (gas: 179749 → 180064 | 315 0.175%)
↑ CowEvcClosePositionWrapperUnitTest::test_HelperRepay_PartialRepayWhenInsufficientBalance() (gas: 179708 → 180023 | 315 0.175%)
↑ CowEvcClosePositionWrapperUnitTest::test_EvcInternalSettle_RequiresCorrectCalldata() (gas: 40302 → 40378 | 76 0.189%)
↑ CowEvcClosePositionWrapperUnitTest::test_Constructor_SetsImmutables() (gas: 15806 → 15836 | 30 0.190%)
↑ CowEvcOpenPositionWrapperUnitTest::test_Constructor_SetsImmutables() (gas: 15556 → 15586 | 30 0.193%)
↑ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_Subaccount() (gas: 832224 → 833879 | 1655 0.199%)
↑ CowEvcClosePositionWrapperUnitTest::test_HelperRepay_OnlyEVC() (gas: 11850 → 11876 | 26 0.219%)
↑ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_MainAccount() (gas: 754466 → 756251 | 1785 0.237%)
↑ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_NonSolverCannotSettle() (gas: 23877 → 23935 | 58 0.243%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_PartialRepay() (gas: 1160452 → 1163316 | 2864 0.247%)
↑ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_ThreeUsers_TwoSameOneOpposite() (gas: 3227984 → 3236108 | 8124 0.252%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_WithPreApprovedHash() (gas: 1121302 → 1124410 | 3108 0.277%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_SuccessFullRepay() (gas: 1150134 → 1153699 | 3565 0.310%)
↑ CowEvcOpenPositionWrapperUnitTest::test_GetSignedCalldata_EnableControllerItem() (gas: 346019 → 347147 | 1128 0.326%)
↑ CowEvcOpenPositionWrapperUnitTest::test_GetSignedCalldata_EnableCollateralItem() (gas: 345765 → 346893 | 1128 0.326%)
↑ CowEvcOpenPositionWrapperUnitTest::test_GetSignedCalldata_BorrowItem() (gas: 344090 → 345218 | 1128 0.328%)
↑ CowEvcOpenPositionWrapperUnitTest::test_GetSignedCalldata_DepositItem() (gas: 343713 → 344841 | 1128 0.328%)
↑ CowEvcOpenPositionWrapperUnitTest::test_MaxBorrowAmount() (gas: 343645 → 344773 | 1128 0.328%)
↑ CowEvcOpenPositionWrapperUnitTest::test_ZeroCollateralAmount() (gas: 343183 → 344311 | 1128 0.329%)
↓ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_UnauthorizedInternalSwap() (gas: 13642 → 13597 | -45 -0.330%)
↑ CowEvcOpenPositionWrapperUnitTest::test_SameOwnerAndAccount() (gas: 339821 → 340949 | 1128 0.332%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_ParseWrapperData_WithExtraData() (gas: 18082 → 18148 | 66 0.365%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_ThreeUsers_TwoSameOneOpposite() (gas: 2924476 → 2935336 | 10860 0.371%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_WithPreApprovedHash() (gas: 808296 → 811528 | 3232 0.400%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_NonSolverCannotSettle() (gas: 23858 → 23956 | 98 0.411%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_GetSignedCalldata_EnablesNewCollateral() (gas: 115346 → 115830 | 484 0.420%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_GetSignedCalldata_UsesCorrectAccount() (gas: 109357 → 109841 | 484 0.443%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_SwapAmount_Max() (gas: 109040 → 109524 | 484 0.444%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_SwapAmount_Zero() (gas: 108444 → 108928 | 484 0.446%)
↑ CowEvcClosePositionWrapperUnitTest::test_GetSignedCalldata_RepayItem() (gas: 147780 → 148444 | 664 0.449%)
↑ CowEvcClosePositionWrapperUnitTest::test_GetSignedCalldata_PartialRepay() (gas: 143447 → 144111 | 664 0.463%)
↑ CowEvcClosePositionWrapperUnitTest::test_MaxRepayAmount() (gas: 143445 → 144109 | 664 0.463%)
↑ CowEvcClosePositionWrapperUnitTest::test_SameOwnerAndAccount() (gas: 143044 → 143708 | 664 0.464%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_WrappedSettle_OnlySolver() (gas: 16349 → 16429 | 80 0.489%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_GetApprovalHash_MatchesEIP712() (gas: 13509 → 13577 | 68 0.503%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_Success() (gas: 831084 → 835269 | 4185 0.504%)
↑ CowEvcClosePositionWrapperUnitTest::test_ZeroDebt() (gas: 123403 → 124067 | 664 0.538%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_NonSolverCannotSettle() (gas: 23338 → 23474 | 136 0.583%)
↑ CowEvcClosePositionWrapperUnitTest::test_WrappedSettle_OnlySolver() (gas: 16330 → 16428 | 98 0.600%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_ThreeUsers_TwoSameOneOpposite() (gas: 2120651 → 2133415 | 12764 0.602%)
↑ CowEvcClosePositionWrapperUnitTest::test_ParseWrapperData_WithExtraData() (gas: 18188 → 18298 | 110 0.605%)
↑ CowEvcClosePositionWrapperUnitTest::test_WrappedSettle_WithPreApprovedHash() (gas: 370724 → 373062 | 2338 0.631%)
↑ CowEvcClosePositionWrapperUnitTest::test_EvcInternalSettle_WithSubaccountTransfer() (gas: 145792 → 146712 | 920 0.631%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_SetPreApprovedHash() (gas: 39028 → 39277 | 249 0.638%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_WrappedSettle_WithPreApprovedHash() (gas: 182305 → 183499 | 1194 0.655%)
↑ CowEvcOpenPositionWrapperUnitTest::test_ParseWrapperData_WithExtraData() (gas: 13672 → 13770 | 98 0.717%)
↑ CowEvcClosePositionWrapperUnitTest::test_EvcInternalSettle_CanBeCalledByEVC() (gas: 40396 → 40701 | 305 0.755%)
↑ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_ParseWrapperData() (gas: 12157 → 12249 | 92 0.757%)
↑ CowEvcClosePositionWrapperUnitTest::test_WrappedSettle_WithPermitSignature() (gas: 244682 → 246540 | 1858 0.759%)
↑ CowEvcClosePositionWrapperUnitTest::test_ParseWrapperData_EmptySignature() (gas: 13772 → 13882 | 110 0.799%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_SetPreApprovedHash() (gas: 39086 → 39416 | 330 0.844%)
↑ CowEvcOpenPositionWrapperUnitTest::test_WrappedSettle_OnlySolver() (gas: 16095 → 16231 | 136 0.845%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_WrappedSettle_WithPermitSignature() (gas: 130701 → 131839 | 1138 0.871%)
↑ CowEvcClosePositionWrapperUnitTest::test_WrappedSettle_PreApprovedHashRevertsIfDeadlineExceeded() (gas: 137808 → 139020 | 1212 0.879%)
↑ CowEvcOpenPositionWrapperUnitTest::test_ParseWrapperData_EmptySignature() (gas: 9281 → 9379 | 98 1.056%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_WrappedSettle_BuildsCorrectBatchWithPreApproved() (gas: 108444 → 109693 | 1249 1.152%)
↑ CowEvcOpenPositionWrapperUnitTest::test_GetApprovalHash_MatchesEIP712() (gas: 8805 → 8913 | 108 1.227%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_GetApprovalHash_DifferentForDifferentParams() (gas: 17733 → 17980 | 247 1.393%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_ParseWrapperData() (gas: 12271 → 12462 | 191 1.557%)
↑ CowEvcClosePositionWrapperUnitTest::test_GetApprovalHash_MatchesEIP712() (gas: 13434 → 13653 | 219 1.630%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_ParseWrapperData() (gas: 12446 → 12649 | 203 1.631%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_UnauthorizedInternalSettle() (gas: 13306 → 13563 | 257 1.931%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_WrappedSettle_BuildsCorrectBatchWithPermit() (gas: 58149 → 59309 | 1160 1.995%)
↑ CowEvcOpenPositionWrapperUnitTest::test_EvcInternalSettle_RequiresCorrectCalldata() (gas: 33537 → 34244 | 707 2.108%)
↑ CowEvcOpenPositionWrapperUnitTest::test_WrappedSettle_WithPreApprovedHash() (gas: 141235 → 144363 | 3128 2.215%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_WrappedSettle_PreApprovedHashRevertsIfDeadlineExceeded() (gas: 86960 → 89013 | 2053 2.361%)
↑ CowEvcOpenPositionWrapperUnitTest::test_WrappedSettle_PreApprovedHashRevertsIfDeadlineExceeded() (gas: 108507 → 111228 | 2721 2.508%)
↑ CowEvcOpenPositionWrapperUnitTest::test_GetApprovalHash_DifferentForDifferentParams() (gas: 12059 → 12362 | 303 2.513%)
↑ CowEvcOpenPositionWrapperUnitTest::test_EvcInternalSettle_OnlyEVC() (gas: 9736 → 9993 | 257 2.640%)
↑ CowEvcOpenPositionWrapperUnitTest::test_EvcInternalSettle_CanBeCalledByEVC() (gas: 26013 → 26711 | 698 2.683%)
↑ CowEvcClosePositionWrapperUnitTest::test_GetApprovalHash_DifferentForDifferentParams() (gas: 17412 → 18048 | 636 3.653%)
↑ CowEvcOpenPositionWrapperUnitTest::test_WrappedSettle_WithPermitSignature() (gas: 55570 → 58475 | 2905 5.228%)

New tests:
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_CanBeCalledByEVC()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_OnlyEVC()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_RequiresCorrectCalldata()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_SameOwnerAndAccount()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_SubaccountMustBeControlledByOwner()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_WithRemainingWrapperData()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_WithSubaccount_KindBuy()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_WithSubaccount_KindSell()

--------------------------------------------------------------------------------
Total tests: 116, ↑ 81, ↓ 5, ━ 30
Overall gas change: 96864 (0.348%)

The gas impact is visible (especially for opening a position, since it adds a function parameter to an external function) but overall limited (max ~3k gas), which I consider a reasonable tradeoff.

bytes calldata settleData,
bytes calldata wrapperData,
bytes calldata remainingWrapperData,
ParamsLocation paramMemoryLocation,
Copy link
Collaborator

Choose a reason for hiding this comment

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

i actually think creating type here makes it more confusing to tell that this is just a regular bytes32 under the hood. I'm guessing the reason you wanted this is to prevent accidentally mixing the params location with another bytes32 elsewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that's the only reason. This is a function that already has a lot of parameters and so I just wanted to make sure the type system prevents issues here.

}
}

if (timing == SettlementTiming.After) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

for the current set of wrapeprs it is indeed the case that settlement either entirely happens before or entirely happens after the specified batch operations. but in previous iterations we had it both before and after (for example, when we were clearing the no-longer in use collateral types; this was determined as not necessary by the euler team, however).

so it may be worth considering having both BatchItem beforeItems and BatchItem afterItems instead of doing an enum

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it's nicer to have before/after. I didn't want to change too much, but it would have been a cleaner implementation to do so.

@kaze-cow kaze-cow marked this pull request as ready for review December 4, 2025 03:42
@kaze-cow kaze-cow merged commit ea12b7c into feat/collateral-swap-wrapper Dec 4, 2025
7 checks passed
@kaze-cow kaze-cow deleted the collect-common-components branch December 4, 2025 03:42
@kaze-cow
Copy link
Collaborator

kaze-cow commented Dec 4, 2025

i merged this because its a significant improvementi n the direction of where we want to take the codebase, and much faster to just merge this and I can clean up the merge stack rather than to do it manually

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