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: 11/199
Findings: 3
Award: $933.75
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: decade
Also found by: 0x3b, 0xDACA, 0xWaitress, 0xWeiss, 0xkaju, Arz, Aymen0909, BPZ, EloiManuel, HaCk0, J4de, Jerry0x, Jiamin, John, Juntao, Kek, Lalanda, MiloTruck, Mukund, PNS, RedTiger, Ruhum, Satyam_Sharma, ToonVH, Tricko, Udsen, ak1, anodaram, bin2chen, carrotsmuggler, cccz, circlelooper, deadrxsezzz, giovannidisiena, jasonxiale, joestakey, juancito, karanctf, kenta, kodyvim, ladboy233, lil_eth, lukino, markus_ether, marwen, mrpathfindr, nobody2018, parlayan_yildizlar_takimi, peakbolt, ravikiranweb3, rbserver, rvierdiiev, silviaxyz, volodya, zhuXKET, zzebra83
0.0748 USDC - $0.07
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L309-L316
It is not behaving as expected or operating normally. There appears to be an issue with the functionality of the 'restructureCapTable' function in 'Equity.sol'. Further investigation is necessary to identify the root cause of the problem and determine the appropriate solution to resolve it.
function restructureCapTable(address[] calldata helpers, address[] calldata addressesToWipe) public { require(zchf.equity() < MINIMUM_EQUITY); checkQualified(msg.sender, helpers); for (uint256 i = 0; i<addressesToWipe.length; i++){ address current = addressesToWipe[0]; _burn(current, balanceOf(current)); } }
Currently, the purpose of this function is to burn tokens in the first address of the addressesToWipe
array. However, the goal is to burn tokens in all addresses listed in the array.
This requires modifying the function's implementation to account for each address within the array, not just the first.
We can modify this function like this:
function restructureCapTable(address[] calldata helpers, address[] calldata addressesToWipe) public { require(zchf.equity() < MINIMUM_EQUITY); checkQualified(msg.sender, helpers); for (uint256 i = 0; i<addressesToWipe.length; i++){ address current = addressesToWipe[i]; _burn(current, balanceOf(current)); } }
#0 - c4-pre-sort
2023-04-20T14:28:34Z
0xA5DF marked the issue as duplicate of #941
#1 - c4-judge
2023-05-18T14:27:02Z
hansfriese marked the issue as satisfactory
911.0761 USDC - $911.08
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L132-L152
The adjust
function in Position.sol
is designed to adjust the outstanding amount of ZCHF, the collateral amount, and the price in a single transaction.
However, there are certain cases where this function always reverts.
Assuming the new price is greater than the current price, if the value of newCollateral
is less than colbal
or the value of newMinted
is greater than minted
, the adjust
function will always revert with a customized error message reading Hot
.
If the value of newPrice
is great than value of price
, the restrictMinting
function is triggered.
In this case, if the value of cooldown
exceeds block.timestamp
+ 3 days, cooldown
will be update to block.timestamp + 3 days
, therefore, both the withdrawCollateral
and mint
functions will be reverted with custom error because of noCooldown
modifier.
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L132-L152
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L159-L167
I will share the test code
it("Will revert adjest tx when newPrice is greater than prevPrice", async () => { let collateral = mockVOL.address; let fliqPrice = floatToDec18(1000); let minCollateral = floatToDec18(1); let fInitialCollateral = floatToDec18(initialCollateral); let duration = BN.from(14*86_400); let fFees = BN.from(fee * 1000_000); let fReserve = BN.from(reserve * 1000_000); let openingFeeZCHF = await mintingHubContract.OPENING_FEE(); let challengePeriod = BN.from(7 * 86400); // 7 days await mockVOL.connect(accounts[0]).approve(mintingHubContract.address, fInitialCollateral); let balBefore = await ZCHFContract.balanceOf(owner); let balBeforeVOL = await mockVOL.balanceOf(owner); let tx = await mintingHubContract["openPosition(address,uint256,uint256,uint256,uint256,uint256,uint32,uint256,uint32)"] (collateral, minCollateral, fInitialCollateral, initialLimit, duration, challengePeriod, fFees, fliqPrice, fReserve); let rc = await tx.wait(); const topic = '0x591ede549d7e337ac63249acd2d7849532b0a686377bbf0b0cca6c8abd9552f2'; // PositionOpened const log = rc.logs.find(x => x.topics.indexOf(topic) >= 0); positionAddr = log.address; let balAfter = await ZCHFContract.balanceOf(owner); let balAfterVOL = await mockVOL.balanceOf(owner); let dZCHF = dec18ToFloat(balAfter.sub(balBefore)); let dVOL = dec18ToFloat(balAfterVOL.sub(balBeforeVOL)); expect(dVOL).to.be.equal(-initialCollateral); expect(dZCHF).to.be.equal(-dec18ToFloat(openingFeeZCHF)); positionContract = await ethers.getContractAt('Position', positionAddr, accounts[0]); console.log("price:",await positionContract.price()); console.log("minted:",await positionContract.minted()); await ethers.provider.send('evm_increaseTime', [7 * 86_400 + 60]); await ethers.provider.send("evm_mine"); let erx = positionContract.adjust(1, floatToDec18(8), floatToDec18(1200)) await expect(erx).to.be.revertedWithCustomError(positionContract, "Hot"); console.log("price:",await positionContract.price()); console.log("minted:",await positionContract.minted()); });
VS Code
To solve this problem, we can modify the "adjust" function like this;
function adjust(uint256 newMinted, uint256 newCollateral, uint256 newPrice) public onlyOwner { uint256 colbal = collateralBalance(); if (newCollateral > colbal){ collateral.transferFrom(msg.sender, address(this), newCollateral - colbal); } // Must be called after collateral deposit, but before withdrawal if (newMinted < minted){ zchf.burnFrom(msg.sender, minted - newMinted, reserveContribution); minted = newMinted; } if (newCollateral < colbal){ withdrawCollateral(msg.sender, colbal - newCollateral); } // Must be called after collateral withdrawal if (newMinted > minted){ mint(msg.sender, newMinted - minted); } if (newPrice != price){ adjustPrice(newPrice); } }
#0 - c4-pre-sort
2023-04-22T19:01:09Z
0xA5DF marked the issue as primary issue
#1 - 0xA5DF
2023-04-22T19:02:31Z
Might be a design choice, need sponsor's input on this one
#2 - luziusmeisser
2023-05-03T06:27:59Z
I wouldn't call this a 'vulnerability' but convenience is definitely improved by the recommended mitigation.
#3 - c4-sponsor
2023-05-03T06:28:19Z
luziusmeisser marked the issue as sponsor confirmed
#4 - c4-judge
2023-05-18T05:05:53Z
hansfriese marked the issue as selected for report