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: 37/199
Findings: 4
Award: $231.96
🌟 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
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Position.sol#L109-L114 https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/MintingHub.sol#L252-L276 https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Frankencoin.sol#L280-L288
Once a position has surpassed its start time it can no longer be vetoed by calling the function deny()
. The only way to remove the position is to challenge it. When a position is challenged, and the challenge is successful the challenger is paid the challenger-reward based on the potential damage the position could have done. If the bid of the bidder for the challenge is not enough to cover the repayment and the challenger-reward the difference is calculated and covered by the reserve using notifyLoss()
. If the reserve does not have enough funds to cover the reward the missing amount is just minted and sent to the attackers wallet. If the reward becomes to high funds of the reserve can be lost and all holders of the Frankencoin Pool Shares will lost their money. In addition a depegg might happen if in addition there were additional ZCHF minted to cover the reward.
A minter could open a position for eg. 1 ETH and price it reasonably (eg. 1000 ZCHF) so the position does not get vetoed by anyone. Once the start time has passed, he changes the price to 1 billion ZCHF and challenges his own position (from another wallet). Since the challenge will be successful because no one will pay 1 billion ZCHF for 1 ETH he will get a reward of 2% of the potential damage of 1 Billion ZCHF. With a CHALLENGER_REWARD
of 2% this would be 20 million ZCHF. Since this amount cannot be covered by the price paid for the 1 ETH the reserves will cover the difference and thereby drain funds from the reserve. If the reward is high enough all the funds of the reserve will be drained or even worth, the funds that the reserve cannot cover will just be minted and send to the attackers wallet.
When the attacker sells/swaps all this additional ZCHF that where minted without the backing of collateral the ZCHF will depegg and all holders of ZCHF will loose their money since the ZCHF becomes worthless.
The reward could be caped at a certain value, eg. 1000 ZCHF so challenging positions is still profitable for the challenger but the amount that is rewarded does not get out of hand. Also, the code could be changed so a position where the price has been increased can be denied again. This way the system can be protected against such an attack.
#0 - c4-pre-sort
2023-04-26T12:10:38Z
0xA5DF marked the issue as duplicate of #458
#1 - c4-pre-sort
2023-04-26T12:11:28Z
0xA5DF marked the issue as low quality report
#2 - 0xA5DF
2023-04-26T12:12:14Z
Marking low quality just to flag that it didn't utilize setting the challenge period to 0
#3 - c4-judge
2023-05-18T14:41:30Z
hansfriese marked the issue as satisfactory
🌟 Selected for report: yellowBirdy
Also found by: BenRai, ChrisTina, GreedyGoblin, Norah, carrotsmuggler
153.271 USDC - $153.27
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Position.sol#L112 https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Position.sol#L263 https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Position.sol#L373-L376
If a minter has a positive outlook on ZCHF and suggest a position with a long duration and the position is denied, the cool down for the position is set to the expiration date. Since the owner of a position with a cooldown cannot withdraw collateral the collateral he already deposited when creating the position will be looked untill the position expires.
A Minter creates a position with a duration of 1 year and deposits the collateral into the position. A bad actor (eg. a competing CHF stable coin) that holds 3% of the reserve shares denies the position and thereby sets the cooldown to the expiration date 1 year from now. The collateral of the position owner is now locked for 1 year since one cannot withdraw collateral from a position with a cooldown. Trust in the ZCHF sinks since there is no way to make sure that the new positions can not be denied by the bad actor. This could mean, that no new positions are suggested and the marketshare of ZCHF can no longer grow.
Instead of setting the cool down to the expiration date there should be an additional variable that tracks if a position was denied or not. If a position was denied the owner should be able to withdraw the collateral right away.
#0 - c4-pre-sort
2023-04-21T11:25:51Z
0xA5DF marked the issue as duplicate of #874
#1 - c4-judge
2023-05-17T18:53:08Z
hansfriese changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-05-18T13:22:26Z
hansfriese marked the issue as satisfactory
🌟 Selected for report: juancito
Also found by: 0xAgro, 0xNorman, 0xSmartContract, 0xStalin, 0xTheC0der, 0xWaitress, 0xhacksmithh, 0xnev, 3dgeville, 8olidity, Arz, Aymen0909, BGSecurity, BRONZEDISC, Bauchibred, Bauer, BenRai, ChainHunters, ChrisTina, CodeFoxInc, DedOhWale, DishWasher, EloiManuel, IceBear, Inspex, Jorgect, Kaysoft, LeoGold, LewisBroadhurst, Madalad, MiloTruck, MohammedRizwan, Nyx, Polaris_tow, RaymondFam, SaharDevep, SanketKogekar, Sathish9098, SolidityATL, Udsen, W0RR1O, aria, ayden, berlin-101, bin2chen, catellatech, codeslide, crc32, decade, descharre, evmboi32, eyexploit, fatherOfBlocks, georgits, giovannidisiena, joestakey, karanctf, kodyvim, ltyu, lukris02, m9800, matrix_0wl, mov, mrpathfindr, nadin, niser93, p0wd3r, parlayan_yildizlar_takimi, pavankv, pontifex, qpzm, ravikiranweb3, rbserver, santipu_, shealtielanz, slvDev, tnevler, wonjun, xmxanuel, yixxas
22.6007 USDC - $22.60
Errors, events, modifiers and interfaces in the following contracts are scattered all over the code and/or sit below the functions. To meet the convention and have a better overview they should be grouped together and put above the functions of the contract. The affected contracts are:
Frankencoin.sol MintingHub..sol Position.sol
258: // notify the position that will send the collateral to the bidder Should be: notify the position that the collateral will be send to the bidder
358: * This is also a good creterion when deciding whether it should be shown in a frontend. Criterion should be Critirion
153: * have the liquidity available to bid a sufficient amount. With this function, the can split of smaller slices of Should be: With this function, the position owner can split / or “anyone can split”
56: * Initiates the Frankencoin with the provided minimum application period for new plugins Comment should not say plugins but minters for better understanding 188: * Design rule: Minters calling this method are only allowed to so for tokens amounts they previously minted with the same _reservePPM amount.
Should be: … only allowed to do so for…
52: event PostPonedReturn(address collateral, address indexed beneficiary, uint256 amount);
To be consistent with the other comments the comments for the example should be above the code not within the code of the function.
lines 235 - 240
74: * TODO: in future versions, it might be better to fix the interest and not the fee
transfareAndCall() is no inhareded from IERC20 so it should no have the keyword “override” in it
file: contract/ERC20.sol 162: function transferAndCall(address recipient, uint256 amount, bytes calldata data) external override returns (bool) {
In the comment in line 36-38 two function are mentioned that do not exist in the contract. For clarity this lines should be removed since there are no decreaseAllowance
and increaseAllowance
functions in the contract.
36-38: * Finally, the non-standard decreaseAllowance
and increaseAllowance
IERC20.approve
. restructurCapital()
can run out of gasIf the address array for the address to wipe gets to long the function might run out of gas and therefore restructuring the capital might not be possible. File: equity.sol 312: for (uint256 i = 0; i<addressesToWipe.length; i++){
#0 - c4-judge
2023-05-15T17:39:46Z
hansfriese marked the issue as grade-b
🌟 Selected for report: c3phas
Also found by: 0xDACA, 0xRB, 0xSmartContract, 0xhacksmithh, 0xnev, Aymen0909, BenRai, Breeje, DishWasher, Erko, EvanW, JCN, MohammedRizwan, NoamYakov, Polaris_tow, Proxy, Rageur, Raihan, RaymondFam, ReyAdmirado, SAAJ, Sathish9098, Satyam_Sharma, Udsen, __141345__, aria, codeslide, decade, fatherOfBlocks, hunter_w3b, karanctf, matrix_0wl, nadin, naman1778, niser93, pavankv, petrichor, pfapostol, sebghatullah, slvDev, trysam2003, xmxanuel
21.0255 USDC - $21.03
The function launchChallenge()
in the file MintingHub.sol checks in line 145 with the function notifyChallengeStarted()
if the amount the user is challenging is equal or bigger the minimumCollateral
of the position. By moving this check from line 145 below line 141 will save the user gas in case he accidentally challenged a position that is smaller than the minimumCollateral
.
File: contracts/MintingHub.sol
145: position.notifyChallengeStarted(_collateralAmount);
Using calldata for arrays needs less gas than using memory.
188: function minBid(Challenge storage challenge) internal view returns (uint256) { 287: function returnCollateral(Challenge storage challenge, bool postpone) internal {
The function end() in line 235 is for saving gas by making an internal call to the function end() in line 252. But since the function end() in line 252 is not internal but public calling the function end() in line 235 does not save gas but cost more than calling end() in line 252. Therefore to save gas on deployment the function end() in line 235 can be removed.
235: function end(uint256 _challengeNumber) external { end(_challengeNumber, false); }
The value for minBid(challenge) can be saved in a variable to save gas instead of calling it twice in one function
216: if (_bidAmountZCHF < minBid(challenge)) revert BidTooLow(_bidAmountZCHF, minBid(challenge))
Some checks(if/require) can be moved up in the functions to save gas for the user in case the checks fail.
109: require(_initialCollateral >= _minCollateral, "must start with min col")
81: if (price > _price) revert InsufficientCollateral();
#0 - 0xA5DF
2023-04-27T13:13:28Z
Use calldata instead of storage to save gas
False
#1 - c4-judge
2023-05-16T14:22:08Z
hansfriese marked the issue as grade-b