#38398 [BC-High] Malicious Signers can initiate repeated contract calls to cause the multi-sign wall

Submitted on Jan 2nd 2025 at 13:36:39 UTC by @f4lc0n for Attackathon | Stacks

  • Report ID: #38398

  • Report Type: Blockchain/DLT

  • Report severity: High

  • Target: https://github.com/stacks-network/sbtc/tree/immunefi_attackaton_0.9/signer

  • Impacts:

    • Direct loss of funds

Description

Brief/Intro

When a signer acts as a coordinator, it will initiate some sBTC stacks contract calls.

The problem now is that signers do not check if the call have already been made. Therefore, a malicious signer initiate contract calls that has already executed to make the multi-sign wallet lose transaction fees.

Vulnerability Details

The signer/src/transaction_signer.rs::handle_stacks_transaction_sign_request code is as follow.

    async fn handle_stacks_transaction_sign_request(
        &mut self,
        request: &StacksTransactionSignRequest,
        bitcoin_chain_tip: &model::BitcoinBlockHash,
        origin_public_key: &PublicKey,
    ) -> Result<(), Error> {
        let instant = std::time::Instant::now();
        let validation_status = self
            .assert_valid_stacks_tx_sign_request(request, bitcoin_chain_tip, origin_public_key)
            .await;

        metrics::histogram!(
            Metrics::ValidationDurationSeconds,
            "blockchain" => STACKS_BLOCKCHAIN,
            "kind" => request.tx_kind(),
        )
        .record(instant.elapsed());
        metrics::counter!(
            Metrics::SignRequestsTotal,
            "blockchain" => STACKS_BLOCKCHAIN,
            "kind" => request.tx_kind(),
            "status" => if validation_status.is_ok() { "success" } else { "failed" },
        )
        .increment(1);
        validation_status?;

        // We need to set the nonce in order to get the exact transaction
        // that we need to sign.
        let wallet = SignerWallet::load(&self.context, bitcoin_chain_tip).await?;
        wallet.set_nonce(request.nonce);

        let multi_sig = MultisigTx::new_tx(&request.contract_tx, &wallet, request.tx_fee);
        let txid = multi_sig.tx().txid();

        debug_assert_eq!(txid, request.txid);

        let signature = crate::signature::sign_stacks_tx(multi_sig.tx(), &self.signer_private_key);

        let msg = message::StacksTransactionSignature { txid, signature };

        self.send_message(msg, bitcoin_chain_tip).await?;

        Ok(())
    }

In the above code, it checks whether the coordinator's contract call request is valid through the assert_valid_stacks_tx_sign_request function, but it does not check whether the contract call has been executed.

Therefore, when it is the malicious signer's turn as coordinator, it can request to execute a contract call that has already been executed. These calls will fail, but will consume the STX tokens of the multi-sign wallet.

Impact Details

It will cause signers multi-signature wallets to lose STX tokens.

The tx fees for these failed calls are rewarded to the miner. If the malicious signer cooperates with the miner, he can steal these funds.

References

None

Fix

The signer should check the coordinator's call request to ensure it is not a call that has already been executed.

Proof of Concept

Proof of Concept

  1. Base on: https://github.com/stacks-network/sbtc/releases/tag/0.0.9-rc4

  2. Patch signer/src/config/mod.rs, add attacker flag config

  3. Patch signer/src/main.rs, load attacker flag

  4. Patch docker/docker-compose.yml, add attacker flag

  5. Patch signer/src/transaction_coordinator.rs, add attack action

  6. Run docker

  7. This PoC sets sbtc-signer-3 as an attacker, which will automatically attack if it is the coordinator. It executes each contract call twice to simulate the attack scenario.

  8. Keep running the demo until the trigger the coordinator is sbtc-signer-3.

  9. Track the transaction initiated by sbtc-signer-3 on explorer, and you will find some contract calls that fail but still consume execution fees.

Last updated

Was this helpful?