The protocol expects a user to pay 0.5% fee upon minting EUROs against their deposited collateral. 50% of this fee is distributed to the stakers and 50% taken by the protocol. A user can get away with reclaiming up to 50% of his paid fee by staking + minting
instead of simply minting. Imagine the following scenario:
Even if there were additional stakers present in the pool since many days, their rightful share of the fee is diluted because Alice will always stake + mint
instead of simply mint
.
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { ETH, getNFTMetadataContract, DEFAULT_EUR_USD_PRICE, DEFAULT_ETH_USD_PRICE, DEFAULT_COLLATERAL_RATE, HUNDRED_PC, fastForward, DAY, POOL_FEE_PERCENTAGE, fullyUpgradedSmartVaultManager, PROTOCOL_FEE_RATE } = require("./common");
describe('FeeInspection', async () => {
let LiquidationPoolManager, LiquidationPool, TokenManager,
TST, EUROs, ERC20MockFactory, admin, user1, protocol, liquidator, ClEurUsd, ClEthUsd, ClCoinUsd, VaultManager, Vault, collateralCoin;
beforeEach(async () => {
[admin, user1, protocol, liquidator] = await ethers.getSigners();
ERC20MockFactory = await ethers.getContractFactory('ERC20Mock');
TST = await ERC20MockFactory.deploy('The Standard Token', 'TST', 18);
EUROs = await (await ethers.getContractFactory('EUROsMock')).deploy();
ClEurUsd = await (await ethers.getContractFactory('ChainlinkMock')).deploy('EUR / USD');
await ClEurUsd.setPrice(DEFAULT_EUR_USD_PRICE);
ClEthUsd = await (await ethers.getContractFactory('ChainlinkMock')).deploy('ETH / USD');
await ClEthUsd.setPrice(DEFAULT_ETH_USD_PRICE);
TokenManager = await (await ethers.getContractFactory('TokenManagerMock')).deploy(ETH, ClEthUsd.address);
const SmartVaultDeployer = await (await ethers.getContractFactory('SmartVaultDeployerV3')).deploy(ETH, ClEurUsd.address);
const SmartVaultIndex = await (await ethers.getContractFactory('SmartVaultIndex')).deploy();
const NFTMetadataGenerator = await (await getNFTMetadataContract()).deploy();
const SwapRouterMock = await (await ethers.getContractFactory('SwapRouterMock')).deploy();
const MockWeth = await (await ethers.getContractFactory('WETHMock')).deploy();
VaultManager = await fullyUpgradedSmartVaultManager(
DEFAULT_COLLATERAL_RATE, PROTOCOL_FEE_RATE, EUROs.address, protocol.address,
liquidator.address, TokenManager.address, SmartVaultDeployer.address,
SmartVaultIndex.address, NFTMetadataGenerator.address, MockWeth.address,
SwapRouterMock.address
);
await SmartVaultIndex.setVaultManager(VaultManager.address);
await EUROs.grantRole(await EUROs.DEFAULT_ADMIN_ROLE(), VaultManager.address);
await VaultManager.connect(user1).mint();
const _vault = (await VaultManager.connect(user1).vaults())[0];
const vaultAddress = _vault.status.vaultAddress;
Vault = await ethers.getContractAt('SmartVaultV3', vaultAddress);
const LiquidationPoolManagerContract = await ethers.getContractFactory('LiquidationPoolManager');
LiquidationPoolManager = await LiquidationPoolManagerContract.deploy(
TST.address, EUROs.address, VaultManager.address, ClEurUsd.address, protocol.address, POOL_FEE_PERCENTAGE
);
await VaultManager.setLiquidatorAddress(LiquidationPoolManager.address);
await VaultManager.setProtocolAddress(LiquidationPoolManager.address);
LiquidationPool = await ethers.getContractAt('LiquidationPool', await LiquidationPoolManager.pool());
await EUROs.grantRole(await EUROs.BURNER_ROLE(), LiquidationPool.address);
collateralCoin = await ERC20MockFactory.deploy('Collateral Coin', 'cCOIN', 18);
ClCoinUsd = await (await ethers.getContractFactory('ChainlinkMock')).deploy('cCOIN / USD');
await TokenManager.addAcceptedToken(collateralCoin.address, ClCoinUsd.address);
});
describe('lower fee payment on minting', async () => {
it('allows getting away with paying lesser fees for minting EUROs', async () => {
await ClEurUsd.setPrice(100000000);
await ClCoinUsd.setPrice(120600000);
const tstStake = 1;
const eurosStake = 1;
await TST.mint(user1.address, tstStake);
await EUROs.mint(user1.address, eurosStake);
await TST.connect(user1).approve(LiquidationPool.address, tstStake);
await EUROs.connect(user1).approve(LiquidationPool.address, eurosStake);
await LiquidationPool.connect(user1).increasePosition(tstStake, eurosStake);
let { _position } = await LiquidationPool.position(user1.address);
expect(_position.TST).to.equal(tstStake);
expect(_position.EUROs).to.equal(eurosStake);
const userCollateral = ethers.utils.parseEther('1000');
await collateralCoin.mint(Vault.address, userCollateral);
const mintedEUROs = ethers.utils.parseEther('1000');
const mintFee = mintedEUROs.mul(PROTOCOL_FEE_RATE).div(HUNDRED_PC);
await Vault.connect(user1).mint(user1.address, mintedEUROs);
expect(await EUROs.balanceOf(user1.address)).to.equal(mintedEUROs);
expect(await EUROs.balanceOf(LiquidationPoolManager.address)).to.equal(mintFee, "protocolFee");
const euroPlusMintFeeShare = mintFee.div(2).add(eurosStake);
await LiquidationPoolManager.connect(user1).distributeFees();
({ _position } = await LiquidationPool.position(user1.address));
expect(_position.EUROs).to.equal(euroPlusMintFeeShare, "euroPlusMintFeeShare");
});
});
});
Malicious user is able to game the system which dilutes the fee share of other legitimate users.
Since the malicious user may use multiple wallets to carry out this attack (one for staking, one for minting), checking for a wallet's _position
or balance won't be an effective solution.
A good approach could be to introduce a time lag such that a staker has to remain in the system for a week or so before he is eligible to get a share of the fee. This will discourage such attacks which result in immediate gains.