Summary
In the GlobalConfigurationBranch::configureMarginCollateral
function, there is no check to prevent setting a zero value for the priceFeedHeartbeatSeconds
parameter, despite the tests explicitly stating the value shouldn't be 0.
Additionally, the MarginCollateralConfiguration::configure
function, which is called by configureMarginCollateral
, also lacks this crucial check. This dual oversight could allow the configuration of a price feed with no minimum update frequency, potentially leading to the use of stale price data.
Vulnerability Details
function configureMarginCollateral(
address collateralType,
uint128 depositCap,
uint120 loanToValue,
address priceFeed,
uint32 priceFeedHeartbeatSeconds
)
external
onlyOwner
{
@> if (decimals > Constants.SYSTEM_DECIMALS || priceFeed == address(0) || decimals == 0) {
revert Errors.InvalidMarginCollateralConfiguration(collateralType, decimals, priceFeed);
}
MarginCollateralConfiguration.configure(
collateralType, depositCap, loanToValue, decimals, priceFeed, priceFeedHeartbeatSeconds
);
}
And MarginCollateralConfiguration::configure
doesn't check for zero heartbeat duration either:
function configure(
address collateralType,
uint128 depositCap,
uint120 loanToValue,
uint8 decimals,
address priceFeed,
uint32 priceFeedHearbeatSeconds
)
internal
{
Data storage self = load(collateralType);
self.depositCap = depositCap;
self.loanToValue = loanToValue;
self.decimals = decimals;
self.priceFeed = priceFeed;
self.priceFeedHearbeatSeconds = priceFeedHearbeatSeconds;
}
Proof of Cocnept
Modify one of the contract inside the script/margin-collaterals
folder and change the XXX_PRICE_FEED_HEARBEAT_SECONDS
value to 0
Deploy the configurations of the Perpetual Engine by running script/02_ConfigurePerpsEngine.s.sol
as the owner
Observe that the transaction succeeds, setting a zero heartbeat duration
Proof of Code:
Insert the following test in the test/integration/perpetuals/global-configuration-branch/configureMarginCollateral/configureMarginCollateral.t.sol
file:
function testFuzz_DontRevertEvenIfPriceFeedHeartbeatSecondsIs0(
uint128 depositCap,
uint120 loanToValue
)
external
whenCollateralThatHasDecimals
whenCollateralDecimalsIsNotGreaterThanSystemDecimals
whenPriceFeedIsNotZero
{
changePrank({ msgSender: users.owner.account });
address priceFeed = address(0x20);
MockERC20 collateral = new MockERC20({
name: "Collateral",
symbol: "COL",
decimals_: Constants.SYSTEM_DECIMALS,
deployerBalance: 100_000_000e18
});
vm.expectEmit({ emitter: address(perpsEngine) });
emit GlobalConfigurationBranch.LogConfigureMarginCollateral(
users.owner.account,
address(collateral),
depositCap,
loanToValue,
Constants.SYSTEM_DECIMALS,
priceFeed,
0
);
perpsEngine.configureMarginCollateral(address(collateral), depositCap, loanToValue, priceFeed, 0);
}
Impact
A zero heartbeat duration could lead to the acceptance and use of stale price data indefinitely. This may result in:
Incorrect valuation of collateral assets
Potential creation of undercollateralized positions
Failure to trigger necessary liquidations
Exploitation of price discrepancies by malicious actors
While the function is only callable by the owner and typically used during deployment, and can be reverted with an upgrade, an accidental misconfiguration could have severe economic consequences for the protocol and its users.
Tools Used
Manual review
Recommendations
Change the error Errors::InvalidMarginCollateralConfiguration
in src/utils/Errors.sol
to add the priceFeedHeartbeatSeconds
variable:
- error InvalidMarginCollateralConfiguration(address collateralType, uint8 decimals, address priceFeed);
+ error InvalidMarginCollateralConfiguration(address collateralType, uint8 decimals, address priceFeed, uint32 priceFeedHearbeatSeconds);
Explicitly check for zero in the validation of GlobalConfigurationBranch::configureMarginCollateral
:
-if (decimals > Constants.SYSTEM_DECIMALS || priceFeed == address(0) || decimals == 0) {
- revert Errors.InvalidMarginCollateralConfiguration(collateralType, decimals, priceFeed);
-}
+if (decimals > Constants.SYSTEM_DECIMALS || priceFeed == address(0) || decimals == 0 || priceFeedHeartbeatSeconds == 0) {
+ revert Errors.InvalidMarginCollateralConfiguration(collateralType, decimals, priceFeed, priceFeedHeartbeatSeconds);
+}
// ...
catch {
- revert Errors.InvalidMarginCollateralConfiguration(collateralType, 0, priceFeed);
+ revert Errors.InvalidMarginCollateralConfiguration(collateralType, 0, priceFeed, priceFeedHeartbeatSeconds);
}
Note:
This error slipped through because the ConfigureMarginCollateral_Integration_Test::testFuzz_RevertWhen_PriceFeedHeartbeatSecondsIsZero
test should use a price feed address different from 0. However, the price feed is set to address(0)
, causing the test to revert but for the wrong reason.