DeFiFoundrySolidity
16,653 OP
View results
Submission Details
Severity: high
Invalid

Denial of Service in `StrategyArb`, `StrategyMainnet`, and `StrategyOp` Contracts

Summary

A strict requirement in the claimAndSwap() (and related _swapUnderlyingToAsset()) functions across StrategyArb, StrategyMainnet, and StrategyOp can cause a Denial of Service (DoS) under certain market conditions. Specifically, the condition:

require(_minOut > _amountClaim, "minOut too low");

only permits swaps when a strict “premium” ratio of alETH to WETH is achievable. If the real market conditions do not allow _minOut > _amountClaim, the swap reverts and can repeatedly fail. This leads to inoperability in the strategy’s core functionality—converting WETH back to alETH and depositing it into the transmuter—causing a potential operational lock or severe disruption in yield generation.

Note: While the original codebase might treat this restriction as a “feature” to secure profitable swaps only, it can unintentionally lock the strategy under unfavorable or even near-1:1 market conditions.

Vulnerability Details

1. Location in the Codebase

2. Why the Condition Causes a DoS

  • Strict Premium Requirement: Swaps only execute if _minOut (the desired output amount in alETH) strictly exceeds the input WETH amount. If the market price of alETH vs. WETH does not allow a ratio > 1:1 (i.e., no real premium), the transaction reverts.

  • Repeated Failures: Subsequent attempts at claimAndSwap() or _swapUnderlyingToAsset() with the same or similar parameters also revert, effectively blocking the strategy from its main function—turning claimed WETH back into alETH.

  • Potential “Lock-In” of Funds: Depending on whether the claim portion and the swap are atomic, users (or keepers) cannot proceed with normal operations if they must always expect a premium. This can leave funds unutilized or stuck in a state where no reallocation occurs.

Counterargument (Design Intent):
A defender might say that “requiring a premium is by design: the strategy only executes profitable swaps.” However, this design choice leads to a scenario where the strategy cannot operate at all if market conditions never (or rarely) yield a premium, hence a practical DoS in real-world use.

Impact

  1. Yield Generation Block

    • Because the strategy cannot re-deposit alETH (if the swap never completes), any yield mechanism tied to having alETH in the transmuter stalls. This can severely impact overall earnings.

  2. Partial or Extended Protocol Lock

    • While not all of Alchemix necessarily stops functioning, the affected strategies become inoperative, preventing normal claim-and-swap cycles.

    • In severe or prolonged depeg conditions (e.g., alETH < WETH but not sufficiently below to yield a premium), repeated reverts can create a practical lock of the strategy functionality—a form of DoS.

  3. Capital Efficiency Loss

    • WETH that is intended to be swapped to alETH remains “unconverted,” reducing the protocol’s capital utilization.

    • Even if the entire transaction reverts (and thus the WETH remains in the transmuter or elsewhere), the strategy cannot progress to generate yield.

  4. Extended Depeg Scenarios

    • If alETH is trading consistently below 1:1 with WETH, or the transmuter’s slow conversion is drawn out, the condition _minOut > _amountClaim becomes harder to satisfy. This exacerbates the blocking effect.

Severity Justification:
The inability to perform intended operations can be classified as a high-severity DoS or major operational risk, impacting user confidence and the protocol’s overall functionality.

Proof of Concept (PoC)

Test Code Exposing the Vulnerability

The following suite of tests in Foundry reproduces various DoS scenarios. Each test sets up an environment with mocks, assigns a claimable balance in the Transmuter, and verifies whether the swap fails (reverts) or succeeds.

/**
* @dev Test #1: Confirm DoS due to 'minOut <= amountClaim' => revert("minOut too low")
*/
function testRevert_MinOutBelowOrEqualAmountClaim() public {
console2.log("==== testRevert_MinOutBelowOrEqualAmountClaim: Start ====");
// Set claimable WETH in the transmuter for the strategy
console2.log("Setting 50e18 as claimable WETH in MockTransmuter...");
mockTransmuter.setClaimable(address(strategy), 50e18);
vm.startPrank(keeper);
// Test case 1: _minOut == _amountClaim => Revert expected
console2.log(
"Attempting claimAndSwap with minOut == amountClaim (50e18)... Expecting revert."
);
vm.expectRevert(bytes("minOut too low"));
strategy.claimAndSwap(50e18, 50e18, 0);
// Test case 2: _minOut < _amountClaim => Revert expected
console2.log(
"Attempting claimAndSwap with minOut < amountClaim (40e18)... Expecting revert."
);
vm.expectRevert(bytes("minOut too low"));
strategy.claimAndSwap(50e18, 40e18, 0);
vm.stopPrank();
console2.log(
"==== testRevert_MinOutBelowOrEqualAmountClaim: Completed ====\n"
);
}
/**
* @dev Test #2: Revert if router cannot meet minOut => "Router: Not enough output"
*/
function testRevert_SlippageTooHigh() public {
console2.log("==== testRevert_SlippageTooHigh: Start ====");
// Set claimable WETH in the transmuter for the strategy
console2.log("Setting 100e18 as claimable WETH in MockTransmuter...");
mockTransmuter.setClaimable(address(strategy), 100e18);
vm.startPrank(keeper);
// Test case: Router cannot satisfy minOut (minOut=101, but router can only provide 100)
console2.log(
"Attempting claimAndSwap with minOut > router's output capacity... Expecting revert."
);
vm.expectRevert(bytes("Router: Not enough output"));
strategy.claimAndSwap(100e18, 101e18, 0);
vm.stopPrank();
console2.log("==== testRevert_SlippageTooHigh: Completed ====\n");
}
/**
* @dev Test #3: Successful claimAndSwap when ratio >= minOut
*/
function testClaimAndSwapSuccess() public {
console2.log("==== testClaimAndSwapSuccess: Start ====");
// Deploy a new router with a premium ratio of 105 (1.05:1)
console2.log("Deploying new MockRouter with ratio=105 (1.05:1)...");
MockCurveRouter router105 = new MockCurveRouter(105, address(mockSynthetic));
// Deploy a new strategy using the new router
console2.log("Deploying a new StrategyMainnetMock with updated router...");
vm.startPrank(keeper);
StrategyMainnetMock newStrategy = new StrategyMainnetMock(
address(mockSynthetic),
address(mockTransmuter),
address(mockUnderlying),
address(router105)
);
vm.stopPrank();
// Make 100e18 claimable for the new strategy
console2.log(
"Setting 100e18 as claimable WETH in MockTransmuter for the new strategy..."
);
mockTransmuter.setClaimable(address(newStrategy), 100e18);
// Transfer 100 synthetic tokens to the new strategy
console2.log("Transferring 100e18 synthetic tokens to the new strategy...");
MockToken(address(mockSynthetic)).transfer(address(newStrategy), 100e18);
vm.startPrank(keeper);
// Successful claimAndSwap when minOut is satisfied (minOut=101, ratio=1.05:1)
console2.log("Attempting claimAndSwap with minOut=101 and ratio=1.05:1...");
newStrategy.claimAndSwap(100e18, 101e18, 0);
vm.stopPrank();
console2.log("ClaimAndSwap succeeded with ratio > 1:1 and minOut satisfied.");
console2.log("==== testClaimAndSwapSuccess: Completed ====\n");
}
/**
* @dev Test #4: Multiple actors (A, B, C) fail to claim/swap due to a 1:1 depeg scenario,
* illustrating a DoS.
*/
function testDOSDueToDepeg() public {
console2.log("==== testDOSDueToDepeg: Start ====");
// Let's assume the ratio is now 100 => exactly 1:1 or even lower.
// Because the 'Strategy' demands _minOut > _amountClaim,
// no actor can succeed if minOut == amountClaim (1:1 scenario).
console2.log(
"Setting 3 different claimable amounts for 3 different calls..."
);
// Give the strategy enough claimable in total, e.g., 150e18:
mockTransmuter.setClaimable(address(strategy), 150e18);
// We simulate 3 distinct attempts representing 3 users
// (in practice, the strategy uses a single 'keeper', but we can conceptually treat them as separate calls).
uint256 actor1Amount = 50e18;
uint256 actor2Amount = 50e18;
uint256 actor3Amount = 50e18;
// All attempt minOut = exactly the same as they claim, e.g., 50 => "DePeg" scenario (cannot get premium).
vm.startPrank(keeper);
console2.log(
"Actor A attempts claimAndSwap(50, 50) => expecting revert due to 'minOut too low'"
);
vm.expectRevert(bytes("minOut too low"));
strategy.claimAndSwap(actor1Amount, actor1Amount, 0);
console2.log(
"Actor B attempts claimAndSwap(50, 50) => expecting revert as well..."
);
vm.expectRevert(bytes("minOut too low"));
strategy.claimAndSwap(actor2Amount, actor2Amount, 0);
console2.log(
"Actor C attempts claimAndSwap(50, 50) => expecting revert again..."
);
vm.expectRevert(bytes("minOut too low"));
strategy.claimAndSwap(actor3Amount, actor3Amount, 0);
vm.stopPrank();
console2.log(
"All attempts failed => system is effectively locked under 1:1 ratio (depeg scenario)."
);
console2.log("==== testDOSDueToDepeg: Completed ====\n");
}
/**
* @dev Test #5: DoS scenario triggered by a sub-1:1 ratio (e.g., 0.95), where no premium is possible
* and the strategy reverts with "minOut too low" before calling the router.
*/
function testDoSScenarioWithDePeg() public {
console2.log("==== testDoSScenarioWithDePeg: Start ====");
// 1) Deploy a new router with ratio=95 => 0.95:1
console2.log("Deploying a MockRouter with ratio=95 (depegged)...");
MockCurveRouter router95 = new MockCurveRouter(95, address(mockSynthetic));
// 2) Deploy a new strategy using the depegged router
console2.log("Deploying StrategyMainnetMock with depegged ratio...");
vm.startPrank(keeper);
StrategyMainnetMock newStrategy = new StrategyMainnetMock(
address(mockSynthetic),
address(mockTransmuter),
address(mockUnderlying),
address(router95)
);
vm.stopPrank();
// 3) Set 200e18 WETH as claimable for the new strategy
console2.log("Configuring 200e18 as claimable WETH for the new strategy...");
mockTransmuter.setClaimable(address(newStrategy), 200e18);
// 4) Transfer 200e18 synthetic tokens to the new strategy
console2.log("Transferring 200e18 synthetic tokens to the new strategy...");
MockToken(address(mockSynthetic)).transfer(address(newStrategy), 200e18);
// We assume the sub-1:1 ratio means no user can meet "_minOut > _amountClaim"
// if they try exactly 1:1 or less. So, all calls should revert with "minOut too low"
// *prior* to calling the router.
vm.startPrank(keeper);
console2.log("==== Multiple user attempts under DoPeg < 1:1 ratio ====");
// User #1 (Alice) => minOut == amountClaim => revert in the strategy
console2.log(
"User#1: claimAndSwap(100, 100) => expecting 'minOut too low'..."
);
vm.expectRevert(bytes("minOut too low"));
newStrategy.claimAndSwap(100e18, 100e18, 0);
// User #2 (Bob) => minOut == amountClaim => revert
console2.log("User#2: claimAndSwap(50, 50) => expecting 'minOut too low'...");
vm.expectRevert(bytes("minOut too low"));
newStrategy.claimAndSwap(50e18, 50e18, 0);
// User #3 (Thomas) => minOut < amountClaim => revert
console2.log("User#3: claimAndSwap(30, 25) => expecting 'minOut too low'...");
vm.expectRevert(bytes("minOut too low"));
newStrategy.claimAndSwap(30e18, 25e18, 0);
// User #4 (Julia) => minOut < amountClaim => revert
console2.log("User#4: claimAndSwap(20, 15) => expecting 'minOut too low'...");
vm.expectRevert(bytes("minOut too low"));
newStrategy.claimAndSwap(20e18, 15e18, 0);
vm.stopPrank();
console2.log(
"==== All attempts reverted => DoS scenario under sub-1:1 ratio ===="
);
console2.log("==== testDoSScenarioWithDePeg: Completed ====\n");
}

Test Results

The logs confirm that in 1:1 or sub-1:1 scenarios, transactions fail with minOut too low, causing an operational lock for the strategy:

[PASS] testClaimAndSwapSuccess() (gas: 1102377)
Logs:
==== setUp: Initializing test environment ====
Deploying MockToken for synthetic and underlying assets...
Deploying MockTransmuter and MockRouter with 1:1 ratio...
Deploying StrategyMainnetMock with the Keeper...
Transferring 100 synthetic tokens to the strategy...
==== setUp: Completed test environment initialization ====
==== testClaimAndSwapSuccess: Start ====
Deploying new MockRouter with ratio=105 (1.05:1)...
Deploying a new StrategyMainnetMock with updated router...
Setting 100e18 as claimable WETH in MockTransmuter for the new strategy...
Transferring 100e18 synthetic tokens to the new strategy...
Attempting claimAndSwap with minOut=101 and ratio=1.05:1...
ClaimAndSwap succeeded with ratio > 1:1 and minOut satisfied.
==== testClaimAndSwapSuccess: Completed ====
[PASS] testDOSDueToDepeg() (gas: 67382)
Logs:
==== setUp: Initializing test environment ====
Deploying MockToken for synthetic and underlying assets...
Deploying MockTransmuter and MockRouter with 1:1 ratio...
Deploying StrategyMainnetMock with the Keeper...
Transferring 100 synthetic tokens to the strategy...
==== setUp: Completed test environment initialization ====
==== testDOSDueToDepeg: Start ====
Setting 3 different claimable amounts for 3 different calls...
Actor A attempts claimAndSwap(50, 50) => expecting revert due to 'minOut too low'
Actor B attempts claimAndSwap(50, 50) => expecting revert as well...
Actor C attempts claimAndSwap(50, 50) => expecting revert again...
All attempts failed => system is effectively locked under 1:1 ratio (depeg scenario).
==== testDOSDueToDepeg: Completed ====
[PASS] testDoSScenarioWithDePeg() (gas: 1009643)
Logs:
==== setUp: Initializing test environment ====
Deploying MockToken for synthetic and underlying assets...
Deploying MockTransmuter and MockRouter with 1:1 ratio...
Deploying StrategyMainnetMock with the Keeper...
Transferring 100 synthetic tokens to the strategy...
==== setUp: Completed test environment initialization ====
==== testDoSScenarioWithDePeg: Start ====
Deploying a MockRouter with ratio=95 (depegged)...
Deploying StrategyMainnetMock with depegged ratio...
Configuring 200e18 as claimable WETH for the new strategy...
Transferring 200e18 synthetic tokens to the new strategy...
==== Multiple user attempts under DoPeg < 1:1 ratio ====
User#1: claimAndSwap(100, 100) => expecting 'minOut too low'...
User#2: claimAndSwap(50, 50) => expecting 'minOut too low'...
User#3: claimAndSwap(30, 25) => expecting 'minOut too low'...
User#4: claimAndSwap(20, 15) => expecting 'minOut too low'...
==== All attempts reverted => DoS scenario under sub-1:1 ratio ====
==== testDoSScenarioWithDePeg: Completed ====
[PASS] testRevert_MinOutBelowOrEqualAmountClaim() (gas: 42600)
Logs:
==== setUp: Initializing test environment ====
Deploying MockToken for synthetic and underlying assets...
Deploying MockTransmuter and MockRouter with 1:1 ratio...
Deploying StrategyMainnetMock with the Keeper...
Transferring 100 synthetic tokens to the strategy...
==== setUp: Completed test environment initialization ====
==== testRevert_MinOutBelowOrEqualAmountClaim: Start ====
Setting 50e18 as claimable WETH in MockTransmuter...
Attempting claimAndSwap with minOut == amountClaim (50e18)... Expecting revert.
Attempting claimAndSwap with minOut < amountClaim (40e18)... Expecting revert.
==== testRevert_MinOutBelowOrEqualAmountClaim: Completed ====
[PASS] testRevert_SlippageTooHigh() (gas: 134691)
Logs:
==== setUp: Initializing test environment ====
Deploying MockToken for synthetic and underlying assets...
Deploying MockTransmuter and MockRouter with 1:1 ratio...
Deploying StrategyMainnetMock with the Keeper...
Transferring 100 synthetic tokens to the strategy...
==== setUp: Completed test environment initialization ====
==== testRevert_SlippageTooHigh: Start ====
Setting 100e18 as claimable WETH in MockTransmuter...
Attempting claimAndSwap with minOut > routers output capacity... Expecting revert.
==== testRevert_SlippageTooHigh: Completed ====
Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 1.84ms (2.36ms CPU time)

All tests passed; reverts confirm the DoS behavior when no premium >1:1 is available.

Test Code and Mitigation Results

To avoid the DoS, the verification > _amountClaim is relaxed to >= _amountClaim. Two new tests are added to validate the mitigation:

/**
* @dev Test: Multiple users executing claimAndSwap calls in sequence,
* verifying the mitigation logic. The Strategy starts with 300 synthetic
* tokens and claims are made at a 1:1 ratio.
*/
function testMultipleUsersWithMitigation() public {
console2.log("==== testMultipleUsersWithMitigation: start ====");
// Check Strategy starts with 300 synthetic
console2.log("Check strategy synthetic balance = 300e18 ...");
assertEq(
mockSynthetic.balanceOf(address(strategy)),
300e18,
"Strategy should start with 300 synthetic"
);
vm.startPrank(keeper);
console2.log("Keeper calls claimAndSwap(100, 100, 0) ...");
strategy.claimAndSwap(100e18, 100e18, 0);
console2.log("Keeper calls claimAndSwap(150, 150, 0) ...");
strategy.claimAndSwap(150e18, 150e18, 0);
console2.log("Keeper calls claimAndSwap(50, 50, 0) ...");
strategy.claimAndSwap(50e18, 50e18, 0);
vm.stopPrank();
console2.log(
"==== testMultipleUsersWithMitigation: all calls succeeded ===="
);
assertTrue(true, "All claimAndSwap calls succeeded with 1:1 ratio");
}
/**
* @dev Test: Confirm no strict DoS when router ratio is 0.95.
*/
function testMitigationNoStrictDoS_Ratio95() public {
console2.log("==== testMitigationNoStrictDoS_Ratio95: start ====");
// Deploy router95 (ratio=95 => 0.95)
console2.log("Deploying a second router with ratio=95...");
MockCurveRouter router95 = new MockCurveRouter(95, address(mockSynthetic));
// Transfer 1e24 to the second router
console2.log("Transfer 1e24 to router95 so it can do swaps...");
MockToken(address(mockSynthetic)).transfer(address(router95), 1e24);
// Deploy new Strategy using the second router
console2.log("Deploying a new Strategy with router95...");
vm.startPrank(keeper);
StrategyMainnetMockMitigated newStrategy = new StrategyMainnetMockMitigated(
address(mockSynthetic),
address(mockTransmuter),
address(mockUnderlying),
address(router95)
);
vm.stopPrank();
// Transfer 200 synthetic to the new Strategy
console2.log("Transfer 200e18 to newStrategy...");
MockToken(address(mockSynthetic)).transfer(address(newStrategy), 200e18);
// Set 200 claimable
console2.log("Set 200 claimable for newStrategy...");
mockTransmuter.setClaimable(address(newStrategy), 200e18);
console2.log(
"Keeper calls claimAndSwap(200, 200, 0) expecting revert due to slippage..."
);
vm.startPrank(keeper);
vm.expectRevert(bytes("Router: Not enough output"));
newStrategy.claimAndSwap(200e18, 200e18, 0);
console2.log(
"Keeper calls claimAndSwap(200, 190, 0) expecting revert because minOut condition not met..."
);
vm.expectRevert(bytes("minOut condition not met"));
newStrategy.claimAndSwap(200e18, 190e18, 0);
vm.stopPrank();
console2.log("==== testMitigationNoStrictDoS_Ratio95: done ====");
}

Mitigation Test Results

[PASS] testMitigationNoStrictDoS_Ratio95() (gas: 1269831)
Logs:
==== setUp: Deploying tokens with 3e24 supply each ====
==== setUp: Deploying Transmuter and Router (ratio=100) ====
==== setUp: Transfer 1e24 to primary Router ====
==== setUp: Deploying Strategy with keeper ====
==== setUp: Transfer 300e18 to Strategy ====
==== setUp: Set 300 claimable for Strategy ====
==== setUp: Done ====
==== testMitigationNoStrictDoS_Ratio95: start ====
Deploying a second router with ratio=95...
Transfer 1e24 to router95 so it can do swaps...
Deploying a new Strategy with router95...
Transfer 200e18 to newStrategy...
Set 200 claimable for newStrategy...
Keeper calls claimAndSwap(200, 200, 0) expecting revert due to slippage...
Keeper calls claimAndSwap with amountClaim = 200000000000000000000 minOut = 200000000000000000000
Calling transmuter.claim with _amountClaim = 200000000000000000000
Balance before swap: 200000000000000000000
Keeper calls claimAndSwap(200, 190, 0) expecting revert because minOut condition not met...
Keeper calls claimAndSwap with amountClaim = 200000000000000000000 minOut = 190000000000000000000
Calling transmuter.claim with _amountClaim = 200000000000000000000
==== testMitigationNoStrictDoS_Ratio95: done ====
[PASS] testMultipleUsersWithMitigation() (gas: 253420)
Logs:
==== setUp: Deploying tokens with 3e24 supply each ====
==== setUp: Deploying Transmuter and Router (ratio=100) ====
==== setUp: Transfer 1e24 to primary Router ====
==== setUp: Deploying Strategy with keeper ====
==== setUp: Transfer 300e18 to Strategy ====
==== setUp: Set 300 claimable for Strategy ====
==== setUp: Done ====
==== testMultipleUsersWithMitigation: start ====
Check strategy synthetic balance = 300e18 ...
Keeper calls claimAndSwap(100, 100, 0) ...
Keeper calls claimAndSwap with amountClaim = 100000000000000000000 minOut = 100000000000000000000
Calling transmuter.claim with _amountClaim = 100000000000000000000
Balance before swap: 300000000000000000000
Balance after swap: 400000000000000000000
Re-depositing into Transmuter: 400000000000000000000
Keeper calls claimAndSwap(150, 150, 0) ...
Keeper calls claimAndSwap with amountClaim = 150000000000000000000 minOut = 150000000000000000000
Calling transmuter.claim with _amountClaim = 150000000000000000000
Balance before swap: 0
Balance after swap: 150000000000000000000
Re-depositing into Transmuter: 150000000000000000000
Keeper calls claimAndSwap(50, 50, 0) ...
Keeper calls claimAndSwap with amountClaim = 50000000000000000000 minOut = 50000000000000000000
Calling transmuter.claim with _amountClaim = 50000000000000000000
Balance before swap: 0
Balance after swap: 50000000000000000000
Re-depositing into Transmuter: 50000000000000000000
==== testMultipleUsersWithMitigation: all calls succeeded ====
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.54ms (1.14ms CPU time)
  • It is confirmed that after relaxing the requirement to require(_minOut >= _amountClaim, "minOut condition not met"), swaps can be executed at 1:1 without reverting.

  • This eliminates the strict DoS caused by the “premium > 1:1” condition.

  • However, if the pool objectively cannot provide sufficient tokens (due to actual slippage), the transaction will still revert, which is expected behavior for a slippage check, not a design-induced DoS.

Observations

  1. Depeg Scenarios: The original vulnerability causes a permanent revert if no premium above 1:1 exists. With the mitigation, the strategy can continue operating even at parity or slight negative deviations.

  2. Slippage Checks Are Retained: The mitigation does not remove the revert in cases where liquidity or minimum output is not met, but it prevents a “total lock” caused solely by requiring >1:1.

  3. Security Impact: Allowing 1:1 removes the DoS risk, but projects can implement additional safeguards to ensure swaps are not executed at excessive losses (e.g., using dynamic _minOut, oracles, etc.).

Conclusion

This PoC demonstrates how the restriction require(_minOut > _amountClaim) in claimAndSwap() causes a Denial of Service when the alETH/WETH ratio does not exceed 1:1. The tests highlight the failure in multiple scenarios (1:1, sub-1:1, and insufficient router capacity) and subsequently validate the fix by applying a >= check instead of >. This change resolves the DoS in 1:1 scenarios, allowing the strategy to function uninterrupted under normal market conditions.

In summary:

  • Vulnerability: All swaps are blocked if no premium > 1:1 is available.

  • Mitigation: Change require(_minOut > _amountClaim) to require(_minOut >= _amountClaim) with an updated message, such as minOut condition not met.

  • Result: Swaps at 1:1 (or close) are allowed without automatic reverts, preventing the strategy from becoming inoperable.

Tools Used

  1. Manual Code Review

    • Examined _swapUnderlyingToAsset() and claimAndSwap() logic to identify the strict requirement.

    • Correlated lines with the repository’s main branch to ensure accuracy.

  2. Foundry

    • Created test scenarios in Foundry to simulate calls to claimAndSwap() or _swapUnderlyingToAsset() with _minOut > _amountClaim.

    • Verified repeated transaction reverts in environments mimicking actual user or keeper calls.

Recommendations

  1. Relax or Remove the Strict > _amountClaim Check

    • Currently:

      require(_minOut > _amountClaim, "minOut too low");
    • Suggestion:

      require(_minOut >= _amountClaim, "minOut condition not met");
    • Outcome: Allows a 1:1 swap (or a near match) without reverting. This prevents a total block if the premium is not strictly above 1.

  2. Incorporate a Flexible Slippage / Oracle Mechanism

    • Dynamically compute _minOut based on real-time on-chain data or oracles, adding a small buffer for slippage.

    • Example: If 1 WETH ~= 0.99 alETH, allow _minOut = 0.98 * _amountClaim. This avoids a revert while still enforcing a safety margin.

  3. Introduce a Fallback / Standby Mode

    • If market conditions do not allow profitable swaps (premium > 1:1), permit a fallback path:

      • Deposit WETH directly into a safe holding pattern, or

      • Wait for a better ratio but without blocking all further interactions.

  4. “Force Swap” or Management Override

    • Let onlyManagement or a similar privileged role bypass the strict check in emergencies.

    • This ensures the strategy does not remain stuck if the premium is never reached.

  5. Parameterizable Route Updates

    • The current code references multiple routes. Ensure that management or keepers can update _minOut or route parameters on demand, preventing a scenario where unchangeable, overly strict slippage settings cause permanent lock.

Rationale:
While some operators might prefer only profitable swaps, a total inability to execute any swap under less-than-ideal conditions is more damaging in the long term. Introducing the above improvements would prevent a “hard revert” scenario, thereby avoiding DoS.

Conclusion

By relaxing the rigid “premium-only” requirement or adding fallback logic, the protocol can avoid an unintended DoS that renders these Alchemix-based strategies inoperable under certain (potentially prolonged) market conditions. This ensures capital efficiency, continuous yield generation, and overall protocol stability.

Updates

Appeal created

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.