DEV Community

Daniel
Daniel

Posted on

VOLOSC 194: How an Accepted Direct Theft Primitive Ended as a USD 500 Medium

A technical account of a direct theft primitive, 151 days of review, and the incentive problem behind responsible disclosure in Web3

On 27 March 2026, at 00:05, I submitted VOLOSC 194 to the Volo Smart Contracts bug bounty program through HackenProof. The report was titled Curator valuation updater can overwrite the principal slot and reopen direct NAV manipulation, and I submitted it as Critical because the issue did not stop at an incorrect number inside the vault. The proof of concept showed a complete accounting path in which an ordinary caller could redirect a legitimately approved curator value into the vault principal slot, inflate NAV, inflate the share ratio, and ultimately cause the normal withdrawal logic to pay substantially more real principal for the same number of shares.

The numbers in the proof were deliberately simple. In the honest control path, 100000000000 shares returned 100000000000 units of principal. In the exploit path, the same 100000000000 shares returned 1100000000000. The vault ended with free_principal = 0. That was the reason I did not view the issue as a theoretical accounting discrepancy. The corrupted value was consumed by a real withdrawal calculation and reached a real balance split.

The first review response recognized essentially the same root cause and consequence. The vulnerable code was later fixed, and months afterward the final HackenProof triage comment described the report as an accepted direct theft primitive. Even so, after 150 days, 23 hours, and 40 minutes of review, the report ended as Medium.

What makes this case interesting is not simply that I disagreed with the final severity. The technical facts changed as the review progressed. HackenProof eventually produced an onchain chronology showing that the vulnerable package had been fixed shortly before the first curator position was created. I also had to correct one of my own later conclusions about version 10. By the end, the disagreement was no longer about whether the code defect existed. It had become a much narrower question about how severity should be measured when vulnerable code is already deployed, but responsible disclosure allows the team to fix it before the final legitimate state required for exploitation becomes active.

That is the question I want to document here, together with the complete technical path and the evidence that led both sides to their final positions.

To keep the article readable, I am reproducing only four screenshots from the larger evidence archive. The technical and chronological details below still reflect the complete record I reviewed, but the images are limited to the points that are most useful for a reader following the dispute from beginning to end.

Initial VOLOSC 194 report state showing Critical severity and Triaged status

The case in one table

Item Recorded result
Report VOLOSC 194
Target Volo Smart Contracts
Platform HackenProof
Category Business Logic Errors
Submitted 27 March 2026 at 00:05
Submitted severity Critical
Final severity Medium
Payment Paid
Time to first review response 1 day, 9 hours, and 34 minutes
Time from submission to Medium decision 150 days, 23 hours, and 40 minutes

Before going into the code, there is one communication detail worth clarifying. The screenshots preserve two different channels. At several points, an account identified by the HackenProof interface as company admin responded directly inside the report, mainly on technical and operational questions. At other points, especially around goodwill, severity, payout, escalation, and the final decision, the messages came from HackenProof triage. I keep those roles separate throughout the article because attributing every statement to the same party would make the chronology less accurate.

Where the vulnerability lived

The flaw was inside update_curator_position_value. The function existed to take a curator position value that had already gone through the intended curator process and update the value stored by the vault. The dangerous part was not the existence of the curator value itself. The dangerous part was that the function accepted an asset_type string from the caller and never proved that this string represented the curator position being updated.

That detail matters because the principal coin type already exists as an accounting slot when the vault is created. The report highlighted the initialization that registers the principal asset inside the vault:

vault.set_new_asset_type(
    type_name::get<PrincipalCoinType>().into_string()
);
Enter fullscreen mode Exit fullscreen mode

The curator position is also paired with a vault before it later becomes a supported vault asset. The relevant creation path stores that relationship and initializes the value record:

self.curator_position_to_vault.add(
    id_address,
    vault_id
);

self.curator_position_values.add(
    id_address,
    CuratorPositionValue {
        position_value: 0,
        position_value_updated: 0,
        position_value_valid_time: 0,
        valid: false,
    },
);
Enter fullscreen mode Exit fullscreen mode

This distinction became important in the proof of concept. The curator position could already be paired with the vault while the curator asset was still not a normal supported vault asset. The attack therefore did not require adding the curator position as a supported asset first and then corrupting its own value. The state confusion happened through the destination selected by the public updater.

The value used by the exploit also did not need to be fabricated. A curator could submit a legitimate value through the intended privileged path:

self.assert_valid_curator_cap(
    curator_position_id,
    curator_cap.id
);

self.update_position_value(
    curator_position_id,
    position_value,
    now,
    valid_time
)
Enter fullscreen mode Exit fullscreen mode

An operator could then validate that value normally:

if (approved) {
    curator_position_value.valid = true;
}
Enter fullscreen mode Exit fullscreen mode

The attacker comes after those legitimate steps. That distinction is one of the reasons I considered the bug serious: the exploit did not require compromising the curator, forging an approval, or obtaining one of the privileged capability objects. It reused a valid approved value and abused where the public function allowed that value to be written.

The vulnerable updater looked like this in the relevant path:

public fun update_curator_position_value<PrincipalCoinType>(
    self: &mut CuratorConfig,
    vault: &mut Vault<PrincipalCoinType>,
    asset_type: String,
    curator_position_id: address,
    oracle_config: &OracleConfig,
    clock: &Clock,
) {
    let now = clock.timestamp_ms();

    assert!(
        self.curator_position_to_vault[curator_position_id]
            == vault.vault_id(),
        ERR_CURATOR_POSITION_NOT_PAIRED_WITH_VAULT
    );

    self.assert_valid_curator_position_value(
        curator_position_id,
        now
    );

    let principal_based_position_value =
        self.position_value(curator_position_id);

    let principal_price =
        vault_oracle::get_normalized_asset_price(
            oracle_config,
            clock,
            type_name::with_defining_ids<PrincipalCoinType>()
                .into_string(),
        );

    let position_value =
        vault_utils::mul_with_oracle_price(
            principal_based_position_value,
            principal_price,
        );

    vault.finish_update_asset_value(
        asset_type,
        position_value,
        now
    );
}
Enter fullscreen mode Exit fullscreen mode

The function correctly checked that the curator position belonged to the supplied vault, but that check protected a different invariant from the one I reported. It prevented a curator position paired to one vault from simply being used against another vault. It did not prove that the caller supplied asset_type represented the curator position inside the correct vault.

That is why I described the root cause as same vault slot confusion rather than cross vault access. The caller could be operating on the correct vault and still select the wrong registered accounting slot.

The destination string then flowed directly into the generic storage write:

public(package) fun finish_update_asset_value<PrincipalCoinType>(
    self: &mut Vault<PrincipalCoinType>,
    asset_type: String,
    usd_value: u256,
    now: u64,
) {
    let last_update_time =
        &mut self.assets_value_updated[asset_type];

    *last_update_time = now;

    let position_value =
        &mut self.assets_value[asset_type];

    *position_value = usd_value;
}
Enter fullscreen mode Exit fullscreen mode

Once execution reached this function with the principal asset type, the stored principal valuation was replaced. The real principal balance did not increase with it, so the vault accounting could now claim a principal value that was much larger than the actual backing.

There was another detail in the codebase that made the missing check particularly notable. A separate coin asset updater already prevented the principal coin type from going through the generic asset valuation path:

assert!(
    type_name::get<CoinType>()
        != type_name::get<PrincipalCoinType>(),
    ERR_INVALID_COIN_ASSET_TYPE,
);
Enter fullscreen mode Exit fullscreen mode

In other words, the codebase already recognized that the principal slot should not be treated like an ordinary generic asset slot. The curator updater reopened a path to that same storage without enforcing the equivalent identity constraint.

How an accounting overwrite became a withdrawal of real principal

The next question was whether corrupting the stored principal value actually mattered financially. In this case, the value did not remain isolated. The vault aggregated stored asset values through get_total_usd_value:

self.asset_types.do_ref!(|asset_type| {
    let usd_value =
        *self.assets_value.borrow(*asset_type);

    total_usd_value =
        total_usd_value + usd_value;
});
Enter fullscreen mode Exit fullscreen mode

That total then fed into get_share_ratio:

let total_usd_value =
    self.get_total_usd_value(clock);

let share_ratio =
    vault_utils::div_d(
        total_usd_value,
        self.total_shares
    );
Enter fullscreen mode Exit fullscreen mode

The withdrawal path consumed the resulting ratio to calculate the amount of principal associated with the shares being withdrawn:

let ratio =
    self.get_share_ratio(clock);

let usd_value_to_withdraw =
    vault_utils::mul_d(
        shares_to_withdraw,
        ratio
    );

let amount_to_withdraw =
    vault_utils::div_with_oracle_price(
        usd_value_to_withdraw,
        vault_oracle::get_normalized_asset_price(
            config,
            clock,
            type_name::get<PrincipalCoinType>()
                .into_string(),
        ),
    ) as u64;

assert!(
    amount_to_withdraw
        <= self.free_principal.value(),
    ERR_NO_FREE_PRINCIPAL
);

let mut withdraw_balance =
    self.free_principal.split(
        amount_to_withdraw
    );
Enter fullscreen mode Exit fullscreen mode

That created a direct chain from the public corruption primitive to a real balance movement. The approved curator value could be written into the principal accounting slot, the forged value could inflate total NAV, the inflated NAV could increase the share ratio, and the withdrawal calculation could then split more actual principal from free_principal than the same shares should receive.

The proof of concept made that difference visible by running an honest control and the exploit from equivalent state:

Metric Honest control Exploit path
Free principal before withdrawal 1100000000000 1100000000000
Principal slot before overwrite 1100000000000 1100000000000
Principal slot before withdrawal 1100000000000 12100000000000
Share ratio before withdrawal 1000000000 11000000000
Shares withdrawn 100000000000 100000000000
Principal payout 100000000000 1100000000000
Free principal after exploit Residual backing remains 0

The same number of shares therefore received eleven times more principal in the exploit path. The Move test completed successfully with one test passed and zero failures. The curator value had been legitimately approved, the corruption step was performed by an ordinary caller, and the financial consequence appeared when the normal withdrawal path trusted the manipulated share ratio.

That is the technical basis on which I submitted the report as Critical.

From technical validation to a dispute over prior knowledge

The report received its first review response on 28 March at 09:39, 1 day, 9 hours, and 34 minutes after submission. The account visible in the report recognized the core issue and described essentially the same sequence: arbitrary asset type selection, generic asset value storage, principal slot overwrite, inflated total USD value, inflated share ratio, and drainage of free_principal through a normal withdrawal. It also referenced the honest and exploit payout difference from the proof of concept.

At that point, the main technical primitive was understood. The first major dispute appeared on 6 April at 16:30, when a participant identified by the interface as company admin responded directly. The message said the report was a valid issue but claimed that a similar problem had already been received from an audit firm. A screenshot was attached showing another reviewer questioning the same public function and the ability to associate a curator position value with another asset type.

The screenshot was technically relevant, but it did not visibly establish the chronology required for a duplicate determination. It did not show the audit firm name, a report date, a finding identifier, an affected version range, or other metadata that would prove the material predated my 27 March submission. I therefore asked for objective prior discovery evidence rather than treating the screenshot alone as conclusive.

Prior audit screenshot supplied during the duplicate discussion

At the same time, I separated the duplicate question from another issue that would eventually become far more important: production reachability. If the report was not going to be treated as a proven duplicate, what exact production condition prevented the demonstrated path from being reachable when I submitted it?

That question changed the direction of the review.

The argument moved to production reachability

On 16 April at 00:29, the company admin account responded again. The protocol side argued that the attack was not feasible because creating the curator capability required an administrator, because a curator position could only affect the vault to which it was paired, and because an operator remained involved in the withdrawal process.

I disagreed with the way those points were being applied to the reported root cause. The same vault pairing assertion did not prevent the caller from choosing the wrong slot inside that same vault, which was the actual issue. I also did not see the privileged curator and operator steps as equivalent to an attacker needing those privileges. The curator could approve a legitimate value, and the operator could execute the protocol's normal withdrawal flow. The malicious state corruption itself still happened through a public updater that did not require AdminCap, OperatorCap, or CuratorCap.

On 18 April at 02:26, HackenProof triage made an important clarification. It said the available audit material was not sufficient by itself to conclusively prove prior discovery of the same root cause and exploit path before my submission, so the review would not rely on duplicate classification on that evidence alone. The report was described as a meaningful code level issue, but production reachability remained the central concern. HackenProof triage also communicated that the protocol team wanted to provide a goodwill payment while the severity dispute remained unresolved.

I did not consider that goodwill payment a resolution of the severity question. I asked instead for the concrete onchain state that prevented the exploit path from being reachable during the vulnerable deployment window.

A separate security incident, and why I did not attribute it to this bug

On 21 April, while the report was still under discussion, Volo publicly disclosed a separate security incident involving approximately USD 3.5 million in WBTC, XAUm, and USDC removed from Volo Vaults. The protocol said the affected vaults were frozen and that it was prepared to absorb the loss. A later update said approximately half a million dollars associated with the incident had been frozen, and NAVI Protocol also announced a precautionary pause while reviewing the situation.

I added those public statements to the report because the discussion at the time was still focused on whether the reported issue could be practically relevant in production. I did not claim that the April incident used VOLOSC 194, and I do not make that claim now. The later onchain chronology established that the principal overwrite path from my report had already been closed on 3 April, before the first curator position existed. Without transaction level evidence connecting the April incident to the same root cause, attributing it to this finding would go beyond the evidence.

The evidence that finally narrowed the dispute

After my 21 April update, the report became quiet. I followed up again on 5 May at 12:15. The next substantive technical response visible in the evidence did not arrive until 12 August at 17:43, which was another 99 days, 5 hours, and 28 minutes later. Measured from my 18 April request for concrete production state evidence, the detailed answer took 116 days, 9 hours, and 23 minutes.

That August response was important because it finally supplied the onchain history needed to answer the reachability question. The vulnerable package was identified as:

0x7a663d394fa9ea6bac32a860957014ec951730f79f3fa1ec73d261463f13b1b1

It had been deployed on 2 March 2026 at 07:24 UTC. My report was submitted on 27 March at 00:05 in the interface timezone, which means the vulnerable package had already been live for 24 days, 19 hours, and 41 minutes before submission after timezone normalization.

The corrected package was:

0x8d9b38f82fcfc70a869eac1f7cefa871e9f22360aab94224f6bf751c1b9d7a2b

It was deployed on 3 April at 04:18:06 UTC. The first curator position was then created at 05:25:56 UTC, the first curator value was submitted at 07:09:12 UTC, and the first approved curator value appeared at 07:11:31 UTC.

UTC time Event Why it matters
3 April 2026 04:18:06 Corrected package deployed The reported caller selected principal overwrite path was closed
3 April 2026 05:25:56 First curator position created The first curator position required by the path appears
3 April 2026 05:55:02 Curator cap added Additional curator configuration occurs
3 April 2026 06:49:55 Curator position added as DeFi asset The position becomes a supported vault asset
3 April 2026 07:09:12 First curator value submitted A value enters the intended curator flow
3 April 2026 07:11:31 First curator value approved The approved value used by the proof of concept now exists

The corrected package therefore preceded the first curator position by 1 hour, 7 minutes, and 50 seconds. It preceded the first approved curator value by 2 hours, 53 minutes, and 25 seconds.

This is the strongest fact supporting HackenProof's eventual downgrade. The vulnerable package and the legitimate curator state required by the proof of concept did not coexist on mainnet. Before a curator position was registered, the required curator_position_to_vault lookup could not satisfy the attack path.

I accept that chronology.

The same August review also corrected something I had previously gotten wrong. During my own follow up investigation on 3 April and 4 April, I concluded that version 10 still preserved the original vulnerability. That conclusion was incorrect. The later source and bytecode analysis showed that the new validation was an equality allowlist that bound asset_type to the curator asset type:

let curator_asset_type =
    vault_utils::parse_key<CuratorPosition>(0);

assert!(
    self.curator_position_to_vault[curator_position_id]
        == vault.vault_id(),
    ERR_CURATOR_POSITION_NOT_PAIRED_WITH_VAULT
);

assert!(
    asset_type == curator_asset_type,
    ERR_INVALID_CURATOR_ASSET_TYPE
);
Enter fullscreen mode Exit fullscreen mode

Passing the principal coin type used by the original proof of concept now aborts with ERR_INVALID_CURATOR_ASSET_TYPE, value 8013, before the write can occur. The later bytecode comparison also showed that version 10 and the subsequent package contained the same corrected curator module.

I accepted that correction as well. It means the approximately USD 25.7 million in publicly displayed vault TVL that I mapped during my interim analysis cannot be presented as capital that remained exposed to this specific path after version 10. That was an error in my follow up analysis, and a complete public account should say so plainly.

By this point, most of the factual disagreement had disappeared. The earlier deployed code contained the defect. Version 10 fixed it. The fix arrived before the first curator position. Had the curator feature been active under the vulnerable package, the demonstrated path would have been reachable. The question left was how those facts should translate into severity.

From Informational to an independent escalation

On 13 August at 02:22, HackenProof triage communicated that the report would remain Informational. The reasoning was that the required curator state did not exist before the corrected package was deployed, so the reported path had not been reachable against live funds during the relevant production window. The payment being discussed at that stage was described as discretionary recognition for the usefulness and quality of the report rather than as a normal severity based bounty.

That sequence mattered to me because the payment had been presented as goodwill while the report itself was considered Informational. I opened a HackenProof support ticket and asked for an independent review. My question was not simply whether the payment could be increased. I wanted to understand the rule being applied. Specifically, I asked what published criterion applicable on 27 March made a valid vulnerability in deployed, in scope code Informational because a legitimate prerequisite protocol state had not yet been instantiated before the vulnerability was fixed.

The support discussion continued through August. I repeatedly separated the payment question from the severity question because the central issue was not whether the bounty amount was a nice gesture. It was whether the report qualified for a normal bounty tier and, if so, how the demonstrated impact should be classified.

On 24 August at 23:45, HackenProof triage said the matter had been reviewed with the project team and that the report would be reclassified as Medium. The previously discussed payment would be retained and processed. From the original submission to that message, 150 days, 23 hours, and 40 minutes had passed.

I appreciated that the Informational classification had been withdrawn, but the technical question was still open. I asked why the final tier was Medium rather than High or Critical.

The final technical basis for Medium

On 26 August at 08:39, HackenProof finally gave the most precise explanation of the severity decision. The response said the program considered three dimensions together: caller privilege for the corruption step, reachability at the time of submission, and the structure of the monetization path.

On caller privilege, HackenProof agreed with an important part of the original report. update_curator_position_value could be reached without AdminCap, OperatorCap, or CuratorCap, which meant the state corruption itself was unprivileged. The response explicitly said this was what placed the report in a bounty tier at all.

The decisive issue was reachability. HackenProof relied on the August chronology and noted that the vulnerable code and an activated curator position with an approved value never coexisted in production. In its explanation, Critical was calibrated to a demonstrated path against live funds at the time of submission, while High required a reachable path with material fund loss. Because the curator state required by the proof of concept did not exist during the vulnerable deployment window, HackenProof treated that gap as preventing promotion into either tier.

The third factor was monetization structure. HackenProof also argued that even in a reachable state, the transfer from corrupted NAV to the attacker did not complete in one entirely unprivileged call. execute_withdraw was operator gated, and the response also referenced the MAX_UPDATE_INTERVAL = 0 freshness condition. In their view, that was a separate structural constraint that further prevented the finding from clearing High.

This was the first time the final Medium classification had been explained with that level of specificity, and it allowed the remaining disagreement to be stated much more precisely.

I asked where those requirements were written

After receiving that explanation, I reviewed the publicly available Volo Smart Contracts rules and HackenProof's public Smart Contract Vulnerability Classification. I could find broad impact categories covering direct financial loss, unauthorized fund access, unauthorized extraction capable of draining protocol treasury or user deposits, direct theft, and protocol insolvency. I could also find a general statement that privileged actions may justify a downgrade.

What I could not find was an express rule saying that Critical required the complete exploit path to be reachable against live funds at the exact moment of submission, or that High or Critical required a fully unprivileged single caller monetization primitive. I therefore asked HackenProof to identify the specific publicly applicable rule containing those requirements, or to clarify whether they were part of the platform's internal severity methodology.

HackenProof responded at 10:30 on 26 August. The answer did not point to a separate rule containing the exact wording I had requested. Instead, it explained that this was standard smart contract triage practice and not a novel or retrospective methodology. According to the response, severity is tied to demonstrated impact, and impact is evaluated against what the exploit chain can actually accomplish against protocol state at the time of submission rather than against a state that had not yet been instantiated.

The most important sentence in that response was that the reachability gap was dispositive for tier calibration. That clarified the hierarchy of the final reasoning. Operator gated settlement remained a secondary constraint, but even without relying on it, HackenProof considered the production reachability gap enough to prevent High or Critical.

The determination was described as having been made at senior review level. HackenProof also acknowledged that reasonable disagreement was possible about how much weight should be placed on the reachability gap compared with the code level validity of the primitive. That is where the private severity dispute effectively ended.

The phrase that defined the final outcome

At 10:32, HackenProof triage added a final report comment after further review with the project team. That comment confirmed the core root cause in direct terms: the caller supplied asset_type was not bound to the passed curator_position_id, so an approved curator value could be written into another registered assets_value slot, including the vault principal slot. It also confirmed that the version 10 remediation directly closed the reported path and matched the type of fix proposed in the report.

The comment confirmed Medium severity and described the finding as an accepted direct theft primitive whose classification remained constrained by the production reachability gap.

For me, that phrase captures the entire case. By the end of the review, the disagreement was not whether the primitive could produce direct theft under the demonstrated state. It was whether the absence of that legitimate state during the vulnerable production window should prevent the demonstrated impact from controlling the severity.

Final HackenProof triage comment confirming Medium and describing the finding as an accepted direct theft primitive

The bounty was paid

The USD 500 bounty was subsequently paid, and the report moved to a final Medium and Paid state.

There is one economic comparison that deserves to be put into perspective. During my investigation, I had mapped more than USD 25 million in TVL across the vaults I was analyzing. The later review established that the vulnerable package was fixed before the curator state required by the proof of concept became active, so I am not presenting that TVL as capital proven to have been immediately exploitable through VOLOSC 194. The important point is that responsible disclosure prevented the vulnerable implementation and the intended curator state from ever coexisting in production.

Against a reference base of USD 25 million in TVL, the USD 500 bounty I received represents roughly 0.002%. Even if the report had been classified as Critical and the program had paid its advertised maximum bounty of USD 150,000, that maximum would still represent only about 0.6% of USD 25 million.

The contrast is difficult to ignore. A researcher can spend weeks understanding a protocol, finding a vulnerability, building an executable proof of concept, demonstrating a path to real financial loss, documenting the root cause, and giving the team enough time to remediate the issue before users are exposed, while the economic reward for choosing responsible disclosure remains tiny compared with the scale of capital the protocol manages.

Unfortunately, this is still the incentive structure researchers frequently face in Web3. The ecosystem asks researchers to choose responsible disclosure over exploitation, yet the financial reward for protecting protocol capital can be negligible compared with both the capital under management and the potential upside available to an attacker. If the goal is to make researchers report vulnerabilities early, before every prerequisite is live and before users lose funds, then responsible disclosure has to make sense economically as well as ethically.

Final VOLOSC 194 report state showing Medium severity and Paid status

The chronology in context

A compact timeline makes the progression easier to see without replacing the narrative above.

Date What happened
2 March 2026 The vulnerable package was deployed
27 March 2026 VOLOSC 194 was submitted as Critical
28 March 2026 The first review response recognized the root cause and withdrawal consequence
3 April 2026 at 04:18:06 UTC The corrected package was deployed
3 April 2026 at 05:25:56 UTC The first curator position was created
3 April 2026 at 07:11:31 UTC The first curator value was approved
6 April 2026 The protocol side said the issue was valid but already known
18 April 2026 HackenProof stopped relying on the available audit screenshot alone as conclusive duplicate evidence and communicated a goodwill proposal
21 April 2026 Volo disclosed a separate security incident, which I documented without attributing it to this finding
5 May 2026 I followed up after the report became quiet
12 August 2026 The detailed technical and onchain review arrived and corrected my version 10 analysis
13 August 2026 Informational plus a goodwill payment was communicated as the position at that stage
24 August 2026 The report was reclassified to Medium while retaining the previously discussed payment
26 August 2026 at 08:39 HackenProof provided the detailed privilege, reachability, and monetization rationale
26 August 2026 at 10:30 The senior review response said the reachability gap was dispositive for severity calibration
26 August 2026 at 10:32 The final report comment confirmed Medium, the root cause, the fix, and the accepted direct theft primitive characterization
Final state The bounty was paid

Where I agree with the final review

There is no value in pretending the later evidence did not change the case. It did. Version 10 closed the reported principal overwrite path. The corrected package was already live before the first curator position existed, and the first approved curator value appeared only after the fix. My earlier version 10 conclusion was wrong, and the TVL I mapped during that interim analysis cannot be used as evidence that the same path remained exploitable after the correction. The final withdrawal was also operator mediated, which is a real structural characteristic of the monetization path.

I also accept that HackenProof did ultimately arbitrate the dispute rather than simply leaving the protocol's initial position untouched. The case was reopened, Informational became Medium, a senior review was performed, and a detailed technical rationale was finally provided. Any fair account of the process has to acknowledge those facts.

At the same time, those concessions do not erase the original primitive. The vulnerable implementation was deployed before my report. The caller controlled asset type confusion was real. The principal slot could be selected as the destination for an approved curator value. The proof of concept demonstrated the resulting NAV inflation, share ratio inflation, and withdrawal of real free_principal once the legitimate curator state existed. The protocol changed the exact write target, regression coverage was added, and the final HackenProof comment itself described the finding as an accepted direct theft primitive.

That is the factual foundation of my remaining disagreement.

Why I still consider VOLOSC 194 Critical

The strongest argument against Critical is no longer ambiguous: the legitimate curator state required by the proof of concept did not exist while the vulnerable package was live. HackenProof considers that reachability gap decisive. I understand the reasoning, but I do not reach the same severity conclusion.

The vulnerable implementation was already deployed, and the missing state was not an invented configuration unrelated to the protocol. It was the intended curator functionality that the system was about to use. The first curator position appeared only 1 hour, 7 minutes, and 50 seconds after the fixed package went live, and the first approved curator value appeared less than three hours after the fix. Had remediation not landed first, the same public corruption primitive demonstrated in the proof of concept would have become reachable as soon as that intended state appeared.

When reachable, the consequence was direct principal loss. The bug did not merely cause a stale display value or a temporary mismatch that could not affect funds. The manipulated principal slot entered total NAV, the inflated NAV changed the share ratio, and the withdrawal path used that ratio to split real principal from the vault. The final HackenProof comment's own description, accepted direct theft primitive, is consistent with the impact I demonstrated.

This is where my methodology differs from HackenProof's final methodology. I assess the severity of the deployed vulnerable primitive from the demonstrated consequence under the intended protocol state that was about to become active. HackenProof calibrated the tier against the exact production state that existed while the vulnerable package remained deployed. Under that interpretation, successful remediation before curator activation is precisely what prevents the direct theft consequence from reaching High or Critical.

Under mine, the same sequence demonstrates that responsible disclosure worked before the vulnerable code and the intended state could coexist.

That is why my own assessment remains Critical.

The part that matters beyond this one bounty

Beyond the severity label itself, this case raises a broader question about incentives. Bug bounty programs exist because researchers sometimes discover vulnerabilities before attackers exploit them. At that moment, the researcher may already understand how the issue could be monetized. The program asks the researcher to choose disclosure instead: build the proof, document the root cause, avoid harming users, provide the details privately, and give the protocol time to fix the problem.

That model works only when early disclosure is rewarded rather than inadvertently penalized. VOLOSC 194 sits in an uncomfortable edge case because the disclosure happened while the vulnerable implementation was already deployed but before the final legitimate curator state existed. The protocol then fixed the root cause, and only afterward did the first curator position and approved value appear.

From a defensive security perspective, that is close to the ideal outcome. The vulnerability was found early enough that the dangerous combination of vulnerable code and active curator state never existed on mainnet. No user needed to lose funds through this path for the code to be fixed.

The incentive problem appears if that success is also what lowers the severity. Had a researcher intentionally waited for the curator state to be activated before reporting, the same vulnerable implementation would have had a stronger contemporaneous reachability argument. Reporting earlier made the protocol safer. Waiting would have made the severity case stronger.

I do not believe a researcher should wait, and I am not advocating exploitation. Draining a protocol is harmful, unlawful, and incompatible with responsible security research. The point is exactly the opposite: the bounty model should make the safe behavior economically rational as well as ethically correct. Researchers should never have to wonder whether disclosing before the last legitimate prerequisite becomes active will make the vulnerability they prevented worth less than it would have been if they had waited.

That is the incentive inversion I think Web3 bounty programs need to take seriously.

What I think a platform should provide

The final stage of the review also changed how I would describe HackenProof's role. It would be inaccurate to say the platform never arbitrated the dispute. The report was reopened, the Informational classification was changed to Medium, a senior review was performed, and HackenProof eventually provided a detailed explanation of the methodology used.

My remaining concern is predictability rather than the absence of review. If production reachability at the exact moment of submission can be dispositive even when the missing state is an intended feature activated immediately after remediation, that principle has a major effect on how researchers should understand severity. The same is true if operator mediated settlement materially caps a finding even when the operator is executing the protocol's normal flow after an unprivileged corruption.

Those may be familiar considerations inside professional smart contract triage, but the more decisive they are for classification and payout, the more useful it is for researchers to understand them before disclosure. The platform sits between a protocol that wants vulnerabilities fixed while controlling cost and a researcher who gives up the information advantage as soon as the report is submitted. Clear, predictable severity treatment is part of what makes that intermediary relationship valuable.

In my view, the strongest bounty programs are not simply the ones with large maximum rewards. They are the ones where a researcher can reasonably predict how the demonstrated exploit, required privileges, production state, and settlement path will be translated into severity before spending weeks or months defending the report.

Would I report the vulnerability again?

Yes. Exploitation is not an alternative, and users should not become collateral damage in a disagreement about bounty economics. But the ecosystem should not take that answer for granted from every researcher forever.

From a security perspective, the report achieved the outcome responsible disclosure is supposed to achieve. A reproducible proof was delivered, the root cause was understood, the vulnerable write target was fixed, regression coverage was added, and the intended curator state became active only after remediation. The vulnerable code never got the opportunity to coexist with the final prerequisite needed by the proof of concept.

The unresolved question is how a bounty system should value that outcome.

Final thoughts

After almost five months, VOLOSC 194 ended in a much more precise place than where it began. The report was not dismissed as invalid, the available prior audit screenshot was not enough on its own to conclusively establish a duplicate, the version 10 fix was verified, and I corrected my own mistaken follow up analysis. The production reachability gap was established onchain, the Informational classification was withdrawn, and a senior review ultimately concluded Medium. The final report comment confirmed the root cause, confirmed that the remediation closed the reported path, and described the vulnerability as an accepted direct theft primitive.

I respect the fact that HackenProof eventually provided a detailed technical rationale and explicitly acknowledged that reasonable disagreement was possible over the weight assigned to the reachability gap. I still disagree with the severity. To me, a deployed public corruption primitive capable of redirecting a legitimately approved curator value into principal accounting, inflating NAV and the share ratio, and producing direct principal loss under the intended curator state is Critical in technical impact. HackenProof reached a different conclusion because the required curator state was not instantiated until after the vulnerable package had been fixed, making that reachability gap decisive under its impact based methodology.

That is the final disagreement, and it matters because it touches the core incentive behind responsible disclosure. The safest outcome is not for a researcher to wait until every prerequisite is live and capital is immediately exposed. It is to report one step earlier, while the protocol still has time to remove the dangerous primitive before the final state becomes active.

If early disclosure achieves exactly that, the reward system should not leave researchers wondering whether waiting would have produced a better economic outcome. Web3 security depends on researchers choosing disclosure over exploitation, and the ecosystem should make that choice obvious by rewarding the people who find vulnerabilities, prove their impact, report them early, and give protocols the opportunity to protect users before an attacker arrives.

Top comments (0)