Skip to content

Commit 1f64e0e

Browse files
authored
feat: Add DoS tests (SC-930) (#19)
* feat: add buidl and maple dos tests * refactor: inherit test contracts * feat: add morpho coverage * fix: formatting * fix: update buidl test
1 parent 36bb522 commit 1f64e0e

File tree

3 files changed

+278
-14
lines changed

3 files changed

+278
-14
lines changed

test/mainnet-fork/Attacks.t.sol

Lines changed: 273 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,44 @@ pragma solidity >=0.8.0;
33

44
import "./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+
}

test/mainnet-fork/Buidl.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ contract MainnetControllerDepositRedeemBUIDLE2ESuccessTests is MainnetController
233233

234234
IBuidlLike buidl = IBuidlLike(0x7712c34205737192402172409a8F7ccef8aA2AEc);
235235

236-
function setUp() override public {
236+
function setUp() public virtual override {
237237
super.setUp();
238238

239239
vm.startPrank(admin);

test/mainnet-fork/ForkTestBase.t.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ contract ForkTestBase is DssTest {
9494
address freezer = Ethereum.ALM_FREEZER;
9595
address relayer = Ethereum.ALM_RELAYER;
9696

97+
address backstopRelayer = makeAddr("backstopRelayer"); // TODO: Replace with real backstop
98+
9799
bytes32 CONTROLLER;
98100
bytes32 FREEZER;
99101
bytes32 RELAYER;
@@ -274,6 +276,8 @@ contract ForkTestBase is DssTest {
274276
mintRecipients
275277
);
276278

279+
mainnetController.grantRole(mainnetController.RELAYER(), backstopRelayer);
280+
277281
RateLimitData memory standardUsdsData = RateLimitData({
278282
maxAmount : 5_000_000e18,
279283
slope : uint256(1_000_000e18) / 4 hours

0 commit comments

Comments
 (0)