hats-finance / Most--Aleph-Zero-Bridge-0xab7c1d45ae21e7133574746b2985c58e0ae2e61d

Aleph Zero bridge to Ethereum
Apache License 2.0
0 stars 1 forks source link

committee_sizes is not properly updated #27

Open hats-bug-reporter[bot] opened 5 months ago

hats-bug-reporter[bot] commented 5 months ago

Github username: @rodiontr Twitter username: -- Submission hash (on-chain): 0x4b2b1181f80fffc7b9398573841ce7bc0d171de481009f1ef332f2ad124273b8 Severity: medium

Description: Description\

committee_sizes variable is not updated when calling set_committee() function that may put the contract into an undesirable state as the variable is used for accounting purposes.

Attack Scenario\

In the lib.rs, there is a function called set_committee() that effectively can change the committee. However, the new length of the committee is not inserted into committee_sizes like it’s done in the constructor():

https://github.com/Cardinal-Cryptography/most/blob/70ab234cc3322fda82784413f5e0704907a0e1fe/azero/contracts/most/lib.rs#L238-239

  let mut committee_sizes = Mapping::new();
            committee_sizes.insert(committee_id, &(committee.len() as u128));

And how it's done in the set_committee():

self.ensure_owner()?;
            self.ensure_halted()?;
            Self::check_committee(&committee, signature_threshold)?;

            let mut data = self.data()?;

            let committee_id = data.committee_id + 1;
            let mut committee_set = Mapping::new();
            committee.into_iter().for_each(|account| {
                committee_set.insert((committee_id, account), &());
            });

            self.committees = committee_set;
            data.committee_id = committee_id;

            self.data.set(&data);

            self.signature_thresholds
                .insert(committee_id, &signature_threshold);

            Ok(())
        }

As this mapping is used for accounting purposes, all the places where it’s used will refer to outdated size:

   let total_amount = self
                .get_collected_committee_rewards(committee_id)
                .checked_div(
                    self.committee_sizes
                        .get(committee_id)
                        .ok_or(MostError::NoSuchCommittee)?,
                )
                .ok_or(MostError:

Recommendations

Add the following line to the set_commitee() function:

  committee_sizes.insert(committee_id, &(committee.len() as u128));
rodiontr commented 5 months ago

PoC:

As you can see here, we first set commitee_sizes to 5 in the constructor, then set new committee with only alice later again

 #[ink::test] 
        fn incorrect_commitee_size() {
            let accounts = default_accounts::<DefEnv>();
            set_caller::<DefEnv>(accounts.alice);

            // create new contract
            let mut most = Most::new(
                guardian_accounts(), // 5
                THRESHOLD,
                POCKET_MONEY,
                RELAY_GAS_USAGE,
                MIN_FEE,
                MAX_FEE,
                DEFAULT_FEE,
                None,
                accounts.alice,
            ).expect("Threshold is valid.");

            assert_eq!(most.set_committee(vec![accounts.alice], 1), Ok(()));

            let committee_size = most.committee_sizes.get(1).ok_or(MostError::NoSuchCommittee);
            assert_eq!(committee_size, Ok(1));

        }

This throws an error and shows that the commitee_sizes for commitee_id = 1 is not updated. However, if we write the test like this, it'd work:

 #[ink::test] 
        fn incorrect_commitee_size() {
            let accounts = default_accounts::<DefEnv>();
            set_caller::<DefEnv>(accounts.alice);

            // create new contract
            let mut most = Most::new(
                guardian_accounts(), // 5
                THRESHOLD,
                POCKET_MONEY,
                RELAY_GAS_USAGE,
                MIN_FEE,
                MAX_FEE,
                DEFAULT_FEE,
                None,
                accounts.alice,
            ).expect("Threshold is valid.");

            assert_eq!(most.set_committee(vec![accounts.alice], 1), Ok(()));

            let committee_size = most.committee_sizes.get(0).ok_or(MostError::NoSuchCommittee);
            assert_eq!(committee_size, Ok(5));

        }

Still think it can be considered as HIGH as I first stated in the issue #15 even though you initially disagreed. commitee_sizes is used when calculating rewards, and it will throw an error when calculating rewards for alice

krzysztofziobro commented 4 months ago

Valid submission - minor

It just prevents committee members from claiming their rewards, which does not qualify for any higher severity + can be recovered/fixed by contract owner/governance.

rodiontr commented 4 months ago

@krzysztofziobro sorry but why do you think so? This is clearly a bug in the function and it doesn’t depend on the owner actions. The fact that the owner is prevented from claiming doesn’t mean it’s somehow owner-related. In this case, he can be viewed as an ordinary user. Additionally, this can be classified as the loss of value as the one of the committee members will be always not be able to claim rewards. Once the contract deployed, the commitee members cannot be somehow changed properly via set_commitee()and it cannot be somehow be fixed once it’s deployed. This is basically a system malfunction and on any other auditing platform it will be classified as at least medium as DoS happens here or high if you think there is a loss of value here for one of the owners.

Also following this logic of “owner or governance can fix it” - it’s not possible once the contract is deployed. Only if you’ve noticed this vulnerability beforehand, but this argument can be used with any vulnerability - “dev can fix it”

And it’s certainly not on the same level of severity as “missing zero address check” kinda like vulnerabilities

rodiontr commented 4 months ago

And if the owner can’t claim the rewards, it can be also classified as “theft of the rewards” - the terminology that’s described in the severity classification

krzysztofziobro commented 4 months ago

There is a distinction between user funds and committee rewards - note that "theft of committee rewards" has a lower severity that theft of user funds. And temporary freeze of committee rewards isn't listed under the medium severity.

I get your point that there might be issues that are not fixable after the deployment, but that's not the issue in this case - owner can upgrade the contract to fix the bug + add an auxiliary call that will insert missing committee sizes, and then all rewards will be claimable by the corresponding committees.

rodiontr commented 4 months ago

There is a distinction between user funds and committee rewards - note that "theft of committee rewards" has a lower severity that theft of user funds. And temporary freeze of committee rewards isn't listed under the medium severity.

I get your point that there might be issues that are not fixable after the deployment, but that's not the issue in this case - owner can upgrade the contract to fix the bug + add an auxiliary call that will insert missing committee sizes, and then all rewards will be claimable by the corresponding committees.

But any reported vulnerability can be minor by your logic. Of course, you can upgrade contract and fix the issue. But the point of the audit competition is to point out to these issues. And this particular issue has much higher severity due to DoS. It’s basically a broken function

You can say so about any vulnerability - “we can upgrade and make an additional call”. What’s the true vulnerability in this case ?

krzysztofziobro commented 4 months ago

Example: someone is able to steal rewards - that's not recoverable in this way as the funds are no longer in the contract

rodiontr commented 4 months ago

Example: someone is able to steal rewards - that's not recoverable in this way as the funds are no longer in the contract

So only stealing funds is vulnerability then? What about denial of service ?

rodiontr commented 4 months ago

Plus this issue has PoC

rodiontr commented 4 months ago

Because any other vulnerability can be fixed via upgrade

krzysztofziobro commented 4 months ago

DoS also qualifies as Medium if you are able to block the bridging itself, so reliably cause sendRequest/receiveRequest to fail for other users in a way that does not resolve itself over time

rodiontr commented 4 months ago

DoS also qualifies as Medium if you are able to block the bridging itself, so reliably cause sendRequest/receiveRequest to fail for other users in a way that does not resolve itself over time

yes but you can also upgrade => no issue. And also this Theft of committee rewards (on the Aleph Zero contract) by committee accounts. is written under medium severity but you said before that there is no such thing under the medium. I believe it can be viewed as the theft of funds as one of the committee members is not able to claim. Listen, I agree with judgement of my other issues but I think this one is a clear medium. Any other platform would classify this as medium as it's a denial of service + broken important function + freeze of rewards. It's a logical vulnerability

Anyway, would respect any final decision

krzysztofziobro commented 4 months ago

OK, so whilst according to the rules this submission qualifies for minor, we agree that it has a bigger impact than a typical minor, so we want to award the bounty level of low. So our final decision:

Valid submission - low