xanoutas/solidity-security-findings huggingface dataset:
Allowance not updated correctly
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/ERC20.sol#L108
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/ERC20.sol#L125


# Vulnerability details

## Impact
 allowance doesn't get updated properly therefore an attacker contract can exploit the token contract by transferring some tokens and still spend the same amount of allowance. Here are the steps: 
 
calling approve function from ERC20.sol with the spender address of test contract and uint256 value of 69664845694367813355683965970930918025071475522667657600662649263997801130727

and then calling the function transferFrom(address sender, address recipient, uint256 amount) from ERC20.sol with these inputs : 
sender: msg.sender
recipient: 0xdeadbeef
amount : 1
the allowance after should be equal to the allowance before minus the transfer amount which in this case is one. but in this case the result is not compliance to this assertion. 



## Proof of Concept


approve(test contract address,69664845694367813355683965970930918025071475522667657600662649263997801130727)
from: 
 https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/ERC20.sol#L108

test_ERC20external_spendAllowanceAfterTransfer function from test contract(can be an attacker contract) :


    // TransferFrom should decrease allowance
    function test_ERC20external_spendAllowanceAfterTransfer(
        address target,
        uint256 amount
    ) public {
        require(target != address(this) && target != address(0));
        require(target != msg.sender);
        uint256 balance_sender = token.balanceOf(msg.sender);
        uint256 current_allowance = token.allowance(msg.sender, address(this));
        require(balance_sender > 0 && current_allowance > balance_sender);
        uint256 transfer_value = (amount % balance_sender) + 1;
        emit LogUint256("transfer_value: ", transfer_value);
        emit LogUint256("current_allowance: ", current_allowance);
        bool r = token.transferFrom(msg.sender, target, transfer_value);//here it will call transferFrom function 
         //from ERC20.sol that I have addressed above. 
        assertWithMsg(r == true, "transferFrom failed");
        // Some implementations take an allowance of 2**256-1 as infinite, and therefore don't update
        if (current_allowance != type(uint256).max) {
            emit LogUint256("token.allowance :", token.allowance(msg.sender, address(this)));
            emit LogUint256(" current_allowance - transfer_value :",  current_allowance - transfer_value);
            assertEq(
                token.allowance(msg.sender, address(this)),
                current_allowance - transfer_value,
                "Allowance not updated correctly"
            );
        }
    }

 Call sequence:
    1.approve(0xa329c0648769a73afac7f9381e08fb43dbea72,69664845694367813355683965970930918025071475522667657600662649263997801130727)
2.test_ERC20external_spendAllowanceAfterTransfer(0xdeadbeef,0)
Result of transferFrom and transfer are not checked.
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

[M-01] result of `transferFrom` and `transfer` are not checked. It may lead to assets lost if the transfer is somehow failed.

below are some examples:

```solidity
contracts/Position.sol
138: collateral.transferFrom(msg.sender, address(this), newCollateral - colbal);
228: IERC20(zchf).transferFrom(msg.sender, address(this), amount);
253: IERC20(token).transfer(target, amount);
269: IERC20(collateral).transfer(target, amount);

contracts/MintingHub.sol
108: zchf.transferFrom(msg.sender, address(zchf.reserve()), OPENING_FEE);
110: IERC20(_collateralAddress).transferFrom(msg.sender, address(pos), _initialCollateral);
129: existing.collateral().transferFrom(msg.sender, address(pos), _initialCollateral);
142: IERC20(position.collateral()).transferFrom(msg.sender, address(this), _collateralAmount);
204: zchf.transfer(challenge.bidder, challenge.bid); // return old bid
210: zchf.transferFrom(msg.sender, challenge.challenger, _bidAmountZCHF);
211: challenge.position.collateral().transfer(msg.sender, challenge.size);
225: zchf.transferFrom(msg.sender, address(this), _bidAmountZCHF);
263: IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);
268: zchf.transfer(owner, effectiveBid - fundsNeeded);
272: zchf.transfer(challenge.challenger, reward); // pay out the challenger reward
```
`Position` created with `BNB` as collateral will result in locked collateral
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


`withdrawCollateral` allows position owners to withdraw collateral from the position - as long as it is still collateralized afterwards.

```solidity
File: Position.sol
263:     function withdrawCollateral(address target, uint256 amount) public onlyOwner noChallenge noCooldown {
264:         uint256 balance = internalWithdrawCollateral(target, amount);
265:         checkCollateral(balance, price);
266:     }
267: 
268:     function internalWithdrawCollateral(address target, uint256 amount) internal returns (uint256) {
269:         IERC20(collateral).transfer(target, amount);
270:         uint256 balance = collateralBalance();
271:         if (balance < minimumCollateral){
272:             cooldown = expiration;
273:         }
274:         emitUpdate();
275:         return balance;
276:     }
```

The issue is the use of `IERC20.transfer()`: tokens that do not fully comply will result in the call reverting.

## Impact

Tokens that return a boolean for `transferFrom()`, but not `transfer()`, will result in the impossibility to withdraw collateral from a position, on top of reverting all calls to `end()` for successful challenges. `BNB` is a token with this configuration (4th biggest marketcap).

Note: the documentation [mentions collateral tokens must adhere to the ERC-20 standard](https://docs.frankencoin.com/governance/acceptable-collateral), which means the token aforementioned is technically not meant to be supported. It however represents a large portion of the overall tokens marketcap, and not supporting it would affect the availability of `Frankencoin`.

Note 2: the [known issues report](https://gist.github.com/CodingNameKiki/36f3bfb214907d68fdf3a43cb0cb8ae3#low8-use-safetransfer-instead-of-transfer) mentions the calls to `transfer()` reverting (ie no direct loss of funds), while this reports highlights the fact that using `BNB` in a `Position` will result in locked collateral.

## Proof Of Concept

Run the test `test_Audit_Pos_Fail_BNB` in the following gist: https://gist.github.com/joestakey/fbc954de53b7e018f5121212488e847c
You will need to add an Ethereum key in `foundry.toml` to run the test using a fork of mainnet.



## Tools Used

Manual Analysis, Foundry

## Mitigation

Use a `safeERC20` library - such as [OpenZeppelin's one](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol) to perform collateral transfers.
Signature malleability for S
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Ethereum contracts often assumes that the signature is unique, but signatures can be altered without the possession of the private key and still be valid. The EVM specification defines several so-called ‘precompiled’ contracts one of them being ecrecover which executes the elliptic curve public key recovery. A malicious user can slightly modify the three values v, r and s to create other valid signatures. A system that performs signature verification on contract level might be susceptible to attacks if the signature is part of the signed message hash. Valid signatures could be created by a malicious user to replay previously signed messages.
Resource: https://swcregistry.io/docs/SWC-117

This can lead to user's loosing funds or any unexpected behaviour.


## Proof of Concept

## Tools Used
Mannual Review

## Recommended Mitigation Steps
Use ECDSA contract from OpenZeppelin or add additional check for s:
```
uint256 constant MALLEABLE_VALUE_S = (0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0);

// Check for malleable value in s.
if (uint256(s) > MALLEABLE_VALUE_S) {
    revert InvalidS();
}
```
Sound adjusting of a position could fail
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Increasing collateral and decreasing it's liquidation price, such that with new liquidation price and old collateral position is becoming unsound, leads to a revert. 
Such situation can happen often, imagine position that's fully minted and has market price of collateral close to liquidation, in this case, to keep position from liquidation, owner will add collateral and decrease liquidation price. Since it's fully minted, any decrease in liquidation price will lead to revert.

## Proof of Concept
calling adjust with `newPrice < price` and `newCollateral > collateralBalance()` will lead to these consecutive calls
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L134
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L163
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L283
and could revert, since `collateralReserve` equals to `CollateralBalance()`, which wasn't updated yet
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L138


## Recommended Mitigation Steps
Place `if (newPrice != price)` clause after `if (newMinted < minted)`
function `restructureCapTable()` in Equity.sol not functioning as expected 
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Incorrect typo in function `restructureCapTable()` leading to only burning tokens of first address of `addressToWipe` array arguement. 

## Proof of Concept
Here, in L313, addressToWipe[0] only takes first address of the array. While ignoring the rest and also since first address's tokens are burned it will fail `addressesToWipe` array has more than one addresses.
```
    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));
        }
    }
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
Change `address current = addressesToWipe[0];` ==> ` address current = addressesToWipe[i];`
Equity.sol : restructureCapTable is not using correct index in array
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
When array of addresses are passed to restructure in the emergency situation, contract would revert due to accessing the first index always.

Not able to restructure in single call during emergency.

## Proof of Concept
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Equity.sol#L309-L316

    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]; -----------------always 0 is used instead of i
            _burn(current, balanceOf(current));
        }
    }

## Tools Used

## Recommended Mitigation Steps

We suggest to use correct index value to access the element from array.
Lack of removal of minters reduces chances of survival
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

It is reasonable to assume there will be occasions of misbehaving minters that will threaten the functioning of the Frankencoin ecosystem and there is no way to disable any such minter without redeploying the contract.

Ex. an attacker can deploy a malicious minter during bootstrap by backrunning the deployment of the Frankencoin contract 

Ex. a previously approved stablecoin depegs and causes a material loss in the future

## Proof of Concept

While the system is bootstrapping and there is no `totalSupply` minted, any call to `suggestMinter` immediately succeeds allowing for zero values for both the `applicationPeriod` and `applicationFee` parameters.

```Solidity
  function suggestMinter(address _minter, uint256 _applicationPeriod, uint256 _applicationFee, string calldata _message) override external {
      if (_applicationPeriod < MIN_APPLICATION_PERIOD && totalSupply() > 0) revert PeriodTooShort();
      if (_applicationFee < MIN_FEE  && totalSupply() > 0) revert FeeTooLow();
      if (minters[_minter] != 0) revert AlreadyRegistered();
      _transfer(msg.sender, address(reserve), _applicationFee);
      minters[_minter] = block.timestamp + _applicationPeriod;
      emit MinterApplied(_minter, _applicationPeriod, _applicationFee, _message);
   }
```

It is only possible to deny a minter during the application period (which can be zero during bootstrap), but once a minter has been added to the system, it can not be removed/disabled.

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

## Tools Used

Manual review

## Recommended Mitigation Steps

I recommend adding functionality to either suggest removing a minter completely or pausing a minter. 

This would also be beneficial in the case of bankruptcy in combination with `Equity.restructureCapTable` ex. if a previously approved stablecoin depegs and causes the loss.

Having the ability to disable the minter in such a case would help increase the chances of survival of the system and further advances the goal of decentralization and self-organization of the community.
Position ownership could be transferred to zero address
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Ownable.sol#L31-L43


# Vulnerability details

## Impact
Ownership of a Position could be transferred to zero address. This is usually caused by mistake but the consequence is loss of collateral.

## Proof of Concept
The position owner calls [transferOwnership](https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Ownable.sol#L31) but accidentally set newOwner address to 0x0.

## Tools Used
Manual review

## Recommended Mitigation Steps
* Use 2 step ownership transfer like [this OpenZeppellin contract](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable2Step.sol) in which the new owner is required to call `acceptOwnership`. That's the best practice.
* Or at least check for zero address like [the simple OpenZeppellin contract](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/8d633cb7d169f2f8595b273660b00b69e845c2fe/contracts/access/Ownable.sol#L70)
POSITION LIMIT COULD BE FULLY REDUCED TO ZERO BY CLONES
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L126
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L97-L101


# Vulnerability details

## Impact
A newly opened position could have its limit fully reduced to zero as soon as the cooldown period has elapsed. 

## Proof of Concept
As seen in the function below, a newly opened position with `0` Frankencoin minted could have its `limit` turn `0` if the function parameter, `_minimum`, is inputted with an amount equal to `limit`. In this case, `reduction` is equal to `0`, making `limit - _minimum = 0` while the cloner is assigned `reduction + _minimum = 0 + limit = limit`: 

[Position.sol#L97-L101](https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L97-L101)

```
    function reduceLimitForClone(uint256 _minimum) external noChallenge noCooldown alive onlyHub returns (uint256) {
        uint256 reduction = (limit - minted - _minimum)/2; // this will fail with an underflow if minimum is too high
        limit -= reduction + _minimum;
        return reduction + _minimum;
    }
```
With the limit now fully allocated to the cloner, the original position owner is left with zero limit to mint Frankencoin after spending 1000 Frankencoin to open this position. This situation could readily happen especially when it involves popular position contracts.

## Recommended Mitigation Steps
It is recommended position contract charging fees to cloners. Additionally, a reserve limit should be left untouched allocated solely to the original owner to be in line with the context of position opening.
Users with a large number of votes can bypass the minimum holding period
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Bug Description

The [documentation](https://docs.frankencoin.com/reserve/pool-shares#reserve-pool-shares)  states that shares can only be redeemed after a minimum holding period:
> They can be minted at any time and redeemed again after a mimimum holding period of three months.

The comment in [Equity.sol#L53-L58](https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L53-L58) elaborates further:
> You are not allowed to redeem your pool shares if you held them for less than the minimum holding duration at average. For example, if you have two pool shares on your address, one acquired 5 days ago and one acquired 105 days ago, you cannot redeem them as the average holding duration of your shares is only 55 days < 90 days.

In the `Equity` contract, this is enforced using the `canRedeem()` function:

[Equity.sol#L131-L137](https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L131-L137)

```solidity
/**
* Returns whether the given address is allowed to redeem FPS, which is the
* case after their average holding duration is larger than the required minimum.
*/
function canRedeem(address owner) public view returns (bool) {
    return anchorTime() - voteAnchor[owner] >= MIN_HOLDING_DURATION;
}
```

As seen from above, `canRedeem()` determines a user's average holding duration using his `voteAnchor`. A user's `voteAnchor` is set in `adjustRecipientVoteAnchor()`, which is called in the contract's `_beforeTokenTransfer()` hook:

[Equity.sol#L131-L137](https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L131-L137)

```solidity
uint256 recipientVotes = votes(to); // for example 21 if 7 shares were held for 3 blocks
uint256 newbalance = balanceOf(to) + amount; // for example 11 if 4 shares are added
voteAnchor[to] = uint64(anchorTime() - recipientVotes / newbalance); // new example anchor is only 21 / 11 = 1 block in the past
return recipientVotes % newbalance; // we have lost 21 % 11 = 10 votes
```

Where:
- `to` - The address receiving shares.
- `amount` - Amount of shares received.

In the comment above, it can be inferred that the average holding duration should not be affected by the amount of shares owned by a user. However, this is untrue in the implementation of `adjustRecipientVoteAnchor()`. If a user has more shares, he will have more votes, causing his `voteAnchor` to be set to a smaller value as `recipientVotes / newbalance` is larger.

As such, users with a large amount of votes will still be able to redeem their shares, even though their average holding duration is less than the minimum holding period after subsequent deposits.

## Impact

After their first deposit, users with a large amount of votes can bypass the minimum holding period required to redeem shares.

## Proof of Concept

The following Foundry test implements the
Attacker can prevent users from redeeming shares
Audit: 2023-04-frankencoin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Bug Description

In the `Equity` contract, users are only able to redeem their shares after holding them for a minimum duration. This is enforced by the `canRedeem()` function:

[Equity.sol#L131-L137](https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L131-L137)

```solidity
/**
* Returns whether the given address is allowed to redeem FPS, which is the
* case after their average holding duration is larger than the required minimum.
*/
function canRedeem(address owner) public view returns (bool) {
    return anchorTime() - voteAnchor[owner] >= MIN_HOLDING_DURATION;
}
```

As seen from above, `canRedeem()` determines a user's average holding duration using his `voteAnchor`. A user's `voteAnchor` is set in `adjustRecipientVoteAnchor()`, which is called in the contract's `_beforeTokenTransfer()` hook:

[Equity.sol#L131-L137](https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L131-L137)

```solidity
uint256 recipientVotes = votes(to); // for example 21 if 7 shares were held for 3 blocks
uint256 newbalance = balanceOf(to) + amount; // for example 11 if 4 shares are added
voteAnchor[to] = uint64(anchorTime() - recipientVotes / newbalance); // new example anchor is only 21 / 11 = 1 block in the past
return recipientVotes % newbalance; // we have lost 21 % 11 = 10 votes
```

Where:
- `to` - The address receiving shares.
- `amount` - Amount of shares received.

As such, anyone can increase a user's `voteAnchor` by directly transferring shares to the user. An attacker can exploit this to temporarily prevent users from being able to redeem their shares:
- Bob deposits 100 Frankencoin.
- 90 days and 66 hours passes.
- Since the minimum holding duration is over, Bob decides to redeem his shares by calling `redeem()`.
- Alice front-runs his `redeem()` transaction and directly transfers 1 Frankencoin to Bob:
  - The `_beforeTokenTransfer()` hook calls `adjustRecipientVoteAnchor()` with Bob as the `to` address.
  - Bob's `voteAnchor` increases significantly.
- Afterwards, Bob's `redeem()` transaction executes:
  - For him, `anchorTime() - voteAnchor` is now less than `MIN_HOLDING_DURATION`, thus `canRedeem()` returns false.
  - `redeem()` reverts, thus he is unable to redeem his shares.

Note that the cost of increasing someone's `voteAnchor` corresponds to his original deposited amount. For example, if Bob had deposited 10 Frankencoin at the start, Alice would only need to transfer 0.11 Frankencoin to him to cause `redeem()` to revert.

## Impact

A malicious attacker can DOS the `redeem()` function for specific users for period of time.

## Proof of Concept

The following Foundry test demonstrates the scenario described above:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../contracts/Frankencoin.sol";

con