From 3b3d3c37affc33d805cc0534fb01fb6edd72a7fe Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Wed, 25 Feb 2026 19:49:01 +0000 Subject: [PATCH 01/13] . --- src/FeeTracker.sol | 15 ++-- src/adapters/AaveV3Adapter.sol | 2 +- src/adapters/CompoundV2Adapter.sol | 4 +- src/adapters/CompoundV3Adapter.sol | 4 +- .../adapters/AWKCompoundV2Adapter.sol | 1 + test/unit/FeeTracker.t.sol | 60 ++++++++++++++ test/unit/adapters/AaveV3Adapter.t.sol | 78 +++++++++++++++++++ test/unit/adapters/CompoundV2Adapter.t.sol | 27 +++++++ test/unit/adapters/CompoundV3Adapter.t.sol | 70 +++++++++++++++++ 9 files changed, 248 insertions(+), 13 deletions(-) diff --git a/src/FeeTracker.sol b/src/FeeTracker.sol index c0a90f3..af6df9a 100644 --- a/src/FeeTracker.sol +++ b/src/FeeTracker.sol @@ -183,11 +183,11 @@ contract YieldSeekerFeeTracker is AccessControl { * @param vault The vault address * @param assetsReceived The amount of base assets received from the withdrawal * @param totalVaultBalanceBefore The total vault balance (in base asset terms) before withdrawal - * @dev Uses actual vault balance to compute proportional cost basis, avoiding virtual share conversion. - * For rebasing tokens (Aave, CompoundV3), totalVaultBalanceBefore is the token balance. - * For exchange-rate tokens (CompoundV2), totalVaultBalanceBefore is the underlying value. + * @param vaultTokenToBaseAssetRate The exchange rate from vault tokens to base asset (18-decimal fixed point). + * For rebasing tokens (Aave, CompoundV3), this should be 1e18 (1:1 with underlying). + * For exchange-rate tokens (CompoundV2), this should be the cToken exchange rate. */ - function recordAgentVaultAssetWithdraw(address vault, uint256 assetsReceived, uint256 totalVaultBalanceBefore) external { + function recordAgentVaultAssetWithdraw(address vault, uint256 assetsReceived, uint256 totalVaultBalanceBefore, uint256 vaultTokenToBaseAssetRate) external { if (assetsReceived == 0 || totalVaultBalanceBefore == 0) return; address wallet = msg.sender; uint256 totalCostBasis = agentVaultCostBasis[wallet][vault]; @@ -202,10 +202,9 @@ contract YieldSeekerFeeTracker is AccessControl { feeTokenSettled = (vaultTokenFeesOwed * assetsReceived) / totalVaultBalanceBefore; } agentYieldTokenFeesOwed[wallet][vault] = vaultTokenFeesOwed - feeTokenSettled; - if (totalShares > 0) { - feeInBaseAsset = (feeTokenSettled * totalVaultBalanceBefore) / totalShares; - } else { - feeInBaseAsset = feeTokenSettled; + feeInBaseAsset = (feeTokenSettled * vaultTokenToBaseAssetRate) / 1e18; + if (feeInBaseAsset > assetsReceived) { + feeInBaseAsset = assetsReceived; } agentFeesCharged[wallet] += feeInBaseAsset; emit YieldRecorded(wallet, feeInBaseAsset, feeInBaseAsset); diff --git a/src/adapters/AaveV3Adapter.sol b/src/adapters/AaveV3Adapter.sol index a8f97ee..6e1d7aa 100644 --- a/src/adapters/AaveV3Adapter.sol +++ b/src/adapters/AaveV3Adapter.sol @@ -45,6 +45,6 @@ contract YieldSeekerAaveV3Adapter is AWKAaveV3Adapter, YieldSeekerAdapter { _requireBaseAsset(asset); uint256 totalVaultBalanceBefore = IAaveAToken(vault).balanceOf(address(this)); assets = super._withdrawInternal(vault, shares); - _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore}); + _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: 1e18}); } } diff --git a/src/adapters/CompoundV2Adapter.sol b/src/adapters/CompoundV2Adapter.sol index 49815e8..7215d3b 100644 --- a/src/adapters/CompoundV2Adapter.sol +++ b/src/adapters/CompoundV2Adapter.sol @@ -44,9 +44,9 @@ contract YieldSeekerCompoundV2Adapter is AWKCompoundV2Adapter, YieldSeekerAdapte address asset = _getVaultAsset(vault); _requireBaseAsset(asset); uint256 cTokenBalance = ICToken(vault).balanceOf(address(this)); - uint256 exchangeRate = ICToken(vault).exchangeRateStored(); + uint256 exchangeRate = ICToken(vault).exchangeRateCurrent(); uint256 totalVaultBalanceBefore = (cTokenBalance * exchangeRate) / 1e18; assets = super._withdrawInternal(vault, shares); - _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore}); + _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: exchangeRate}); } } diff --git a/src/adapters/CompoundV3Adapter.sol b/src/adapters/CompoundV3Adapter.sol index f89e1e0..dca801d 100644 --- a/src/adapters/CompoundV3Adapter.sol +++ b/src/adapters/CompoundV3Adapter.sol @@ -34,7 +34,7 @@ contract YieldSeekerCompoundV3Adapter is AWKCompoundV3Adapter, YieldSeekerAdapte address asset = _getVaultAsset(vault); _requireBaseAsset(asset); shares = super._depositInternal(vault, amount); - _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: amount, sharesReceived: shares}); + _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: shares, sharesReceived: shares}); } /** @@ -45,6 +45,6 @@ contract YieldSeekerCompoundV3Adapter is AWKCompoundV3Adapter, YieldSeekerAdapte _requireBaseAsset(asset); uint256 totalVaultBalanceBefore = ICompoundV3Comet(vault).balanceOf(address(this)); assets = super._withdrawInternal(vault, shares); - _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore}); + _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: 1e18}); } } diff --git a/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol b/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol index b5373d6..ead13bc 100644 --- a/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol +++ b/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol @@ -29,6 +29,7 @@ interface ICToken { function redeemUnderlying(uint256 redeemAmount) external returns (uint256); function balanceOf(address account) external view returns (uint256); function exchangeRateStored() external view returns (uint256); + function exchangeRateCurrent() external returns (uint256); } /** diff --git a/test/unit/FeeTracker.t.sol b/test/unit/FeeTracker.t.sol index 05e1e26..9927907 100644 --- a/test/unit/FeeTracker.t.sol +++ b/test/unit/FeeTracker.t.sol @@ -767,4 +767,64 @@ contract YieldSeekerFeeTrackerTest is Test { assertApproxEqAbs(totalFees, expectedTotal, 1e3, "Total fees should match calculation"); } + + // ============ Audit Fix: Safety cap on feeInBaseAsset (Issue 2) ============ + + function test_VaultAssetWithdraw_FeeInBaseAsset_CappedAtAssetsReceived() public { + address vault = makeAddr("vault"); + // Deposit 50 USDC → 50 shares + vm.prank(agent1); + feeTracker.recordAgentVaultShareDeposit(vault, 50e6, 50e6); + // Record massive reward: 1000 tokens → 100 token fee owed + vm.prank(agent1); + feeTracker.recordAgentYieldTokenEarned(vault, 1000e6); + uint256 feeOwed = feeTracker.getAgentYieldTokenFeesOwed(agent1, vault); + assertEq(feeOwed, 100e6, "Should owe 100 tokens in fees"); + // Withdraw 200 USDC from a vault with totalBalance = 1050 + // Without the cap, the old buggy formula would compute a fee > 200 and underflow + // With the fix, the fee is capped and this should not revert + vm.prank(agent1); + feeTracker.recordAgentVaultAssetWithdraw(vault, 200e6, 1050e6, 1e18); + // Verify fee was capped at assetsReceived (200e6) + uint256 feesCharged = feeTracker.agentFeesCharged(agent1); + assertTrue(feesCharged <= 200e6, "Fee should be capped at assets received"); + assertTrue(feesCharged > 0, "Fee should be non-zero"); + } + + function test_VaultAssetWithdraw_RebasingRate_CorrectFee() public { + address vault = makeAddr("vault"); + vm.prank(agent1); + feeTracker.recordAgentVaultShareDeposit(vault, 100e6, 100e6); + vm.prank(agent1); + feeTracker.recordAgentYieldTokenEarned(vault, 10e6); + // Withdraw 50 from totalVaultBalance=110, rate=1e18 (rebasing) + vm.prank(agent1); + feeTracker.recordAgentVaultAssetWithdraw(vault, 50e6, 110e6, 1e18); + uint256 expectedFeeTokenSettled = uint256(1e6) * uint256(50e6) / uint256(110e6); + // With 1e18 rate, feeInBaseAsset = feeTokenSettled (1:1) + uint256 feesCharged = feeTracker.agentFeesCharged(agent1); + // The vaultToken fee portion should be exactly feeTokenSettled + // Plus potential profit fee on the remaining netAssets + assertTrue(feesCharged >= expectedFeeTokenSettled, "Fee should include at least the token fee portion"); + } + + function test_VaultAssetWithdraw_ExchangeRate_CorrectConversion() public { + address vault = makeAddr("vault"); + // Simulate CompoundV2: deposit 1000 USDC → 1000 cTokens at 1e18 rate + vm.prank(agent1); + feeTracker.recordAgentVaultShareDeposit(vault, 1000e6, 1000e6); + // Record token fee: 10 cTokens owed + vm.prank(agent1); + feeTracker.recordAgentYieldTokenEarned(vault, 100e6); + uint256 feeOwed = feeTracker.getAgentYieldTokenFeesOwed(agent1, vault); + assertEq(feeOwed, 10e6); + // Exchange rate = 1.1e18 (10% appreciation) + // Withdraw 550 USDC from total 1100 USDC balance + vm.prank(agent1); + feeTracker.recordAgentVaultAssetWithdraw(vault, 550e6, 1100e6, 1.1e18); + // feeTokenSettled = (10e6 * 550e6) / 1100e6 = 5e6 + // feeInBaseAsset = (5e6 * 1.1e18) / 1e18 = 5.5e6 + uint256 feesCharged = feeTracker.agentFeesCharged(agent1); + assertTrue(feesCharged >= 5.5e6, "Should apply exchange rate for non-rebasing tokens"); + } } diff --git a/test/unit/adapters/AaveV3Adapter.t.sol b/test/unit/adapters/AaveV3Adapter.t.sol index 2c743ca..4a643bf 100644 --- a/test/unit/adapters/AaveV3Adapter.t.sol +++ b/test/unit/adapters/AaveV3Adapter.t.sol @@ -155,4 +155,82 @@ contract AaveV3AdapterTest is Test { assertEq(costBasis, depositAmount - proportionalCost, "Cost basis should be reduced proportionally"); assertEq(shares, depositAmount - proportionalCost, "Shares should be reduced proportionally"); } + + // ============ Audit Fix: Rebasing fee conversion uses 1:1 rate (Issue 1) ============ + + function test_RebasingFeeConversion_NotInflated() public { + uint256 depositAmount = 100e6; + wallet.executeAdapter(address(adapter), address(aToken), abi.encodeWithSelector(adapter.deposit.selector, depositAmount)); + vm.prank(address(wallet)); + feeTracker.recordAgentYieldTokenEarned(address(aToken), 10e6); + uint256 feeOwed = feeTracker.getAgentYieldTokenFeesOwed(address(wallet), address(aToken)); + assertEq(feeOwed, 1e6, "Should owe 1 aToken in fees (10% of 10)"); + aToken.addYield(address(wallet), 10e6); + baseAsset.mint(address(aToken), 10e6); + assertEq(aToken.balanceOf(address(wallet)), 110e6); + uint256 feesBefore = feeTracker.agentFeesCharged(address(wallet)); + wallet.executeAdapter(address(adapter), address(aToken), abi.encodeWithSelector(adapter.withdraw.selector, uint256(50e6))); + uint256 feesAfter = feeTracker.agentFeesCharged(address(wallet)); + uint256 feesCharged = feesAfter - feesBefore; + // feeTokenSettled = (1e6 * 50e6) / 110e6 = 454545 + // With 1:1 rate: feeInBaseAsset = 454545 (correct) + // Old buggy formula: 454545 * 110e6 / 100e6 = 500000 (inflated!) + uint256 expectedFeeTokenSettled = uint256(1e6) * uint256(50e6) / uint256(110e6); + uint256 proportionalCost = (depositAmount * uint256(50e6)) / uint256(110e6); + uint256 netAssets = uint256(50e6) - expectedFeeTokenSettled; + uint256 expectedProfitFee = netAssets > proportionalCost ? ((netAssets - proportionalCost) * 1000) / 10_000 : 0; + uint256 expectedTotalFees = expectedFeeTokenSettled + expectedProfitFee; + assertEq(feesCharged, expectedTotalFees, "Fees should not be inflated for rebasing tokens"); + uint256 oldInflatedFee = (expectedFeeTokenSettled * uint256(110e6)) / uint256(100e6); + assertTrue(expectedFeeTokenSettled < oldInflatedFee, "Fee should be less than the old inflated calculation"); + } + + // ============ Audit Fix: No underflow DoS on large rewards (Issue 2) ============ + + function test_LargeReward_NoUnderflowDoS() public { + uint256 depositAmount = 50e6; + wallet.executeAdapter(address(adapter), address(aToken), abi.encodeWithSelector(adapter.deposit.selector, depositAmount)); + vm.prank(address(wallet)); + feeTracker.recordAgentYieldTokenEarned(address(aToken), 1000e6); + aToken.addYield(address(wallet), 1000e6); + baseAsset.mint(address(aToken), 1000e6); + wallet.executeAdapter(address(adapter), address(aToken), abi.encodeWithSelector(adapter.withdraw.selector, uint256(200e6))); + uint256 feesCharged = feeTracker.agentFeesCharged(address(wallet)); + assertTrue(feesCharged > 0, "Fees should be charged"); + assertTrue(feesCharged <= 200e6, "Fee should not exceed withdrawal amount"); + } + + function test_FeeCapAtAssetsReceived() public { + uint256 depositAmount = 10e6; + wallet.executeAdapter(address(adapter), address(aToken), abi.encodeWithSelector(adapter.deposit.selector, depositAmount)); + vm.prank(address(wallet)); + feeTracker.recordAgentYieldTokenEarned(address(aToken), 10_000e6); + aToken.addYield(address(wallet), 10_000e6); + baseAsset.mint(address(aToken), 10_000e6); + wallet.executeAdapter(address(adapter), address(aToken), abi.encodeWithSelector(adapter.withdraw.selector, uint256(5e6))); + uint256 feesCharged = feeTracker.agentFeesCharged(address(wallet)); + assertTrue(feesCharged <= 5e6, "Fee should be capped at assets received"); + } + + // ============ Audit Fix: Full lifecycle with rewards ============ + + function test_FullLifecycle_WithRewards() public { + wallet.executeAdapter(address(adapter), address(aToken), abi.encodeWithSelector(adapter.deposit.selector, uint256(1_000e6))); + aToken.addYield(address(wallet), 50e6); + baseAsset.mint(address(aToken), 50e6); + vm.prank(address(wallet)); + feeTracker.recordAgentYieldTokenEarned(address(aToken), 20e6); + aToken.addYield(address(wallet), 20e6); + baseAsset.mint(address(aToken), 20e6); + assertEq(aToken.balanceOf(address(wallet)), 1070e6); + wallet.executeAdapter(address(adapter), address(aToken), abi.encodeWithSelector(adapter.withdraw.selector, uint256(500e6))); + uint256 feesAfterPartial = feeTracker.agentFeesCharged(address(wallet)); + assertTrue(feesAfterPartial > 0, "Should have charged fees on partial withdraw"); + uint256 remaining = aToken.balanceOf(address(wallet)); + wallet.executeAdapter(address(adapter), address(aToken), abi.encodeWithSelector(adapter.withdraw.selector, remaining)); + (uint256 costBasis, uint256 shares) = feeTracker.getAgentVaultPosition(address(wallet), address(aToken)); + assertEq(costBasis, 0, "Cost basis should be zero after full withdraw"); + assertEq(shares, 0, "Shares should be zero after full withdraw"); + assertEq(feeTracker.getAgentYieldTokenFeesOwed(address(wallet), address(aToken)), 0, "All token fees should be settled"); + } } diff --git a/test/unit/adapters/CompoundV2Adapter.t.sol b/test/unit/adapters/CompoundV2Adapter.t.sol index c7db9ab..df06275 100644 --- a/test/unit/adapters/CompoundV2Adapter.t.sol +++ b/test/unit/adapters/CompoundV2Adapter.t.sol @@ -160,4 +160,31 @@ contract CompoundV2AdapterTest is Test { assertEq(costBasis, depositAmount - proportionalCost, "Cost basis should be reduced proportionally"); assertEq(shares, depositAmount - proportionalCost, "Shares should be reduced proportionally"); } + + // ============ Audit Fix: Uses exchangeRateCurrent (Issue 3) ============ + + function test_ExchangeRateCurrent_CorrectFees() public { + uint256 depositAmount = 1_000e6; + wallet.executeAdapter(address(adapter), address(cToken), abi.encodeWithSelector(adapter.deposit.selector, depositAmount)); + cToken.addYield(1000); + baseAsset.mint(address(cToken), 100e6); + uint256 fullBalance = (depositAmount * 11000) / 10000; + wallet.executeAdapter(address(adapter), address(cToken), abi.encodeWithSelector(adapter.withdraw.selector, fullBalance)); + uint256 profit = fullBalance - depositAmount; + uint256 expectedFee = (profit * 1000) / 10_000; + assertEq(feeTracker.agentFeesCharged(address(wallet)), expectedFee, "Should charge correct fee with current exchange rate"); + } + + function test_CompoundV2_WithVaultTokenFees_UsesExchangeRate() public { + uint256 depositAmount = 1_000e6; + wallet.executeAdapter(address(adapter), address(cToken), abi.encodeWithSelector(adapter.deposit.selector, depositAmount)); + cToken.addYield(1000); + baseAsset.mint(address(cToken), 100e6); + vm.prank(address(wallet)); + feeTracker.recordAgentYieldTokenEarned(address(cToken), 50e6); + uint256 feesBefore = feeTracker.agentFeesCharged(address(wallet)); + wallet.executeAdapter(address(adapter), address(cToken), abi.encodeWithSelector(adapter.withdraw.selector, uint256(550e6))); + uint256 feesAfter = feeTracker.agentFeesCharged(address(wallet)); + assertTrue(feesAfter > feesBefore, "CompoundV2 should charge fees using the exchange rate"); + } } diff --git a/test/unit/adapters/CompoundV3Adapter.t.sol b/test/unit/adapters/CompoundV3Adapter.t.sol index 1dfc1f0..6f670fe 100644 --- a/test/unit/adapters/CompoundV3Adapter.t.sol +++ b/test/unit/adapters/CompoundV3Adapter.t.sol @@ -152,4 +152,74 @@ contract CompoundV3AdapterTest is Test { assertEq(costBasis, depositAmount - proportionalCost, "Cost basis should be reduced proportionally"); assertEq(shares, depositAmount - proportionalCost, "Shares should be reduced proportionally"); } + + // ============ Audit Fix: Rebasing fee conversion uses 1:1 rate (Issue 1) ============ + + function test_RebasingFeeConversion_NotInflated() public { + uint256 depositAmount = 100e6; + wallet.executeAdapter(address(adapter), address(comet), abi.encodeWithSelector(adapter.deposit.selector, depositAmount)); + vm.prank(address(wallet)); + feeTracker.recordAgentYieldTokenEarned(address(comet), 10e6); + comet.addYield(address(wallet), 10e6); + baseAsset.mint(address(comet), 10e6); + uint256 feesBefore = feeTracker.agentFeesCharged(address(wallet)); + wallet.executeAdapter(address(adapter), address(comet), abi.encodeWithSelector(adapter.withdraw.selector, uint256(50e6))); + uint256 feesAfter = feeTracker.agentFeesCharged(address(wallet)); + uint256 feesCharged = feesAfter - feesBefore; + uint256 expectedFeeTokenSettled = uint256(1e6) * uint256(50e6) / uint256(110e6); + uint256 proportionalCost = (depositAmount * uint256(50e6)) / uint256(110e6); + uint256 netAssets = uint256(50e6) - expectedFeeTokenSettled; + uint256 expectedProfitFee = netAssets > proportionalCost ? ((netAssets - proportionalCost) * 1000) / 10_000 : 0; + uint256 expectedTotalFees = expectedFeeTokenSettled + expectedProfitFee; + assertEq(feesCharged, expectedTotalFees, "CompoundV3 fees should not be inflated for rebasing tokens"); + } + + // ============ Audit Fix: Deposit records actual amount, not type(uint256).max (Issue 4) ============ + + function test_DepositRecordsActualAmount() public { + uint256 depositAmount = 500e6; + wallet.executeAdapter(address(adapter), address(comet), abi.encodeWithSelector(adapter.deposit.selector, depositAmount)); + (uint256 costBasis, uint256 shares) = feeTracker.getAgentVaultPosition(address(wallet), address(comet)); + assertEq(costBasis, depositAmount, "Cost basis should be actual deposited amount"); + assertEq(shares, depositAmount, "Shares should be actual deposited amount"); + comet.addYield(address(wallet), 50e6); + baseAsset.mint(address(comet), 50e6); + wallet.executeAdapter(address(adapter), address(comet), abi.encodeWithSelector(adapter.withdraw.selector, uint256(250e6))); + (uint256 costBasisAfter, uint256 sharesAfter) = feeTracker.getAgentVaultPosition(address(wallet), address(comet)); + assertTrue(costBasisAfter < costBasis, "Cost basis should decrease after partial withdrawal"); + assertTrue(sharesAfter < shares, "Shares should decrease after partial withdrawal"); + } + + function test_MultiplePartialWithdraws_NoOverflow() public { + uint256 depositAmount = 1_000e6; + wallet.executeAdapter(address(adapter), address(comet), abi.encodeWithSelector(adapter.deposit.selector, depositAmount)); + comet.addYield(address(wallet), 100e6); + baseAsset.mint(address(comet), 100e6); + wallet.executeAdapter(address(adapter), address(comet), abi.encodeWithSelector(adapter.withdraw.selector, uint256(300e6))); + wallet.executeAdapter(address(adapter), address(comet), abi.encodeWithSelector(adapter.withdraw.selector, uint256(300e6))); + wallet.executeAdapter(address(adapter), address(comet), abi.encodeWithSelector(adapter.withdraw.selector, uint256(300e6))); + (uint256 costBasis, uint256 shares) = feeTracker.getAgentVaultPosition(address(wallet), address(comet)); + assertTrue(costBasis < depositAmount, "Cost basis should be reduced"); + assertTrue(shares < depositAmount, "Shares should be reduced"); + } + + // ============ Audit Fix: Full lifecycle ============ + + function test_FullLifecycle_CorrectFees() public { + wallet.executeAdapter(address(adapter), address(comet), abi.encodeWithSelector(adapter.deposit.selector, uint256(1_000e6))); + (uint256 costBasis, uint256 shares) = feeTracker.getAgentVaultPosition(address(wallet), address(comet)); + assertEq(costBasis, 1_000e6, "Cost basis should be actual amount"); + assertEq(shares, 1_000e6, "Shares should be actual amount"); + comet.addYield(address(wallet), 100e6); + baseAsset.mint(address(comet), 100e6); + wallet.executeAdapter(address(adapter), address(comet), abi.encodeWithSelector(adapter.withdraw.selector, uint256(550e6))); + uint256 remaining = comet.balanceOf(address(wallet)); + wallet.executeAdapter(address(adapter), address(comet), abi.encodeWithSelector(adapter.withdraw.selector, remaining)); + (uint256 costBasisAfter, uint256 sharesAfter) = feeTracker.getAgentVaultPosition(address(wallet), address(comet)); + assertEq(costBasisAfter, 0); + assertEq(sharesAfter, 0); + uint256 totalFees = feeTracker.agentFeesCharged(address(wallet)); + uint256 expectedFee = (100e6 * 1000) / 10_000; + assertEq(totalFees, expectedFee, "Total fees should equal 10% of 100 USDC yield"); + } } From a4f41edb244946a7c5c118f932bb1753b0c82cd3 Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Wed, 25 Feb 2026 19:53:44 +0000 Subject: [PATCH 02/13] . --- src/FeeTracker.sol | 3 ++- src/adapters/AaveV3Adapter.sol | 2 +- src/adapters/Adapter.sol | 1 + src/adapters/CompoundV2Adapter.sol | 2 +- src/adapters/CompoundV3Adapter.sol | 2 +- test/unit/FeeTracker.t.sol | 4 ++-- test/unit/adapters/AdapterFlows.t.sol | 5 ++--- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/FeeTracker.sol b/src/FeeTracker.sol index af6df9a..a99446b 100644 --- a/src/FeeTracker.sol +++ b/src/FeeTracker.sol @@ -29,6 +29,7 @@ error InvalidFeeRate(); */ contract YieldSeekerFeeTracker is AccessControl { uint256 public constant MAX_FEE_RATE_BPS = 5000; + uint256 public constant EXCHANGE_RATE_PRECISION = 1e18; uint256 public feeRateBps; address public feeCollector; @@ -202,7 +203,7 @@ contract YieldSeekerFeeTracker is AccessControl { feeTokenSettled = (vaultTokenFeesOwed * assetsReceived) / totalVaultBalanceBefore; } agentYieldTokenFeesOwed[wallet][vault] = vaultTokenFeesOwed - feeTokenSettled; - feeInBaseAsset = (feeTokenSettled * vaultTokenToBaseAssetRate) / 1e18; + feeInBaseAsset = (feeTokenSettled * vaultTokenToBaseAssetRate) / EXCHANGE_RATE_PRECISION; if (feeInBaseAsset > assetsReceived) { feeInBaseAsset = assetsReceived; } diff --git a/src/adapters/AaveV3Adapter.sol b/src/adapters/AaveV3Adapter.sol index 6e1d7aa..5591a89 100644 --- a/src/adapters/AaveV3Adapter.sol +++ b/src/adapters/AaveV3Adapter.sol @@ -45,6 +45,6 @@ contract YieldSeekerAaveV3Adapter is AWKAaveV3Adapter, YieldSeekerAdapter { _requireBaseAsset(asset); uint256 totalVaultBalanceBefore = IAaveAToken(vault).balanceOf(address(this)); assets = super._withdrawInternal(vault, shares); - _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: 1e18}); + _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: REBASING_EXCHANGE_RATE}); } } diff --git a/src/adapters/Adapter.sol b/src/adapters/Adapter.sol index d2976ed..a3dd0f2 100644 --- a/src/adapters/Adapter.sol +++ b/src/adapters/Adapter.sol @@ -30,6 +30,7 @@ error BaseAssetNotAllowed(); * @dev Extends AWKAdapter with YieldSeeker-specific helpers for baseAsset and feeTracker */ abstract contract YieldSeekerAdapter is AWKAdapter { + uint256 internal constant REBASING_EXCHANGE_RATE = 1e18; // Helper to get the wallet as IAgentWallet instead of IAWKAgentWallet function _ysAgentWallet() internal view returns (IAgentWallet) { return IAgentWallet(address(this)); diff --git a/src/adapters/CompoundV2Adapter.sol b/src/adapters/CompoundV2Adapter.sol index 7215d3b..706646e 100644 --- a/src/adapters/CompoundV2Adapter.sol +++ b/src/adapters/CompoundV2Adapter.sol @@ -45,7 +45,7 @@ contract YieldSeekerCompoundV2Adapter is AWKCompoundV2Adapter, YieldSeekerAdapte _requireBaseAsset(asset); uint256 cTokenBalance = ICToken(vault).balanceOf(address(this)); uint256 exchangeRate = ICToken(vault).exchangeRateCurrent(); - uint256 totalVaultBalanceBefore = (cTokenBalance * exchangeRate) / 1e18; + uint256 totalVaultBalanceBefore = (cTokenBalance * exchangeRate) / _feeTracker().EXCHANGE_RATE_PRECISION(); assets = super._withdrawInternal(vault, shares); _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: exchangeRate}); } diff --git a/src/adapters/CompoundV3Adapter.sol b/src/adapters/CompoundV3Adapter.sol index dca801d..d946f23 100644 --- a/src/adapters/CompoundV3Adapter.sol +++ b/src/adapters/CompoundV3Adapter.sol @@ -45,6 +45,6 @@ contract YieldSeekerCompoundV3Adapter is AWKCompoundV3Adapter, YieldSeekerAdapte _requireBaseAsset(asset); uint256 totalVaultBalanceBefore = ICompoundV3Comet(vault).balanceOf(address(this)); assets = super._withdrawInternal(vault, shares); - _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: 1e18}); + _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: REBASING_EXCHANGE_RATE}); } } diff --git a/test/unit/FeeTracker.t.sol b/test/unit/FeeTracker.t.sol index 9927907..5d15f2c 100644 --- a/test/unit/FeeTracker.t.sol +++ b/test/unit/FeeTracker.t.sol @@ -784,7 +784,7 @@ contract YieldSeekerFeeTrackerTest is Test { // Without the cap, the old buggy formula would compute a fee > 200 and underflow // With the fix, the fee is capped and this should not revert vm.prank(agent1); - feeTracker.recordAgentVaultAssetWithdraw(vault, 200e6, 1050e6, 1e18); + feeTracker.recordAgentVaultAssetWithdraw(vault, 200e6, 1050e6, feeTracker.EXCHANGE_RATE_PRECISION()); // Verify fee was capped at assetsReceived (200e6) uint256 feesCharged = feeTracker.agentFeesCharged(agent1); assertTrue(feesCharged <= 200e6, "Fee should be capped at assets received"); @@ -799,7 +799,7 @@ contract YieldSeekerFeeTrackerTest is Test { feeTracker.recordAgentYieldTokenEarned(vault, 10e6); // Withdraw 50 from totalVaultBalance=110, rate=1e18 (rebasing) vm.prank(agent1); - feeTracker.recordAgentVaultAssetWithdraw(vault, 50e6, 110e6, 1e18); + feeTracker.recordAgentVaultAssetWithdraw(vault, 50e6, 110e6, feeTracker.EXCHANGE_RATE_PRECISION()); uint256 expectedFeeTokenSettled = uint256(1e6) * uint256(50e6) / uint256(110e6); // With 1e18 rate, feeInBaseAsset = feeTokenSettled (1:1) uint256 feesCharged = feeTracker.agentFeesCharged(agent1); diff --git a/test/unit/adapters/AdapterFlows.t.sol b/test/unit/adapters/AdapterFlows.t.sol index f9a9771..4f971f1 100644 --- a/test/unit/adapters/AdapterFlows.t.sol +++ b/test/unit/adapters/AdapterFlows.t.sol @@ -164,10 +164,9 @@ contract AdapterFlowsTest is Test { _withdraw(7e6); // vaultTokenFee settlement: 7/110 of 1e6 fee owed (in vault token units) uint256 totalBalanceBefore = 110e6; - uint256 totalShares = 100e6; uint256 feeTokenSettled = (1e6 * 7e6) / totalBalanceBefore; - // Convert vault token fee to base asset using exchange rate (totalVaultBalance / totalShares) - uint256 feeInBaseAsset = (feeTokenSettled * totalBalanceBefore) / totalShares; + // For rebasing tokens (Aave), 1 vault token = 1 underlying, so rate = 1e18 + uint256 feeInBaseAsset = feeTokenSettled; // proportionalCost = (100e6 * 7e6) / 110e6 uint256 proportionalCost = (100e6 * 7e6) / totalBalanceBefore; // netAssets = 7e6 - feeInBaseAsset, profit = netAssets - proportionalCost From b532e89c4155029c5c5861b18116f9e6389db442 Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Wed, 25 Feb 2026 19:57:51 +0000 Subject: [PATCH 03/13] . --- src/FeeTracker.sol | 1 + src/adapters/AaveV3Adapter.sol | 2 +- src/adapters/Adapter.sol | 1 - src/adapters/CompoundV3Adapter.sol | 2 +- test/unit/FeeTracker.t.sol | 9 ++++++--- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/FeeTracker.sol b/src/FeeTracker.sol index a99446b..fee38f2 100644 --- a/src/FeeTracker.sol +++ b/src/FeeTracker.sol @@ -30,6 +30,7 @@ error InvalidFeeRate(); contract YieldSeekerFeeTracker is AccessControl { uint256 public constant MAX_FEE_RATE_BPS = 5000; uint256 public constant EXCHANGE_RATE_PRECISION = 1e18; + uint256 public constant REBASING_EXCHANGE_RATE = 1e18; uint256 public feeRateBps; address public feeCollector; diff --git a/src/adapters/AaveV3Adapter.sol b/src/adapters/AaveV3Adapter.sol index 5591a89..2f9c696 100644 --- a/src/adapters/AaveV3Adapter.sol +++ b/src/adapters/AaveV3Adapter.sol @@ -45,6 +45,6 @@ contract YieldSeekerAaveV3Adapter is AWKAaveV3Adapter, YieldSeekerAdapter { _requireBaseAsset(asset); uint256 totalVaultBalanceBefore = IAaveAToken(vault).balanceOf(address(this)); assets = super._withdrawInternal(vault, shares); - _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: REBASING_EXCHANGE_RATE}); + _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: _feeTracker().REBASING_EXCHANGE_RATE()}); } } diff --git a/src/adapters/Adapter.sol b/src/adapters/Adapter.sol index a3dd0f2..d2976ed 100644 --- a/src/adapters/Adapter.sol +++ b/src/adapters/Adapter.sol @@ -30,7 +30,6 @@ error BaseAssetNotAllowed(); * @dev Extends AWKAdapter with YieldSeeker-specific helpers for baseAsset and feeTracker */ abstract contract YieldSeekerAdapter is AWKAdapter { - uint256 internal constant REBASING_EXCHANGE_RATE = 1e18; // Helper to get the wallet as IAgentWallet instead of IAWKAgentWallet function _ysAgentWallet() internal view returns (IAgentWallet) { return IAgentWallet(address(this)); diff --git a/src/adapters/CompoundV3Adapter.sol b/src/adapters/CompoundV3Adapter.sol index d946f23..54c15c3 100644 --- a/src/adapters/CompoundV3Adapter.sol +++ b/src/adapters/CompoundV3Adapter.sol @@ -45,6 +45,6 @@ contract YieldSeekerCompoundV3Adapter is AWKCompoundV3Adapter, YieldSeekerAdapte _requireBaseAsset(asset); uint256 totalVaultBalanceBefore = ICompoundV3Comet(vault).balanceOf(address(this)); assets = super._withdrawInternal(vault, shares); - _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: REBASING_EXCHANGE_RATE}); + _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: _feeTracker().REBASING_EXCHANGE_RATE()}); } } diff --git a/test/unit/FeeTracker.t.sol b/test/unit/FeeTracker.t.sol index 5d15f2c..87483e3 100644 --- a/test/unit/FeeTracker.t.sol +++ b/test/unit/FeeTracker.t.sol @@ -783,8 +783,9 @@ contract YieldSeekerFeeTrackerTest is Test { // Withdraw 200 USDC from a vault with totalBalance = 1050 // Without the cap, the old buggy formula would compute a fee > 200 and underflow // With the fix, the fee is capped and this should not revert + uint256 rebasingRate = feeTracker.REBASING_EXCHANGE_RATE(); vm.prank(agent1); - feeTracker.recordAgentVaultAssetWithdraw(vault, 200e6, 1050e6, feeTracker.EXCHANGE_RATE_PRECISION()); + feeTracker.recordAgentVaultAssetWithdraw(vault, 200e6, 1050e6, rebasingRate); // Verify fee was capped at assetsReceived (200e6) uint256 feesCharged = feeTracker.agentFeesCharged(agent1); assertTrue(feesCharged <= 200e6, "Fee should be capped at assets received"); @@ -798,8 +799,9 @@ contract YieldSeekerFeeTrackerTest is Test { vm.prank(agent1); feeTracker.recordAgentYieldTokenEarned(vault, 10e6); // Withdraw 50 from totalVaultBalance=110, rate=1e18 (rebasing) + uint256 rebasingRate = feeTracker.REBASING_EXCHANGE_RATE(); vm.prank(agent1); - feeTracker.recordAgentVaultAssetWithdraw(vault, 50e6, 110e6, feeTracker.EXCHANGE_RATE_PRECISION()); + feeTracker.recordAgentVaultAssetWithdraw(vault, 50e6, 110e6, rebasingRate); uint256 expectedFeeTokenSettled = uint256(1e6) * uint256(50e6) / uint256(110e6); // With 1e18 rate, feeInBaseAsset = feeTokenSettled (1:1) uint256 feesCharged = feeTracker.agentFeesCharged(agent1); @@ -820,8 +822,9 @@ contract YieldSeekerFeeTrackerTest is Test { assertEq(feeOwed, 10e6); // Exchange rate = 1.1e18 (10% appreciation) // Withdraw 550 USDC from total 1100 USDC balance + uint256 exchangeRate = 1.1e18; vm.prank(agent1); - feeTracker.recordAgentVaultAssetWithdraw(vault, 550e6, 1100e6, 1.1e18); + feeTracker.recordAgentVaultAssetWithdraw(vault, 550e6, 1100e6, exchangeRate); // feeTokenSettled = (10e6 * 550e6) / 1100e6 = 5e6 // feeInBaseAsset = (5e6 * 1.1e18) / 1e18 = 5.5e6 uint256 feesCharged = feeTracker.agentFeesCharged(agent1); From 775180256dcd88f91f55f4ed3be92a3a66996bf9 Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Wed, 25 Feb 2026 20:01:31 +0000 Subject: [PATCH 04/13] . --- src/FeeTracker.sol | 1 - src/adapters/AaveV3Adapter.sol | 2 +- src/adapters/CompoundV3Adapter.sol | 4 ++-- test/unit/FeeTracker.t.sol | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/FeeTracker.sol b/src/FeeTracker.sol index fee38f2..a99446b 100644 --- a/src/FeeTracker.sol +++ b/src/FeeTracker.sol @@ -30,7 +30,6 @@ error InvalidFeeRate(); contract YieldSeekerFeeTracker is AccessControl { uint256 public constant MAX_FEE_RATE_BPS = 5000; uint256 public constant EXCHANGE_RATE_PRECISION = 1e18; - uint256 public constant REBASING_EXCHANGE_RATE = 1e18; uint256 public feeRateBps; address public feeCollector; diff --git a/src/adapters/AaveV3Adapter.sol b/src/adapters/AaveV3Adapter.sol index 2f9c696..6ba83d1 100644 --- a/src/adapters/AaveV3Adapter.sol +++ b/src/adapters/AaveV3Adapter.sol @@ -45,6 +45,6 @@ contract YieldSeekerAaveV3Adapter is AWKAaveV3Adapter, YieldSeekerAdapter { _requireBaseAsset(asset); uint256 totalVaultBalanceBefore = IAaveAToken(vault).balanceOf(address(this)); assets = super._withdrawInternal(vault, shares); - _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: _feeTracker().REBASING_EXCHANGE_RATE()}); + _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: _feeTracker().EXCHANGE_RATE_PRECISION()}); } } diff --git a/src/adapters/CompoundV3Adapter.sol b/src/adapters/CompoundV3Adapter.sol index 54c15c3..bfe4be1 100644 --- a/src/adapters/CompoundV3Adapter.sol +++ b/src/adapters/CompoundV3Adapter.sol @@ -34,7 +34,7 @@ contract YieldSeekerCompoundV3Adapter is AWKCompoundV3Adapter, YieldSeekerAdapte address asset = _getVaultAsset(vault); _requireBaseAsset(asset); shares = super._depositInternal(vault, amount); - _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: shares, sharesReceived: shares}); + _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: amount, sharesReceived: shares}); } /** @@ -45,6 +45,6 @@ contract YieldSeekerCompoundV3Adapter is AWKCompoundV3Adapter, YieldSeekerAdapte _requireBaseAsset(asset); uint256 totalVaultBalanceBefore = ICompoundV3Comet(vault).balanceOf(address(this)); assets = super._withdrawInternal(vault, shares); - _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: _feeTracker().REBASING_EXCHANGE_RATE()}); + _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: _feeTracker().EXCHANGE_RATE_PRECISION()}); } } diff --git a/test/unit/FeeTracker.t.sol b/test/unit/FeeTracker.t.sol index 87483e3..8f40bc5 100644 --- a/test/unit/FeeTracker.t.sol +++ b/test/unit/FeeTracker.t.sol @@ -783,7 +783,7 @@ contract YieldSeekerFeeTrackerTest is Test { // Withdraw 200 USDC from a vault with totalBalance = 1050 // Without the cap, the old buggy formula would compute a fee > 200 and underflow // With the fix, the fee is capped and this should not revert - uint256 rebasingRate = feeTracker.REBASING_EXCHANGE_RATE(); + uint256 rebasingRate = feeTracker.EXCHANGE_RATE_PRECISION(); vm.prank(agent1); feeTracker.recordAgentVaultAssetWithdraw(vault, 200e6, 1050e6, rebasingRate); // Verify fee was capped at assetsReceived (200e6) @@ -799,7 +799,7 @@ contract YieldSeekerFeeTrackerTest is Test { vm.prank(agent1); feeTracker.recordAgentYieldTokenEarned(vault, 10e6); // Withdraw 50 from totalVaultBalance=110, rate=1e18 (rebasing) - uint256 rebasingRate = feeTracker.REBASING_EXCHANGE_RATE(); + uint256 rebasingRate = feeTracker.EXCHANGE_RATE_PRECISION(); vm.prank(agent1); feeTracker.recordAgentVaultAssetWithdraw(vault, 50e6, 110e6, rebasingRate); uint256 expectedFeeTokenSettled = uint256(1e6) * uint256(50e6) / uint256(110e6); From 1a08bef81d178461a7e18c13713323767390a7a6 Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Wed, 25 Feb 2026 20:02:14 +0000 Subject: [PATCH 05/13] . --- src/FeeTracker.sol | 4 ++-- src/adapters/AaveV3Adapter.sol | 2 +- src/adapters/CompoundV2Adapter.sol | 2 +- src/adapters/CompoundV3Adapter.sol | 2 +- test/unit/FeeTracker.t.sol | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/FeeTracker.sol b/src/FeeTracker.sol index a99446b..1657025 100644 --- a/src/FeeTracker.sol +++ b/src/FeeTracker.sol @@ -29,7 +29,7 @@ error InvalidFeeRate(); */ contract YieldSeekerFeeTracker is AccessControl { uint256 public constant MAX_FEE_RATE_BPS = 5000; - uint256 public constant EXCHANGE_RATE_PRECISION = 1e18; + uint256 public constant ASSET_EXCHANGE_RATE_PRECISION = 1e18; uint256 public feeRateBps; address public feeCollector; @@ -203,7 +203,7 @@ contract YieldSeekerFeeTracker is AccessControl { feeTokenSettled = (vaultTokenFeesOwed * assetsReceived) / totalVaultBalanceBefore; } agentYieldTokenFeesOwed[wallet][vault] = vaultTokenFeesOwed - feeTokenSettled; - feeInBaseAsset = (feeTokenSettled * vaultTokenToBaseAssetRate) / EXCHANGE_RATE_PRECISION; + feeInBaseAsset = (feeTokenSettled * vaultTokenToBaseAssetRate) / ASSET_EXCHANGE_RATE_PRECISION; if (feeInBaseAsset > assetsReceived) { feeInBaseAsset = assetsReceived; } diff --git a/src/adapters/AaveV3Adapter.sol b/src/adapters/AaveV3Adapter.sol index 6ba83d1..91d77b7 100644 --- a/src/adapters/AaveV3Adapter.sol +++ b/src/adapters/AaveV3Adapter.sol @@ -45,6 +45,6 @@ contract YieldSeekerAaveV3Adapter is AWKAaveV3Adapter, YieldSeekerAdapter { _requireBaseAsset(asset); uint256 totalVaultBalanceBefore = IAaveAToken(vault).balanceOf(address(this)); assets = super._withdrawInternal(vault, shares); - _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: _feeTracker().EXCHANGE_RATE_PRECISION()}); + _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: _feeTracker().ASSET_EXCHANGE_RATE_PRECISION()}); } } diff --git a/src/adapters/CompoundV2Adapter.sol b/src/adapters/CompoundV2Adapter.sol index 706646e..48a6f86 100644 --- a/src/adapters/CompoundV2Adapter.sol +++ b/src/adapters/CompoundV2Adapter.sol @@ -45,7 +45,7 @@ contract YieldSeekerCompoundV2Adapter is AWKCompoundV2Adapter, YieldSeekerAdapte _requireBaseAsset(asset); uint256 cTokenBalance = ICToken(vault).balanceOf(address(this)); uint256 exchangeRate = ICToken(vault).exchangeRateCurrent(); - uint256 totalVaultBalanceBefore = (cTokenBalance * exchangeRate) / _feeTracker().EXCHANGE_RATE_PRECISION(); + uint256 totalVaultBalanceBefore = (cTokenBalance * exchangeRate) / _feeTracker().ASSET_EXCHANGE_RATE_PRECISION(); assets = super._withdrawInternal(vault, shares); _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: exchangeRate}); } diff --git a/src/adapters/CompoundV3Adapter.sol b/src/adapters/CompoundV3Adapter.sol index bfe4be1..3940a57 100644 --- a/src/adapters/CompoundV3Adapter.sol +++ b/src/adapters/CompoundV3Adapter.sol @@ -45,6 +45,6 @@ contract YieldSeekerCompoundV3Adapter is AWKCompoundV3Adapter, YieldSeekerAdapte _requireBaseAsset(asset); uint256 totalVaultBalanceBefore = ICompoundV3Comet(vault).balanceOf(address(this)); assets = super._withdrawInternal(vault, shares); - _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: _feeTracker().EXCHANGE_RATE_PRECISION()}); + _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: _feeTracker().ASSET_EXCHANGE_RATE_PRECISION()}); } } diff --git a/test/unit/FeeTracker.t.sol b/test/unit/FeeTracker.t.sol index 8f40bc5..eae4420 100644 --- a/test/unit/FeeTracker.t.sol +++ b/test/unit/FeeTracker.t.sol @@ -783,7 +783,7 @@ contract YieldSeekerFeeTrackerTest is Test { // Withdraw 200 USDC from a vault with totalBalance = 1050 // Without the cap, the old buggy formula would compute a fee > 200 and underflow // With the fix, the fee is capped and this should not revert - uint256 rebasingRate = feeTracker.EXCHANGE_RATE_PRECISION(); + uint256 rebasingRate = feeTracker.ASSET_EXCHANGE_RATE_PRECISION(); vm.prank(agent1); feeTracker.recordAgentVaultAssetWithdraw(vault, 200e6, 1050e6, rebasingRate); // Verify fee was capped at assetsReceived (200e6) @@ -799,7 +799,7 @@ contract YieldSeekerFeeTrackerTest is Test { vm.prank(agent1); feeTracker.recordAgentYieldTokenEarned(vault, 10e6); // Withdraw 50 from totalVaultBalance=110, rate=1e18 (rebasing) - uint256 rebasingRate = feeTracker.EXCHANGE_RATE_PRECISION(); + uint256 rebasingRate = feeTracker.ASSET_EXCHANGE_RATE_PRECISION(); vm.prank(agent1); feeTracker.recordAgentVaultAssetWithdraw(vault, 50e6, 110e6, rebasingRate); uint256 expectedFeeTokenSettled = uint256(1e6) * uint256(50e6) / uint256(110e6); From 1ca278ef87e284719964242b5cfecf19126ff317 Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Wed, 25 Feb 2026 20:19:08 +0000 Subject: [PATCH 06/13] . --- src/adapters/AaveV3Adapter.sol | 6 +++--- src/adapters/CompoundV2Adapter.sol | 6 +++--- src/adapters/CompoundV3Adapter.sol | 6 +++--- src/adapters/ERC4626Adapter.sol | 6 +++--- src/agentwalletkit/adapters/AWKAaveV3Adapter.sol | 6 ++++-- src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol | 7 ++++--- src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol | 6 ++++-- src/agentwalletkit/adapters/AWKCompoundV3Adapter.sol | 6 ++++-- src/agentwalletkit/adapters/AWKERC4626Adapter.sol | 6 ++++-- 9 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/adapters/AaveV3Adapter.sol b/src/adapters/AaveV3Adapter.sol index 91d77b7..7ae5398 100644 --- a/src/adapters/AaveV3Adapter.sol +++ b/src/adapters/AaveV3Adapter.sol @@ -30,11 +30,11 @@ contract YieldSeekerAaveV3Adapter is AWKAaveV3Adapter, YieldSeekerAdapter { /** * @notice Internal deposit implementation with validation and fee tracking */ - function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares) { + function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares, uint256 actualAmount) { address asset = _getVaultAsset(vault); _requireBaseAsset(asset); - shares = super._depositInternal(vault, amount); - _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: amount, sharesReceived: shares}); + (shares, actualAmount) = super._depositInternal(vault, amount); + _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: actualAmount, sharesReceived: shares}); } /** diff --git a/src/adapters/CompoundV2Adapter.sol b/src/adapters/CompoundV2Adapter.sol index 48a6f86..5fe55ee 100644 --- a/src/adapters/CompoundV2Adapter.sol +++ b/src/adapters/CompoundV2Adapter.sol @@ -30,11 +30,11 @@ contract YieldSeekerCompoundV2Adapter is AWKCompoundV2Adapter, YieldSeekerAdapte /** * @notice Internal deposit implementation with validation and fee tracking */ - function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares) { + function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares, uint256 actualAmount) { address asset = _getVaultAsset(vault); _requireBaseAsset(asset); - shares = super._depositInternal(vault, amount); - _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: amount, sharesReceived: shares}); + (shares, actualAmount) = super._depositInternal(vault, amount); + _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: actualAmount, sharesReceived: shares}); } /** diff --git a/src/adapters/CompoundV3Adapter.sol b/src/adapters/CompoundV3Adapter.sol index 3940a57..9e6712b 100644 --- a/src/adapters/CompoundV3Adapter.sol +++ b/src/adapters/CompoundV3Adapter.sol @@ -30,11 +30,11 @@ contract YieldSeekerCompoundV3Adapter is AWKCompoundV3Adapter, YieldSeekerAdapte /** * @notice Internal deposit implementation with validation and fee tracking */ - function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares) { + function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares, uint256 actualAmount) { address asset = _getVaultAsset(vault); _requireBaseAsset(asset); - shares = super._depositInternal(vault, amount); - _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: amount, sharesReceived: shares}); + (shares, actualAmount) = super._depositInternal(vault, amount); + _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: actualAmount, sharesReceived: shares}); } /** diff --git a/src/adapters/ERC4626Adapter.sol b/src/adapters/ERC4626Adapter.sol index 4e815a2..a0cc182 100644 --- a/src/adapters/ERC4626Adapter.sol +++ b/src/adapters/ERC4626Adapter.sol @@ -31,11 +31,11 @@ contract YieldSeekerERC4626Adapter is AWKERC4626Adapter, YieldSeekerAdapter { * @notice Internal deposit implementation with validation and fee tracking * @dev Overrides AWK logic to add pre-check and post-fee-tracking */ - function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares) { + function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares, uint256 actualAmount) { address asset = IERC4626(vault).asset(); _requireBaseAsset(asset); - shares = super._depositInternal(vault, amount); - _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: amount, sharesReceived: shares}); + (shares, actualAmount) = super._depositInternal(vault, amount); + _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: actualAmount, sharesReceived: shares}); } /** diff --git a/src/agentwalletkit/adapters/AWKAaveV3Adapter.sol b/src/agentwalletkit/adapters/AWKAaveV3Adapter.sol index 9dcadec..79bda85 100644 --- a/src/agentwalletkit/adapters/AWKAaveV3Adapter.sol +++ b/src/agentwalletkit/adapters/AWKAaveV3Adapter.sol @@ -60,16 +60,18 @@ abstract contract AWKAaveV3Adapter is AWKBaseVaultAdapter { * @dev Runs in wallet context via delegatecall. The amount parameter is the underlying asset amount. * For Aave, shares received equals amount deposited (1:1 rebasing). */ - function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares) { + function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares, uint256 actualAmount) { if (amount == 0) revert AWKErrors.ZeroAmount(); address asset = IAaveAToken(vault).UNDERLYING_ASSET_ADDRESS(); address pool = IAaveAToken(vault).POOL(); + uint256 baseAssetBalanceBefore = IERC20(asset).balanceOf(address(this)); uint256 balanceBefore = IAaveAToken(vault).balanceOf(address(this)); IERC20(asset).forceApprove(pool, amount); IAaveV3Pool(pool).supply({asset: asset, amount: amount, onBehalfOf: address(this), referralCode: 0}); uint256 balanceAfter = IAaveAToken(vault).balanceOf(address(this)); shares = balanceAfter - balanceBefore; - emit Deposited(address(this), vault, amount, shares); + actualAmount = baseAssetBalanceBefore - IERC20(asset).balanceOf(address(this)); + emit Deposited(address(this), vault, actualAmount, shares); } /** diff --git a/src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol b/src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol index 334263d..cb63459 100644 --- a/src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol +++ b/src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol @@ -78,7 +78,7 @@ abstract contract AWKBaseVaultAdapter is AWKAdapter { bytes4 selector = bytes4(data[:4]); if (selector == this.deposit.selector) { uint256 amount = abi.decode(data[4:], (uint256)); - uint256 shares = _depositInternal(target, amount); + (uint256 shares,) = _depositInternal(target, amount); return abi.encode(shares); } if (selector == this.depositPercentage.selector) { @@ -104,7 +104,7 @@ abstract contract AWKBaseVaultAdapter is AWKAdapter { * @return shares The amount of vault shares received * @dev Must be implemented by concrete vault adapters. Hooks are called automatically. */ - function _depositInternal(address vault, uint256 amount) internal virtual returns (uint256 shares); + function _depositInternal(address vault, uint256 amount) internal virtual returns (uint256 shares, uint256 actualAmount); /** * @notice Internal deposit percentage implementation @@ -118,7 +118,8 @@ abstract contract AWKBaseVaultAdapter is AWKAdapter { if (percentageBps == 0 || percentageBps > 1e4) revert InvalidPercentage(percentageBps); uint256 balance = baseAsset.balanceOf(address(this)); uint256 amount = (balance * percentageBps) / 1e4; - return _depositInternal(vault, amount); + (shares,) = _depositInternal(vault, amount); + return shares; } /** diff --git a/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol b/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol index ead13bc..b5c9ff3 100644 --- a/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol +++ b/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol @@ -54,16 +54,18 @@ abstract contract AWKCompoundV2Adapter is AWKBaseVaultAdapter { * @dev Runs in wallet context via delegatecall. The amount parameter is the underlying asset amount. * Returns the cTokens received as shares. */ - function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares) { + function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares, uint256 actualAmount) { if (amount == 0) revert AWKErrors.ZeroAmount(); address asset = ICToken(vault).underlying(); + uint256 baseAssetBalanceBefore = IERC20(asset).balanceOf(address(this)); uint256 balanceBefore = ICToken(vault).balanceOf(address(this)); IERC20(asset).forceApprove(vault, amount); uint256 mintResult = ICToken(vault).mint(amount); require(mintResult == 0, "AWKCompoundV2Adapter: mint failed"); uint256 balanceAfter = ICToken(vault).balanceOf(address(this)); shares = balanceAfter - balanceBefore; - emit Deposited(address(this), vault, amount, shares); + actualAmount = baseAssetBalanceBefore - IERC20(asset).balanceOf(address(this)); + emit Deposited(address(this), vault, actualAmount, shares); } /** diff --git a/src/agentwalletkit/adapters/AWKCompoundV3Adapter.sol b/src/agentwalletkit/adapters/AWKCompoundV3Adapter.sol index 1eadd9c..6a214f0 100644 --- a/src/agentwalletkit/adapters/AWKCompoundV3Adapter.sol +++ b/src/agentwalletkit/adapters/AWKCompoundV3Adapter.sol @@ -54,15 +54,17 @@ abstract contract AWKCompoundV3Adapter is AWKBaseVaultAdapter { * @dev Runs in wallet context via delegatecall. The amount parameter is the base token amount. * Returns the change in balance as shares (though Compound V3 uses rebasing, not actual shares). */ - function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares) { + function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares, uint256 actualAmount) { if (amount == 0) revert AWKErrors.ZeroAmount(); address asset = ICompoundV3Comet(vault).baseToken(); + uint256 baseAssetBalanceBefore = IERC20(asset).balanceOf(address(this)); uint256 balanceBefore = ICompoundV3Comet(vault).balanceOf(address(this)); IERC20(asset).forceApprove(vault, amount); ICompoundV3Comet(vault).supply({asset: asset, amount: amount}); uint256 balanceAfter = ICompoundV3Comet(vault).balanceOf(address(this)); shares = balanceAfter - balanceBefore; - emit Deposited(address(this), vault, amount, shares); + actualAmount = baseAssetBalanceBefore - IERC20(asset).balanceOf(address(this)); + emit Deposited(address(this), vault, actualAmount, shares); } /** diff --git a/src/agentwalletkit/adapters/AWKERC4626Adapter.sol b/src/agentwalletkit/adapters/AWKERC4626Adapter.sol index b2e287c..6a07e40 100644 --- a/src/agentwalletkit/adapters/AWKERC4626Adapter.sol +++ b/src/agentwalletkit/adapters/AWKERC4626Adapter.sol @@ -49,12 +49,14 @@ abstract contract AWKERC4626Adapter is AWKBaseVaultAdapter { * @notice Internal deposit implementation * @dev Runs in wallet context via delegatecall */ - function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares) { + function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares, uint256 actualAmount) { if (amount == 0) revert AWKErrors.ZeroAmount(); address asset = IERC4626(vault).asset(); + uint256 baseAssetBalanceBefore = IERC20(asset).balanceOf(address(this)); IERC20(asset).forceApprove(vault, amount); shares = IERC4626(vault).deposit({assets: amount, receiver: address(this)}); - emit Deposited(address(this), vault, amount, shares); + actualAmount = baseAssetBalanceBefore - IERC20(asset).balanceOf(address(this)); + emit Deposited(address(this), vault, actualAmount, shares); } /** From 6328e302a3a5eca79905142b4498b860cfc76318 Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Wed, 25 Feb 2026 20:27:10 +0000 Subject: [PATCH 07/13] . --- test/unit/adapters/ERC4626Adapter.t.sol | 59 ++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/test/unit/adapters/ERC4626Adapter.t.sol b/test/unit/adapters/ERC4626Adapter.t.sol index c3d459f..083938e 100644 --- a/test/unit/adapters/ERC4626Adapter.t.sol +++ b/test/unit/adapters/ERC4626Adapter.t.sol @@ -7,8 +7,9 @@ import {YieldSeekerERC4626Adapter} from "../../../src/adapters/ERC4626Adapter.so import {AWKErrors} from "../../../src/agentwalletkit/AWKErrors.sol"; import {MockERC20} from "../../mocks/MockERC20.sol"; import {MockERC4626} from "../../mocks/MockERC4626.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {AdapterWalletHarness} from "./AdapterHarness.t.sol"; -import {Test} from "forge-std/Test.sol"; +import {Test, console} from "forge-std/Test.sol"; contract ERC4626AdapterTest is Test { YieldSeekerERC4626Adapter adapter; @@ -102,4 +103,60 @@ contract ERC4626AdapterTest is Test { uint256 assets = _decodeUint(result); assertGt(assets, 1_000e6); } + + function test_DepositGasOverhead() public { + MockERC4626 directVault = new MockERC4626(address(baseAsset), "Direct", "dUSDC"); + address directUser = address(0xDEAD); + baseAsset.mint(directUser, 1_000_000e6); + vm.prank(directUser); + baseAsset.approve(address(directVault), type(uint256).max); + uint256 depositAmount = 10_000e6; + // 1) Direct vault deposit (baseline) + vm.prank(directUser); + uint256 g0 = gasleft(); + directVault.deposit(depositAmount, directUser); + uint256 directGas = g0 - gasleft(); + // 2) Full adapter path + uint256 g1 = gasleft(); + wallet.executeAdapter(address(adapter), address(vault), abi.encodeWithSelector(adapter.deposit.selector, depositAmount)); + uint256 adapterGas = g1 - gasleft(); + // 3) Delegatecall overhead (empty call) + uint256 g2 = gasleft(); + (bool ok,) = address(adapter).delegatecall(abi.encodeWithSignature("nonexistent()")); + uint256 delegatecallGas = g2 - gasleft(); + ok; // silence warning + // 4) _getVaultAsset (vault.asset()) + uint256 g3 = gasleft(); + vault.asset(); + uint256 getAssetGas = g3 - gasleft(); + // 5) _requireBaseAsset (reads baseAsset from wallet then compares) + uint256 g4 = gasleft(); + wallet.baseAsset(); + uint256 readBaseAssetGas = g4 - gasleft(); + // 6) Balance-delta: 2x balanceOf on base asset + uint256 g5 = gasleft(); + baseAsset.balanceOf(address(wallet)); + baseAsset.balanceOf(address(wallet)); + uint256 twoBalanceOfGas = g5 - gasleft(); + // 7) forceApprove (IERC20.approve) + uint256 g6 = gasleft(); + baseAsset.approve(address(vault), depositAmount); + uint256 approveGas = g6 - gasleft(); + // 8) FeeTracker.recordAgentVaultShareDeposit (2 SSTORE) + uint256 g7 = gasleft(); + feeTracker.recordAgentVaultShareDeposit(address(vault), depositAmount, depositAmount); + uint256 feeTrackerGas = g7 - gasleft(); + // Summary + console.log("=== ERC4626 Deposit Gas Breakdown ==="); + console.log("Direct vault deposit (baseline): ", directGas); + console.log("Full adapter path: ", adapterGas); + console.log("Overhead: ", adapterGas - directGas); + console.log("--- Component costs ---"); + console.log("Delegatecall dispatch: ", delegatecallGas); + console.log("vault.asset(): ", getAssetGas); + console.log("Read wallet.baseAsset(): ", readBaseAssetGas); + console.log("2x balanceOf (balance-delta): ", twoBalanceOfGas); + console.log("ERC20 approve: ", approveGas); + console.log("FeeTracker record deposit: ", feeTrackerGas); + } } From 9730f78f45516155cf7630c27c2f9add1903e639 Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Wed, 25 Feb 2026 20:47:56 +0000 Subject: [PATCH 08/13] . --- src/adapters/AaveV3Adapter.sol | 6 +++--- src/adapters/CompoundV2Adapter.sol | 6 +++--- src/adapters/CompoundV3Adapter.sol | 6 +++--- src/adapters/ERC4626Adapter.sol | 6 +++--- src/agentwalletkit/adapters/AWKAaveV3Adapter.sol | 6 +++--- src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol | 12 ++++++------ src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol | 6 +++--- src/agentwalletkit/adapters/AWKCompoundV3Adapter.sol | 6 +++--- src/agentwalletkit/adapters/AWKERC4626Adapter.sol | 6 +++--- 9 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/adapters/AaveV3Adapter.sol b/src/adapters/AaveV3Adapter.sol index 7ae5398..963b6cb 100644 --- a/src/adapters/AaveV3Adapter.sol +++ b/src/adapters/AaveV3Adapter.sol @@ -30,11 +30,11 @@ contract YieldSeekerAaveV3Adapter is AWKAaveV3Adapter, YieldSeekerAdapter { /** * @notice Internal deposit implementation with validation and fee tracking */ - function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares, uint256 actualAmount) { + function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares, uint256 assetsDeposited) { address asset = _getVaultAsset(vault); _requireBaseAsset(asset); - (shares, actualAmount) = super._depositInternal(vault, amount); - _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: actualAmount, sharesReceived: shares}); + (shares, assetsDeposited) = super._depositInternal(vault, amount); + _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: assetsDeposited, sharesReceived: shares}); } /** diff --git a/src/adapters/CompoundV2Adapter.sol b/src/adapters/CompoundV2Adapter.sol index 5fe55ee..bf12f3f 100644 --- a/src/adapters/CompoundV2Adapter.sol +++ b/src/adapters/CompoundV2Adapter.sol @@ -30,11 +30,11 @@ contract YieldSeekerCompoundV2Adapter is AWKCompoundV2Adapter, YieldSeekerAdapte /** * @notice Internal deposit implementation with validation and fee tracking */ - function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares, uint256 actualAmount) { + function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares, uint256 assetsDeposited) { address asset = _getVaultAsset(vault); _requireBaseAsset(asset); - (shares, actualAmount) = super._depositInternal(vault, amount); - _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: actualAmount, sharesReceived: shares}); + (shares, assetsDeposited) = super._depositInternal(vault, amount); + _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: assetsDeposited, sharesReceived: shares}); } /** diff --git a/src/adapters/CompoundV3Adapter.sol b/src/adapters/CompoundV3Adapter.sol index 9e6712b..8623395 100644 --- a/src/adapters/CompoundV3Adapter.sol +++ b/src/adapters/CompoundV3Adapter.sol @@ -30,11 +30,11 @@ contract YieldSeekerCompoundV3Adapter is AWKCompoundV3Adapter, YieldSeekerAdapte /** * @notice Internal deposit implementation with validation and fee tracking */ - function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares, uint256 actualAmount) { + function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares, uint256 assetsDeposited) { address asset = _getVaultAsset(vault); _requireBaseAsset(asset); - (shares, actualAmount) = super._depositInternal(vault, amount); - _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: actualAmount, sharesReceived: shares}); + (shares, assetsDeposited) = super._depositInternal(vault, amount); + _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: assetsDeposited, sharesReceived: shares}); } /** diff --git a/src/adapters/ERC4626Adapter.sol b/src/adapters/ERC4626Adapter.sol index a0cc182..33f7f52 100644 --- a/src/adapters/ERC4626Adapter.sol +++ b/src/adapters/ERC4626Adapter.sol @@ -31,11 +31,11 @@ contract YieldSeekerERC4626Adapter is AWKERC4626Adapter, YieldSeekerAdapter { * @notice Internal deposit implementation with validation and fee tracking * @dev Overrides AWK logic to add pre-check and post-fee-tracking */ - function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares, uint256 actualAmount) { + function _depositInternal(address vault, uint256 amount) internal override returns (uint256 shares, uint256 assetsDeposited) { address asset = IERC4626(vault).asset(); _requireBaseAsset(asset); - (shares, actualAmount) = super._depositInternal(vault, amount); - _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: actualAmount, sharesReceived: shares}); + (shares, assetsDeposited) = super._depositInternal(vault, amount); + _feeTracker().recordAgentVaultShareDeposit({vault: vault, assetsDeposited: assetsDeposited, sharesReceived: shares}); } /** diff --git a/src/agentwalletkit/adapters/AWKAaveV3Adapter.sol b/src/agentwalletkit/adapters/AWKAaveV3Adapter.sol index 79bda85..4ca22ab 100644 --- a/src/agentwalletkit/adapters/AWKAaveV3Adapter.sol +++ b/src/agentwalletkit/adapters/AWKAaveV3Adapter.sol @@ -60,7 +60,7 @@ abstract contract AWKAaveV3Adapter is AWKBaseVaultAdapter { * @dev Runs in wallet context via delegatecall. The amount parameter is the underlying asset amount. * For Aave, shares received equals amount deposited (1:1 rebasing). */ - function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares, uint256 actualAmount) { + function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares, uint256 assetsDeposited) { if (amount == 0) revert AWKErrors.ZeroAmount(); address asset = IAaveAToken(vault).UNDERLYING_ASSET_ADDRESS(); address pool = IAaveAToken(vault).POOL(); @@ -70,8 +70,8 @@ abstract contract AWKAaveV3Adapter is AWKBaseVaultAdapter { IAaveV3Pool(pool).supply({asset: asset, amount: amount, onBehalfOf: address(this), referralCode: 0}); uint256 balanceAfter = IAaveAToken(vault).balanceOf(address(this)); shares = balanceAfter - balanceBefore; - actualAmount = baseAssetBalanceBefore - IERC20(asset).balanceOf(address(this)); - emit Deposited(address(this), vault, actualAmount, shares); + assetsDeposited = baseAssetBalanceBefore - IERC20(asset).balanceOf(address(this)); + emit Deposited(address(this), vault, assetsDeposited, shares); } /** diff --git a/src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol b/src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol index cb63459..98f0ee0 100644 --- a/src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol +++ b/src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol @@ -84,8 +84,8 @@ abstract contract AWKBaseVaultAdapter is AWKAdapter { if (selector == this.depositPercentage.selector) { uint256 percentageBps = abi.decode(data[4:], (uint256)); address asset = _getVaultAsset(target); - uint256 shares = _depositPercentageInternal(target, percentageBps, IERC20(asset)); - return abi.encode(shares); + (uint256 shares, uint256 assetsDeposited) = _depositPercentageInternal(target, percentageBps, IERC20(asset)); + return abi.encode(shares, assetsDeposited); } if (selector == this.withdraw.selector) { uint256 shares = abi.decode(data[4:], (uint256)); @@ -104,7 +104,7 @@ abstract contract AWKBaseVaultAdapter is AWKAdapter { * @return shares The amount of vault shares received * @dev Must be implemented by concrete vault adapters. Hooks are called automatically. */ - function _depositInternal(address vault, uint256 amount) internal virtual returns (uint256 shares, uint256 actualAmount); + function _depositInternal(address vault, uint256 amount) internal virtual returns (uint256 shares, uint256 assetsDeposited); /** * @notice Internal deposit percentage implementation @@ -112,14 +112,14 @@ abstract contract AWKBaseVaultAdapter is AWKAdapter { * @param percentageBps The percentage in basis points (10000 = 100%) * @param baseAsset The base asset token * @return shares The amount of vault shares received + * @return assetsDeposited The actual amount of base asset deposited * @dev Calculates amount based on balance and calls _depositInternal */ - function _depositPercentageInternal(address vault, uint256 percentageBps, IERC20 baseAsset) internal returns (uint256 shares) { + function _depositPercentageInternal(address vault, uint256 percentageBps, IERC20 baseAsset) internal returns (uint256 shares, uint256 assetsDeposited) { if (percentageBps == 0 || percentageBps > 1e4) revert InvalidPercentage(percentageBps); uint256 balance = baseAsset.balanceOf(address(this)); uint256 amount = (balance * percentageBps) / 1e4; - (shares,) = _depositInternal(vault, amount); - return shares; + (shares, assetsDeposited) = _depositInternal(vault, amount); } /** diff --git a/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol b/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol index b5c9ff3..506ba13 100644 --- a/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol +++ b/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol @@ -54,7 +54,7 @@ abstract contract AWKCompoundV2Adapter is AWKBaseVaultAdapter { * @dev Runs in wallet context via delegatecall. The amount parameter is the underlying asset amount. * Returns the cTokens received as shares. */ - function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares, uint256 actualAmount) { + function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares, uint256 assetsDeposited) { if (amount == 0) revert AWKErrors.ZeroAmount(); address asset = ICToken(vault).underlying(); uint256 baseAssetBalanceBefore = IERC20(asset).balanceOf(address(this)); @@ -64,8 +64,8 @@ abstract contract AWKCompoundV2Adapter is AWKBaseVaultAdapter { require(mintResult == 0, "AWKCompoundV2Adapter: mint failed"); uint256 balanceAfter = ICToken(vault).balanceOf(address(this)); shares = balanceAfter - balanceBefore; - actualAmount = baseAssetBalanceBefore - IERC20(asset).balanceOf(address(this)); - emit Deposited(address(this), vault, actualAmount, shares); + assetsDeposited = baseAssetBalanceBefore - IERC20(asset).balanceOf(address(this)); + emit Deposited(address(this), vault, assetsDeposited, shares); } /** diff --git a/src/agentwalletkit/adapters/AWKCompoundV3Adapter.sol b/src/agentwalletkit/adapters/AWKCompoundV3Adapter.sol index 6a214f0..6ecbcc3 100644 --- a/src/agentwalletkit/adapters/AWKCompoundV3Adapter.sol +++ b/src/agentwalletkit/adapters/AWKCompoundV3Adapter.sol @@ -54,7 +54,7 @@ abstract contract AWKCompoundV3Adapter is AWKBaseVaultAdapter { * @dev Runs in wallet context via delegatecall. The amount parameter is the base token amount. * Returns the change in balance as shares (though Compound V3 uses rebasing, not actual shares). */ - function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares, uint256 actualAmount) { + function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares, uint256 assetsDeposited) { if (amount == 0) revert AWKErrors.ZeroAmount(); address asset = ICompoundV3Comet(vault).baseToken(); uint256 baseAssetBalanceBefore = IERC20(asset).balanceOf(address(this)); @@ -63,8 +63,8 @@ abstract contract AWKCompoundV3Adapter is AWKBaseVaultAdapter { ICompoundV3Comet(vault).supply({asset: asset, amount: amount}); uint256 balanceAfter = ICompoundV3Comet(vault).balanceOf(address(this)); shares = balanceAfter - balanceBefore; - actualAmount = baseAssetBalanceBefore - IERC20(asset).balanceOf(address(this)); - emit Deposited(address(this), vault, actualAmount, shares); + assetsDeposited = baseAssetBalanceBefore - IERC20(asset).balanceOf(address(this)); + emit Deposited(address(this), vault, assetsDeposited, shares); } /** diff --git a/src/agentwalletkit/adapters/AWKERC4626Adapter.sol b/src/agentwalletkit/adapters/AWKERC4626Adapter.sol index 6a07e40..60a39d6 100644 --- a/src/agentwalletkit/adapters/AWKERC4626Adapter.sol +++ b/src/agentwalletkit/adapters/AWKERC4626Adapter.sol @@ -49,14 +49,14 @@ abstract contract AWKERC4626Adapter is AWKBaseVaultAdapter { * @notice Internal deposit implementation * @dev Runs in wallet context via delegatecall */ - function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares, uint256 actualAmount) { + function _depositInternal(address vault, uint256 amount) internal virtual override returns (uint256 shares, uint256 assetsDeposited) { if (amount == 0) revert AWKErrors.ZeroAmount(); address asset = IERC4626(vault).asset(); uint256 baseAssetBalanceBefore = IERC20(asset).balanceOf(address(this)); IERC20(asset).forceApprove(vault, amount); shares = IERC4626(vault).deposit({assets: amount, receiver: address(this)}); - actualAmount = baseAssetBalanceBefore - IERC20(asset).balanceOf(address(this)); - emit Deposited(address(this), vault, actualAmount, shares); + assetsDeposited = baseAssetBalanceBefore - IERC20(asset).balanceOf(address(this)); + emit Deposited(address(this), vault, assetsDeposited, shares); } /** From e1779c7afe5ac593fe099f0b00d130fde3885a21 Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Wed, 25 Feb 2026 20:48:23 +0000 Subject: [PATCH 09/13] . --- test/unit/adapters/CompoundV3Adapter.t.sol | 2 +- test/unit/adapters/ERC4626Adapter.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/adapters/CompoundV3Adapter.t.sol b/test/unit/adapters/CompoundV3Adapter.t.sol index 6f670fe..b1aed93 100644 --- a/test/unit/adapters/CompoundV3Adapter.t.sol +++ b/test/unit/adapters/CompoundV3Adapter.t.sol @@ -176,7 +176,7 @@ contract CompoundV3AdapterTest is Test { // ============ Audit Fix: Deposit records actual amount, not type(uint256).max (Issue 4) ============ - function test_DepositRecordsActualAmount() public { + function test_DepositRecordsAssetsDeposited() public { uint256 depositAmount = 500e6; wallet.executeAdapter(address(adapter), address(comet), abi.encodeWithSelector(adapter.deposit.selector, depositAmount)); (uint256 costBasis, uint256 shares) = feeTracker.getAgentVaultPosition(address(wallet), address(comet)); diff --git a/test/unit/adapters/ERC4626Adapter.t.sol b/test/unit/adapters/ERC4626Adapter.t.sol index 083938e..5ee64c8 100644 --- a/test/unit/adapters/ERC4626Adapter.t.sol +++ b/test/unit/adapters/ERC4626Adapter.t.sol @@ -7,8 +7,8 @@ import {YieldSeekerERC4626Adapter} from "../../../src/adapters/ERC4626Adapter.so import {AWKErrors} from "../../../src/agentwalletkit/AWKErrors.sol"; import {MockERC20} from "../../mocks/MockERC20.sol"; import {MockERC4626} from "../../mocks/MockERC4626.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {AdapterWalletHarness} from "./AdapterHarness.t.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Test, console} from "forge-std/Test.sol"; contract ERC4626AdapterTest is Test { From 438e682dc76f139bfa0d8f11cbee32ff2627bf8c Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Wed, 25 Feb 2026 20:52:14 +0000 Subject: [PATCH 10/13] . --- src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol b/src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol index 98f0ee0..6d8c527 100644 --- a/src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol +++ b/src/agentwalletkit/adapters/AWKBaseVaultAdapter.sol @@ -34,9 +34,10 @@ abstract contract AWKBaseVaultAdapter is AWKAdapter { * @notice Deposit assets into a vault (public interface, should not be called directly) * @param amount The amount of assets to deposit * @return shares The amount of vault shares received + * @return assetsDeposited The actual amount of base asset deposited * @dev This is a placeholder function signature. Actual execution happens via execute() -> _depositInternal() */ - function deposit(uint256 amount) external pure returns (uint256 shares) { + function deposit(uint256 amount) external pure returns (uint256 shares, uint256 assetsDeposited) { revert AWKErrors.DirectCallForbidden(); } @@ -44,9 +45,10 @@ abstract contract AWKBaseVaultAdapter is AWKAdapter { * @notice Deposit a percentage of base asset balance into a vault (public interface, should not be called directly) * @param percentageBps The percentage in basis points (10000 = 100%) * @return shares The amount of vault shares received + * @return assetsDeposited The actual amount of base asset deposited * @dev This is a placeholder function signature. Actual execution happens via execute() -> _depositPercentageInternal() */ - function depositPercentage(uint256 percentageBps) external pure returns (uint256 shares) { + function depositPercentage(uint256 percentageBps) external pure returns (uint256 shares, uint256 assetsDeposited) { revert AWKErrors.DirectCallForbidden(); } @@ -78,8 +80,8 @@ abstract contract AWKBaseVaultAdapter is AWKAdapter { bytes4 selector = bytes4(data[:4]); if (selector == this.deposit.selector) { uint256 amount = abi.decode(data[4:], (uint256)); - (uint256 shares,) = _depositInternal(target, amount); - return abi.encode(shares); + (uint256 shares, uint256 assetsDeposited) = _depositInternal(target, amount); + return abi.encode(shares, assetsDeposited); } if (selector == this.depositPercentage.selector) { uint256 percentageBps = abi.decode(data[4:], (uint256)); From 1607957d0e103f2a582a9d3355c7794bbd5a5d67 Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Fri, 27 Feb 2026 12:57:58 +0000 Subject: [PATCH 11/13] Fiddle: gas improvement for fee tracker (#12) --- src/FeeTracker.sol | 53 +++++++++++++------ src/adapters/CompoundV2Adapter.sol | 7 ++- .../adapters/AWKCompoundV2Adapter.sol | 1 + test/mocks/MockCompoundV2.sol | 4 +- test/mocks/MockFeeTracker.sol | 42 ++++++++++----- test/unit/adapters/ERC4626Adapter.t.sol | 1 - 6 files changed, 73 insertions(+), 35 deletions(-) diff --git a/src/FeeTracker.sol b/src/FeeTracker.sol index 1657025..fca93cd 100644 --- a/src/FeeTracker.sol +++ b/src/FeeTracker.sol @@ -18,6 +18,7 @@ pragma solidity 0.8.28; import {AWKErrors} from "./agentwalletkit/AWKErrors.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; error InvalidFeeRate(); @@ -28,18 +29,24 @@ error InvalidFeeRate(); * All recording functions use msg.sender, so callers can only affect their own accounting. */ contract YieldSeekerFeeTracker is AccessControl { + using SafeCast for uint256; + uint256 public constant MAX_FEE_RATE_BPS = 5000; uint256 public constant ASSET_EXCHANGE_RATE_PRECISION = 1e18; + struct VaultPosition { + uint128 costBasis; + uint128 shares; + } + uint256 public feeRateBps; address public feeCollector; mapping(address wallet => uint256) public agentFeesCharged; mapping(address wallet => uint256) public agentFeesPaid; - // Position tracking - mapping(address wallet => mapping(address vault => uint256)) public agentVaultCostBasis; - mapping(address wallet => mapping(address vault => uint256)) public agentVaultShares; + // Position tracking (packed into single slot per wallet+vault) + mapping(address wallet => mapping(address vault => VaultPosition)) internal _agentVaultPositions; mapping(address wallet => mapping(address token => uint256)) public agentYieldTokenFeesOwed; event YieldRecorded(address indexed wallet, uint256 yield, uint256 fee); @@ -100,8 +107,17 @@ contract YieldSeekerFeeTracker is AccessControl { * @return shares The vault shares held */ function getAgentVaultPosition(address wallet, address vault) external view returns (uint256 costBasis, uint256 shares) { - costBasis = agentVaultCostBasis[wallet][vault]; - shares = agentVaultShares[wallet][vault]; + VaultPosition storage pos = _agentVaultPositions[wallet][vault]; + costBasis = pos.costBasis; + shares = pos.shares; + } + + function agentVaultCostBasis(address wallet, address vault) external view returns (uint256) { + return _agentVaultPositions[wallet][vault].costBasis; + } + + function agentVaultShares(address wallet, address vault) external view returns (uint256) { + return _agentVaultPositions[wallet][vault].shares; } /** @@ -123,8 +139,9 @@ contract YieldSeekerFeeTracker is AccessControl { * @param sharesReceived The amount of shares received */ function recordAgentVaultShareDeposit(address vault, uint256 assetsDeposited, uint256 sharesReceived) external { - agentVaultCostBasis[msg.sender][vault] += assetsDeposited; - agentVaultShares[msg.sender][vault] += sharesReceived; + VaultPosition storage pos = _agentVaultPositions[msg.sender][vault]; + pos.costBasis = (uint256(pos.costBasis) + assetsDeposited).toUint128(); + pos.shares = (uint256(pos.shares) + sharesReceived).toUint128(); } function _chargeFeesOnProfit(address wallet, uint256 profit) internal { @@ -142,8 +159,9 @@ contract YieldSeekerFeeTracker is AccessControl { function recordAgentVaultShareWithdraw(address vault, uint256 sharesSpent, uint256 assetsReceived) external { if (sharesSpent == 0) return; address wallet = msg.sender; - uint256 totalShares = agentVaultShares[wallet][vault]; - uint256 totalCostBasis = agentVaultCostBasis[wallet][vault]; + VaultPosition storage pos = _agentVaultPositions[wallet][vault]; + uint256 totalShares = pos.shares; + uint256 totalCostBasis = pos.costBasis; uint256 vaultTokenFeesOwed = agentYieldTokenFeesOwed[wallet][vault]; uint256 feeInBaseAsset = 0; if (vaultTokenFeesOwed > 0) { @@ -163,8 +181,8 @@ contract YieldSeekerFeeTracker is AccessControl { _chargeFeesOnProfit(wallet, profit); } } - agentVaultCostBasis[wallet][vault] = 0; - agentVaultShares[wallet][vault] = 0; + pos.costBasis = 0; + pos.shares = 0; return; } else if (totalShares > 0) { // Normal withdrawal within tracked deposits @@ -174,8 +192,8 @@ contract YieldSeekerFeeTracker is AccessControl { uint256 profit = netAssets - proportionalCost; _chargeFeesOnProfit(wallet, profit); } - agentVaultCostBasis[wallet][vault] = totalCostBasis - proportionalCost; - agentVaultShares[wallet][vault] = totalShares - sharesSpent; + pos.costBasis = (totalCostBasis - proportionalCost).toUint128(); + pos.shares = (totalShares - sharesSpent).toUint128(); } } @@ -191,8 +209,9 @@ contract YieldSeekerFeeTracker is AccessControl { function recordAgentVaultAssetWithdraw(address vault, uint256 assetsReceived, uint256 totalVaultBalanceBefore, uint256 vaultTokenToBaseAssetRate) external { if (assetsReceived == 0 || totalVaultBalanceBefore == 0) return; address wallet = msg.sender; - uint256 totalCostBasis = agentVaultCostBasis[wallet][vault]; - uint256 totalShares = agentVaultShares[wallet][vault]; + VaultPosition storage pos = _agentVaultPositions[wallet][vault]; + uint256 totalCostBasis = pos.costBasis; + uint256 totalShares = pos.shares; uint256 vaultTokenFeesOwed = agentYieldTokenFeesOwed[wallet][vault]; uint256 feeInBaseAsset = 0; if (vaultTokenFeesOwed > 0) { @@ -224,8 +243,8 @@ contract YieldSeekerFeeTracker is AccessControl { uint256 profit = netAssets - proportionalCost; _chargeFeesOnProfit(wallet, profit); } - agentVaultCostBasis[wallet][vault] = totalCostBasis - proportionalCost; - agentVaultShares[wallet][vault] = totalShares - proportionalShares; + pos.costBasis = (totalCostBasis - proportionalCost).toUint128(); + pos.shares = (totalShares - proportionalShares).toUint128(); } /** diff --git a/src/adapters/CompoundV2Adapter.sol b/src/adapters/CompoundV2Adapter.sol index bf12f3f..50a4fe0 100644 --- a/src/adapters/CompoundV2Adapter.sol +++ b/src/adapters/CompoundV2Adapter.sol @@ -18,6 +18,7 @@ pragma solidity 0.8.28; import {AWKCompoundV2Adapter, ICToken} from "../agentwalletkit/adapters/AWKCompoundV2Adapter.sol"; import {YieldSeekerAdapter} from "./Adapter.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; /** * @title YieldSeekerCompoundV2Adapter @@ -45,8 +46,10 @@ contract YieldSeekerCompoundV2Adapter is AWKCompoundV2Adapter, YieldSeekerAdapte _requireBaseAsset(asset); uint256 cTokenBalance = ICToken(vault).balanceOf(address(this)); uint256 exchangeRate = ICToken(vault).exchangeRateCurrent(); - uint256 totalVaultBalanceBefore = (cTokenBalance * exchangeRate) / _feeTracker().ASSET_EXCHANGE_RATE_PRECISION(); + uint256 compoundExchangeRateScale = 10 ** (18 + uint256(IERC20Metadata(asset).decimals()) - uint256(ICToken(vault).decimals())); + uint256 totalVaultBalanceBefore = (cTokenBalance * exchangeRate) / compoundExchangeRateScale; + uint256 normalizedRate = (_feeTracker().ASSET_EXCHANGE_RATE_PRECISION() * exchangeRate) / compoundExchangeRateScale; assets = super._withdrawInternal(vault, shares); - _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: exchangeRate}); + _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: normalizedRate}); } } diff --git a/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol b/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol index 506ba13..9586985 100644 --- a/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol +++ b/src/agentwalletkit/adapters/AWKCompoundV2Adapter.sol @@ -25,6 +25,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol */ interface ICToken { function underlying() external view returns (address); + function decimals() external view returns (uint8); function mint(uint256 mintAmount) external returns (uint256); function redeemUnderlying(uint256 redeemAmount) external returns (uint256); function balanceOf(address account) external view returns (uint256); diff --git a/test/mocks/MockCompoundV2.sol b/test/mocks/MockCompoundV2.sol index ac33fc0..f21d0e1 100644 --- a/test/mocks/MockCompoundV2.sol +++ b/test/mocks/MockCompoundV2.sol @@ -10,10 +10,12 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract MockCToken is ERC20 { IERC20 private immutable _UNDERLYING; uint256 private _exchangeRateStored; - uint256 private constant EXCHANGE_RATE_SCALE = 1e18; + uint256 private immutable EXCHANGE_RATE_SCALE; constructor(address underlying_, string memory name_, string memory symbol_) ERC20(name_, symbol_) { _UNDERLYING = IERC20(underlying_); + uint8 underlyingDecimals = ERC20(underlying_).decimals(); + EXCHANGE_RATE_SCALE = 10 ** (18 + uint256(underlyingDecimals) - 8); _exchangeRateStored = EXCHANGE_RATE_SCALE; // 1:1 initially } diff --git a/test/mocks/MockFeeTracker.sol b/test/mocks/MockFeeTracker.sol index 3ff1ebc..dbc5bc4 100644 --- a/test/mocks/MockFeeTracker.sol +++ b/test/mocks/MockFeeTracker.sol @@ -4,10 +4,13 @@ pragma solidity 0.8.28; import {InvalidFeeRate} from "../../src/FeeTracker.sol"; import {AWKErrors} from "../../src/agentwalletkit/AWKErrors.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; /// @title MockFeeTracker /// @notice Mock implementation of FeeTracker for isolated unit testing contract MockFeeTracker is AccessControl { + using SafeCast for uint256; + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); uint256 public constant BASIS_POINTS = 10000; @@ -18,9 +21,13 @@ contract MockFeeTracker is AccessControl { mapping(address => uint256) private _feesOwed; + struct VaultPosition { + uint128 costBasis; + uint128 shares; + } + // Position tracking - mapping(address wallet => mapping(address vault => uint256)) public agentVaultCostBasis; - mapping(address wallet => mapping(address vault => uint256)) public agentVaultShares; + mapping(address wallet => mapping(address vault => VaultPosition)) internal _agentVaultPositions; mapping(address wallet => mapping(address token => uint256)) public agentYieldTokenFeesOwed; event FeeConfigUpdated(uint256 indexed feeRate, address indexed collector); @@ -79,32 +86,39 @@ contract MockFeeTracker is AccessControl { // ============ Position Tracking ============ function recordAgentVaultShareDeposit(address wallet, address vault, uint256 assetsDeposited, uint256 sharesReceived) external onlyRole(ADMIN_ROLE) { - agentVaultCostBasis[wallet][vault] += assetsDeposited; - agentVaultShares[wallet][vault] += sharesReceived; + VaultPosition storage pos = _agentVaultPositions[wallet][vault]; + pos.costBasis = (uint256(pos.costBasis) + assetsDeposited).toUint128(); + pos.shares = (uint256(pos.shares) + sharesReceived).toUint128(); } function recordAgentVaultShareWithdraw(address wallet, address vault, uint256 sharesSpent, uint256 assetsReceived) external onlyRole(ADMIN_ROLE) { - uint256 totalShares = agentVaultShares[wallet][vault]; - uint256 totalCostBasis = agentVaultCostBasis[wallet][vault]; - + VaultPosition storage pos = _agentVaultPositions[wallet][vault]; + uint256 totalShares = pos.shares; + uint256 totalCostBasis = pos.costBasis; if (totalShares == 0) return; - uint256 proportionalCost = (totalCostBasis * sharesSpent) / totalShares; - if (assetsReceived > proportionalCost) { uint256 profit = assetsReceived - proportionalCost; uint256 fee = (profit * _feeRate) / BASIS_POINTS; _feesOwed[wallet] += fee; emit YieldRecorded(wallet, profit, fee); } - - agentVaultCostBasis[wallet][vault] = totalCostBasis - proportionalCost; - agentVaultShares[wallet][vault] = totalShares - sharesSpent; + pos.costBasis = (totalCostBasis - proportionalCost).toUint128(); + pos.shares = (totalShares - sharesSpent).toUint128(); } function getAgentVaultPosition(address wallet, address vault) external view returns (uint256 costBasis, uint256 shares) { - costBasis = agentVaultCostBasis[wallet][vault]; - shares = agentVaultShares[wallet][vault]; + VaultPosition storage pos = _agentVaultPositions[wallet][vault]; + costBasis = pos.costBasis; + shares = pos.shares; + } + + function agentVaultCostBasis(address wallet, address vault) external view returns (uint256) { + return _agentVaultPositions[wallet][vault].costBasis; + } + + function agentVaultShares(address wallet, address vault) external view returns (uint256) { + return _agentVaultPositions[wallet][vault].shares; } function recordAgentYieldTokenEarned(address wallet, address token, uint256 amount) external onlyRole(ADMIN_ROLE) { diff --git a/test/unit/adapters/ERC4626Adapter.t.sol b/test/unit/adapters/ERC4626Adapter.t.sol index 5ee64c8..56d4a1a 100644 --- a/test/unit/adapters/ERC4626Adapter.t.sol +++ b/test/unit/adapters/ERC4626Adapter.t.sol @@ -8,7 +8,6 @@ import {AWKErrors} from "../../../src/agentwalletkit/AWKErrors.sol"; import {MockERC20} from "../../mocks/MockERC20.sol"; import {MockERC4626} from "../../mocks/MockERC4626.sol"; import {AdapterWalletHarness} from "./AdapterHarness.t.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Test, console} from "forge-std/Test.sol"; contract ERC4626AdapterTest is Test { From 566ffc512ce0f2326dfa4721e955c662c841f7c5 Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Fri, 27 Feb 2026 13:17:06 +0000 Subject: [PATCH 12/13] . --- src/adapters/AaveV3Adapter.sol | 1 + src/adapters/CompoundV2Adapter.sol | 2 ++ src/adapters/CompoundV3Adapter.sol | 1 + 3 files changed, 4 insertions(+) diff --git a/src/adapters/AaveV3Adapter.sol b/src/adapters/AaveV3Adapter.sol index 963b6cb..1882288 100644 --- a/src/adapters/AaveV3Adapter.sol +++ b/src/adapters/AaveV3Adapter.sol @@ -43,6 +43,7 @@ contract YieldSeekerAaveV3Adapter is AWKAaveV3Adapter, YieldSeekerAdapter { function _withdrawInternal(address vault, uint256 shares) internal override returns (uint256 assets) { address asset = _getVaultAsset(vault); _requireBaseAsset(asset); + // aTokens rebase 1:1 with underlying, so balanceOf is already in base asset terms uint256 totalVaultBalanceBefore = IAaveAToken(vault).balanceOf(address(this)); assets = super._withdrawInternal(vault, shares); _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: _feeTracker().ASSET_EXCHANGE_RATE_PRECISION()}); diff --git a/src/adapters/CompoundV2Adapter.sol b/src/adapters/CompoundV2Adapter.sol index 50a4fe0..1c90b6c 100644 --- a/src/adapters/CompoundV2Adapter.sol +++ b/src/adapters/CompoundV2Adapter.sol @@ -47,7 +47,9 @@ contract YieldSeekerCompoundV2Adapter is AWKCompoundV2Adapter, YieldSeekerAdapte uint256 cTokenBalance = ICToken(vault).balanceOf(address(this)); uint256 exchangeRate = ICToken(vault).exchangeRateCurrent(); uint256 compoundExchangeRateScale = 10 ** (18 + uint256(IERC20Metadata(asset).decimals()) - uint256(ICToken(vault).decimals())); + // cTokens don't rebase, so convert cToken balance to base asset terms via exchange rate uint256 totalVaultBalanceBefore = (cTokenBalance * exchangeRate) / compoundExchangeRateScale; + // Normalize exchange rate from Compound's scale to FeeTracker's 1e18 precision uint256 normalizedRate = (_feeTracker().ASSET_EXCHANGE_RATE_PRECISION() * exchangeRate) / compoundExchangeRateScale; assets = super._withdrawInternal(vault, shares); _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: normalizedRate}); diff --git a/src/adapters/CompoundV3Adapter.sol b/src/adapters/CompoundV3Adapter.sol index 8623395..017cc69 100644 --- a/src/adapters/CompoundV3Adapter.sol +++ b/src/adapters/CompoundV3Adapter.sol @@ -43,6 +43,7 @@ contract YieldSeekerCompoundV3Adapter is AWKCompoundV3Adapter, YieldSeekerAdapte function _withdrawInternal(address vault, uint256 shares) internal override returns (uint256 assets) { address asset = _getVaultAsset(vault); _requireBaseAsset(asset); + // Compound V3 rebases 1:1 with underlying, so balanceOf is already in base asset terms uint256 totalVaultBalanceBefore = ICompoundV3Comet(vault).balanceOf(address(this)); assets = super._withdrawInternal(vault, shares); _feeTracker().recordAgentVaultAssetWithdraw({vault: vault, assetsReceived: assets, totalVaultBalanceBefore: totalVaultBalanceBefore, vaultTokenToBaseAssetRate: _feeTracker().ASSET_EXCHANGE_RATE_PRECISION()}); From fd5645501c86204228902e870b44bdd59c09f902 Mon Sep 17 00:00:00 2001 From: Krishan Patel Date: Thu, 5 Mar 2026 15:42:12 +0000 Subject: [PATCH 13/13] . --- src/FeeTracker.sol | 5 +-- test/unit/FeeTracker.t.sol | 75 +++++++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/FeeTracker.sol b/src/FeeTracker.sol index fca93cd..a52a5e7 100644 --- a/src/FeeTracker.sol +++ b/src/FeeTracker.sol @@ -172,10 +172,11 @@ contract YieldSeekerFeeTracker is AccessControl { agentFeesCharged[wallet] += feeInBaseAsset; emit YieldRecorded(wallet, feeInBaseAsset, feeInBaseAsset); } + uint256 netAssets = assetsReceived - feeInBaseAsset; if (sharesSpent > totalShares) { // Withdrawing more shares than deposits tracked - treat as full withdrawal if (totalCostBasis > 0 && totalShares > 0) { - uint256 depositSharesValue = (assetsReceived * totalShares) / sharesSpent; + uint256 depositSharesValue = (netAssets * totalShares) / sharesSpent; if (depositSharesValue > totalCostBasis) { uint256 profit = depositSharesValue - totalCostBasis; _chargeFeesOnProfit(wallet, profit); @@ -183,11 +184,9 @@ contract YieldSeekerFeeTracker is AccessControl { } pos.costBasis = 0; pos.shares = 0; - return; } else if (totalShares > 0) { // Normal withdrawal within tracked deposits uint256 proportionalCost = (totalCostBasis * sharesSpent) / totalShares; - uint256 netAssets = assetsReceived - feeInBaseAsset; if (netAssets > proportionalCost) { uint256 profit = netAssets - proportionalCost; _chargeFeesOnProfit(wallet, profit); diff --git a/test/unit/FeeTracker.t.sol b/test/unit/FeeTracker.t.sol index eae4420..a1ef8f6 100644 --- a/test/unit/FeeTracker.t.sol +++ b/test/unit/FeeTracker.t.sol @@ -757,17 +757,80 @@ contract YieldSeekerFeeTrackerTest is Test { uint256 feesAfter = feeTracker.getFeesOwed(agent1); uint256 totalFees = feesAfter - feesBefore; - // New calculation: only charge profit on deposit shares portion // feeInBaseAsset = (13.2 * 0.2) / 12 = 0.22 USDC (fee on yield token) - // depositSharesValue = (13.2 * 10) / 12 = 11.0 USDC - // profit on deposit = 11.0 - 10.0 = 1.0 USDC - // profit fee = 1.0 * 10% = 0.1 USDC - // Total = 0.22 + 0.1 = 0.32 USDC - uint256 expectedTotal = 0.32e6; + // depositSharesValue = ((13.2 - 0.22) * 10) / 12 = 10.8166... USDC + // profit on deposit = 10.8166 - 10.0 = 0.8166 USDC + // profit fee = 0.8166 * 10% = 0.08166 USDC + // Total = 0.22 + 0.08166 = 0.30166 USDC + uint256 expectedTotal = 0.30166e6; assertApproxEqAbs(totalFees, expectedTotal, 1e3, "Total fees should match calculation"); } + // ============ Audit: Over-withdrawal branch double-counts vault token fees ============ + + function test_VaultShareWithdraw_OverWithdrawalBranch_DoubleCounts_VaultTokenFee() public { + // This test demonstrates the double-counting bug in the over-withdrawal branch + // of recordAgentVaultShareWithdraw (sharesSpent > totalShares). + // + // Setup: + // - Deposit: 1000 USDC -> 1000 shares (cost basis = 1000 USDC) + // - Yield token earned: 200 shares -> 20 shares fee owed (10%) + // - Withdraw: 1200 shares (> 1000 tracked) for 1320 USDC (10% appreciation) + // + // Over-withdrawal branch triggers because 1200 > 1000 tracked shares. + // + // Step 1 (vault token fee conversion): + // feeTokenSwapped = min(1200, 20) = 20 + // feeInBaseAsset = (1320 * 20) / 1200 = 22 USDC + // agentFeesCharged += 22 + // + // Step 2 (profit fee - THE BUG): + // BUGGY: depositSharesValue = (1320 * 1000) / 1200 = 1100 + // CORRECT: depositSharesValue = ((1320 - 22) * 1000) / 1200 = 1081.666... + // + // BUGGY profit = 1100 - 1000 = 100 -> fee = 10 + // CORRECT profit = 1081 - 1000 = 81 -> fee = 8 (approximately) + // + // The buggy path charges ~32 USDC total, correct path charges ~30 USDC. + // The difference (~2 USDC) is the double-counted portion. + + address vault = makeAddr("vault"); + + // Deposit 1000 USDC -> 1000 shares + vm.prank(agent1); + feeTracker.recordAgentVaultShareDeposit(vault, 1000e6, 1000e18); + + // Earn 200 shares of yield -> 20 shares fee owed at 10% + vm.prank(agent1); + feeTracker.recordAgentYieldTokenEarned(vault, 200e18); + assertEq(feeTracker.getAgentYieldTokenFeesOwed(agent1, vault), 20e18); + + uint256 feesBefore = feeTracker.getFeesOwed(agent1); + + // Withdraw all 1200 shares for 1320 USDC (triggers over-withdrawal branch) + vm.prank(agent1); + feeTracker.recordAgentVaultShareWithdraw(vault, 1200e18, 1320e6); + + uint256 totalFeesCharged = feeTracker.getFeesOwed(agent1) - feesBefore; + + // Calculate what the CORRECT fees should be: + // Block 1: vault token fee = (1320e6 * 20e18) / 1200e18 = 22e6 + uint256 feeInBaseAsset = (1320e6 * 20e18) / 1200e18; + assertEq(feeInBaseAsset, 22e6, "vault token fee should be 22 USDC"); + + // Block 2 (correct): depositSharesValue using net assets + uint256 correctDepositSharesValue = ((1320e6 - feeInBaseAsset) * 1000e18) / 1200e18; + uint256 correctProfit = correctDepositSharesValue - 1000e6; // ~81.666e6 + uint256 correctProfitFee = (correctProfit * 1000) / 10000; + uint256 correctTotalFees = feeInBaseAsset + correctProfitFee; + + // The contract should charge the CORRECT fees, not the inflated buggy amount. + // This assertion will FAIL until the bug is fixed (line 178 in FeeTracker.sol + // should use `assetsReceived - feeInBaseAsset` instead of `assetsReceived`). + assertEq(totalFeesCharged, correctTotalFees, "Fees should not double-count vault token fee in over-withdrawal branch"); + } + // ============ Audit Fix: Safety cap on feeInBaseAsset (Issue 2) ============ function test_VaultAssetWithdraw_FeeInBaseAsset_CappedAtAssetsReceived() public {