- 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
- HM awards: up to $20,000 USDC (Notion: HM (main) pool)
- 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.
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.
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.
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.
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.
Miniscule decimal rounding of <1 unit may be observed when large numbers are utilized across the system.
Any issues around oracles misbehaving, misconfigured Time-Weighted Average Price (TWAP) setups, or other administrative / external misbehaviours are considered out-of-scope.
The system has been adequately equipped to handle slashing risks by querying the true staked amounts of a validator wherever needed.
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.
- Previous audits:
- Private Zellic Audit Report
- Private Zenith Audit Report
- Documentation:
- High-Level: https://thecabal.xyz/docs/cabal
- Technical: https://github.com/code-423n4/2025-04-cabal/blob/main/DOCUMENTATION.md *
- Website: https://thecabal.xyz/
- X/Twitter: https://x.com/CabalVIP
- Discord: https://discord.gg/thecabal
* This link will be updated during the contest to a live version of the project's documentation
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.movecabal::initialize: The// USE THIS FOR PRODfunction variant should be considered in scope as the// USE THIS FOR TESTINGvariant is not meant for production
snapshots.movesnapshots::update_snapshot: Themock_voting_power_weightinvocation should be commented out and the precedingpool_router::get_voting_power_weightinvocation should be uncommented as themock_voting_power_weightfunction is out-of-scope
| Contract | SLOC | Purpose | Libraries used |
|---|---|---|---|
| sources/bribe.move | 317 | Handles the deposit and tracking of bribe rewards offered by external parties | std, initia_std |
| sources/cabal.move | 993 | The main staking engine and user interaction hub | std, initia_std, vip |
| sources/cabal_token.move | 352 | Manages Cabal-specific tokens (xINIT, sxINIT, Cabal LPTs) and implements the lazy balance snapshotting mechanism | std, initia_std |
| sources/package.move | 81 | Shared addresses / signers, manages commission fee storage address | std, initia_std |
| sources/pool_router.move | 480 | Acts as an abstraction layer managing interactions with underlying validators for different staked assets | std, initia_std, vip |
| sources/snapshots.move | 124 | Utility implementation for snapshots | std, initia_std |
| sources/utils.move | 65 | Oracle-related utility functions | std, initia_std |
| sources/voting_reward.move | 162 | Calculates and distributes bribe rewards to eligible Cabal token holders based on historical snapshots | std, initia_std |
| Total SLoC | 2574 |
See scope.txt for a machine-friendly list of in-scope files for the contest
| 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
| Question | Answer |
|---|---|
| ERC20 used by the protocol | Cabal LPTs, xINIT, sxINIT |
| Test coverage | ~76.94% Total, ~70.54% Scope* |
| ERC721 used by the protocol | No |
| ERC777 used by the protocol | No |
| ERC1155 used by the protocol | No |
| Chains the protocol will be deployed on | Initia (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
| Question | Answer |
|---|---|
| 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 |
N/A
The total supply of INIT locked in the system should approximately equate the total supply of xINIT.
A unit of xINIT should approximately equate a unit of INIT, ignoring fees, slashing, and other mechanisms that might affect the conversion rate.
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.
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.
If no bribes have been observed in a particular cycle that has ended, then the sum should equate 0.
For any block_height that has not been snapshotted the result of this sum is indeterminate (i.e. can be any value).
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.
For any cycle that has not been observed the result of this sum is indeterminate (i.e. can be any value).
We are most interested in any vunlerabilities or bugs revolving around the following functions:
sources/cabal.movedeposit_init_for_xinitprocess_xinit_stakeprocess_lp_stakeprocess_xinit_unstakeprocess_lp_unstake
sources/cabal_token.moveget_snapshot_balance
sources/bribe.movedeposit_bribe
sources/voting_reward.moveget_cycle_reward_share
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.
Regardless of the outcome of bribes and voting as well as the processes involved, user principle amounts (xINIT, sxINIT, and Cabal LPTs) remain safe.
| Role | Description |
|---|---|
| Deployer | Pool Router (pool_router)- Can change the validator of the pool router ( change_validator)Package ( package)- Can configure the commission fee address for bribes ( set_commission_fee_store_addr)Cabal Token ( cabal_token)- Can initialize the cabal_token implementation (initialize)Any Module - Can initialize several modules ( init_module) |
| Manager | Manager (manager)- Can request a change of the manager address ( change_manager_address)- Can create roles and set their administrators ( create_role, set_role_admin)Emergency ( emergency)- Can set an emergency pause ( set_pause)Cabal Token ( cabal_token)- Can update L2 snapshot data ( update_l2_data)Voting Rewards ( voting_reward)- Can snapshot voting rewards ( snapshot)- Can finalize a voting reward cycle ( finalize_reward_cycle)Cabal ( cabal)- Can configure the stake token ( config_stake_token)- Can issue VIP votes ( vote, vote_using_bribe_weights)- Can exempt an address from fees ( init_fees_exempt)Pool Router ( pool_router)- Can add a pool to the pool router ( add_pool) |
| Pending Manager | Manager (manager)- Can accept a manager change ( accept_manager_proposal) |
| Role Administrator | Manager (manager)- Can add and remove role members ( add_role_member, remove_role_member)- Can renounce administratorship of a role ( renounce_role_admin) |
The mechanism utilized for snapshotting involves lazy writes to save on gas; an approach that is unique to this project.
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.
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 testIn order to generate code coverage, the initiad move test command should be issued alongside the --test and --coverage flags:
initiad move test --coverage --testEmployees 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.