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: 55/199
Findings: 3
Award: $90.26
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: peanuts
Also found by: GreedyGoblin, J4de, KIntern_NA, Kumpa, LegendFenGuin, T1MOH, __141345__, deadrxsezzz, deliriusz, ltyu, m9800, rvierdiiev
33.835 USDC - $33.83
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L177-L179 https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L263-L266 https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L140-L148
Both Position::mint()
and Position::withdrawCollateral()
require that the position is not actively challenged:
function mint(address target, uint256 amount) public onlyOwner noChallenge noCooldown alive { ... }
This makes sense, however launching a challenge is very cheap, making DoSing specific users viable option. Every time that a position is challenged, it has to wait minimum period, or be overbidded to remove it. This results in really cheap and easy challenges, and expensive and cumbersome averts. Hence, I believe that this issue is HIGH, as a user may loose access to their positions as long as an attacker is willing to pay neglegible amounts.
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; }
User positions are DoSed
Proof of concept for this issue is already a part of test cases. File GeneralTest.t.sol
and test case test09EndingChallenge. I'll describe the most interestin parts here:
address posAddress = test04Mint(); // create a position to be challenged ... uint256 first = bob.challenge(hub, posAddress, 300);
alice.avertChallenge(hub, swap, first);
function avertChallenge(MintingHub hub, StablecoinBridge swap, uint256 first) public { { (address challenger, IPosition p, uint256 size, uint256 a, address b, uint256 bid) = hub.challenges(first); uint256 amount = size * p.price() / 10 ** 18; obtainFrankencoins(swap, amount); hub.bid(first, amount, size); // avert challenge } (address challenger, IPosition p, uint256 size, uint256 a, address b, uint256 bid) = hub.challenges(first); require(challenger == address(0x0), "challenge not averted"); require(!hub.isChallengeOpen(first)); }
Manual analysis
Introduce minimum challenge payment, that will make sure that no user can DoS anyone else
#0 - c4-pre-sort
2023-04-24T19:40:01Z
0xA5DF marked the issue as duplicate of #745
#1 - 0xA5DF
2023-04-24T19:41:26Z
Challenging costs as much as it costs for the position owner to remove it. In any case this isn't very different than #770 by the same warden, duping to the same primary.
#2 - c4-pre-sort
2023-04-24T19:59:12Z
0xA5DF marked the issue as not a duplicate
#3 - c4-pre-sort
2023-04-24T19:59:23Z
0xA5DF marked the issue as duplicate of #674
#4 - c4-pre-sort
2023-04-28T12:54:33Z
0xA5DF marked the issue as duplicate of #745
#5 - c4-judge
2023-05-18T13:47:49Z
hansfriese changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-05-18T13:47:55Z
hansfriese marked the issue as satisfactory
33.835 USDC - $33.83
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Frankencoin.sol#L152-L157
Let's start with the fact that minters are permissionless, so anyone can create their own minter contract and register it in Frankencoin. If it's not challenged in required time, it's functional. Having said that, the only moment that a minter can be removed, is the initial challenge period:
function denyMinter(address _minter, address[] calldata _helpers, string calldata _message) override external { if (block.timestamp > minters[_minter]) revert TooLate(); reserve.checkQualified(msg.sender, _helpers); delete minters[_minter]; emit MinterDenied(_minter, _message); }
After the challenge period is over, there is no possibility to remove it. However, there are many possible issues that may be very harmful:
No possibility to stop misbehaving minter
I believe that PoC is not required here, as I described the issue enough in the first section
Manual analysis
Allow denying minters any time. Because it's important role, please consider increasing voting power from 3% to 5%.
#0 - c4-pre-sort
2023-04-21T14:17:26Z
0xA5DF marked the issue as duplicate of #230
#1 - c4-judge
2023-05-18T13:40:49Z
hansfriese marked the issue as satisfactory