@@ -3,25 +3,44 @@ pragma solidity >=0.8.0;
33
44import "./ForkTestBase.t.sol " ;
55
6- contract CompromisedRelayerTests is ForkTestBase {
6+ import { MainnetControllerBUIDLTestBase } from "./Buidl.t.sol " ;
7+ import { MainnetControllerEthenaE2ETests } from "./Ethena.t.sol " ;
8+ import { MapleTestBase } from "./Maple.t.sol " ;
79
8- // Backstop relayer for this situation, larger multisig from governance
9- address backstopRelayer = makeAddr ("backstopRelayer " );
10+ import { Id, MarketParamsLib, MorphoTestBase, MarketAllocation } from "./MorphoAllocations.t.sol " ;
1011
11- bytes32 key ;
12+ import { IMapleTokenLike } from " ../../src/MainnetController.sol " ;
1213
13- function setUp () public override {
14- super .setUp ();
14+ interface IBuidlLike is IERC20 {
15+ function issueTokens (address to , uint256 amount ) external ;
16+ }
1517
16- key = mainnetController.LIMIT_SUSDE_COOLDOWN ();
18+ interface IMapleTokenExtended is IMapleTokenLike {
19+ function manager () external view returns (address );
20+ }
1721
18- vm.startPrank (SPARK_PROXY);
19- rateLimits.setRateLimitData (key, 5_000_000e18 , uint256 (1_000_000e18 ) / 4 hours);
20- mainnetController.grantRole (RELAYER, backstopRelayer);
21- vm.stopPrank ();
22- }
22+ interface IPermissionManagerLike {
23+ function admin () external view returns (address );
24+ function setLenderAllowlist (
25+ address poolManager_ ,
26+ address [] calldata lenders_ ,
27+ bool [] calldata booleans_
28+ ) external ;
29+ }
30+
31+ interface IPoolManagerLike {
32+ function withdrawalManager () external view returns (address );
33+ function poolDelegate () external view returns (address );
34+ }
2335
24- function test_compromisedRelayer_lockingFundsInEthenaSilo () external {
36+ interface IWhitelistLike {
37+ function addWallet (address account , string memory id ) external ;
38+ function registerInvestor (string memory id , string memory collisionHash ) external ;
39+ }
40+
41+ contract EthenaAttackTests is MainnetControllerEthenaE2ETests {
42+
43+ function test_attack_compromisedRelayer_lockingFundsInEthenaSilo () external {
2544 deal (address (susde), address (almProxy), 1_000_000e18 );
2645
2746 address silo = susde.silo ();
@@ -70,3 +89,244 @@ contract CompromisedRelayerTests is ForkTestBase {
7089 }
7190
7291}
92+
93+ contract MapleAttackTests is MapleTestBase {
94+
95+ function test_attack_compromisedRelayer_delayRequestMapleRedemption () external {
96+ deal (address (usdc), address (almProxy), 1_000_000e6 );
97+
98+ vm.prank (relayer);
99+ mainnetController.depositERC4626 (address (syrup), 1_000_000e6 );
100+
101+ // Malicious relayer delays the request for redemption for 1m
102+ // because new requests can't be fulfilled until the previous is fulfilled or cancelled
103+ vm.prank (relayer);
104+ mainnetController.requestMapleRedemption (address (syrup), 1 );
105+
106+ // Cannot process request
107+ vm.prank (relayer);
108+ vm.expectRevert ("WM:AS:IN_QUEUE " );
109+ mainnetController.requestMapleRedemption (address (syrup), 500_000e6 );
110+
111+ // Frezer can remove the compromised relayer and fallback to the governance relayer
112+ vm.prank (freezer);
113+ mainnetController.removeRelayer (relayer);
114+
115+ // Compromised relayer cannot perform attack anymore
116+ vm.prank (relayer);
117+ vm.expectRevert (abi.encodeWithSignature (
118+ "AccessControlUnauthorizedAccount(address,bytes32) " ,
119+ relayer,
120+ RELAYER
121+ ));
122+ mainnetController.requestMapleRedemption (address (syrup), 1 );
123+
124+ // Governance relayer can cancel and submit the real request
125+ vm.startPrank (backstopRelayer);
126+ mainnetController.cancelMapleRedemption (address (syrup), 1 );
127+ mainnetController.requestMapleRedemption (address (syrup), 500_000e6 );
128+ vm.stopPrank ();
129+ }
130+
131+ }
132+
133+ contract BUIDLAttackTests is MainnetControllerBUIDLTestBase {
134+
135+ address admin = 0xe01605f6b6dC593b7d2917F4a0940db2A625b09e ;
136+
137+ IBuidlLike buidl = IBuidlLike (0x7712c34205737192402172409a8F7ccef8aA2AEc );
138+ IWhitelistLike whitelist = IWhitelistLike (0x0Dac900f26DE70336f2320F7CcEDeE70fF6A1a5B );
139+
140+
141+ uint256 internal speedup = 10 ;
142+
143+ function setUp () public virtual override {
144+ super .setUp ();
145+
146+ vm.label (address (0x0A65a40a4B2F64D3445A628aBcFC8128625483A4 ), "LOCK_MANAGER " );
147+ vm.label (address (0x1dc378568cefD4596C5F9f9A14256D8250b56369 ), "COMPLIANCE_CONFIGURATION_SERVICE " );
148+ vm.label (address (0x07A1EBFb9a9A421249DDC71Bddb8860cc077E3a9 ), "COMPLIANCE_SERVICE " );
149+
150+ bytes32 depositKey = RateLimitHelpers.makeAssetDestinationKey (
151+ mainnetController.LIMIT_ASSET_TRANSFER (), address (usdc), address (buidlDeposit)
152+ );
153+
154+ bytes32 redeemKey = mainnetController.LIMIT_BUIDL_REDEEM_CIRCLE ();
155+
156+ vm.startPrank (Ethereum.SPARK_PROXY);
157+ rateLimits.setRateLimitData (depositKey, 2_000_000e6 , uint256 (2_000_000e6 ) / 1 days);
158+ rateLimits.setRateLimitData (redeemKey, 2_000_000e6 , uint256 (2_000_000e6 ) / 1 days);
159+ vm.stopPrank ();
160+
161+ vm.startPrank (admin);
162+ whitelist.registerInvestor ("spark-almProxy " , "collisionHash " );
163+ whitelist.addWallet (address (almProxy), "spark-almProxy " );
164+ vm.stopPrank ();
165+
166+ deal (address (usdc), address (almProxy), 2_000_000e6 );
167+
168+ // Step 1: Deposit into BUIDL
169+ vm.prank (relayer);
170+ mainnetController.transferAsset (address (usdc), buidlDeposit, 1_000_000e6 );
171+
172+ // Step 2: BUIDL gets minted into proxy
173+ assertEq (buidl.balanceOf (address (almProxy)), 0 );
174+
175+ vm.prank (admin);
176+ buidl.issueTokens (address (almProxy), 1_000_000e6 );
177+
178+ assertEq (buidl.balanceOf (address (almProxy)), 1_000_000e6 );
179+
180+ // Step 3: Malicious relayer spams transfers & redemptions
181+ // Every iteration uses a cold `sload` so need at most 30e6 / 2100 = 14_286
182+ // The iterations gas cost is roughly linear per iteration, to speed up the test we scale
183+ // down both iterations and gas used.
184+ for (uint256 i; i < 10_000 / speedup; i++ ) {
185+ vm.prank (relayer);
186+ mainnetController.transferAsset (address (usdc), buidlDeposit, 1e6 );
187+ vm.prank (admin);
188+ buidl.issueTokens (address (almProxy), 1e6 );
189+ }
190+
191+ // Skip time lock
192+ skip (24 hours);
193+ }
194+
195+ // Run test in its own transaction so the sloads are cold
196+ function test_attack_issuanceDos () public {
197+ // Step 4: Redeem non-malicious BUIDL after timelock is passed
198+ vm.startPrank (relayer);
199+ vm.expectRevert ("SafeERC20: low-level call failed " );
200+ mainnetController.redeemBUIDLCircleFacility {gas: 30e6 / speedup}(1_000_000e6 );
201+ vm.stopPrank ();
202+ }
203+ }
204+
205+ contract MorphoAttackTests is MorphoTestBase {
206+
207+ function test_attack_compromisedRelayer_setSupplyQueue () external {
208+ Id[] memory supplyQueueUSDC = new Id [](2 );
209+ supplyQueueUSDC[0 ] = MarketParamsLib.id (market1);
210+ supplyQueueUSDC[1 ] = MarketParamsLib.id (market2);
211+
212+ // No supply queue to start, but caps are above zero
213+ assertEq (morphoVault.supplyQueueLength (), 0 );
214+
215+ vm.prank (relayer);
216+ mainnetController.setSupplyQueueMorpho (address (morphoVault), supplyQueueUSDC);
217+
218+ assertEq (morphoVault.supplyQueueLength (), 2 );
219+
220+ assertEq (Id.unwrap (morphoVault.supplyQueue (0 )), Id.unwrap (MarketParamsLib.id (market1)));
221+ assertEq (Id.unwrap (morphoVault.supplyQueue (1 )), Id.unwrap (MarketParamsLib.id (market2)));
222+
223+ vm.startPrank (Ethereum.SPARK_PROXY);
224+ rateLimits.setRateLimitData (
225+ RateLimitHelpers.makeAssetKey (
226+ mainnetController.LIMIT_4626_DEPOSIT (),
227+ address (morphoVault)
228+ ),
229+ 25_000_000e18 ,
230+ uint256 (5_000_000e18 ) / 1 days
231+ );
232+ vm.stopPrank ();
233+
234+ deal (address (dai), address (almProxy), 1_000_000e18 );
235+
236+ // Able to deposit
237+ vm.prank (relayer);
238+ mainnetController.depositERC4626 (address (morphoVault), 500_000e18 );
239+
240+ Id[] memory emptySupplyQueue = new Id [](0 );
241+
242+ // Malicious relayer empties the supply queue
243+ vm.prank (relayer);
244+ mainnetController.setSupplyQueueMorpho (address (morphoVault), emptySupplyQueue);
245+
246+ // DOS deposits into morpho vault
247+ vm.prank (relayer);
248+ vm.expectRevert (abi.encodeWithSignature ("AllCapsReached() " ));
249+ mainnetController.depositERC4626 (address (morphoVault), 500_000e18 );
250+
251+ // Frezer can remove the compromised relayer and fallback to the governance relayer
252+ vm.prank (freezer);
253+ mainnetController.removeRelayer (relayer);
254+
255+ // Compromised relayer can no longer perform the attack
256+ vm.prank (relayer);
257+ vm.expectRevert (abi.encodeWithSignature (
258+ "AccessControlUnauthorizedAccount(address,bytes32) " ,
259+ relayer,
260+ RELAYER
261+ ));
262+ mainnetController.setSupplyQueueMorpho (address (morphoVault), emptySupplyQueue);
263+
264+ // Backstop relayer can restore original supply queue
265+ vm.prank (backstopRelayer);
266+ mainnetController.setSupplyQueueMorpho (address (morphoVault), supplyQueueUSDC);
267+
268+ // Deposit works again
269+ vm.prank (backstopRelayer);
270+ mainnetController.depositERC4626 (address (morphoVault), 500_000e18 );
271+ }
272+
273+ function test_attack_compromisedRelayer_reallocateMorpho () public {
274+ vm.startPrank (Ethereum.SPARK_PROXY);
275+ rateLimits.setRateLimitData (
276+ RateLimitHelpers.makeAssetKey (
277+ mainnetController.LIMIT_4626_DEPOSIT (),
278+ address (morphoVault)
279+ ),
280+ 25_000_000e6 ,
281+ uint256 (5_000_000e6 ) / 1 days
282+ );
283+ vm.stopPrank ();
284+
285+ uint256 market1Position = positionAssets (market1);
286+ uint256 market2Position = positionAssets (market2);
287+
288+ // Move 1m from market1 to market2
289+ MarketAllocation[] memory reallocations = new MarketAllocation [](2 );
290+ reallocations[0 ] = MarketAllocation ({
291+ marketParams : market1,
292+ assets : market1Position - 1_000_000e18
293+ });
294+ reallocations[1 ] = MarketAllocation ({
295+ marketParams : market2,
296+ assets : type (uint256 ).max
297+ });
298+
299+ // Malicious relayer reallocates freely
300+ vm.prank (relayer);
301+ mainnetController.reallocateMorpho (address (morphoVault), reallocations);
302+
303+ // Frezer can remove the compromised relayer and fallback to the governance relayer
304+ vm.prank (freezer);
305+ mainnetController.removeRelayer (relayer);
306+
307+ // Compromised relayer can no longer perform the attack
308+ vm.prank (relayer);
309+ vm.expectRevert (abi.encodeWithSignature (
310+ "AccessControlUnauthorizedAccount(address,bytes32) " ,
311+ relayer,
312+ RELAYER
313+ ));
314+ mainnetController.reallocateMorpho (address (morphoVault), reallocations);
315+
316+ market1Position = positionAssets (market1);
317+ market2Position = positionAssets (market2);
318+
319+ // Backstop relayer can restore original allocations
320+ reallocations[0 ] = MarketAllocation ({
321+ marketParams : market2,
322+ assets : market2Position - 1_000_000e18
323+ });
324+ reallocations[1 ] = MarketAllocation ({
325+ marketParams : market1,
326+ assets : type (uint256 ).max
327+ });
328+ vm.prank (backstopRelayer);
329+ mainnetController.reallocateMorpho (address (morphoVault), reallocations);
330+ }
331+
332+ }
0 commit comments