A very helpful sign
I was trying to exploit a function in Thena protocol that allows users to merge two NFTs into one. It’s a functionality I haven’t seen before, so I thought it could be a soft spot worth investigating.
I threw at it every trick I had in the bag, without much success. At this point I’m used to it, most of the time my ideas just don’t work. I spent a big chunk of time on it and at last decided to move on.
I moved my attention to another uncommon, but related, function. One that allows splitting a single NFT into multiple ones.
That’s when I stumbled upon a very helpful sign:
// reset supply, _deposit_for increase it
supply = supply - value;
I paused for a moment. Splitting and merging are two complementary operations, but the merge function I tried to exploit for the past several hours wasn’t resetting the supply. How did I miss that? Maybe it actually shouldn’t, but the discrepancy was worth re-investigating.
The bug
In Thena users can lock an amount of tokens for a period of time to get a veNFT
, which represents voting power in the protocol. The merge()
function allows users to merge two different veNFT
into one, it looks like this:
function merge(uint _from, uint _to) external {
...SNIPPET...
LockedBalance memory _locked0 = locked[_from];
LockedBalance memory _locked1 = locked[_to];
uint value0 = uint(int256(_locked0.amount));
uint end = _locked0.end >= _locked1.end ? _locked0.end : _locked1.end;
...SNIPPET...
_burn(_from);
_deposit_for(_to, value0, end, _locked1, DepositType.MERGE_TYPE);
}
It takes two veNFT
as input, _from
and _to
, and then does two things:
- [L12] Burns
_from
, by calling_burn(_from)
- [L13] Adds the amount that was locked in
_from
to_to
, by calling_deposit_for(...)
:
function _deposit_for(uint _tokenId,
uint _value,
uint unlock_time,
LockedBalance memory locked_balance,
DepositType deposit_type
) internal {
...SNIPPET...
uint supply_before = supply;
supply = supply_before + _value;
...SNIPPET...
}
which takes _value
, the amount of tokens locked in _from
, and adds it to supply
, a variable responsible for keeping track of the current amount of tokens locked in the system.
However, merging two veNFT
should not increase the amount of tokens locked in the system, since the tokens are already locked in.
The 3 exploits
Cool, now we have a bug. What’s the maximum amount of damage we can cause by abusing it?
What we can do is artificially inflate the supply
variable by merging veNFTs
over and over again. After some research it turned out the supply
variable is used for two critical purposes:
- [A] Calculating the amount of weekly emissions
- [B] Calculating the share of weekly emissions destined to
veNFT
holders
[A] Calculating the amount of weekly emissions
Here the supply
variable is accessed as _ve.supply()
, [L3]:
function weekly_emission() public view returns (uint) {
uint256 calculate_emission = (weekly * EMISSION) / PRECISION;
uint256 circulating_emission = (_thena.totalSupply() - _ve.supply() * TAIL_EMISSION) / PRECISION;
return Math.max(calculate_emission, circulating_emission);
}
It returns the highest value, at [L5], between calculate_emission
, which we can’t control, and circulating_emission
which interests us because of the operation:
_thena.totalSupply() - _ve.supply() * TAIL_EMISSION
Knowing we can increase _ve.supply()
at will, we have the choice between:
- Having the operation return a lower value than expected: we can lower the amount of weekly emissions (in certain states)
- Having the operation revert by underflow: we increase
_ve.supply()
until_ve.supply() * TAIL_EMISSION
is bigger than_thena.totalSupply()
to DOS the emissions of weekly rewards
[B] Calculating the share of weekly emissions destined to veNFT
holders
The share is calculated as follows, where the supply
variable is called as _ve.supply()
, [L2]:
function calculate_rebate(uint _weeklyMint) public view returns (uint) {
uint lockedShare = _ve.supply() * PRECISION / _thena.totalSupply();
if(lockedShare >= REBASEMAX){
return _weeklyMint * REBASEMAX / PRECISION;
} else {
return _weeklyMint * lockedShare / PRECISION;
}
}
the function calculates the percentage of tokens locked in the system at [L2], knowing we can increase _ve.supply()
at will we can:
- Trick the function into believing the percentage of tokens locked is higher than expected: we can increase the share of weekly emissions given to
veNFT
holders up to a maximum ofREBASEMAX
Disclosure
The bug leads to an attacker being able to:
- lower the amount of weekly emissions
- DOS the emissions of weekly rewards
- increase the share of weekly emissions given to
veNFT
holders
On 11/01/22, in collaboration with trust__90, we disclosed the bug to Thena via immunefi. It was classified as high severity and rewarded a bounty of $20k.