Thanks to visit codestin.com
Credit goes to www.scribd.com

0% found this document useful (0 votes)
47 views49 pages

Audit Report

The Smart Contract Code Review and Security Analysis Report for Zilliqa outlines the findings from an audit of their Delegated Staking system, identifying a total of 13 findings, with 4 categorized as medium severity and 3 as low. Key vulnerabilities include unauthorized state migration and missing checks for address staking data, which could lead to security risks and potential fund loss. The report emphasizes the importance of addressing these vulnerabilities to ensure the integrity and security of the smart contract system.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
47 views49 pages

Audit Report

The Smart Contract Code Review and Security Analysis Report for Zilliqa outlines the findings from an audit of their Delegated Staking system, identifying a total of 13 findings, with 4 categorized as medium severity and 3 as low. Key vulnerabilities include unauthorized state migration and missing checks for address staking data, which could lead to security risks and potential fund loss. The report emphasizes the importance of addressing these vulnerabilities to ensure the integrity and security of the smart contract system.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 49

Smart Contract Code

Review And Security


Analysis Report

Customer: Zilliqa

Date: 05/06/2025
We express our gratitude to the Zilliqa team for the collaborative engagement that enabled
the execution of this Smart Contract Security Assessment.

​ illiqa's Delegated Staking system enables ZIL holders to participate in network security and
Z
earn rewards by delegating their assets to validator nodes.

Document

Name Smart Contract Code Review and Security Analysis Report for Zilliqa

Audited By Olesia Bilenka; Ivan Bondar

Approved By Ataberk Yavuzer

Website https://www.zilliqa.com/

Changelog 02/04/2025 - Preliminary Report

05/06/2025 - Final Report

Platform Zilliqa

Language Solidity

Tags LiquidStaking; Upgradable; Fungible Token

Methodology https://hackenio.cc/sc_methodology

Review Scope

Repository https://github.com/Zilliqa/delegated_staking/

Commit 160eadc

Retest b3333fa

2
Audit Summary

The system users should acknowledge all the risks summed up in the risks section of the
report

13 11 0 2
Total Findings Resolved Accepted Mitigated

Findings by Severity

Severity Count
Critical 0
High 0
Medium 4
Low 3

Vulnerability Severity
F-2025-9408 - Unauthorized Reinitialization Permits Arbitrary State Migration Medium
F-2025-9420 - Missing New Address Staking Data Check for replaceOldAddress Medium
function
F-2025-9429 - Potential Overwrite of Validator Data in _depositAndAddToPool Medium
function in BaseDelegation
F-2025-9437 - Potential Denial of Service due to Iteration Over All Validators Medium
During Deposits Updates
F-2025-9442 - Delegator Can Forcibly Transfer Funds Leading to Reward Inflation Low
and Price Manipulation
F-2025-9456 - Commission Rate Configuration Allows Excessively High Rates Low
F-2025-9502 - Inefficient Handling of Zero-Amount Stakings in Reward Calculation Low
F-2025-9404 - Incorrect Comments in Function for Version Change in Info
BaseDelegation Contract
F-2025-9440 - Lack of Events In Delegations Info
F-2025-9450 - Floating Pragma Info
F-2025-9460 - Redundant State Change in Activation Flag Results in Unnecessary Info
Gas Usage
F-2025-9513 - Missing Non-Zero Address Validations Info
F-2025-9581 - Centralized Control Address Setting Info

3
Documentation quality
Functional requirements are detailed.
Project overview is detailed
All roles in the system are described.
Use cases are described and detailed.
For each contract, all futures are described.
All interactions are described.
Technical description is detailed.
Run instructions are provided.
Technical specification is provided.
The NatSpec documentation is sufficient.

Code quality
The development environment is configured.

Test coverage
Code coverage of the project is 30.09% (branch coverage).

Deployment and basic user interactions are covered with tests.


Interactions by several users are tested thoroughly.
Not all branches are covered by tests.

4
Table of Contents

System Overview 6
Privileged Roles 6
Potential Risks 8
Findings 9
Vulnerability Details 9
Observation Details 44
Disclaimers 45
Appendix 1. Definitions 46
Severities 46
Potential Risks 46
Appendix 2. Scope 47
Appendix 3. Additional Valuables 48
System Overview

The Delegated Staking system comprises several core smart contracts, each fulfilling specific
roles to facilitate staking operations:​

BaseDelegation.sol: This abstract contract serves as the foundation for staking


functionalities. It defines common behaviors and structures that both liquid and non-liquid
staking contracts inherit, ensuring consistency across different staking implementations.
LiquidDelegation.sol: Extending from BaseDelegation , this contract implements liquid
staking. Delegators receive a non-rebasing Liquid Staking Token (LST) proportional to their
staked amount. The LST represents the staked ZIL and accumulates rewards over time.
Delegators can transfer LSTs, providing liquidity, and redeem them to withdraw their stake
along with accrued rewards.
NonLiquidDelegation.sol: Also extending from BaseDelegation , this contract facilitates
non-liquid staking. Delegators stake their ZIL without receiving transferable tokens.
Instead, they can manually claim their rewards periodically and withdraw their principal
stake after a specified unbonding period.​
NonRebasingLST.sol: This contract manages the Liquid Staking Tokens issued in the
liquid staking model. As a non-rebasing token, its supply remains constant, but its value
appreciates over time, reflecting the accrued staking rewards.
WithdrawalQueue.sol: This library handles the queuing mechanism for stake
withdrawals. It ensures that withdrawal requests are processed in an orderly manner,
adhering to the protocol's unbonding periods.

Privileged roles
NonRebasingLST.sol:

Owner ( LiquidDelegation contract):


Can mint tokens to any address, reflecting deposits/stakes.
Can burn tokens from any address, reflecting withdrawals/unstakes.

BaseDelegation.sol(shared privileges for both NonLiquidDelegation &


LiquidDelegation):

Owner:
Can upgrade contracts ( _authorizeUpgrade ).
Can add validators to staking pools ( _addToPool ).
Can set commission rates ( setCommissionNumerator ).
Can set commission receiver addresses ( setCommissionReceiver ).

LiquidDelegation.sol:

Owner:
Can deposit ZIL directly into validator pools ( depositFromPool ).
Can onboard validators into the pool ( joinPool ).
Can stake accrued rewards from the contract balance ( stakeRewards ).

6
Can collect outstanding commissions ( collectCommission ).

NonLiquidDelegation.sol:

Owner:
Can deposit ZIL directly into validator pools ( depositFromPool ).
Can onboard validators into the pool ( joinPool ).
Can collect outstanding commissions ( collectCommission ).

7
Potential Risks

System Reliance on External Contracts: The functioning of the system significantly


relies on specific external contract ( DEPOSIT_CONTRACT ). Any flaws or vulnerabilities in that
contract adversely affect the audited project, potentially leading to security breaches or
loss of funds.
Dynamic Array Iteration Gas Limit Risks: The project iterates over large dynamic
arrays, which leads to excessive gas costs, risking denial of service due to out-of-gas
errors, directly impacting contract usability and reliability.
Owner's Unrestricted State Modification: The absence of restrictions on state
variable modifications by the owner leads to arbitrary changes, affecting contract integrity
and user trust, especially during critical operations like minting phases.
Absence of Time-lock Mechanisms for Critical Operations: Without time-locks on
critical operations, there is no buffer to review or revert potentially harmful actions,
increasing the risk of rapid exploitation and irreversible changes.
Single Points of Failure and Control: The project is fully or partially centralized,
introducing single points of failure and control. This centralization can lead to
vulnerabilities in decision-making and operational processes, making the system more
susceptible to targeted attacks or manipulation.
Administrative Key Control Risks: The digital contract architecture relies on
administrative keys for critical operations. Centralized control over these keys presents a
significant security risk, as compromise or misuse can lead to unauthorized actions or loss
of funds.
Single Entity Upgrade Authority: The token ecosystem grants a single entity the
authority to implement upgrades or changes. This centralization of power risks unilateral
decisions that may not align with the community or stakeholders' interests, undermining
trust and security.

8
Findings

Vulnerability Details

F-2025-9408 - Unauthorized Reinitialization Permits


Arbitrary State Migration - Medium

Description: A public reinitializer function, inherited from BaseDelegation


and implemented in both NonLiquidDelegation and
LiquidDelegation contracts, permits arbitrary invocation of
migration logic. In a fresh deployment, the initial version is set
to 1 while the system version is defined as encodeVersion(0, 7, 0) .
This discrepancy allows any external actor to call the
reinitializer, triggering critical state migrations.

The affected function in both contracts is defined as follows:

function reinitialize(uint64 fromVersion) public reinitializer(VERSION

) {

_migrate(fromVersion);

// Additional logic

The current initialization logic sets the contract version to 1

upon the first call to initialize , as shown in the snippet below:

modifier initializer() {

InitializableStorage storage $ = _getInitializableStorage();

bool isTopLevelCall = !$._initializing;

uint64 initialized = $._initialized;

bool initialSetup = initialized == 0 && isTopLevelCall;

bool construction = initialized == 1 && address(this).code.length

== 0;

if (!initialSetup && !construction) {

revert InvalidInitialization();

$._initialized = 1;

if (isTopLevelCall) {

$._initializing = true;

_;

if (isTopLevelCall) {

$._initializing = false;

9
emit Initialized(1);

The reinitializer modifier permits execution if the stored


version is below the provided VERSION (i.e., encodeVersion(0, 7, 0) ).

As a result, any actor may invoke this function on a fresh


deployment, initiating the _migrate logic defined in
BaseDelegation.

The following excerpt from the _migrate function illustrates the


portions of code that manage legacy migration:

BaseDelegationStorage storage $ = _getBaseDelegationStorage();

if (fromVersion < encodeVersion(0, 4, 0))

$.pendingRebalancedDeposit = 0;

if (fromVersion < encodeVersion(0, 3, 0))

$.commissionReceiver = owner();

if (fromVersion >= encodeVersion(0, 2, 0))

return;

if (!$.activated)

return;

DeprecatedStorage storage temp;

uint256 peerIdLength;

assembly {

temp.slot := BaseDelegationStorageLocation

peerIdLength := sload(add(BaseDelegationStorageLocation, 1))

if (peerIdLength == 1)

return;

bytes memory callData =

abi.encodeWithSignature("getFutureStake(bytes)",

temp.blsPubKey

);

(bool success, bytes memory data) = DEPOSIT_CONTRACT.call(callData);

require(success, DepositContractCallFailed(callData, data));

uint256 futureStake = abi.decode(data, (uint256));

10
$.validators.push(Validator(

temp.blsPubKey,

futureStake,

owner(),

owner(),

0,

ValidatorStatus.Active

));

$.validatorIndex[temp.blsPubKey] = $.validators.length;

Legacy Migration Paths:


The migration logic is designed to handle upgrades
from multiple legacy versions (e.g., versions below
0.4.0, 0.3.0, and 0.2.0). In a fresh deployment on main
net, these paths are no longer necessary. The
conditions checking for fromVersion values less than
these versions should be removed to prevent
unintended state modifications.
State Integrity:
The current implementation automatically updates
sensitive state variables (e.g., $.pendingRebalancedDeposit ,
$.commissionReceiver , and validator settings) during
migration. If left unaltered, an unauthorized call could
corrupt these values.

The vulnerability affects both NonLiquidDelegation and


LiquidDelegation contracts, as they share the migration
logic.

Assets:
BaseDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
NonLiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
LiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Fixed

Classification

Impact: 5/5

Likelihood: 2/5

Exploitability: Independent

11
Complexity: Medium

Severity: Medium

Recommendations

Remediation: It is advised to either completely remove the migrate and


reinitialize functionality prior to main net launch or refactor

the _migrate function to eliminate legacy migration paths and


restrict access to reinitialize . The updated implementation
should remove any code related to old version migrations and
enforce strict access control in both NonLiquidDelegation
and LiquidDelegation contracts to ensure that migration
functionality cannot be arbitrarily invoked.

Resolution: Fixed in 5de07ba : Restricted reinitialize function to onlyOwner and


revised _migrate to accept only fromVersion == 1 , effectively
disabling legacy migration paths and preventing unauthorized
or unintended state changes in fresh deployments.

Evidences

Unauthorized Reinitialization

Reproduce:
PoC Steps:

The contract initializes with a stored version = 1, which is


below the system VERSION = encodeVersion(0, 7, 0) .
reinitialize() is public, so any account can call it.
Passing fromVersion < encodeVersion(0,4,0) triggers parts of
_migrate() that unintentionally alter sensitive state
variables (such as commissionReceiver or
pendingRebalancedDeposit ).

The test sets a custom receiver, then confirms that an


attacker can revert it to the owner by calling reinitialize() .

PoC Code:

function testUnauthorizedMigration() public {

// Owner sets a commission receiver

vm.prank(owner);

delegation.setCommissionReceiver(commissionReceiver);

assertEq(delegation.getCommissionReceiver(), commissionReceiver);

// Attacker calls reinitialize(...) with a lower "fromVersion" to

trigger legacy migration

12
vm.prank(attacker);

delegation.reinitialize(2);

// State has been forcibly overwritten by migration logic:

// commissionReceiver is reset to the owner due to fromVersion < e

ncodeVersion(0,3,0)

assertEq(delegation.getCommissionReceiver(), delegation.owner());

Results:
Ran 1 test for test/UnauthorizedMigration.t.sol:UnauthorizedMigrationT

est

[PASS] testUnauthorizedMigration() (gas: 38804)

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.76ms (1

66.20µs CPU time)

Files: UnauthorizedMigration.t.sol

13
F-2025-9420 - Missing New Address Staking Data Check for
replaceOldAddress function - Medium

Description: In the LiquidDelegation and NonLiquidDelegation


contracts, the setNewAddress function allows setting a new
address to which the current address migrates. During this
process, it checks that the new address is not a staker.

function setNewAddress(address to) public {

...

require(

$.stakingIndices[to].length == 0,

StakerAlreadyExists(to)

);

$.newAddress[_msgSender()] = to;

After that, replaceOldAddress should be called by the new address


to accept the migration, where all staking data is copied from
the previous address.

function replaceOldAddress(address old) public {

...

$.firstStakingIndex[sender] = $.firstStakingIndex[old];

$.availableTaxedRewards[sender] = $.availableTaxedRewards[old];

$.lastTaxedStakingIndex[sender] = $.lastTaxedStakingIndex[old];

$.taxedSinceLastStaking[sender] = $.taxedSinceLastStaking[old];

...

However, in the replaceAddress function, there is no check to


ensure that the new address still does not contain any stakes.
This can result in the staking data being overwritten, and in
the worst case, the funds may become locked if the new
address mistakenly accepts the request or assumes that the
data from the previous address will simply be added to its own
data.

To prevent this, it’s essential to add a check in replaceAddress to


ensure that the new address does not already have staking
data before allowing the migration to proceed.

Assets:
NonLiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

14
LiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Fixed

Classification

Impact: 4/5

Likelihood: 2/5

Exploitability: Independent

Complexity: Simple

Severity: Medium

Recommendations

Remediation: The vulnerability can be mitigated by adding a check in the


replaceOldAddress function to ensure that the new address does

not already have staking data before allowing the migration to


proceed. This check can be similar to the one in the
setNewAddress function.

Resolution: Fixed in f8872d8 : Added validation in replaceOldAddress to disallow


address replacement if the new address has become a staker,
preventing unintended overwrites and potential loss or locking
of staking data.

15
F-2025-9429 - Potential Overwrite of Validator Data in
_depositAndAddToPool function in BaseDelegation - Medium

Description: In the BaseDelegation contract, the _depositAndAddToPool

function allows for the creation of a validator even if the


provided blsPubKey already has existing staking information.

function _depositAndAddToPool(

bytes calldata blsPubKey,

bytes calldata peerId,

bytes calldata signature

) internal virtual {

...

$.validators.push(Validator(

blsPubKey,

availableStake,

owner(),

owner(),

0,

ValidatorStatus.Active

));

$.validatorIndex[blsPubKey] = $.validators.length;

...

This can result in the validatorIndex being overwritten, causing


the previous validator data to be lost. Since the index is
overwritten, it becomes impossible to process the previous
validator's information, leading to potential data loss or
incorrect processing.

Assets:
BaseDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Mitigated

Classification

Impact: 5/5

16
Likelihood: 3/5

Exploitability: Dependent

Complexity: Simple

Severity: Medium

Recommendations

Remediation: To prevent potential overwrite of validator data, it's


recommended to implement a check before adding a new
validator to ensure that the blsPubKey does not already exist in
the validatorIndex . If the blsPubKey already exists, the function
should either reject the new validator or update the existing
validator's data instead of overwriting it.

Resolution: The check is implemented in the external contract call which is


out of audit scope. The additional comment was provided by
the client:

The reason why _depositAndAddToPool() doesn't need to


check if a validator with that blsPubKey already exists is
that it calls the deposit contract's deposit() function
which fails if the blsPubKey belongs to a registered
validator regardless of whether that validator is part of
the pool or not, and _depositAndAddToPool() reverts with
DepositContractCallFailed .

17
F-2025-9437 - Potential Denial of Service due to Iteration
Over All Validators During Deposits Updates - Medium

Description: In the _increaseDeposit function of the BaseDelegation


contract, the validators array is iterated twice to calculate the
totalDeposited amount and to call depositTopup in the external
contract. If the number of validators grows large enough, the
function may exceed the block gas limit.

function _increaseDeposit(uint256 amount) internal virtual {

BaseDelegationStorage storage $ = _getBaseDelegationStorage();

uint256[] memory contribution = new uint256[]($.validators.length)

uint256 totalContribution;

uint256 len = $.validators.length;

for (uint256 i = 0; i < len; i++)

if ($.validators[i].status < ValidatorStatus.ReadyToLeave) {

contribution[i] = $.validators[i].futureStake;

totalContribution += contribution[i];

uint256 totalDeposited;

for (uint256 i = 0; i < len; i++)

if (contribution[i] > 0) {

uint256 value = amount * contribution[i] / totalContributi

on;

totalDeposited += value;

$.validators[i].futureStake += value;

bytes memory callData =

abi.encodeWithSignature("depositTopup(bytes)",

$.validators[i].blsPubKey

);

(bool success, bytes memory data) = DEPOSIT_CONTRACT.call{

value: value

}(callData);

require(success, DepositContractCallFailed(callData, data)

);

$.nonRewards -= totalDeposited;

The same is applicable to the _decreaseDeposit function.

function _decreaseDeposit(uint256 amount) internal virtual {

...

uint256 len = $.validators.length;

18
for (uint256 i = 0; i < len; i++)

if ($.validators[i].status == ValidatorStatus.Active) {

contribution[i] = $.validators[i].futureStake - minimumDep

osit;

totalContribution += contribution[i];

uint256 j = len;

while (

j > 0 &&

$.nonRewards + totalContribution <

$.undepositedClaims + $.depositedClaims + amount

if ($.validators[--j].status == ValidatorStatus.Active) {

$.validators[j].pendingWithdrawals +=

$.validators[j].futureStake;

callData =

abi.encodeWithSignature("unstake(bytes,uint256)",

$.validators[j].blsPubKey,

$.validators[j].futureStake

);

(success, data) = DEPOSIT_CONTRACT.call(callData);

require(success, DepositContractCallFailed(callData, d

ata));

$.validators[j].status = ValidatorStatus.FullyUndeposi

ted;

totalContribution -= contribution[j];

contribution[j] = 0;

if (amount > $.validators[j].futureStake) {

amount -= $.validators[j].futureStake;

$.validators[j].futureStake = 0;

else {

// store the excess deposit withdrawn that was not

requested

// by the unstaking delegator so that we can distu

ingish it

// later from the claimable amount unstaked by the

delegator

$.validators[j].futureStake -= amount;

$.pendingRebalancedDeposit += $.validators[j].futu

reStake;

return;

if (totalContribution < amount) {

$.depositedClaims += amount - totalContribution;

amount = totalContribution;

19
}

uint256[] memory undeposited = new uint256[]($.validators.leng

th);

uint256 totalUndeposited;

for (uint256 i = 0; i < len; i++)

if (contribution[i] > 0) {

undeposited[i] = amount * contribution[i] / totalContr

ibution;

totalUndeposited += undeposited[i];

// rounding error that was not unstaked from the deposits but

can be

// claimed after the unbonding period

uint256 delta = amount - totalUndeposited;

// increment the values to be undeposited unless they equal th

e respective

// validator's contribution i.e. the validator can't contribut

e more, until

// the delta bounded by the number of contributing validators

becomes zero

for (uint256 i = 0; i < len; i++) {

uint256 value = undeposited[i];

if (value > 0) {

if (delta > 0 && value < contribution[i]) {

value++;

delta--;

$.validators[i].futureStake -= value;

$.validators[i].pendingWithdrawals += value;

callData =

abi.encodeWithSignature("unstake(bytes,uint256)",

$.validators[i].blsPubKey,

value

);

(success, data) = DEPOSIT_CONTRACT.call(callData);

require(success, DepositContractCallFailed(callData, data)

);

Additionality, the getStake function which is called in the


_unstake , _stake and getPrice functions of the LiquidDelegation
contract iterates over all validators.

function getStake() public virtual view returns(uint256 total) {

BaseDelegationStorage storage $ = _getBaseDelegationStorage();

total = $.nonRewards;

20
uint256 len = $.validators.length;

for (uint256 i = 0; i < len; i++)

if ($.validators[i].status < ValidatorStatus.ReadyToLeave)

total += $.validators[i].futureStake;

total += $.pendingRebalancedDeposit;

total -= $.undepositedClaims;

total -= $.depositedClaims;

In the _withdrawDeposit and totalPendingWithdrawals functions of the


BaseDelegation contract, the validators array is iterated to
withdraw the pending unstaked deposits of all validators, call
withdraw in the external contract and to calculate the

pendingWithdrawals .

function _withdrawDeposit() internal virtual returns(uint256 total) {

BaseDelegationStorage storage $ = _getBaseDelegationStorage();

uint256 len = $.validators.length;

// we accept the constant amount of gas wasted per validator whose

// unbonding period is not over i.e. there is nothing to withdraw

yet

for (uint256 j = 1; j <= len; j++) {

uint256 i = len - j;

if (

$.validators[i].pendingWithdrawals > 0 &&

$.validators[i].status != ValidatorStatus.ReadyToLeave

) {

// currently all validators have the same reward address,

// which is the address of this delegation contract

uint256 amount = address(this).balance;

bytes memory callData =

abi.encodeWithSignature("withdraw(bytes)",

$.validators[i].blsPubKey

);

(bool success, bytes memory data) = DEPOSIT_CONTRACT.call(call

Data);

require(success, DepositContractCallFailed(callData, data));

amount = address(this).balance - amount;

$.validators[i].pendingWithdrawals -= amount;

total += amount;

if (

$.validators[i].pendingWithdrawals == 0 &&

$.validators[i].status == ValidatorStatus.FullyUndeposited

) {

total -= $.validators[i].futureStake;

$.pendingRebalancedDeposit -= $.validators[i].futureStake;

_increaseDeposit($.validators[i].futureStake);

$.validators[i].futureStake = 0;

21
_removeFromPool(i);

function totalPendingWithdrawals() public virtual view returns(uint256

total) {

BaseDelegationStorage storage $ = _getBaseDelegationStorage();

uint256 len = $.validators.length;

for(uint256 i = 0; i < len; i++)

if ($.validators[i].status < ValidatorStatus.ReadyToLeave)

total += $.validators[i].pendingWithdrawals;

This could prevent the successful execution functionalities


such as withdrawal and joining the pool. As a result, the
contract may become unusable under high validator count,
leading to a denial of service for users.

This could prevent the successful execution of _increaseDeposit

and _decreaseDeposit and getStake functions, which in turn blocks


critical functionalities such as staking, withdrawal, and joining
the pool. As a result, the contract may become unusable under
high validator count, leading to a denial of service for users.

Assets:
BaseDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
NonLiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
LiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Fixed

Classification

Impact: 5/5

Likelihood: 2/5

Exploitability: Independent

Complexity: Simple

Severity: Medium

Recommendations

22
Remediation: The vulnerability described can be mitigated by avoiding the
iteration over all validators in the _increaseDeposit and
_decreaseDeposit functions. This can be achieved by maintaining
a separate variable that keeps track of the totalDeposited

amount and moving calls to the Delegator to separate


transactions. This way, the totalDeposited amount can be
retrieved in constant time, without the need to iterate over all
validators. Alternatively, implemente the limit of validators in
the contracts.

Resolution: Fixed in edfb906 : Introduced a hard cap of 255 validators via the
MAX_VALIDATORS constant to prevent denial-of-service scenarios
caused by excessive gas consumption in validator iteration
loops. Critical functions have been tested for gas limits up to
this threshold.

23
F-2025-9442 - Delegator Can Forcibly Transfer Funds
Leading to Reward Inflation and Price Manipulation - Low

Description: A malicious actor can forcibly send funds directly to the


LiquidDelegation contract using Ethereum's selfdestruct()

mechanism. This bypasses the receive() function, resulting in


an imbalance between the actual contract balance
( address(this).balance ) and internal accounting variables
( nonRewards , taxedRewards ). Such imbalance can inflate the
rewards pool upon pool activation.

EVM-compatible chains permit forcibly transferring chain


native currency (in current case ZIL ) to a contract via the
selfdestruct() opcode, bypassing any revert logic in the
receive() function:

receive() external payable {

require(

_msgSender() == DEPOSIT_CONTRACT,

InvalidCaller(_msgSender(), DEPOSIT_CONTRACT)

);

BaseDelegationStorage storage $ = _getBaseDelegationStorage();

$.nonRewards += msg.value;

When funds are forcibly sent through selfdestruct() , the


receive() logic is never triggered, so critical internal accounting
variables remain unchanged. For example:

The attacker forcibly sends 10_000e18 ZIL , raising


address(this).balance by 10000 ZIL.
Internal state variables such as nonRewards , undepositedClaims ,

and depositedClaims remain unaffected.


If the pool is not yet activated (no validators deposited),
the forcibly added funds remain invisible, as getRewards()

returns zero.

When the first delegator stakes even a minimal amount (e.g.,


10 ZIL):

_isActivated() remains false; thus, forcibly transferred funds


still remain unaccounted.
The first staker obtains liquid staking tokens (LST) at a 1:1
ratio because the token supply initially is zero.

Once the pool is activated (validators deposited):

24
getRewards() computes rewards as address(this).balance -

nonRewards . This suddenly recognizes the previously hidden


forcibly transferred funds (≈1000 ZIL) as “newly arrived
rewards”.

function getRewards() public virtual view returns(uint256 total) {

if (!_isActivated())

return 0;

BaseDelegationStorage storage $ = _getBaseDelegationStorage();

total = address(this).balance - $.nonRewards;

Upon recognition, _taxRewards() deducts commission and


includes the remainder in taxedRewards , artificially inflating
the perceived rewards and potentially skewing later
conversions between ZIL and liquid staking tokens (LST).

function _taxRewards() internal {

LiquidDelegationStorage storage $ = _getLiquidDelegationStorag

e();

uint256 rewards = getRewards();

uint256 commission = (rewards - $.taxedRewards) * getCommissio

nNumerator() / DENOMINATOR;

$.taxedRewards = rewards - commission;

if (commission == 0)

return;

// commissions are not subject to the unbonding period

(bool success, ) = getCommissionReceiver().call{

value: commission

}("");

require(success, TransferFailed(getCommissionReceiver(), commi

ssion));

emit CommissionPaid(getCommissionReceiver(), commission);

Although this behavior does not directly allow unauthorized


user gains, it can cause certain edge cases and rounding
discrepancies. Subsequent stakers might observe dilution or
unexpected token price behavior due to the sudden inclusion
of forcibly transferred funds.

Assets:
LiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Mitigated

25
Classification

Impact: 3/5

Likelihood: 2/5

Exploitability: Independent

Complexity: Complex

Severity: Low

Recommendations

Remediation: Consider implementing robust internal accounting to ensure


the contract accurately reflects all changes in its balance.
Specifically:

Establish a general mechanism that, prior to any critical


state transitions (e.g., validator activation or stake
delegation), verifies the consistency between
address(this).balance and the sum of internal tracked

variables, reverting or isolating surplus funds if


discrepancies are detected.
Adopt preventive measures inspired by ERC-4626 inflation
protections, such as requiring an initial deposit or
permanently locking a minimal number of shares to
prevent disproportionate inflation from forced fund
transfers.

Resolution: The minimum stake requirement in the pool mitigates the risk,
therefore, added funds can be considered as a donation that is
harmless and actually benefits all stakers. An additional
comment was provided by the client:

The funds that an attacker can forcibly add to the


contract balance are treated as a donation that
benefits all delegators in the same way as rewards.
The minimum stake ensures that the increase in the
price of the liquid staking tokens caused by the
donation remains within a reasonable range.

26
F-2025-9456 - Commission Rate Configuration Allows
Excessively High Rates - Low

Description: The contract allows the owner to set the commission rate up to
100% of the generated rewards. This could result in delegators
losing all their staking rewards unexpectedly.

The setCommissionNumerator function currently allows the owner to


set the commission up to DENOMINATOR - 1 (9,999), effectively
permitting a maximum commission of 99.99%. Given the logic
of the _taxRewards function, the commission rate directly
impacts the rewards distribution:

uint256 public constant DENOMINATOR = 10_000;

function setCommissionNumerator(uint256 _commissionNumerator) public v

irtual onlyOwner {

require(_commissionNumerator < DENOMINATOR, InvalidCommissionRate(

_commissionNumerator));

BaseDelegationStorage storage $ = _getBaseDelegationStorage();

$.commissionNumerator = _commissionNumerator;

function _taxRewards(uint256 untaxedRewards) internal returns (uint256

) {

uint256 commission = untaxedRewards * getCommissionNumerator() / D

ENOMINATOR;

if (commission == 0)

return untaxedRewards;

// commissions are not subject to the unbonding period

(bool success, ) = getCommissionReceiver().call{

value: commission

}("");

require(success, TransferFailed(getCommissionReceiver(), commissio

n));

emit CommissionPaid(getCommissionReceiver(), commission);

return untaxedRewards - commission;

Because there is no practical upper limit or delay mechanism,


the owner could abruptly increase the commission to an
excessively high value without prior notice to delegators,
causing unexpected losses of rewards.

27
Assets:
BaseDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Fixed

Classification

Impact: 4/5

Likelihood: 2/5

Exploitability: Dependent

Complexity: Simple

Severity: Low

Recommendations

Remediation:
Implement a reasonable upper bound (e.g., 10–20%) on
the commission rate to ensure fairness and predictability.
Introduce a time delay between changes to the
commission rate becoming effective, allowing delegators
adequate time to respond or withdraw their assets if they
disagree with proposed commission changes.

Resolution: Fixed in a59fe4b : Introduced a 1-day delay mechanism and


imposed a maximum allowable change of 2 percentage
points for commission rate updates when the pool holds stake
or rewards. This prevents abrupt or excessive commission
changes, safeguarding delegators from unexpected loss of
rewards.

28
F-2025-9502 - Inefficient Handling of Zero-Amount Stakings
in Reward Calculation - Low

Description: The additionalSteps mechanism in the NonLiquidDelegation


contract is intended to determine how many steps can be
processed when calculating rewards. However, if one of the
user's Stakings has an amount of 0, the _rewards function still
processes that staking and continues processing all
subsequent stakings until the next eligible staking is reached.

while (

posInStakingIndices == $.stakingIndices[_msgSender()].length - 1 ?

nextStakingIndex < $.stakings.length :

nextStakingIndex <= $.stakingIndices[_msgSender()][posInStakingInd

ices + 1]

) {

if (total > 0) {

resultInTotal += $.stakings[nextStakingIndex].rewards * am

ount / total;

roundingError +=

1 ether * $.stakings[nextStakingIndex].rewards * amoun

t / total -

1 ether * ($.stakings[nextStakingIndex].rewards * amou

nt / total);

total = $.stakings[nextStakingIndex].total;

nextStakingIndex++;

if (nextStakingIndex - firstStakingIndex > additionalSteps

) {

...

This results in unnecessary gas consumption and redundant


transaction steps, as stakings that do not yield any rewards for
the user are still processed. Moreover, this behavior leads to
incorrect calculation of the number of steps in both the
withdrawRewards (when passing them) function and the

getAdditionalSteps function.

Assets:
NonLiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Fixed

29
Classification

Impact: 2/5

Likelihood: 3/5

Exploitability: Independent

Complexity: Simple

Severity: Low

Recommendations

Remediation: The code should be updated to skip the stakings with zero
amount. This can be achieved by adding a condition to check if
the staking amount is zero before processing it in the loop. If
the amount is zero, the loop should continue to the next
staking without processing the current one. This will prevent
unnecessary gas consumption and redundant transaction
steps.

Resolution: Fixed in 5de07ba : Modified _rewards to skip zero-amount stakings,


ensuring each processed step contributes to rewards and
preserving the integrity of additionalSteps logic while optimizing
gas usage.

Evidences

Proof of Concept

Reproduce:
function test_RedundantAdditionalSteps() public {

uint256 depositAmount = 10_000_000 ether;

addValidator(BaseDelegation(delegation), depositAmount, Deposi

tMode.Fundraising);

address initialUser = vm.randomAddress();

vm.deal(initialUser, 100 ether);

vm.startPrank(initialUser);

delegation.stake{value: 100 ether}();

// the initial user unstakes and claims everything

// therefore the next stakings should not be processed for the

delegation.unstake(100 ether);

vm.roll(block.number + delegation.unbondingPeriod());

30
delegation.claim();

vm.stopPrank();

depositFromPool(BaseDelegation(delegation), depositAmount, 2);

// 20 more stakings are added by the second staker

// the rewards for the initial staker is 0 during these new st

akings

address secondUser = vm.randomAddress();

for (uint i; i < 20; i++) {

vm.deal(secondUser, depositAmount);

vm.startPrank(secondUser);

delegation.stake{value: depositAmount}();

//initialUser adds new staking

vm.startPrank(initialUser);

//but to receive the reward for the new staking,

// all the 26 steps should be looped

delegation.stake{value: 100 ether}();

uint256 steps = delegation.getAdditionalSteps();

assertEq(steps, 26);

Results:

31
F-2025-9404 - Incorrect Comments in Function for Version
Change in BaseDelegation Contract - Info

Description: DRAFT

In the _migrate function of the BaseDelegation contract, the


validation checks whether the fromVersion is correct. However,
the comments in the code before each validation step state
that " the contract has been upgraded to a version which... ". This
suggests that the validation should be performed for the
toVersion instead of the fromVersion .

function _migrate(uint64 fromVersion) internal {

...

// the contract has been upgraded to a version which

// is higher or same as the current version

if (fromVersion >= VERSION)

return;

...

if (fromVersion < encodeVersion(0, 4, 0))

// the contract has been upgraded to a version which may have

// changed the totalWithdrawals which has to be zero initially

$.pendingRebalancedDeposit = 0;

if (fromVersion < encodeVersion(0, 3, 0))

// the contract has been upgraded to a version which did not

// set the commission receiver or allow the owner to change it

$.commissionReceiver = owner();

if (fromVersion >= encodeVersion(0, 2, 0))

// the contract has been upgraded to a version which has

// already migrated the blsPubKey to the validators array

return;

...

This discrepancy between the code comments and the actual


implementation could lead to incorrect validation behavior. The
validation logic needs to be adjusted to check the toVersion as
indicated in the comments.

32
The client confirmed that the comments in the code are not
correct.

Assets:
BaseDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Fixed

Classification

Impact: 2/5

Likelihood: 2/5

Exploitability: Dependent

Complexity: Simple

Severity: Info

Recommendations

Remediation: Update the comments to accurately reflect what the code is


doing. This will help avoid any confusion for developers who
may be reading the code in the future.

Resolution: Fixed in ec89e78 : Comments in the _migrate function were


updated to accurately reflect the use of fromVersion in validation
checks, and most of the function body was commented out to
prepare for mainnet launch.

33
F-2025-9440 - Lack of Events In Delegations - Info

Description: In the BaseDelegation contract there is a partial lack of


events. The same is applicable to the NonLiquidDelegation
contracts.

This might result in decreased transparency and increased


difficulty in monitoring changes effectively.

This issue affects the following functions:

BaseDelegation:

setCommissionReceiver

setCommissionNumerator

completeLeaving

NonLiquidDelegation:

collectCommission

replaceOldAddress

setNewAddress

Assets:
BaseDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
NonLiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
LiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Fixed

Classification

Impact: 1/5

Likelihood: 1/5

Exploitability: Independent

Complexity: Simple

Severity: Info

Recommendations

34
Remediation: Consider implementing additional event logging for critical
changes to enhance monitoring and ensure operational
transparency in the contracts.

Resolution: Fixed in 539233e : Events have been added to key functions in


the BaseDelegation and NonLiquidDelegation contracts.

35
F-2025-9450 - Floating Pragma - Info

Description: In Solidity development, the pragma directive specifies the


compiler version to be used, ensuring consistent compilation
and reducing the risk of issues caused by version changes.
However, using a floating pragma (e.g., ^0.8.28 ) introduces
uncertainty, as it allows contracts to be compiled with any
version within a specified range. This can result in
discrepancies between the compiler used in testing and the
one used in deployment, increasing the likelihood of
vulnerabilities or unexpected behavior due to changes in
compiler versions.

The project currently uses floating pragma declarations


( ^0.8.28 ) in its Solidity contracts. This increases the risk of
deploying with a compiler version different from the one
tested, potentially reintroducing known bugs from older
versions or causing unexpected behavior with newer versions.
These inconsistencies could result in security vulnerabilities,
system instability, or financial loss. Locking the pragma
version to a specific, tested version is essential to prevent
these risks and ensure consistent contract behavior.

Assets:
BaseDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
NonLiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
LiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
WithdrawalQueue.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
NonRebasingLST.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Fixed

Classification

Impact: 3/5

Likelihood: 1/5

Exploitability: Dependent

Complexity: Simple

36
Severity: Info

Recommendations

Remediation: It is recommended to lock the pragma version to the


specific version that was used during development and
testing. This ensures that the contract will always be compiled
with a known, stable compiler version, preventing unexpected
changes in behavior due to compiler updates. For example,
instead of using ^0.8.28 , explicitly define the version with pragma
solidity 0.8.28; .

Before selecting a version, review known bugs and


vulnerabilities associated with each Solidity compiler release.
This can be done by referencing the official Solidity compiler
release notes: Solidity GitHub releases or Solidity Bugs by
Version. Choose a compiler version with a good track record
for stability and security.

Resolution: Fixed in 8c64f45 : Pragma versions have been locked to 0.8.28 in


all contracts to ensure consistent compilation.

37
F-2025-9460 - Redundant State Change in Activation Flag
Results in Unnecessary Gas Usage - Info

Description: The state variable $.activated is repeatedly set to true without


prior verification in multiple functions ( _migrate ,
_depositAndAddToPool , _addToPool ), even if it has already been
activated. This redundant state change unnecessarily
consumes gas without any additional benefit.

In the implementations of the _migrate , _depositAndAddToPool , and


_addToPool functions of the BaseDelegation, the activation flag
$.activated is always explicitly set to true :

$.activated = true;

This assignment occurs regardless of the current state of


activation. If the contract has already been activated
previously (i.e., $.activated is already true ), repeating this
assignment results in unnecessary gas usage due to redundant
state storage operations.

Example from _depositAndAddToPool :

function _depositAndAddToPool(

bytes calldata blsPubKey,

bytes calldata peerId,

bytes calldata signature

) internal virtual {

BaseDelegationStorage storage $ = _getBaseDelegationStorage();

$.activated = true; // always sets activated to true without check

// additional logic...

This redundancy is also observed in the _addToPool and _migrate

functions.

Although this issue doesn't pose a direct security threat, it


negatively affects the efficiency and economic optimization of
contract interactions.

Assets:
BaseDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Fixed

38
Classification

Impact: 1/5

Likelihood: 5/5

Exploitability: Independent

Complexity: Simple

Severity: Info

Recommendations

Remediation: Add conditional checks before assigning $.activated to true .

This ensures the variable is set only once when transitioning


from a non-activated to activated state:

if (!$.activated) {

$.activated = true;

Resolution: Fixed in 4f030cb : Redundant assignments to the activated state


variable have been removed by introducing conditional checks
to prevent unnecessary gas consumption when the value is
already true .

39
F-2025-9513 - Missing Non-Zero Address Validations - Info

Description: Several functions in the project do not validate input addresses


to ensure they are non-zero before use. This lack of validation
can lead to unintended behaviors such as sending tokens to
the zero address, incorrectly setting critical contract
dependencies, or assigning key roles to an invalid address.

BaseDelegation contract:

setCommissionReceiver : _commissionReceiver

LiquidDelegation contract:

joinPool : controlAddress

NonLiquidDelegation contract:

joinPool : controlAddress

Tokens may be mistakenly sent to the zero address, making


them permanently unrecoverable. Critical addresses (e.g.,
commission receiver) may be set to zero, leading to system
malfunctions or unintentional contract behavior.

Assets:
BaseDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
NonLiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
LiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Fixed

Classification

Impact: 1/5

Likelihood: 1/5

Exploitability: Dependent

Complexity: Simple

Severity: Info

Recommendations

40
Remediation: To fix this vulnerability non-zero address validation checks
should be added to all the functions mentioned above. This
can be done by adding a simple condition to check if the
address is non-zero before proceeding with the rest of the
function.

Resolution: Fixed in 433cb46 : Zero address validation was added to


setCommissionReceiver , and the joinPool functions were refactored
to eliminate the need for the controlAddress parameter.

41
F-2025-9581 - Centralized Control Address Setting - Info

Description: In the current implementation, the operators of validator


nodes joining a staking pool must trust the pool owner to use
their address ( controlAddress ) when calling the joinPool()

function.

Assets:
NonLiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]
LiquidDelegation.sol
[https://github.com/Zilliqa/delegated_staking/tree/main/src]

Status: Fixed

Classification

Impact: 2/5

Likelihood: 2/5

Exploitability: Dependent

Complexity: Simple

Severity: Info

Recommendations

Remediation: This trust assumption can be minimized by implementing

a registerControlAddress(bytes calldata blsPubKey) function that


checks if msg.sender is indeed the current controlAddress of
the validator node identified by blsPubKey by calling the
deposit contract's getControlAddress(blsPubKey) function and
stores the mapping from blsPubKey to controlAddress which
the joinPool() function must use instead of a parameter to
determine the controlAddress

an unregisterControlAddress(bytes calldata blsPubKey) function that


allows the controlAddress to undo the handover of the
validator node to the pool even after already having called
the setControlAddress() function of the deposit contract in
case the contract owner does not call joinPool() to finalize
the handover

42
Resolution: Fixed in 766bbc8 : Implemented registerControlAddress and
unregisterControlAddress functions to securely manage control
address registration using on-chain verification via the deposit
contract, eliminating the need for operators to trust the pool
owner to provide the correct address during joinPool() calls.

43
Observation Details

44
Disclaimers

Hacken Disclaimer

The smart contracts given for audit have been analyzed based on best industry practices at
the time of the writing of this report, with cybersecurity vulnerabilities and issues in smart
contract source code, the details of which are disclosed in this report (Source Code); the
Source Code compilation, deployment, and functionality (performing the intended functions).

The report contains no statements or warranties on the identification of all vulnerabilities and
security of the code. The report covers the code submitted and reviewed, so it may not be
relevant after any modifications. Do not consider this report as a final and sufficient
assessment regarding the utility and safety of the code, bug-free status, or any other contract
statements.

While we have done our best in conducting the analysis and producing this report, it is
important to note that you should not rely on this report only — we recommend proceeding
with several independent audits and a public bug bounty program to ensure the security of
smart contracts.

English is the original language of the report. The Consultant is not responsible for the
correctness of the translated versions.

Technical Disclaimer

Smart contracts are deployed and executed on a blockchain platform. The platform, its
programming language, and other software related to the smart contract can have
vulnerabilities that can lead to hacks. Thus, the Consultant cannot guarantee the explicit
security of the audited smart contracts.

45
Appendix 1. Definitions

Severities
When auditing smart contracts, Hacken is using a risk-based approach that considers
Likelihood, Impact, Exploitability and Complexity metrics to evaluate findings and score
severities.

Reference on how risk scoring is done is available through the repository in our Github
organization:

hknio/severity-formula

Severity Description
Critical vulnerabilities are usually straightforward to exploit and can lead to the
Critical
loss of user funds or contract state manipulation.
High vulnerabilities are usually harder to exploit, requiring specific conditions, or
High have a more limited scope, but can still lead to the loss of user funds or contract
state manipulation.
Medium vulnerabilities are usually limited to state manipulations and, in most
Medium cases, cannot lead to asset loss. Contradictions and requirements violations. Major
deviations from best practices are also in this category.
Major deviations from best practices or major Gas inefficiency. These issues will
Low
not have a significant impact on code execution.

Potential Risks
The "Potential Risks" section identifies issues that are not direct security vulnerabilities but
could still affect the project’s performance, reliability, or user trust. These risks arise from
design choices, architectural decisions, or operational practices that, while not immediately
exploitable, may lead to problems under certain conditions. Additionally, potential risks can
impact the quality of the audit itself, as they may involve external factors or components
beyond the scope of the audit, leading to incomplete assessments or oversight of key areas.
This section aims to provide a broader perspective on factors that could affect the project's
long-term security, functionality, and the comprehensiveness of the audit findings.

46
Appendix 2. Scope

The scope of the project includes the following smart contracts from the provided repository:

Scope Details
Repository https://github.com/Zilliqa/delegated_staking/
Commit 160eadc6470c856dd555433e3b367dc40b32abd0
Retest b3333fa8ffeba5258ff68fce69d6dc89b695289d
Whitepaper N/A
Requirements NatSpec
Technical Requirements NatSpec

Asset Type
Smart
BaseDelegation.sol [https://github.com/Zilliqa/delegated_staking/tree/main/src]
Contract
Smart
IDelegation.sol [https://github.com/Zilliqa/delegated_staking/tree/main/src]
Contract
LiquidDelegation.sol Smart
[https://github.com/Zilliqa/delegated_staking/tree/main/src] Contract
NonLiquidDelegation.sol Smart
[https://github.com/Zilliqa/delegated_staking/tree/main/src] Contract
NonRebasingLST.sol Smart
[https://github.com/Zilliqa/delegated_staking/tree/main/src] Contract
WithdrawalQueue.sol Smart
[https://github.com/Zilliqa/delegated_staking/tree/main/src] Contract

47
Appendix 3. Additional Valuables

Verification of System Invariants

During the audit of Z


​ illiqa, Hacken followed its methodology by performing fuzz-testing on the
project's main functions. Foundry, a tool used for fuzz-testing, was employed to check how the
protocol behaves under various inputs. Due to the complex and dynamic interactions within
the protocol, unexpected edge cases might arise. Therefore, it was important to use fuzz-
testing to ensure that several system invariants hold true in all situations.

Fuzz-testing allows the input of many random data points into the system, helping to identify
issues that regular testing might miss. A specific Foundry fuzzing suite was prepared for this
task, and throughout the assessment, 5 invariants were tested over 100,000 runs. This
thorough testing ensured that the system works correctly even with unexpected or unusual
inputs.

Test Run
Invariant
Result Count
Index Consistency: The queue’s indices remain consistent (i.e. first <= last ). Passed 100K+
Queue Length Accuracy: The active queue length ( last - first ) exactly
Passed 100K+
equals the total enqueues minus dequeues.
Item Readiness Consistency: For every active item (from first to last - 1 ),

its ready status is determined by comparing block.number with the stored


Passed 100K+
blockNumber (i.e. if block.number >= item.blockNumber then the item is ready and not
marked as not-ready, and vice versa).
Dequeued Item Clearance: Items that have been dequeued (indices less
Passed 100K+
than first ) are cleared (i.e. both amount and blockNumber equal zero).
Ready Function Consistency: The overall ready function (which returns the
ready status of the first item) is consistent with the specific check on the Passed 100K+
item at the first index.

Additional Recommendations

The smart contracts in the scope of this audit could benefit from the introduction of automatic
emergency actions for critical activities, such as unauthorized operations like ownership
changes or proxy upgrades, as well as unexpected fund manipulations, including large
withdrawals or minting events. Adding such mechanisms would enable the protocol to react
automatically to unusual activity, ensuring that the contract remains secure and functions as
intended.

To improve functionality, these emergency actions could be designed to trigger under specific
conditions, such as:

Detecting changes to ownership or critical permissions.


Monitoring large or unexpected transactions and minting events.
Pausing operations when irregularities are identified.

48
These enhancements would provide an added layer of security, making the contract more
robust and better equipped to handle unexpected situations while maintaining smooth
operations.

49

You might also like