Skip to content

Commit bfde12e

Browse files
authored
Merge pull request #642 from VenusProtocol/feat/quantstamp-audit-mitigations
[VPD-226] Flash Loan Quantstamp Audit Mitigations
2 parents 7fd685c + ec80cec commit bfde12e

File tree

8 files changed

+125
-1
lines changed

8 files changed

+125
-1
lines changed

contracts/Comptroller/ComptrollerInterface.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ interface ComptrollerInterface {
207207
uint96 marketPoolId,
208208
bool isBorrowAllowed
209209
);
210+
211+
function isFlashLoanPaused() external view returns (bool);
210212
}
211213

212214
interface IVAIVault {

contracts/Comptroller/ComptrollerStorage.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,7 @@ contract ComptrollerV17Storage is ComptrollerV16Storage {
324324
contract ComptrollerV18Storage is ComptrollerV17Storage {
325325
/// @notice Mapping of accounts authorized to execute flash loans
326326
mapping(address => bool) public authorizedFlashLoan;
327+
328+
/// @notice Whether flash loans are paused system-wide
329+
bool public flashLoanPaused;
327330
}

contracts/Comptroller/Diamond/facets/FlashLoanFacet.sol

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { ReentrancyGuardTransient } from "../../../Utils/ReentrancyGuardTransien
1818
* The contract supports protocol fee collection and integrates with the Venus lending protocol.
1919
*/
2020
contract FlashLoanFacet is IFlashLoanFacet, FacetBase, ReentrancyGuardTransient {
21+
/// @notice Maximum number of assets that can be flash loaned in a single transaction
22+
uint256 public constant MAX_FLASHLOAN_ASSETS = 200;
23+
2124
/// @notice Emitted when the flash loan is successfully executed
2225
event FlashLoanExecuted(address indexed receiver, VToken[] assets, uint256[] amounts);
2326

@@ -39,7 +42,9 @@ contract FlashLoanFacet is IFlashLoanFacet, FacetBase, ReentrancyGuardTransient
3942
* @param underlyingAmounts The amounts of each underlying assets to be loaned.
4043
* @param param The bytes passed in the executeOperation call.
4144
* @custom:error FlashLoanNotEnabled is thrown if the flash loan is not enabled for the asset.
45+
* @custom:error FlashLoanPausedSystemWide is thrown if flash loans are paused system-wide.
4246
* @custom:error InvalidAmount is thrown if the requested amount is zero.
47+
* @custom:error TooManyAssetsRequested is thrown if the number of requested assets exceeds the maximum limit.
4348
* @custom:error NoAssetsRequested is thrown if no assets are requested for the flash loan.
4449
* @custom:error InvalidFlashLoanParams is thrown if the flash loan params are invalid.
4550
* @custom:error MarketNotListed is thrown if the specified vToken market is not listed.
@@ -54,13 +59,23 @@ contract FlashLoanFacet is IFlashLoanFacet, FacetBase, ReentrancyGuardTransient
5459
uint256[] memory underlyingAmounts,
5560
bytes memory param
5661
) external nonReentrant {
62+
if (flashLoanPaused) {
63+
revert FlashLoanPausedSystemWide();
64+
}
65+
66+
ensureNonzeroAddress(onBehalf);
67+
5768
uint256 len = vTokens.length;
5869
Market storage market;
5970

6071
// vTokens array must not be empty
6172
if (len == 0) {
6273
revert NoAssetsRequested();
6374
}
75+
// Add maximum array length check to prevent gas limit issues
76+
if (len > MAX_FLASHLOAN_ASSETS) {
77+
revert TooManyAssetsRequested(len, MAX_FLASHLOAN_ASSETS);
78+
}
6479

6580
// All arrays must have the same length and not be zero
6681
if (len != underlyingAmounts.length) {

contracts/Comptroller/Diamond/facets/SetterFacet.sol

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ contract SetterFacet is ISetterFacet, FacetBase {
125125
/// @notice Emitted when pool Fallback status is updated
126126
event PoolFallbackStatusUpdated(uint96 indexed poolId, bool oldStatus, bool newStatus);
127127

128+
/// @notice Emitted when flash loan pause status changes
129+
event FlashLoanPauseChanged(bool oldPaused, bool newPaused);
130+
128131
/**
129132
* @notice Compare two addresses to ensure they are different
130133
* @param oldAddress The original address to compare
@@ -640,6 +643,24 @@ contract SetterFacet is ISetterFacet, FacetBase {
640643
emit IsAccountFlashLoanWhitelisted(account, isWhiteListed);
641644
}
642645

646+
/**
647+
* @notice Pause or unpause flash loans system-wide
648+
* @param paused True to pause flash loans, false to unpause
649+
* @custom:access Only Governance
650+
* @custom:event Emits FlashLoanPauseChanged event
651+
*/
652+
function setFlashLoanPaused(bool paused) external {
653+
ensureAllowed("setFlashLoanPaused(bool)");
654+
655+
// Check if value is actually changing
656+
if (flashLoanPaused == paused) {
657+
return; // No change needed
658+
}
659+
660+
emit FlashLoanPauseChanged(flashLoanPaused, paused);
661+
flashLoanPaused = paused;
662+
}
663+
643664
/**
644665
* @notice Updates the label for a specific pool (excluding the Core Pool)
645666
* @param poolId ID of the pool to update

contracts/Comptroller/Diamond/interfaces/ISetterFacet.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,6 @@ interface ISetterFacet {
103103
function setPoolLabel(uint96 poolId, string calldata newLabel) external;
104104

105105
function setAllowCorePoolFallback(uint96 poolId, bool allowFallback) external;
106+
107+
function setFlashLoanPaused(bool paused) external;
106108
}

contracts/Utils/ErrorReporter.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ contract ComptrollerErrorReporter {
7676
/// @notice Thrown when the vToken market is not listed
7777
error MarketNotListed(address vToken);
7878

79+
/// @notice Thrown when flash loans are paused system-wide
80+
error FlashLoanPausedSystemWide();
81+
82+
/// @notice Thrown when too many assets are requested in a single flash loan
83+
error TooManyAssetsRequested(uint256 requested, uint256 maximum);
84+
7985
enum Error {
8086
NO_ERROR,
8187
UNAUTHORIZED,

contracts/test/ComptrollerMock.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import "../Comptroller/Diamond/facets/MarketFacet.sol";
44
import "../Comptroller/Diamond/facets/PolicyFacet.sol";
55
import "../Comptroller/Diamond/facets/RewardFacet.sol";
66
import "../Comptroller/Diamond/facets/SetterFacet.sol";
7+
import "../Comptroller/Diamond/facets/FlashLoanFacet.sol";
78
import "../Comptroller/Unitroller.sol";
89

910
// This contract contains all methods of Comptroller implementation in different facets at one place for testing purpose
1011
// This contract does not have diamond functionality(i.e delegate call to facets methods)
11-
contract ComptrollerMock is MarketFacet, PolicyFacet, RewardFacet, SetterFacet {
12+
contract ComptrollerMock is MarketFacet, PolicyFacet, RewardFacet, SetterFacet, FlashLoanFacet {
1213
constructor() {
1314
admin = msg.sender;
1415
}

tests/hardhat/Comptroller/Diamond/flashLoan.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const flashLoanTestFixture = async (): Promise<FlashLoanContractsFixture> => {
6262
const result = await deployDiamond("");
6363
const unitroller = result.unitroller;
6464
const comptroller = await ethers.getContractAt("ComptrollerMock", unitroller.address);
65+
6566
const comptrollerLens = await ComptrollerLensFactory.deploy();
6667
await comptroller._setAccessControl(accessControlManager.address);
6768
await comptroller._setComptrollerLens(comptrollerLens.address);
@@ -185,6 +186,26 @@ describe("FlashLoan", async () => {
185186
await mockReceiverContract.deployed();
186187
});
187188

189+
it("Should revert flash loan when paused system-wide", async () => {
190+
// Pause flash loans system-wide
191+
await comptroller.setFlashLoanPaused(true);
192+
193+
// Verify pause status
194+
expect(await comptroller.flashLoanPaused()).to.be.true;
195+
196+
// Should revert when paused
197+
await expect(
198+
mockReceiverContract
199+
.connect(alice)
200+
.requestFlashLoan(
201+
[vTokenA.address, vTokenB.address],
202+
[flashLoanAmount1, flashLoanAmount2],
203+
mockReceiverContract.address,
204+
"0x",
205+
),
206+
).to.be.revertedWithCustomError(comptroller, "FlashLoanPausedSystemWide");
207+
});
208+
188209
it("Should revert if flashLoan is not enabled", async () => {
189210
expect(await vTokenA.isFlashLoanEnabled()).to.be.false;
190211
expect(await vTokenB.isFlashLoanEnabled()).to.be.false;
@@ -210,6 +231,33 @@ describe("FlashLoan", async () => {
210231
);
211232
});
212233

234+
it("Should revert when onBehalf is address zero", async () => {
235+
await expect(
236+
comptroller.executeFlashLoan(
237+
ethers.constants.AddressZero, // ❌ Zero address
238+
mockReceiverContract.address,
239+
[vTokenA.address],
240+
[flashLoanAmount1],
241+
"0x",
242+
),
243+
).to.be.revertedWith("can't be zero address");
244+
});
245+
246+
it("Should revert when too many assets are requested", async () => {
247+
await vTokenA.setFlashLoanEnabled(true);
248+
// Create array with more assets than MAX_FLASHLOAN_ASSETS
249+
const tooManyAssets = new Array(201).fill(vTokenA.address); // Assuming MAX is 200
250+
const tooManyAmounts = new Array(201).fill(flashLoanAmount1);
251+
252+
await comptroller.setWhiteListFlashLoanAccount(alice.address, true);
253+
254+
await expect(
255+
comptroller.executeFlashLoan(alice.address, mockReceiverContract.address, tooManyAssets, tooManyAmounts, "0x"),
256+
)
257+
.to.be.revertedWithCustomError(comptroller, "TooManyAssetsRequested")
258+
.withArgs(201, 200);
259+
});
260+
213261
it("Should revert if the market is not listed", async () => {
214262
await vTokenC.setFlashLoanEnabled(true);
215263
expect(await vTokenC.isFlashLoanEnabled()).to.be.true;
@@ -314,6 +362,32 @@ describe("FlashLoan", async () => {
314362
).to.be.revertedWithCustomError(comptroller, "ExecuteFlashLoanFailed");
315363
});
316364

365+
it("Should emit FlashLoanPauseChanged event when pause status changes", async () => {
366+
// Test pausing
367+
await expect(comptroller.setFlashLoanPaused(true))
368+
.to.emit(comptroller, "FlashLoanPauseChanged")
369+
.withArgs(false, true);
370+
371+
// Test unpausing
372+
await expect(comptroller.setFlashLoanPaused(false))
373+
.to.emit(comptroller, "FlashLoanPauseChanged")
374+
.withArgs(true, false);
375+
});
376+
377+
it("Should not emit event when setting same pause status", async () => {
378+
// Flash loans are not paused by default
379+
expect(await comptroller.flashLoanPaused()).to.be.false;
380+
381+
// Setting to false again should not emit event (no change)
382+
await expect(comptroller.setFlashLoanPaused(false)).to.not.emit(comptroller, "FlashLoanPauseChanged");
383+
384+
// Pause first
385+
await comptroller.setFlashLoanPaused(true);
386+
387+
// Setting to true again should not emit event (no change)
388+
await expect(comptroller.setFlashLoanPaused(true)).to.not.emit(comptroller, "FlashLoanPauseChanged");
389+
});
390+
317391
it("User has not supplied in venus - Should not create debt position if receiver repays full amount + fee", async () => {
318392
await vTokenA.setFlashLoanEnabled(true);
319393
await vTokenB.setFlashLoanEnabled(true);

0 commit comments

Comments
 (0)