DeFiFoundry
60,000 USDC
View results
Submission Details
Severity: low
Invalid

Low-Risk-Report by K42

Low Risk Report for Zaros by K42

Low Severity Issues

Issue ID Description Location Link Impact Recommended Mitigation Steps
L-01 No use of two-step ownership transfer in BaseKeeper BaseKeeper.sol Low Use a two-step ownership transfer process
L-02 Unbounded iterations in checkLiquidatableAccounts LiquidationKeeper.sol Low Maximum iteration limit
L-03 No pause mechanism in MarketOrderKeeper MarketOrderKeeper.sol Low For safety and prevention, use pausable pattern for critical functions here
L-04 Centralization risk in configureSystemParameters GlobalConfigurationBranch.sol Low Add in a time-lock or multi-sig mechanism for critical parameter changes
L-05 Risk of DOS in liquidateAccounts due to gas limit LiquidationBranch.sol Low Batched liquidations
L-06 Lack of minimum order size check beyond zero in createMarketOrder OrderBranch.sol Low Add in a minimum order size check
L-07 Risk of precision loss in getMarkPrice calculation PerpMarketBranch.sol Low Use higher precision intermediates in current calculations
L-08 No signature malleability protection in fillOffchainOrders SettlementBranch.sol Low EIP-2098 for signature compaction and malleability protection
L-09 Risk of front-running in createTradingAccount TradingAccountBranch.sol Low Better use of commit-reveal scheme for account creation
L-10 Not enough event emission for critical configuration changes here GlobalConfiguration.sol Low Emit events for more of the critical configuration changes
L-11 Vector for price manipulation in getPrice MarginCollateralConfiguration.sol Low TWAP or median price mechanism
L-12 No slippage protection in getMarkPrice PerpMarket.sol Low Slippage protection parameters
L-13 Risk of unexpected results with extremely large position sizes or prices in getNotionalValue Position.sol Low More precise checks for extremely large values
L-14 Centralization risk in verifyDataStreamsReport SettlementConfiguration.sol Low Multiple data sources or fallback mechanism
L-15 No gas limit for external calls in deductAccountMargin TradingAccount.sol Low Gas limits for external calls
L-16 Vector of reentrancy in initializeRootUpgrade RootUpgrade.sol Low Reentrancy guard for initialization process to be extra safe

Detailed Breakdown of All Low Severity Issues:

L-01: No use of two-step ownership transfer in BaseKeeper

Proof of Concept

In BaseKeeper.sol, the ownership transfer is done in a single step, which can be risky if the new owner address is incorrect:

function __BaseKeeper_init(address owner) internal onlyInitializing {
if (owner == address(0)) {
revert Errors.ZeroInput("owner");
}
__Ownable_init(owner);
}
Impact

If the owner address is set incorrectly, by accident the contract could become permanently ownerless, locking critical functionalities.

Recommended Mitigation Steps

Use the method of a two-step ownership transfer process:

address private _pendingOwner;
function transferOwnership(address newOwner) public virtual onlyOwner {
_pendingOwner = newOwner;
emit OwnershipTransferInitiated(owner(), newOwner);
}
function acceptOwnership() public virtual {
require(msg.sender == _pendingOwner, "Only pending owner can accept ownership");
_transferOwnership(_pendingOwner);
_pendingOwner = address(0);
}

L-02: Unbounded loop in checkLiquidatableAccounts

Proof of Concept

In LiquidationKeeper.sol, the checkLiquidatableAccounts function contains unbounded loop:

for (uint256 i = lowerBound; i < upperBound; i++) {
if (i >= cachedAccountsIdsWithActivePositionsLength) break;
// same
}
Impact

If the difference between upperBound and lowerBound is large, the function could consume excessive gas or even reach the block gas limit, preventing liquidations.

Recommended Mitigation Steps

Use here a maximum iteration limit to stop this:

uint256 constant MAX_ITERATIONS = 100;
for (uint256 i = lowerBound; i < upperBound && i < lowerBound + MAX_ITERATIONS; i++) {
if (i >= cachedAccountsIdsWithActivePositionsLength) break;
// same here
}

L-03: No use of pause mechanism in MarketOrderKeeper

Proof of Concept

MarketOrderKeeper.sol does not use pause mechanism, which could be crucial in emergency, un-predictable situations, given the context of the contract and its dynamic uses:

contract MarketOrderKeeper is ILogAutomation, IStreamsLookupCompatible, BaseKeeper {
// No pause mechanism used at all
}
Impact

In case of real time vulnerabilities, black hats do the wildest things, and/or in case of market anomalies, there's no way to quickly pause the system to prevent losses.

Recommended Mitigation Steps

Use a pausable pattern to be safe:

import "@openzeppelin/contracts/utils/Pausable.sol";
contract MarketOrderKeeper is ILogAutomation, IStreamsLookupCompatible, BaseKeeper, Pausable {
function fillMarketOrder(
uint128 tradingAccountId,
uint128 marketId,
bytes calldata priceData
)
external
onlyMarketOrderKeeper(marketId)
whenNotPaused
{
// same
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
}

L-04: Centralization risk in configureSystemParameters

Proof of Concept

In GlobalConfigurationBranch.sol, the configureSystemParameters function allows immediate changes to critical system parameters:

function configureSystemParameters(
uint128 maxPositionsPerAccount,
uint128 marketOrderMinLifetime,
uint128 liquidationFeeUsdX18,
address marginCollateralRecipient,
address orderFeeRecipient,
address settlementFeeRecipient,
address liquidationFeeRecipient,
uint256 maxVerificationDelay
)
external
onlyOwner
{
// current is same
}
Impact

Immediate changes to critical parameters by a single account poses ab obvious centralization risk and could disrupt the system if misused.

Recommended Mitigation Steps

Use a time-lock or multi-sig mechanism for critical parameter changes, make it more decentralised:

struct PendingConfig {
uint128 maxPositionsPerAccount;
uint128 marketOrderMinLifetime;
uint128 liquidationFeeUsdX18;
address marginCollateralRecipient;
address orderFeeRecipient;
address settlementFeeRecipient;
address liquidationFeeRecipient;
uint256 maxVerificationDelay;
uint256 effectiveTime;
}
PendingConfig public pendingConfig;
uint256 public constant CONFIG_DELAY = 2 days;
function proposeSystemParameters(
uint128 maxPositionsPerAccount,
uint128 marketOrderMinLifetime,
uint128 liquidationFeeUsdX18,
address marginCollateralRecipient,
address orderFeeRecipient,
address settlementFeeRecipient,
address liquidationFeeRecipient,
uint256 maxVerificationDelay
)
external
onlyOwner
{
pendingConfig = PendingConfig({
maxPositionsPerAccount: maxPositionsPerAccount,
marketOrderMinLifetime: marketOrderMinLifetime,
liquidationFeeUsdX18: liquidationFeeUsdX18,
marginCollateralRecipient: marginCollateralRecipient,
orderFeeRecipient: orderFeeRecipient,
settlementFeeRecipient: settlementFeeRecipient,
liquidationFeeRecipient: liquidationFeeRecipient,
maxVerificationDelay: maxVerificationDelay,
effectiveTime: block.timestamp + CONFIG_DELAY
});
}
function applySystemParameters() external onlyOwner {
require(block.timestamp >= pendingConfig.effectiveTime, "Config not ready");
// Apply pending config
// Same
delete pendingConfig;
}

L-05: Risk of DOS in liquidateAccounts due to gas limit

Proof of Concept

In LiquidationBranch.sol, the liquidateAccounts function processes all provided accounts in a single transaction:

function liquidateAccounts(uint128[] calldata accountsIds) external {
// same
for (uint256 i; i < accountsIds.length; i++) {
// same
}
}
Impact

If a large number of accounts need liquidation, the function might hit the block gas limit, preventing any liquidations from occurring.

Recommended Mitigation Steps

Use batched liquidations to better this:

uint256 constant MAX_LIQUIDATIONS_PER_BATCH = 10;
function liquidateAccounts(uint128[] calldata accountsIds) external {
// current is same
uint256 endIndex = Math.min(accountsIds.length, MAX_LIQUIDATIONS_PER_BATCH);
for (uint256 i; i < endIndex; i++) {
// same
}
}

L-06: No minimum order size check beyond zero in createMarketOrder

Proof of Concept

In OrderBranch.sol, the createMarketOrder function only checks for zero size, but does not enforce a minimum order size beyond that:

function createMarketOrder(CreateMarketOrderParams calldata params) external {
// same as current
if (params.sizeDelta == 0) {
revert Errors.ZeroInput("sizeDelta");
}
// same
}
Impact

While zero-sized orders are prevented currently, very small orders could still be created. This could then be used to spam the system or manipulate market data with minimal economic commitmentz.

Recommended Mitigation Steps

Use a minimum order size check:

// Add this to Constants.sol
uint128 internal constant MIN_ORDER_SIZE = 1e6; // Adjust as needed
import { Constants } from "@zaros/utils/Constants.sol";
function createMarketOrder(CreateMarketOrderParams calldata params) external {
// same
if (params.sizeDelta == 0 || Math.abs(params.sizeDelta) < Constants.MIN_ORDER_SIZE) {
revert Errors.InvalidOrderSize(params.sizeDelta);
}
// same
}

L-07: Risk of precision loss in getMarkPrice calculation

Proof of Concept

In PerpMarketBranch.sol, the getMarkPrice function performs division that could lead to precision loss:

function getMarkPrice(uint128 marketId, uint256 indexPrice, int256 skewDelta) external view returns (UD60x18) {
PerpMarket.Data storage perpMarket = PerpMarket.load(marketId);
return perpMarket.getMarkPrice(sd59x18(skewDelta), ud60x18(indexPrice));
}
Impact

Precision loss in price calculations could lead to small or large inaccuracies in order execution and position valuations.

Recommended Mitigation Steps

Use higher precision intermediates in calculations:

function getMarkPrice(uint128 marketId, uint256 indexPrice, int256 skewDelta) external view returns (UD60x18) {
PerpMarket.Data storage perpMarket = PerpMarket.load(marketId);
return perpMarket.getMarkPrice(sd59x18(skewDelta), ud60x18(indexPrice).scale(1e9)); // Scale up for higher precision
}

L-08: No signature malleability protection in fillOffchainOrders

Recommended Mitigation Steps

Use EIP-2098 for signature compaction and malleability protection:

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
function fillOffchainOrders(
uint128 marketId,
OffchainOrder.Data[] calldata offchainOrders,
bytes calldata priceData
)
external
onlyOffchainOrdersKeeper(marketId)
{
// current is same
for (uint256 i; i < offchainOrders.length; i++) {
ctx.offchainOrder = offchainOrders[i];
bytes32 digest = _hashTypedDataV4(ctx.structHash);
ctx.signer = ECDSA.recover(digest, ctx.offchainOrder.signature);
// current is same
}
}

Above method uses a single signature field instead of separate v, r, and s components, reducing gas and protecting against signature malleability.


L-09: Risk of front-running in createTradingAccount

Proof of Concept

In TradingAccountBranch.sol, the createTradingAccount function is susceptible to front-running:

function createTradingAccount(
bytes memory referralCode,
bool isCustomReferralCode
)
public
virtual
returns (uint128 tradingAccountId)
{
// current is same
tradingAccountId = ++globalConfiguration.nextAccountId;
// current is same
}
Impact

An attacker could game it and front-run account creation transactions to obtain specific account IDs, which might be desirable for some.

Recommended Mitigation Steps

Use the commit-reveal pattern for account creation:

mapping(address => bytes32) public accountCommitments;
uint256 public constant COMMITMENT_TIMEOUT = 1 hours;
function commitToAccountCreation(bytes32 commitment) external {
accountCommitments[msg.sender] = commitment;
}
function createTradingAccount(
bytes memory referralCode,
bool isCustomReferralCode,
bytes32 salt
)
public
virtual
returns (uint128 tradingAccountId)
{
bytes32 commitment = keccak256(abi.encodePacked(msg.sender, referralCode, isCustomReferralCode, salt));
require(accountCommitments[msg.sender] == commitment, "Invalid commitment");
require(block.timestamp <= accountCommitments[msg.sender] + COMMITMENT_TIMEOUT, "Commitment expired");
delete accountCommitments[msg.sender];
// current is same
}

Above method requires users to commit to their account creation parameters before actually creating the account, making front-running much more difficult.


L-10: Not enough event emission for critical configuration changes

Proof of Concept

In GlobalConfiguration.sol, many configuration changes don't emit events:

function removeCollateralFromLiquidationPriority(Data storage self, address collateralType) internal {
// same
// so no event emission
}
Impact

No events logged for critical changes makes it difficult for off-chain systems to track and react to focused contract state changes.

Recommended Mitigation Steps

Use more events for all critical configuration changes:

event CollateralRemovedFromLiquidationPriority(address indexed collateralType);
function removeCollateralFromLiquidationPriority(Data storage self, address collateralType) internal {
// current same
emit CollateralRemovedFromLiquidationPriority(collateralType);
}

L-11: Risk of price manipulation in getPrice

Proof of Concept

In MarginCollateralConfiguration.sol, the getPrice function relies on a single price feed:

function getPrice(Data storage self) internal view returns (UD60x18 price) {
address priceFeed = self.priceFeed;
// same as current
price = ChainlinkUtil.getPrice(
IAggregatorV3(priceFeed),
priceFeedHearbeatSeconds,
IAggregatorV3(sequencerUptimeFeed)
);
}
Impact

Reliance on a one single price feed could make the system vulnerable to price manipulations or temporary price feed failures in real time.

Recommended Mitigation Steps

A TWAP or median price mechanism using multiple price feeds, for cleaner and more precise price data:

function getPrice(Data storage self) internal view returns (UD60x18 price) {
address[] memory priceFeeds = self.priceFeeds;
require(priceFeeds.length >= 3, "Insufficient price feeds");
UD60x18[] memory prices = new UD60x18[](priceFeeds.length);
for (uint i = 0; i < priceFeeds.length; i++) {
prices[i] = ChainlinkUtil.getPrice(
IAggregatorV3(priceFeeds[i]),
self.priceFeedHearbeatSeconds,
IAggregatorV3(self.sequencerUptimeFeed)
);
}
return MedianLib.median(prices);
}

L-12: No slippage protection in getMarkPrice

Proof of Concept

In PerpMarket.sol, the getMarkPrice function doesn't include slippage protection:

function getMarkPrice(
Data storage self,
SD59x18 skewDelta,
UD60x18 indexPriceX18
)
internal
view
returns (UD60x18 markPrice)
{
// current is same
return markPrice;
}
Impact

Large trades could significantly move the mark price, therefore enabling unfavourable executions for users.

Recommended Mitigation Steps

Put in slippage protection parameters:

function getMarkPrice(
Data storage self,
SD59x18 skewDelta,
UD60x18 indexPriceX18,
UD60x18 maxSlippage
)
internal
view
returns (UD60x18 markPrice)
{
// current is same
UD60x18 maxAllowedPrice = indexPriceX18.mul(ud60x18(1e18).add(maxSlippage));
UD60x18 minAllowedPrice = indexPriceX18.mul(ud60x18(1e18).sub(maxSlippage));
require(markPrice.gte(minAllowedPrice) && markPrice.lte(maxAllowedPrice), "Price slippage too high");
return markPrice;
}

L-13: Risk of unexpected results with extremely large position sizes or prices in getNotionalValue

Proof of Concept

In Position.sol, the getNotionalValue function performs multiplication that could lead to unexpected results with extremely large values:

function getNotionalValue(Data storage self, UD60x18 price) internal view returns (UD60x18) {
return sd59x18(self.size).abs().intoUD60x18().mul(price);
}
Impact

Current solidity version in use prevents overflows, but extremely large position sizes or prices could still lead to wrong results due to the limitations of fixed-point arithmetic.

Recommended Mitigation Steps

Refine checks for extremely large values:

// Use hese to Constants.sol
uint256 internal constant MAX_POSITION_SIZE = 1e27; // Adjust
uint256 internal constant MAX_PRICE = 1e27; // Adjust
import { Constants } from "@zaros/utils/Constants.sol";
function getNotionalValue(Data storage self, UD60x18 price) internal view returns (UD60x18) {
UD60x18 absSize = sd59x18(self.size).abs().intoUD60x18();
if (absSize.gt(ud60x18(Constants.MAX_POSITION_SIZE)) || price.gt(ud60x18(Constants.MAX_PRICE))) {
revert Errors.ExcessivePositionSize();
}
return absSize.mul(price);
}

L-14: Centralization risk in verifyDataStreamsReport

Proof of Concept

In SettlementConfiguration.sol, the verifyDataStreamsReport function relies on a single data source:

function verifyDataStreamsReport(
DataStreamsStrategy memory dataStreamsStrategy,
bytes memory signedReport
)
internal
returns (bytes memory verifiedReportData)
{
IVerifierProxy chainlinkVerifier = dataStreamsStrategy.chainlinkVerifier;
// current is same
}
Impact

Reliance on a single data source could lead to system-wide issues if that source fails or is compromised.

Recommended Mitigation Steps

Use multiple data sources or a fallback mechanism:

function verifyDataStreamsReport(
DataStreamsStrategy memory dataStreamsStrategy,
bytes memory signedReport
)
internal
returns (bytes memory verifiedReportData)
{
for (uint i = 0; i < dataStreamsStrategy.verifiers.length; i++) {
try dataStreamsStrategy.verifiers[i].verifyReport(signedReport) returns (bytes memory data) {
return data;
} catch {
continue;
}
}
revert("All verifiers failed");
}

L-15: No gas limit for external calls in deductAccountMargin

Proof of Concept

In TradingAccount.sol, the deductAccountMargin function makes external calls without gas limits:

function deductAccountMargin(
Data storage self,
FeeRecipients.Data memory feeRecipients,
UD60x18 pnlUsdX18,
UD60x18 settlementFeeUsdX18,
UD60x18 orderFeeUsdX18
)
internal
returns (UD60x18 marginDeductedUsdX18)
{
// current same
IERC20(collateralType).safeTransfer(recipient, amountToTransfer);
// current same
}
Impact

Without gas limits, external calls could lead to consuming all available gas, causing the transaction to revert and preventing margin deductions.

Recommended Mitigation Steps

Use gas limits for external calls:

import "@openzeppelin/contracts/utils/Address.sol;
function deductAccountMargin(
Data storage self,
FeeRecipients.Data memory feeRecipients,
UD60x18 pnlUsdX18,
UD60x18 settlementFeeUsdX18,
UD60x18 orderFeeUsdX18
)
internal
returns (UD60x18 marginDeductedUsdX18)
{
// current is same
Address.functionCallWithGas(
address(IERC20(collateralType)),
abi.encodeWithSelector(IERC20.transfer.selector, recipient, amountToTransfer),
50000 // like this gas limit
);
// current is same
}

L-16: Risk of reentrancy in initializeRootUpgrade

Proof of Concept

In RootUpgrade.sol, the initializeRootUpgrade function makes external calls without clear and aggressive protection against reentrancy:

function initializeRootUpgrade(
RootProxy.BranchUpgrade[] memory,
address[] memory initializables,
bytes[] memory initializePayloads
)
internal
{
for (uint256 i; i < initializables.length; i++) {
address initializable = initializables[i];
bytes memory data = initializePayloads[i];
Address.functionDelegateCall(initializable, data);
}
}
Impact

If any of the initialized contracts have buggy code, they could possibly do re-entrancy attack during the initialization process.

Recommended Mitigation Steps

Be safe and add a reentrancy guard for the initialization process, for best practice:

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract RootUpgrade is ReentrancyGuard {
// current is same
function initializeRootUpgrade(
RootProxy.BranchUpgrade[] memory,
address[] memory initializables,
bytes[] memory initializePayloads
)
internal
nonReentrant
{
for (uint256 i; i < initializables.length; i++) {
address initializable = initializables[i];
bytes memory data = initializePayloads[i];
Address.functionDelegateCall(initializable, data);
}
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.