Frankencoin - yixxas'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: 25/199

Findings: 2

Award: $306.44

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: MiloTruck

Also found by: DedOhWale, giovannidisiena, yixxas

Labels

bug
2 (Med Risk)
downgraded by judge
low quality report
satisfactory
duplicate-915

Awards

283.8353 USDC - $283.84

External Links

Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Equity.sol#L241-L255 https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Equity.sol#L266-L270

Vulnerability details

Impact

Initial minter can steal from the next investor. The amount stolen is based on how much the next investor chooses to invest.

Proof of Concept

In observing the onTokenTransfer() function, we notice that if equity <= amount, then only 1000 shares will be minted to from address no matter the amount deposited. This is used to mint 1000 FPS for the initial deposit, otherwise calculateSharesInternal() is used to calculate shares.

function onTokenTransfer(address from, uint256 amount, bytes calldata) external returns (bool) {
	require(msg.sender == address(zchf), "caller must be zchf");
	uint256 equity = zchf.equity();
	require(equity >= MINIMUM_EQUITY, "insuf equity"); // ensures that the initial deposit is at least 1000 ZCHF

	// Assign 1000 FPS for the initial deposit, calculate the amount otherwise
	uint256 shares = equity <= amount ? 1000 * ONE_DEC18 : calculateSharesInternal(equity - amount, amount);
	_mint(from, shares);
	emit Trade(msg.sender, int(shares), amount, price());

	// limit the total supply to a reasonable amount to guard against overflows with price and vote calculations
	// the 128 bits are 68 bits for magnitude and 60 bits for precision, as calculated in an above comment
	require(totalSupply() < 2**128, "total supply exceeded");
	return true;
}

calculateSharesInternal sets newTotalShares to 1000e18 if totalShares < 1000. This is irrespective of amount of investment.

function calculateSharesInternal(uint256 capitalBefore, uint256 investment) internal view returns (uint256) {
	uint256 totalShares = totalSupply();
	uint256 newTotalShares = totalShares < 1000 * ONE_DEC18 ? 1000 * ONE_DEC18 : _mulD18(totalShares, _cubicRoot(_divD18(capitalBefore + investment, capitalBefore)));
	return newTotalShares - totalShares;
}

A malicious first depositor can steal from the next investor due to this.

Steps of attack:

  1. Malicious attacker mints 1000 shares
  2. Malicious attacker waits for the next investor to invest and deposit ZCHF. He frontruns the transaction and then calls redeem() to redeem 1 share. One condition here is that sufficient blocks must have passed such that redemption is possible.
  3. Investor is then minted 1 share and attacker holds 999 shares. If the amount invested in this transaction is large, attacker would benefit significantly.

Tools Used

Manual Review

Consider disallowing redemption of shares such that totalSupply() < 1000.

#0 - c4-pre-sort

2023-04-21T22:03:20Z

0xA5DF marked the issue as primary issue

#1 - c4-pre-sort

2023-04-24T09:40:18Z

0xA5DF marked the issue as duplicate of #784

#2 - c4-pre-sort

2023-04-24T09:40:35Z

0xA5DF marked the issue as low quality report

#3 - c4-pre-sort

2023-04-24T09:42:19Z

0xA5DF marked the issue as duplicate of #983

#4 - c4-pre-sort

2023-04-24T09:54:41Z

0xA5DF marked the issue as not a duplicate

#5 - c4-pre-sort

2023-04-24T09:55:02Z

0xA5DF marked the issue as duplicate of #915

#6 - c4-judge

2023-05-17T18:48:55Z

hansfriese changed the severity to 2 (Med Risk)

#7 - c4-judge

2023-05-18T10:47:20Z

hansfriese marked the issue as satisfactory

QA - Low-01

3% Quorum seems to be too low. Being a qualified user has significant privileges. A qualified user can perpetually veto the addition of a new minter and can also restructure the system during dire situations. Considering that chain delegation is used, it is not difficult to reach a 3% quorum. In fact, there can be many delegates who will reach this threshold.

For example, if we only have 500 votes evenly distributed across 5 users A,B,C,D,E. Total votes is 9000.

A delegates to B who delegates to C who delegates to D who delegates to E.

In this case, C,D,E all have enough votes from their delegatees. C has 300, D has 400, E has 500. Because votes are stacked cumulatively and the delegate inherits all from delegatee without actually losing any votes of itself, quorum should be set to a higher amount.

8-10% seems a better choice of QUORUM.

#0 - c4-judge

2023-05-16T16:43:04Z

hansfriese marked the issue as grade-c

#1 - c4-judge

2023-05-18T06:18:08Z

hansfriese marked the issue as grade-b

#2 - yixxas

2023-05-18T20:40:40Z

With 5 downgraded issues to QA in #570, #628, #580, #634 and #630, along with this issue, I have 6 lows. Would greatly appreciate if this report can be reconsidered for grade-a :pray:

#3 - hansfriese

2023-05-19T04:50:25Z

6 lows are just below the threshhold, so it's B in the current setup. But this is very close anyway, so I will reconsider again.

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