Judging

Cabal Liquid Staking Token

The highest-yielding LST on Initia. Stake once, earn forever.

  • Start date28 Apr 2025
  • End date5 May 2025
  • Total awards$23,000 in USDC
  • Duration7 days

Cabal audit details

  • Total Prize Pool: $23,000 in USDC
    • HM awards: up to $20,000 USDC (Notion: HM (main) pool)
      • If no valid Highs or Mediums are found, the HM pool is $0
    • Judge awards: $2,500 in USDC
    • Scout awards: $500 in USDC
  • Read our guidelines for more details
  • Starts April 28, 2025 20:00 UTC
  • Ends May 5, 2025 20:00 UTC

Note re: risk level upgrades/downgrades

Two important notes about judging phase risk adjustments:

  • High- or Medium-risk submissions downgraded to Low-risk (QA) will be ineligible for awards.
  • Upgrading a Low-risk finding from a QA report to a Medium- or High-risk finding is not supported.

As such, wardens are encouraged to select the appropriate risk level carefully during the submission phase.

Automated Findings / Publicly Known Issues

Note for C4 wardens: Anything included in this Automated Findings / Publicly Known Issues section is considered a publicly known issue and is ineligible for awards.

Donation Inflation Attack

Someone can donate a huge amount of raw INIT (to the tune of millions) to the pool without getting anything in return which would then cause an inflation attack as the new stakers would get a minuscule amount of sxINIT when staking.

Due to the absence of economic incentives around the attack, we consider it to be an acknowledged risk of the system.

Snapshot Balance Queries

Evaluating the balance of a user at a block height where a snapshot has not been taken is inaccurate. This is acceptable behaviour as the system concerns itself with accurate user balances after a snapshot has been taken.

L2 Desynchronization

When tokens are bridged to an L2, the system will lose track of them. In such cases, we trust that the L2 will provide the necessary data (balances, etc.) for us.

Rounding Errors

Miniscule decimal rounding of <1 unit may be observed when large numbers are utilized across the system.

Price Guarantees

Any issues around oracles misbehaving, misconfigured Time-Weighted Average Price (TWAP) setups, or other administrative / external misbehaviours are considered out-of-scope.

Stake Slashing

The system has been adequately equipped to handle slashing risks by querying the true staked amounts of a validator wherever needed.

Overview

Cabal is a liquid staking protocol built on Initia, allowing users to stake INIT (via xINIT/sxINIT) and whitelisted LP tokens, while also participating in a bribe marketplace to influence Initia's VIP gauge voting.

For an in-depth technical overview of the system including user flows, please consult the relevant documentation of the project. This link presently points to the C4 GitHub repository documentation and will be updated during the contest to a live documentation link.

The system has undergone two distinct audits with Zenith and Zellic. While the reports are not presently available, all issues have been fixed except for one known issue that has been explicitly outlined.

* This link will be updated during the contest to a live version of the project's documentation


Scope

Any test implementations within in-scope files (f.e. fun declarations prefixed with [#test-only]) are considered out-of-scope for the purposes of the contest. Additionally, TODO comments in relation to the configuration of the system are considered known issues.

The current state of the codebase is meant for a TESTING environment to ensure that the project's test suites run as smoothly as possible.

There are two instances in the code where this can be observed and needs to be changed to achieve a production-ready state of the system:

  • cabal.move
    • cabal::initialize: The // USE THIS FOR PROD function variant should be considered in scope as the // USE THIS FOR TESTING variant is not meant for production
  • snapshots.move
    • snapshots::update_snapshot: The mock_voting_power_weight invocation should be commented out and the preceding pool_router::get_voting_power_weight invocation should be uncommented as the mock_voting_power_weight function is out-of-scope

Files in scope

ContractSLOCPurposeLibraries used
sources/bribe.move317Handles the deposit and tracking of bribe rewards offered by external partiesstd, initia_std
sources/cabal.move993The main staking engine and user interaction hubstd, initia_std, vip
sources/cabal_token.move352Manages Cabal-specific tokens (xINIT, sxINIT, Cabal LPTs) and implements the lazy balance snapshotting mechanismstd, initia_std
sources/package.move81Shared addresses / signers, manages commission fee storage addressstd, initia_std
sources/pool_router.move480Acts as an abstraction layer managing interactions with underlying validators for different staked assetsstd, initia_std, vip
sources/snapshots.move124Utility implementation for snapshotsstd, initia_std
sources/utils.move65Oracle-related utility functionsstd, initia_std
sources/voting_reward.move162Calculates and distributes bribe rewards to eligible Cabal token holders based on historical snapshotsstd, initia_std
Total SLoC2574

See scope.txt for a machine-friendly list of in-scope files for the contest

Files out of scope

File
vip-contract/**.**
sources/emergency.move
sources/manager.move
tests/bribing_test.move
tests/core_staking_test.move
tests/core_unstaking_test.move
tests/deployer_auth_test.move
tests/emergency_stop_test.move
tests/lp_voting_test.move
tests/snapshot_test.move
tests/xinit_voting_test.move
Totals: 8

See out_of_scope.txt for a machine-friendly list of out-of-scope files for the contest

Scoping Q & A

General questions

QuestionAnswer
ERC20 used by the protocolCabal LPTs, xINIT, sxINIT
Test coverage~76.94% Total, ~70.54% Scope*
ERC721 used by the protocolNo
ERC777 used by the protocolNo
ERC1155 used by the protocolNo
Chains the protocol will be deployed onInitia (MoveVM)

* The practical code coverage of the system is significantly higher as several real-world user flows have been tested albeit with mock implementations due to the difficulty in executing live-code integrations in test suites

ERC20 token behaviors in scope

External integrations (e.g., Uniswap) behavior in scope:

QuestionAnswer
Enabling/disabling fees (e.g. Blur disables/enables fees)No
Pausability (e.g. Uniswap pool gets paused)No
Upgradeability (e.g. Uniswap gets upgraded)No

EIP compliance checklist

N/A

Additional context

Main invariants

xINIT Total Supply

The total supply of INIT locked in the system should approximately equate the total supply of xINIT.

totalinitlockedtotalsupplyxinittotal\\_init\\_locked \approx total\\_supply\\_xinit

xINIT / INIT Ratio

A unit of xINIT should approximately equate a unit of INIT, ignoring fees, slashing, and other mechanisms that might affect the conversion rate.

1 xINIT1 INIT1\ xINIT \approx 1\ INIT

Snapshot Balances

The sum of all individual snapshot balances (cabal_token::get_snapshot_balance) for a particular block height should equate the total snapshot supply (cabal_token::get_snapshot_supply) of the block height.

user=1totalusersgetsnapshotbalance(user,token,blockheight)=getsnapshotsupply(token,blockheight)\sum_{user=1}^{total\\_users}{get\\_snapshot\\_balance(user,token,block\\_height)} = get\\_snapshot\\_supply(token,block\\_height)

Cycle Reward Shares

The sum of voting_reward::get_cycle_reward_share measurements for all users should equate 1 for all block_height values that are linked to a cycle end (i.e. have been snapshotted) and wherein bribes have been observed.

user=1totalusersgetcyclerewardshare(user,blockheight)1,blockheightpastsnapshot\sum_{user=1}^{total\\_users}{get\\_cycle\\_reward\\_share(user, block\\_height)} \approx 1, block\\_height \in past\\_snapshot

If no bribes have been observed in a particular cycle that has ended, then the sum should equate 0.

user=1totalusersgetcyclerewardshare(user,blockheight)=0,blockheightpastsnapshot\sum_{user=1}^{total\\_users}{get\\_cycle\\_reward\\_share(user, block\\_height)} = 0, block\\_height \in past\\_snapshot

For any block_height that has not been snapshotted the result of this sum is indeterminate (i.e. can be any value).

Cycle Bribe Weights

The sum of individual weights in the calculation result of bribe weights for a particular cycle (bribe::calculate_bribe_weights_for_cycle) should approximately equate 1.

In other words, the sum of the weights of each individual Minitia (L2 bridge supported) should reach very close to or equate 1.

cyclebribeweights=calculatebribeweightsforcycle(cycle)cycle\\_bribe\\_weights = calculate\\_bribe\\_weights\\_for\\_cycle(cycle)
n=0length(cyclebribeweights)cyclebribeweights[n]1,cyclecalculatedcycles\sum_{n=0}^{length(cycle\\_bribe\\_weights)}{cycle\\_bribe\\_weights[n]} \approx 1, cycle \in calculated\\_cycles

For any cycle that has not been observed the result of this sum is indeterminate (i.e. can be any value).

Attack ideas (where to focus for bugs)

Function Correctness Focus

We are most interested in any vunlerabilities or bugs revolving around the following functions:

  • sources/cabal.move
    • deposit_init_for_xinit
    • process_xinit_stake
    • process_lp_stake
    • process_xinit_unstake
    • process_lp_unstake
  • sources/cabal_token.move
    • get_snapshot_balance
  • sources/bribe.move
    • deposit_bribe
  • sources/voting_reward.move
    • get_cycle_reward_share

Snapshotting Process

The snapshotting process implemented in sources/cabal_token.move cannot be manipulated in any way, both in terms of its validity during the maintenance of the snapshot as well as after it has been finalized.

Bribes & Voting Funds

Regardless of the outcome of bribes and voting as well as the processes involved, user principle amounts (xINIT, sxINIT, and Cabal LPTs) remain safe.

All trusted roles in the protocol

RoleDescription
DeployerPool Router (pool_router)<br>- Can change the validator of the pool router (change_validator)<br>Package (package)<br>- Can configure the commission fee address for bribes (set_commission_fee_store_addr)<br>Cabal Token (cabal_token)<br>- Can initialize the cabal_token implementation (initialize)<br>Any Module<br>- Can initialize several modules (init_module)
ManagerManager (manager)<br>- Can request a change of the manager address (change_manager_address)<br>- Can create roles and set their administrators (create_role, set_role_admin)<br>Emergency (emergency)<br>- Can set an emergency pause (set_pause)<br>Cabal Token (cabal_token)<br>- Can update L2 snapshot data (update_l2_data)<br>Voting Rewards (voting_reward)<br>- Can snapshot voting rewards (snapshot)<br>- Can finalize a voting reward cycle (finalize_reward_cycle)<br>Cabal (cabal)<br>- Can configure the stake token (config_stake_token)<br>- Can issue VIP votes (vote, vote_using_bribe_weights)<br>- Can exempt an address from fees (init_fees_exempt)<br>Pool Router (pool_router)<br>- Can add a pool to the pool router (add_pool)
Pending ManagerManager (manager)<br>- Can accept a manager change (accept_manager_proposal)
Role AdministratorManager (manager)<br>- Can add and remove role members (add_role_member, remove_role_member)<br>- Can renounce administratorship of a role (renounce_role_admin)

Describe any novel or unique curve logic or mathematical models implemented in the contracts:

The mechanism utilized for snapshotting involves lazy writes to save on gas; an approach that is unique to this project.

Running tests

Prerequisites

The project requires the initiad toolkit of the Initia project to compile the move codebase. Specifically:

The initiad toolkit has a direct dependency to Golang, so please make sure you have Golang setup for your machine. The compilation instructions were tested with Golang v1.24.2.

Building & Testing

The project relies on several #test-only preprocessing flags and thus cannot be compiled via the initiad move build command.

Additionally, the vip-contract dependency's compilation seems to fail even if the initiad move build --test flag is specified due to a potential compiler bug.

Instead, the initiad move test command should be issued to simultaneously compile the codebase and run tests:

initiad move test

Code Coverage

In order to generate code coverage, the initiad move test command should be issued alongside the --test and --coverage flags:

initiad move test --coverage --test

Miscellaneous

Employees of Cabal and employees' family members are ineligible to participate in this audit.

Code4rena's rules cannot be overridden by the contents of this README. In case of doubt, please check with C4 staff.