Skip to content

Conversation

@hacker-DOM
Copy link
Contributor

@hacker-DOM hacker-DOM commented Oct 15, 2025

  • Backup: Before refactoring to use interface instead of address.
  • Backup: Passing tests, setup complete.
  • Backup: Right before removing morpho vaults to test donation attack.
  • Backup: MetaMorpho v1.1 full attack [doesn't work].
  • S Feat: Small changes.
  • S Feat: Finalize morpho vault attack.

Summary by CodeRabbit

  • Tests
    • Added two mainnet-fork end-to-end tests for an ERC4626 donation-attack scenario: one verifying slippage-protected failure and one verifying successful deposit after an attack.
    • Exercises realistic governance/timelock and vault setup, per-function rate limits, relayer interactions, and attacker funding flow.
    • Validates post-attack vault state: shares, assets, total supply, asset↔share conversions and rounding edge cases.

@hacker-DOM hacker-DOM self-assigned this Oct 15, 2025
@hacker-DOM hacker-DOM force-pushed the sc-1183-test-maxSlippage-with-4626-donation-attack branch from ed6341f to f24d6ce Compare October 15, 2025 19:15

contract ERC4626DonationAttackTestBase is ForkTestBase {

IMetaMorpho morpho_vault = IMetaMorpho(0xe41a0583334f0dc4E023Acd0bFef3667F6FE0597);
Copy link
Contributor

Choose a reason for hiding this comment

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

camelCase for all values in this PR

Copy link
Contributor Author

Choose a reason for hiding this comment

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

assertEq(morpho_vault.totalSupply(), 0);

// Initialization
vm.startPrank(Ethereum.SPARK_PROXY);
Copy link
Contributor

Choose a reason for hiding this comment

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

New line after this and before stopPrank to make it clearer that its for the whole block between

Copy link
Contributor Author

Choose a reason for hiding this comment

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

assertEq(morpho_vault.curator(), curator);
assertEq(morpho_vault.guardian(), guardian);
assertEq(morpho_vault.feeRecipient(), fee_recipient);
assertTrue(morpho_vault.isAllocator(allocator));
Copy link
Contributor

Choose a reason for hiding this comment

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

New line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

contract ERC4626DonationAttack is ERC4626DonationAttackTestBase {

function test_donationAttackERC4626_usds() external {
address mallory = makeAddr("mallory");
Copy link
Contributor

Choose a reason for hiding this comment

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

attacker = "attacker"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 106 to 107
assertEq(morpho_vault.balanceOf(mallory), 1);
assertEq(morpho_vault.totalSupply(), 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

Align

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 119 to 128
try mainnetController.depositERC4626(address(morpho_vault), 2_000_000e18) {
// The deposit went through. The only time this is permissible is if the attack had no
// effect.
uint256 assetsOfProxy = morpho_vault.convertToAssets(morpho_vault.balanceOf(address(almProxy)));
assertEq(assetsOfProxy, 2_000_000e18);
assertEq(morpho_vault.balanceOf(address(almProxy)), 2_000_000e24);
} catch Error(string memory reason) {
// The deposit was correctly reverted.
assertEq(reason, "MainnetController/slippage-too-high");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be two separate tests, one with a really conservative slippage set and one with a loose slippage set and show success and revert cases. Abstract attack setup into a helper function. Tabish did the same thing for the AAVE testing so can follow his implementation for this.

Should assert result of successful attack, showing lost funds.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@coderabbitai
Copy link

coderabbitai bot commented Oct 16, 2025

Walkthrough

Adds a new mainnet-fork test suite that deploys/configures a Morpho ERC4626 vault, sets timelock-driven caps/queues and per-function rate limits, simulates a donation-style supply attack, and verifies depositERC4626 behavior under low- and high-slippage scenarios.

Changes

Cohort / File(s) Summary
New ERC4626 donation-attack tests
test/mainnet-fork/ERC4626DonationAttack.t.sol
Adds ERC4626DonationAttackTestBase (public setUp(), _getBlock() override) to deploy/configure Morpho ERC4626 vault, assign governance roles, initialize market cap and supply queue via timelock, and set per-function rate limits. Adds ERC4626DonationAttack with _doAttack() helper and two tests: test_depositERC4626_donationAttackFailure() (near-zero slippage, expects revert) and test_depositERC4626_donationAttackSuccess() (near-100% slippage, performs deposit and asserts shares, totalAssets, totalSupply, and asset↔share conversions including rounding).

Sequence Diagram(s)

sequenceDiagram
  participant Test as Test Contract
  participant Timelock as Timelock
  participant Vault as Morpho ERC4626 Vault
  participant Controller as Controller/Market
  participant Relayer as Relayer/Proxy

  Note right of Test #DDEEFF: Setup & governance
  Test->>Timelock: schedule & execute cap/supplyQueue/rate-limit changes
  Timelock-->>Controller: apply caps/queues/roles

  Note right of Test #E8F8E0: Attack simulation
  Test->>Vault: _doAttack() — supply (donation) into market
  Vault-->>Vault: update totalAssets / totalSupply

  Note right of Controller #FFF2CC: depositERC4626 flows
  Relayer->>Controller: depositERC4626(amount, slippage)
  alt slippage too low
    Controller-->>Relayer: revert (slippage)
  else slippage allowed
    Controller->>Vault: mint shares / update totals
    Vault-->>Relayer: return shares
    Test->>Test: assert shares, totals, conversions
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay attention to timelock-scheduled actions and their exact parameters.
  • Review _doAttack() for correctness of funding, supply/donation steps, and assertions around rounding.
  • Verify overrides (setUp(), _getBlock()) and visibility changes for test harness compatibility.

Poem

🐰 I hopped into tests with a curious twitch,

Timelock bells chimed, I nudged a coin switch,
A gift slipped in, totals waltzed and spun,
One try was blocked, the next one gleefully won,
Carrots tallied — vaults green, my work is done.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: Test maxSlippage with ERC4626 donation attack" meets all specified requirements. It follows the required format by starting with "feat:" (one word followed by a colon), is concise at 51 characters, and is descriptive. The title accurately reflects the main change in the changeset, which adds two new test contracts with tests that specifically verify maxSlippage behavior (configured as near-zero and near-100% slippage) in the context of ERC4626 donation attacks. The title is directly related to the primary objective of the PR as indicated in the PR description and objectives.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sc-1183-test-maxSlippage-with-4626-donation-attack

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f83a98 and 3cd11be.

📒 Files selected for processing (1)
  • test/mainnet-fork/ERC4626DonationAttack.t.sol (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: coverage
🔇 Additional comments (1)
test/mainnet-fork/ERC4626DonationAttack.t.sol (1)

124-125: Verify hardcoded market state at fork block 22932160.
Lines 124–125 assert exact on-chain totals; they’ll break if the fork block or asset balances change. Fetch these values dynamically in your setup or pin the fork to block 22932160 to ensure test stability.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (5)
test/mainnet-fork/ERC4626DonationAttack.t.sol (5)

43-43: Use direct string equality in assertion.

No need for keccak hashing here.

-        assertEq(keccak256(abi.encode(morphoVault.symbol())), keccak256(abi.encode("sparkUSDS")));
+        assertEq(morphoVault.symbol(), "sparkUSDS");

26-31: Adopt camelCase for identifiers (consistency with project style).

Rename fee_recipient/skim_recipient and update their usages.

-    address fee_recipient  = makeAddr("fee_recipient");
-    address skim_recipient = makeAddr("skim_recipient");
+    address feeRecipient  = makeAddr("fee_recipient");
+    address skimRecipient = makeAddr("skim_recipient");
@@
-        morphoVault.setFeeRecipient(fee_recipient);
+        morphoVault.setFeeRecipient(feeRecipient);
@@
-        morphoVault.setSkimRecipient(skim_recipient);
+        morphoVault.setSkimRecipient(skimRecipient);
@@
-        assertEq(morphoVault.feeRecipient(), fee_recipient);
+        assertEq(morphoVault.feeRecipient(), feeRecipient);

Also applies to: 50-55, 67-70


48-65: Add a blank line before stopPrank for readability.

Clarifies the prank scope block.

         morphoVault.setSupplyQueue(supplyOrder);
 
+        
         vm.stopPrank();

89-89: Clarify the conservative slippage setting.

Document the actual tolerance to avoid confusion.

-        mainnetController.setMaxSlippage(address(morphoVault), 1e18 - 1e4);  // Rounding slippage
+        // Extremely conservative: allows only 1e-14 relative slippage (~0.000000000001%), i.e., rounding-only
+        mainnetController.setMaxSlippage(address(morphoVault), 1e18 - 1e4);

98-101: Explain the permissive slippage configuration.

Make intent explicit for the success path.

-        // Set max slippage to (close to) 100%
+        // Permissive: nearly 100% slippage allowed (minOut ≈ 1 / 1e18 of expected); enables attack success
         vm.prank(Ethereum.SPARK_PROXY);
         mainnetController.setMaxSlippage(address(morphoVault), 1);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3cd11be and bb0e658.

📒 Files selected for processing (1)
  • test/mainnet-fork/ERC4626DonationAttack.t.sol (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test
  • GitHub Check: coverage
  • GitHub Check: build
🔇 Additional comments (1)
test/mainnet-fork/ERC4626DonationAttack.t.sol (1)

92-95: Ensure revert expectation matches implementation (string vs custom error).

If depositERC4626 uses a custom error, switch to selector-based expectRevert.

-        vm.expectRevert("MainnetController/slippage-too-high");
+        // If depositERC4626 uses a custom error, prefer:
+        // vm.expectRevert(MainnetController.SlippageTooHigh.selector);
+        vm.expectRevert("MainnetController/slippage-too-high");

@hacker-DOM hacker-DOM force-pushed the sc-1183-test-maxSlippage-with-4626-donation-attack branch from 05f9350 to 39607a3 Compare October 16, 2025 18:48
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
test/mainnet-fork/ERC4626DonationAttack.t.sol (2)

26-30: Use camelCase consistently for address variables.

Variables fee_recipient and skim_recipient use snake_case, while other similar variables like attacker use camelCase. Past review comments requested camelCase for all values in this PR.

Apply this diff to use consistent camelCase naming:

-    address fee_recipient  = makeAddr("fee_recipient");
+    address feeRecipient  = makeAddr("feeRecipient");
-    address skim_recipient = makeAddr("skim_recipient");
+    address skimRecipient = makeAddr("skimRecipient");

Update all references to these variables throughout the file (lines 52, 54, 69).


129-133: Consider renaming market variable for clarity.

The market variable captures the pre-attack snapshot and is correctly used at line 147 to verify expected shares. However, as noted in past review comments, renaming it to marketBefore would make it clearer that this snapshot represents state before the mutation.

Apply this diff:

-        Market memory market = morpho.market(marketId);
+        Market memory marketBefore = morpho.market(marketId);
 
-        assertEq(market.totalSupplyAssets, 36_095_481.319542091092211965e18); // ~36M USDS
-        assertEq(market.totalSupplyShares, 36_095_481.319542091092211965000000e24);
+        assertEq(marketBefore.totalSupplyAssets, 36_095_481.319542091092211965e18); // ~36M USDS
+        assertEq(marketBefore.totalSupplyShares, 36_095_481.319542091092211965000000e24);

Also update line 147:

-        assertEq(shares, uint256(1_000_000e18) * market.totalSupplyShares / market.totalSupplyAssets);
+        assertEq(shares, uint256(1_000_000e18) * marketBefore.totalSupplyShares / marketBefore.totalSupplyAssets);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05f9350 and 39607a3.

📒 Files selected for processing (1)
  • test/mainnet-fork/ERC4626DonationAttack.t.sol (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build
  • GitHub Check: test
  • GitHub Check: coverage
🔇 Additional comments (5)
test/mainnet-fork/ERC4626DonationAttack.t.sol (5)

34-77: Well-structured test setup.

The setUp function properly configures the Morpho vault with all necessary roles, market parameters, timelock handling, and rate limits. The sequencing of operations (cap submission → timelock wait → cap acceptance → supply queue setup) correctly follows the protocol's requirements.


87-95: Test correctly validates slippage protection.

The test properly demonstrates that conservative slippage settings (allowing only ~0.01% deviation) successfully prevent the donation attack from causing excessive loss. The explicit revert check confirms the controller's slippage protection is working as intended.


97-114: Test demonstrates successful donation attack under permissive slippage.

The test correctly shows that with near-100% slippage allowance, the donation attack succeeds, resulting in significant value extraction (proxy deposits 2M but only receives 3 shares worth ~1.8M). The assertions properly verify the post-attack state.

The calculation comments (lines 106-110) accurately explain the MetaMorpho inflation protection mechanism that adds +1 to both numerator and denominator during share minting.


115-126: Asset conversion assertions properly demonstrate attack impact.

The test correctly verifies that the donation attack causes value loss: the proxy deposits 2M USDS but recovers only 1.8M worth of assets, demonstrating a ~10% immediate loss due to the inflated share price. The attacker also experiences losses relative to their 1M deposit. These assertions effectively prove the vulnerability under permissive slippage settings.


79-81: Confirm block timestamp for test fork

The block number 22932160 is annotated as July 16 2025, but couldn’t be fetched via Cloudflare RPC. Please verify with a reliable mainnet RPC (or Etherscan) that this block exists and has the intended timestamp before merging.

@hacker-DOM hacker-DOM force-pushed the sc-1183-test-maxSlippage-with-4626-donation-attack branch from 6b5b2ea to 49ba71a Compare October 17, 2025 13:56
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (5)
test/mainnet-fork/ERC4626DonationAttack.t.sol (5)

147-148: Verify that share calculation uses pre-supply market state.

The assertion uses market (captured at line 129) to validate shares received from the supply operation. Confirm this is comparing against the intended pre-supply snapshot, not post-supply state. The memory snapshot approach is correct, but renaming to marketBefore would make the intent explicit, as previously suggested.


43-43: Inefficient string comparison remains.

This line still uses keccak256(abi.encode(...)) for string comparison despite being flagged in a previous review. Direct string comparison is clearer and cheaper.

Apply this diff:

-        assertEq(keccak256(abi.encode(morphoVault.symbol())), keccak256(abi.encode("sparkUSDS")));
+        assertEq(morphoVault.symbol(), "sparkUSDS");

89-89: Consider documenting the slippage tolerance.

The value 1e18 - 1e4 represents approximately 0.01% slippage tolerance. Adding an inline comment would improve readability, as noted in a previous review.


100-100: Document the permissive slippage setting.

Setting maxSlippage to 1 allows nearly 100% slippage. An explanatory comment would clarify this is intentional for demonstrating the attack's success case, as previously noted.


160-160: Missing explanation for test setup.

The deal() call funds almProxy for the subsequent deposit test in the calling function but lacks an explanatory comment, as noted in a previous review.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39607a3 and 49ba71a.

📒 Files selected for processing (1)
  • test/mainnet-fork/ERC4626DonationAttack.t.sol (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test
  • GitHub Check: coverage
  • GitHub Check: build
🔇 Additional comments (6)
test/mainnet-fork/ERC4626DonationAttack.t.sol (6)

17-24: Market params correctly configured for supply-only vault.

The zero values for collateralToken, oracle, irm, and lltv appropriately model a supply-only market with no borrowing. The market ID derivation is correct.


34-77: Well-structured setUp with proper timelock flow.

The initialization sequence correctly follows the Morpho vault's governance flow: role assignment → cap submission → timelock wait → cap acceptance → supply queue configuration → rate limit setup. The assertions validate each step.


79-81: LGTM!

The block number override correctly targets a specific mainnet state for deterministic fork testing.


87-95: Test correctly validates slippage protection.

The test properly configures conservative slippage limits, executes the attack, and verifies that the controller rejects the deposit due to excessive slippage. The revert message check is specific and appropriate.


106-126: Excellent verification of donation attack impact.

The test thoroughly validates the attack mechanics with clear inline math explanations. The assertions correctly demonstrate:

  • Share dilution (victim receives 3 shares instead of expected value)
  • Value extraction (victim's 2M deposit converts to ~1.8M assets)
  • Attacker profit (1 share converts to 600K assets from 1M+1 investment)

The mathematical comments make the attack vector transparent.


128-161: Donation attack simulation is technically sound.

The helper correctly implements the ERC4626 donation attack vector:

  1. Attacker establishes minimal position (1 wei → 1 share)
  2. Attacker donates assets directly via Morpho.supply() to inflate totalAssets
  3. Subsequent depositors suffer severe share dilution

The assertions validate both the Morpho protocol behavior and the MetaMorpho vault's +1 offset logic. The inline math comments at lines 154-158 are particularly helpful.

usds.approve(address(morphoVault), 1);
morphoVault.deposit(1, attacker);
usds.approve(address(morpho), 1_000_000e18);
// Donation attack
Copy link
Contributor

Choose a reason for hiding this comment

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

Donation attack performed by donating shares of morpho market supply to morpho vault

Copy link
Contributor

Choose a reason for hiding this comment

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

Also new line above

Copy link
Contributor Author

Choose a reason for hiding this comment

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

vm.prank(relayer);
uint256 shares = mainnetController.depositERC4626(address(morphoVault), 2_000_000e18);

// We can compute:
Copy link
Contributor

Choose a reason for hiding this comment

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

No "we" or "us" language

Copy link
Contributor Author

Choose a reason for hiding this comment

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

coderabbitai[bot]
coderabbitai bot previously approved these changes Oct 27, 2025
@github-actions
Copy link

Coverage after merging sc-1183-test-maxSlippage-with-4626-donation-attack into dev will be

99.85%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
deploy
   ControllerDeploy.sol100%100%100%100%
   ForeignControllerInit.sol100%100%100%100%
   MainnetControllerInit.sol97.37%93.33%100%100%152, 90
src
   ALMProxy.sol100%100%100%100%
   ALMProxyFreezable.sol100%100%100%100%
   ForeignController.sol98.75%95.45%100%99.12%296–297
   MainnetController.sol100%100%100%100%
   OTCBuffer.sol100%100%100%100%
   RateLimitHelpers.sol100%100%100%100%
   RateLimits.sol100%100%100%100%
src/libraries
   CCTPLib.sol100%100%100%100%
   CurveLib.sol100%100%100%100%
   PSMLib.sol100%100%100%100%

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (6)
test/mainnet-fork/ERC4626DonationAttack.t.sol (6)

43-43: Simplify string comparison.

Past review feedback identified this, but it remains unaddressed. The keccak256(abi.encode(...)) pattern is unnecessarily expensive for a simple string equality check.

Apply this diff:

-        assertEq(keccak256(abi.encode(morphoVault.symbol())), keccak256(abi.encode("sparkUSDS")));
+        assertEq(morphoVault.symbol(), "sparkUSDS");

161-161: Add comment explaining test setup.

The deal() call funds the almProxy for the subsequent deposit operation in the calling test. A brief comment improves test flow clarity.

+        // Fund almProxy for subsequent deposit test
         deal(address(usds), address(almProxy), 2_000_000e18);

147-149: Clarify share type distinction.

Line 149 asserts shares == 1e30, representing Morpho protocol shares from the morpho.supply() call. This is distinct from MetaMorpho vault shares (which total 1 at this point). Adding a comment prevents confusion.

         assertEq(assets, 1_000_000e18);
         assertEq(shares, uint256(1_000_000e18) * market.totalSupplyShares / market.totalSupplyAssets);
-        assertEq(shares, 1e30);
+        assertEq(shares, 1e30);  // Morpho protocol shares (not vault shares)

88-89: Clarify the slippage tolerance value.

The inline comment "Rounding slippage" doesn't explain what 1e18 - 1e4 represents. This value means 99.99% (allowing only ~1 basis point of slippage), which is critical for understanding why the attack fails.

Apply this diff:

         vm.prank(Ethereum.SPARK_PROXY);
-        mainnetController.setMaxSlippage(address(morphoVault), 1e18 - 1e4);  // Rounding slippage
+        // Conservative slippage: 99.99% of expected shares (allows only ~1 basis point slippage)
+        mainnetController.setMaxSlippage(address(morphoVault), 1e18 - 1e4);

98-100: Enhance comment explaining permissive slippage.

While the existing comment mentions "close to 100%", it could be more explicit about the implications for demonstration purposes.

Apply this diff:

-        // Set max slippage to (close to) 100%
+        // Set max slippage to nearly 100% (allows up to ~99.9999...% slippage)
+        // This permissive setting demonstrates the attack succeeds with inadequate protection
         vm.prank(Ethereum.SPARK_PROXY);
         mainnetController.setMaxSlippage(address(morphoVault), 1);

136-144: Consider adding step comments for attack clarity.

The attack sequence is correct but would benefit from brief step annotations to improve readability.

         deal(address(usds), attacker, 1_000_000e18 + 1);
 
         vm.startPrank(attacker);
+        // Step 1: Deposit 1 wei to establish minimal vault share
         usds.approve(address(morphoVault), 1);
         morphoVault.deposit(1, attacker);
+        
+        // Step 2: Direct donation to Morpho inflates totalAssets without minting vault shares
         usds.approve(address(morpho), 1_000_000e18);
-
         // Donation attack performed by donating shares of Morpho market supply to Morpho vault
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49ba71a and ade133d.

📒 Files selected for processing (1)
  • test/mainnet-fork/ERC4626DonationAttack.t.sol (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: coverage
  • GitHub Check: build
  • GitHub Check: test
🔇 Additional comments (1)
test/mainnet-fork/ERC4626DonationAttack.t.sol (1)

79-81: LGTM: Block override for fork testing.

The fixed block number with date comment provides clear context for the test environment.

Comment on lines +106 to +126
// One can compute:
// shares == assets * (totalSupply + 1) / (totalAssets + 1)
// == 2_000_000e18 * (1 + 1) / (1_000_000e18 + 1 + 1)
// == 3.9..
// Rounding down, the proxy receives 3 shares.
assertEq(shares, 3);
assertEq(morphoVault.totalAssets(), 3_000_000e18 + 1);
assertEq(morphoVault.totalSupply(), 4);

uint256 assetsOfProxy = morphoVault.convertToAssets(morphoVault.balanceOf(address(almProxy)));
uint256 assetsOfAttacker = morphoVault.convertToAssets(morphoVault.balanceOf(attacker));

// convertToAssets(shares) == shares * (totalAssets + 1) / (totalSupply + 1)
// convertToAssets(3) == 3 * (3_000_000e18 + 1 + 1) / (4 + 1)
// == 1_800_000e18 + 1
assertEq(assetsOfProxy, 1_800_000e18 + 1);
assertLt(assetsOfProxy, 2_000_000e18); // The proxy owns less than it deposited
// convertToAssets(1) == 1 * (3_000_000e18 + 1 + 1) / (4 + 1)
// == 600_000e18
assertEq(assetsOfAttacker, 600_000e18);
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider asserting that attacker loses money in the attack.

The detailed math shows the proxy loses ~200k, but notably the attacker invests 1_000_000e18 + 1 and can only redeem 600_000e18 (line 125), making this attack unprofitable for the attacker. Adding an assertion would strengthen the test's security demonstration.

         assertEq(assetsOfAttacker, 600_000e18);
+        assertLt(assetsOfAttacker, 1_000_000e18);  // Attacker loses more than the victim
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// One can compute:
// shares == assets * (totalSupply + 1) / (totalAssets + 1)
// == 2_000_000e18 * (1 + 1) / (1_000_000e18 + 1 + 1)
// == 3.9..
// Rounding down, the proxy receives 3 shares.
assertEq(shares, 3);
assertEq(morphoVault.totalAssets(), 3_000_000e18 + 1);
assertEq(morphoVault.totalSupply(), 4);
uint256 assetsOfProxy = morphoVault.convertToAssets(morphoVault.balanceOf(address(almProxy)));
uint256 assetsOfAttacker = morphoVault.convertToAssets(morphoVault.balanceOf(attacker));
// convertToAssets(shares) == shares * (totalAssets + 1) / (totalSupply + 1)
// convertToAssets(3) == 3 * (3_000_000e18 + 1 + 1) / (4 + 1)
// == 1_800_000e18 + 1
assertEq(assetsOfProxy, 1_800_000e18 + 1);
assertLt(assetsOfProxy, 2_000_000e18); // The proxy owns less than it deposited
// convertToAssets(1) == 1 * (3_000_000e18 + 1 + 1) / (4 + 1)
// == 600_000e18
assertEq(assetsOfAttacker, 600_000e18);
}
// One can compute:
// shares == assets * (totalSupply + 1) / (totalAssets + 1)
// == 2_000_000e18 * (1 + 1) / (1_000_000e18 + 1 + 1)
// == 3.9..
// Rounding down, the proxy receives 3 shares.
assertEq(shares, 3);
assertEq(morphoVault.totalAssets(), 3_000_000e18 + 1);
assertEq(morphoVault.totalSupply(), 4);
uint256 assetsOfProxy = morphoVault.convertToAssets(morphoVault.balanceOf(address(almProxy)));
uint256 assetsOfAttacker = morphoVault.convertToAssets(morphoVault.balanceOf(attacker));
// convertToAssets(shares) == shares * (totalAssets + 1) / (totalSupply + 1)
// convertToAssets(3) == 3 * (3_000_000e18 + 1 + 1) / (4 + 1)
// == 1_800_000e18 + 1
assertEq(assetsOfProxy, 1_800_000e18 + 1);
assertLt(assetsOfProxy, 2_000_000e18); // The proxy owns less than it deposited
// convertToAssets(1) == 1 * (3_000_000e18 + 1 + 1) / (4 + 1)
// == 600_000e18
assertEq(assetsOfAttacker, 600_000e18);
assertLt(assetsOfAttacker, 1_000_000e18); // Attacker loses more than the victim
}

@lucas-manuel lucas-manuel merged commit 405be22 into dev Oct 29, 2025
4 checks passed
@lucas-manuel lucas-manuel deleted the sc-1183-test-maxSlippage-with-4626-donation-attack branch October 29, 2025 01:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants