Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ee6eb57
feat: pull in from PoC and minify and adjust for easy integratoin
Sep 11, 2025
0172171
remove unnecessary console logs
Sep 11, 2025
3e72e99
fix: some security lockdowns
Sep 11, 2025
453c007
it builds
Sep 11, 2025
0f635c7
fix: new strategy
Sep 11, 2025
5ebd847
compiles
kaze-cow Sep 29, 2025
b168d45
fix tests
kaze-cow Sep 29, 2025
e56eac3
forge fmt
kaze-cow Sep 29, 2025
2e7f051
enable optimizer
kaze-cow Sep 29, 2025
de12e99
setup secrets and fix build failure
kaze-cow Sep 29, 2025
c3dc655
remove sizes for now
kaze-cow Sep 29, 2025
217af04
Revert "forge fmt"
kaze-cow Sep 29, 2025
4f6c81e
use a settle state instead of original sender
kaze-cow Sep 29, 2025
2c9704a
feat: pull in required vendor dependencies
kaze-cow Sep 30, 2025
8982db0
feat: pull in from PoC and minify and adjust for easy integratoin
kaze-cow Sep 30, 2025
e9afd40
remove unnecessary console logs
Sep 11, 2025
35cf23b
fix: some security lockdowns
Sep 11, 2025
938b8e9
remove cannon things
kaze-cow Sep 29, 2025
5ca3ae5
remove unnecessary file
kaze-cow Sep 29, 2025
ec9d67d
update all pragmas
kaze-cow Sep 29, 2025
9b7a904
undo undos
kaze-cow Sep 30, 2025
164efff
fix license
kaze-cow Sep 30, 2025
2d7989b
Merge pull request #3 from cowprotocol/feat/new-wrapper
kaze-cow Sep 30, 2025
413d162
Merge branch 'feat/initial' into feat/security
kaze-cow Sep 30, 2025
3ff8dc0
Merge pull request #2 from cowprotocol/feat/security
kaze-cow Sep 30, 2025
ca207e7
cleanup SwapVerifier
kaze-cow Oct 1, 2025
b2a9cae
begin adding test for closing position theoretical impl
kaze-cow Oct 2, 2025
d65ca1a
add close loan test/poc concept
kaze-cow Oct 2, 2025
dad08de
significant improvement--no more excess on swapping out
kaze-cow Oct 2, 2025
093c0f1
general flow works in e2e
kaze-cow Oct 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:

env:
FOUNDRY_PROFILE: ci
FORK_RPC_URL: ${{ secrets.RPC_URL_1 }}

jobs:
check:
Expand All @@ -31,7 +32,7 @@ jobs:

- name: Run Forge build
run: |
forge build --sizes
forge build
id: build

- name: Run Forge tests
Expand Down
10 changes: 10 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/cow"]
path = lib/cow
url = https://github.com/cowprotocol/contracts
branch = feat/wrapper
[submodule "lib/euler-vault-kit"]
path = lib/euler-vault-kit
url = https://github.com/euler-xyz/euler-vault-kit
[submodule "lib/evc"]
path = lib/evc
url = https://github.com/euler-xyz/ethereum-vault-connector
17 changes: 13 additions & 4 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
{
"lib/forge-std": {
"tag": {
"name": "v1.10.0",
"rev": "8bbcf6e3f8f62f419e5429a0bd89331c85c37824"
"lib/cow": {
"branch": {
"name": "feat/wrapper",
"rev": "1e8127f476f8fef7758cf25033a0010d325dba8d"
}
},
"lib/euler-vault-kit": {
"rev": "5b98b42048ba11ae82fb62dfec06d1010c8e41e6"
},
"lib/evc": {
"rev": "34bb788288a0eb0fbba06bc370cb8ca3dd42614e"
},
"lib/forge-std": {
"rev": "0768d9c08c085c79bb31d88683a78770764fec49"
}
}
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
src = "src"
out = "out"
libs = ["lib"]
optimize = true
via_ir = true

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1 change: 1 addition & 0 deletions lib/cow
Submodule cow added at 1e8127
1 change: 1 addition & 0 deletions lib/euler-vault-kit
Submodule euler-vault-kit added at 5b98b4
1 change: 1 addition & 0 deletions lib/evc
Submodule evc added at 34bb78
4 changes: 4 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
cow/=lib/cow/src/contracts
evc/=lib/euler-vault-kit/lib/ethereum-vault-connector/src/
ethereum-vault-connector/=lib/euler-vault-kit/lib/ethereum-vault-connector/src/
openzeppelin/=lib/euler-vault-kit/lib/ethereum-vault-connector/lib/openzeppelin-contracts/contracts/
19 changes: 0 additions & 19 deletions script/Counter.s.sol

This file was deleted.

111 changes: 111 additions & 0 deletions soljson-latest.js

Large diffs are not rendered by default.

14 changes: 0 additions & 14 deletions src/Counter.sol

This file was deleted.

199 changes: 199 additions & 0 deletions src/CowEvcClosePositionWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8;

import {IEVC} from "evc/EthereumVaultConnector.sol";

import {CowWrapper, CowAuthentication, CowSettlement} from "./vendor/CowWrapper.sol";
import {IERC4626, IBorrowing, IERC20} from "euler-vault-kit/src/EVault/IEVault.sol";

import "forge-std/console.sol";

/// @title CowEvcClosePositionWrapper
/// @notice A specialized wrapper for closing leveraged positions with EVC
/// @dev This wrapper hardcodes the EVC operations needed to close a position:
/// 1. Execute settlement to acquire repayment assets
/// 2. Repay debt and return remaining assets to user
/// @dev The settle call by this order should be performing the necessary swap
/// from collateralVault -> IERC20(borrowVault.asset()). The recipient of the
/// swap should *THIS* contract so that it can repay on behalf of the user. Furthermore,
/// the order should be of type GPv2Order.KIND_BUY to prevent excess from being sent to the contract.
/// If a full close is being performed, leave a small buffer for intrest accumultation, and the dust will
/// be returned to the user's wallet.
contract CowEvcClosePositionWrapper is CowWrapper {
IEVC public immutable EVC;

/// @notice Tracks the number of times this wrapper has been called
uint256 public transient depth;
/// @notice Tracks the number of times `evcInternalSettle` has been called
uint256 public transient settleCalls;

uint256 public immutable nonceNamespace;

error Unauthorized(address msgSender);
error NotEVCSettlement();
error InsufficientRepaymentAsset(address vault, uint256 balanceAmount, uint256 repayAmount);

constructor(address _evc, CowAuthentication _authentication) CowWrapper(_authentication) {
EVC = IEVC(_evc);
nonceNamespace = uint256(uint160(address(this)));
}

struct ClosePositionParams {
address user;
uint256 deadline;
address borrowVault;
address collateralVault;
uint256 maxRepayAmount; // Use a number greater than the actual debt to repay full debt
}

function _parseClosePositionParams(bytes calldata wrapperData) internal pure returns (ClosePositionParams memory params, bytes memory signature, bytes calldata remainingWrapperData) {
(params, signature) = abi.decode(wrapperData, (ClosePositionParams, bytes));

// Calculate consumed bytes for abi.encode(ClosePositionParams, bytes)
// Structure:
// - 32 bytes: offset to params (0x40)
// - 32 bytes: offset to signature
// - 160 bytes: params data (5 fields × 32 bytes)
// - 32 bytes: signature length
// - N bytes: signature data (padded to 32-byte boundary)
// We can just math this out
uint256 consumed = 160 + 64 + ((signature.length + 31) / 32 ) * 32;

remainingWrapperData = wrapperData[consumed:];
}

function parseWrapperData(bytes calldata wrapperData) external pure override returns (bytes calldata remainingWrapperData) {
(, , remainingWrapperData) = _parseClosePositionParams(wrapperData);
}

function getSignedCalldata(ClosePositionParams memory params) external view returns (bytes memory) {
return _getSignedCalldata(params);
}

function _getSignedCalldata(ClosePositionParams memory params) internal view returns (bytes memory) {
// get current user debt, and find out if we are repaying all
uint256 debtAmount = IBorrowing(params.borrowVault).debtOf(params.user);
bool repayAll = params.maxRepayAmount >= debtAmount;

IEVC.BatchItem[] memory items = new IEVC.BatchItem[](repayAll ? 4 : 3);

// 1. Set account operator to allow this contract to act on behalf of user
items[0] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(EVC),
value: 0,
data: abi.encodeCall(IEVC.setAccountOperator, (params.user, address(this), true))
});

// 2. Repay debt and return remaining assets
items[1] = IEVC.BatchItem({
onBehalfOfAccount: params.user,
targetContract: address(this),
value: 0,
data: abi.encodeCall(this.helperRepayAndReturn, (params.borrowVault, params.user, params.maxRepayAmount, repayAll))
});

// 3. If we are repaying all, we should disable the collateral from the user's account
if (repayAll) {
items[2] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(EVC),
value: 0,
data: abi.encodeCall(IEVC.disableCollateral, (params.user, params.collateralVault))
});
}

// 4. Revoke operator permission
items[repayAll ? 3 : 2] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(EVC),
value: 0,
data: abi.encodeCall(IEVC.setAccountOperator, (params.user, address(this), false))
});

return abi.encodeCall(IEVC.batch, (items));
}

// helper function to execute repay
function helperRepayAndReturn(address vault, address beneficiary, uint256 maxRepay, bool repayAll) external {
IERC20 asset = IERC20(IERC4626(vault).asset());

// the settlement contract should have sent us `maxRepay` money
// if we dont have enough money, then either:
// 1. the CowOrder was not configured to correctly give us enough money
// 2. Somebody else using this wrapper (nesting the wrappers) did #1 (and the solver borked up)
require(asset.balanceOf(address(this)) >= maxRepay, InsufficientRepaymentAsset(vault, asset.balanceOf(address(this)), maxRepay));

asset.approve(vault, type(uint256).max);
uint256 actualRepay = IBorrowing(vault).repay(repayAll ? type(uint256).max : maxRepay, beneficiary);

// transfer any remaining dust back to the user
if (actualRepay < maxRepay) {
asset.transfer(beneficiary, maxRepay - actualRepay);
}
}

/// @notice Implementation of GPv2Wrapper._wrap - executes EVC operations to close a position
/// @param settleData Data which will be used for the parameters in a call to `CowSettlement.settle`
/// @param wrapperData Additional data containing ClosePositionParams
function _wrap(bytes calldata settleData, bytes calldata wrapperData) internal override {
depth = depth + 1;

// Decode wrapper data into ClosePositionParams
ClosePositionParams memory params;
bytes memory signature;
(params, signature, wrapperData) = _parseClosePositionParams(wrapperData);

// Build the EVC batch items for closing a position
IEVC.BatchItem[] memory items = new IEVC.BatchItem[](2);

// 1. Settlement call
items[0] = IEVC.BatchItem({
onBehalfOfAccount: address(this),
targetContract: address(this),
value: 0,
data: abi.encodeCall(this.evcInternalSettle, (settleData, wrapperData))
});

// 2. Acquire operator permissions and execute signed actions (repay, disable collateral if needed, revoke operator)
items[1] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(EVC),
value: 0,
data: abi.encodeCall(IEVC.permit, (
params.user,
address(this),
uint256(nonceNamespace),
EVC.getNonce(bytes19(bytes20(params.user)), nonceNamespace),
params.deadline,
0,
_getSignedCalldata(params),
signature
))
});

// 3. Account status check (automatically done by EVC at end of batch)
// No explicit item needed - EVC handles this

// Execute all items in a single batch
EVC.batch(items);
}

/// @notice Internal settlement function called by EVC
function evcInternalSettle(bytes calldata settleData, bytes calldata wrapperData) external payable {
if (msg.sender != address(EVC)) {
revert Unauthorized(msg.sender);
}

// depth should be > 0 (actively wrapping a settle call) and no settle call should have been performed yet
if (depth == 0 || settleCalls != 0) {
revert Unauthorized(address(0));
}

settleCalls = settleCalls + 1;

// Use GPv2Wrapper's _internalSettle to call the settlement contract
// wrapperData is empty since we've already processed it in _wrap
_internalSettle(settleData, wrapperData);
}
}
Loading
Loading