Buttonwood is a collection of DeFi primitives for building powerful decentralized financial instruments. One of Buttonwood's insights is that everything in finance is some form of a tranche. The ButtonTranche contracts serve as a foundation for marketplaces such as liquidation-free debt, convertible bonds, and options, and assets like fiat-free stablecoins.
This write-up covers some bug bounty reports hosted by Buttonwood on Immunefi.
The mint()
and mintFor()
function on the ButtonToken contract allow any user to mint a free amount of bits
. Relatively, the mint
function takes a number of bits to be minted for the user by calculating through _amountTobits
. When certain bits are calculated, the amount of the underlying token(Uamount) to be deposited is calculated. Which results in 0
amount of underlying tokens being deposited by the user. The button token generally accepts Uamount in the _deposit()
function.
In this bug report, when a user gives a low amount for minting, which can be measured in 5 decimals of uint Eg: 55555. The contract mints a free X
amount of bits
for the user.
The root cause of the issue is actually due to the floor division rounding when converting the share amount into the underlying amount. This means that the absolute maximum that can be extracted is the equivalent of 1 underlying "wei" worth of share tokens for free.
This vulnerability can be exploited through the repeated call of the mint() function through multicall contracts to save some gas.
Proof of Concept:
As said in the bug report, the mint function can be exploited through the dust amount input for tokens.
This POC can be demonstrated through the Tenderly mainnet fork.
Add ButtonToken(bWbtc) contract to the Tenderly Dashboard.
Fork the Ethereum mainnet using the Tenderly Fork tab.
Create a new simulation and choose the button token that has been added before.
Select the mint()
function and give input for the number of tokens in 5 decimals, eg: 55555
The default caller address will be address(0)
changed to any random address.
Now click simulate, The portion of bits will be transferred to that random caller without receiving any amount of underlying tokens from the caller.
From the attacker's side, to make a profit from the mint()
function, the attacker can use multicall code to repeatedly call the mint()
function with low gas fees.
While this is certainly a valid concern, the exploitable value is extremely limited, even with an automated loop calling the function iteratively. On a chain where gas has any value at all, we don't see this being a profitable exploit under any reasonable circumstances.
We truly appreciate the report and are implementing a fix for the numerical instability in future versions. Due to the extremely limited scope of the exploit, we propose to change the severity to `Medium`
and offer a $10,000 payout.
The bond should mature on a pre-defined maturity date. But the mature()
function can be called before the maturity date due to the wrong operator used in the require statement.
function mature() external {
require(owner() == _msgSender() || maturityDate < block.timestamp, "BondController: Invalid call to mature");
}
The ||
(OR) operator is either or, operator means either one statement can be true to pass the require statement. So, in that case, if the owner calls mature(), the statement is passed and able to mature the bond before the maturity date.
This will cause certain functionalities to be unable to call and malicious owners can transfer the collateral token before the maturity date.
Proof of Concept:
The attacker creates a bond.
Then the bond contract is rug pulled by the malicious owner(attacker) by calling the mature function before the maturity date.
pragma solidity 0.8.0;
interface IBondFactory {
function createBondWithDepositLimit(
address _collateralToken,
uint256[] memory trancheRatios,
uint256 maturityDate,
uint256 depositLimit
) external;
}
interface IBondController {
function mature() external;
}
contract exploit {
IBondFactory bf = IBondFactory(0x000001);
IBondController bc = IBondController(0x000002);
//Creating Malicious bond
function createbond(address _collateralToken, uint256[] calldata trancheRatios, uint256 maturityDate, uint256 depositLimit) external {
bf.createBondWithDepositLimit(_collateralToken, trancheRatios, maturityDate, depositLimit);
}
//Malicious owner can call mature at any time/ before the maturity date
function attack() external {
bc.mature();
}
}
It is recommended to use the &&
operator to make it callable by the owner on the maturity date.
require(owner() == _msgSender() && maturityDate < block.timestamp, "BondController: Invalid call to mature");