Frankencoin - GreedyGoblin'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: 42/199

Findings: 3

Award: $192.79

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: yellowBirdy

Also found by: BenRai, ChrisTina, GreedyGoblin, Norah, carrotsmuggler

Labels

bug
2 (Med Risk)
satisfactory
edited-by-warden
duplicate-874

Awards

153.271 USDC - $153.27

External Links

Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L109 https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L249 https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L263

Vulnerability details

Impact

The collateral of an open position gets stuck for the whole expiration time when it gets denied and nobody challenges it during initialization time. This basically locks down the collateral of the position leaving the user unable to do anything with that collateral. No frankencoins can be minted, and the collateral cannot be withdrawn for the whole expiration time.

Proof of Concept

A position contains a variable called expiration. This variable contains the value of the timestamp at which the position will expire. Position also contains a variable called cooldown. This variable is used as a check to delay a specific interaction between the owner and the position.

    modifier noCooldown() {
        if (block.timestamp <= cooldown) revert Hot();
        _;
    }

When the owner wants to withdraw all the collateral deposited in the position, it calls the function withdrawCollateral():

    /**
     * Withdraw collateral from the position up to the extent that it is still well collateralized afterwards.
     * Not possible as long as there is an open challenge or the contract is subject to a cooldown.
     *
     * Withdrawing collateral below the minimum collateral amount formally closes the position.
     */
    function withdrawCollateral(address target, uint256 amount) public onlyOwner noChallenge noCooldown {
        uint256 balance = internalWithdrawCollateral(target, amount);
        checkCollateral(balance, price);
    }

This function contains the noCooldown modifier, therefore if we are currently in the cooldown time span we cannot withdrawal any collateral.

When a position is denied by a qualified user the cooldown variable is assigned the expiration value.

    function deny(address[] calldata helpers, string calldata message) public {
        if (block.timestamp >= start) revert TooLate();
        IReserve(zchf.reserve()).checkQualified(msg.sender, helpers);
        cooldown = expiration; // since expiration is immutable, we put it under cooldown until the end
        emit PositionDenied(msg.sender, message);
    }

It is not uncommon to open positions for long periods of time, for example 1 year (the documentation explains that large periods of time are not uncommon and uses 365 days as an example). If a user gets its position denied, it cannot recover the collateral for the whole expiration time, since the cooldown has now the expiration value and in order to withdraw the collateral there has to be no cooldown. It is a strong issue for the user since if the position gets denied and nobody is interested in it, the user is unable to close the position since the only way to close a position is to withdraw all the collateral. This is explained as a comment over the withdrawCollateral() function.

It might look far fetched that nobody will challenge the denied position, but this can easily happen for example when not so popular tokens are used as collateral. There are a plethora of reasons for which an open position might not get challenged, and it is important that the protocol is consistent through all of the different ramifications.

Tools Used

IDE

A possible solution could be to implement a check that makes sure that the position has been denied and the initialization period has ended and no challenges have been issued. When all the requirements are met the user should be able to withdraw the collateral.

#0 - c4-pre-sort

2023-04-24T07:14:59Z

0xA5DF marked the issue as duplicate of #874

#1 - c4-judge

2023-05-18T08:57:14Z

hansfriese marked the issue as satisfactory

Findings Information

Labels

bug
2 (Med Risk)
low quality report
partial-50
edited-by-warden
duplicate-745

Awards

16.9175 USDC - $16.92

External Links

Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L265

Vulnerability details

Impact

A challenger reward can be farmed by biding market prices in own challenge.

Proof of Concept

The protocol has 3 main actors on a position: the position owner, the challenger and the bidder.

When a position is opened, users have a chance to challenge the position for the duration of the position. When a position is challenged the challenger places the same amount of collateral out for auction. The highest bid determines if the challenge has been successful. If the highest bid is superior to the challenged price, which was the one suggested by the position, the bidder will buy from the challenger and the user is unaffected. If the highest bid is below the price suggested by the position owner the challenge succeeds and the bidder "buys" the collateral from the position owner.

When a challenge succeeds, the challenger gets rewarded with 2% of the volume. This opens a possible attack where the attacker has to find open positions where the price suggested is in line or less than 1% over the real price. Then challenge it and place a bid on the challenge for that same price minus a negligible amount (this is done to avoid failing the challenge). Because of this, other possible bidders are heavily discouraged to bid, because the bided price is already the price of the asset, therefore no possible revenue from the transaction. Also since positions can be challenged more than once, we can repeat this attack over and over. Steps:

1 - Find an open position with a realistic price (shouldn't be too hard since position owners that wish their position to be accepted should place realistic prices). 2 - Challenge the position 3 - Bid with a real price that does not fail the challenge and discourages other bidders for lack of revenue. 4 - Collect 2% from challenge succeeded. 5 - Repeat (if position has not reached limit).

If somebody outbids us, therefore failing the challenge. Its not a big deal since we are going to get the same or more Frankencoin for that collateral than it is actually worth (it should be highly improbable that somebody outbids us because they will generate no revenue, but is still a possibility).

The hardest parts of this attack are: 1 - Finding an open position with a collateral value ratio 1:1 (or at least of 1% variance so we can make some profit). 2 - Finding the price at which we don't lose value when winning the bid, and that discourages the bidders. This can be done by checking latest transactions and setting a median value. Also checking oracles that could track both coins.

Again the attack is far fetched, but it is definitely possible. The worst thing of this is that it encourages users to challenge open positions that are correctly collateralized jus to farm the reward.

Tools Used

IDE

This does not have an easy way to solve. The main idea here is that since the challenger gets rewarded by challenging we can force a challenge and bid on it with a price that generates no profit for other bidders, but that still does not fail the challenge.

The simplest way to solve this would be to reduce the maximum possible profit of this attack, therefore making it too hard to execute. To do this the reward of the challenger could be lowered so that finding an open position with the right price becomes harder because the margin of profit is no longer 2% but smaller. This does not eliminate the attack, but makes it significantly more unprofitable and harder to execute.

#0 - c4-pre-sort

2023-04-28T15:22:22Z

0xA5DF marked the issue as duplicate of #745

#1 - c4-pre-sort

2023-04-28T15:23:42Z

0xA5DF marked the issue as low quality report

#2 - 0xA5DF

2023-04-28T15:23:44Z

Partial dupe

#3 - c4-judge

2023-05-18T14:54:58Z

hansfriese marked the issue as partial-50

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