xanoutas/solidity-security-findings huggingface dataset:
abuse concurrent challenges to steal collateral and reserve
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/MintingHub.sol#L252-L276
https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Position.sol#L329-L354


# Vulnerability details

## Impact

Concurrent challenges are allowed in the auction as per the comment in `Position.sol`
> 333:             // Challenge is larger than the position. This can for example happen if there are multiple concurrent
334:             // challenges that exceed the collateral balance in size. In this case, we need to redimension the bid and
335:             // tell the caller that a part of the bid needs to be returned to the bidder.

So in one position, there could be many challenges at the same time, the `challengedAmount` can exceed the total collateral balance. If there is no limitation to launch challenges, attackers can abuse the challenge rule to steal collateral and reserve, innocent bidders and the protocol will lose fund. There might also some side effects that innocent challengers and bidders collateral and bid fund could not be returned and get stuck. 


## Proof of Concept

Imagine the following, a position with 20,000 zchf collateral, `minimumCollateral` for challenge is 1,000 zchf. The attacker can launch 20 challenges with `minimumCollateral`. But at the same time, there is another challenge with exactly 20,000 challenge size. At the end of the auction, the 20 challenges of the attacker have no bids, and the other 1 has a bid less than the position price(success challenge).

Right at the moment of `challenge.end`, the attacker will call `MintingHub.sol#end()` for all the 20 challenges. Even if the bidder of 20,000 challenge tries to call `end()` also, the attacker can frontrun to make sure the 20 rogue transactions are processed first. The 20 challenges with 0 bids will be considered succeed but with a loss. As a result, the attacker will receive collaterals of 1,000 zchf 20 times plus challenger reward, but paying nothing.

For challenges with no bids, the `msg.sender` of `end()` will be the `recipient`, in this case it will be the attacker. When calls function `notifyChallengeSucceeded()`, the `_bidder`(`recipient`) will be the attacker, `_bid` will be 0, `_size` will be 1,000 zchf (`minimumCollateral`). For each `end()` call, the attacker will get the collateral of minimum amount 1,000 zchf in line 352 of `Position.sol`. Since there is no bid fund, in line 270 of `MintingHub.sol`, `notifyLoss()` will be called, leaving the protocol to take the loss.

```solidity
File: contracts/MintingHub.sol
252:     function end(uint256 _challengeNumber, bool postponeCollateralReturn) publ
End () steals zchf by rewards
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
`rewards` does not depend on the status of Position or `minted` , malicious user can open a Position with fake collateral token and still receive rewards during the `cooldown` period

## Proof of Concept
In `end ()`, the `challenger` will be rewarded


`end () `code is as follows


```solidity
    function end(uint256 _challengeNumber, bool postponeCollateralReturn) public {
...
        (address owner, uint256 effectiveBid, uint256 volume, uint256 repayment, uint32 reservePPM) = challenge.position.notifyChallengeSucceeded(recipient, challenge.bid, challenge.size);
        if (effectiveBid < challenge.bid) {
            // overbid, return excess amount
            IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);
        }

        uint256 reward = (volume * CHALLENGER_REWARD) / 1000_000;    //<----------Calculating rewards using volume
        uint256 fundsNeeded = reward + repayment;
        if (effectiveBid > fundsNeeded){
            zchf.transfer(owner, effectiveBid - fundsNeeded);
        } else if (effectiveBid < fundsNeeded){
            zchf.notifyLoss(fundsNeeded - effectiveBid); // ensure we have enough to pay everything
        }
        zchf.transfer(challenge.challenger, reward); // pay out the challenger reward  //<----------transfer rewards to  challenger
        zchf.burn(repayment, reservePPM); // Repay the challenged part
        emit ChallengeSucceeded(address(challenge.position), challenge.bid, _challengeNumber);
        delete challenges[_challengeNumber];    
```


`rewards` are calculated as follows `uint256 reward = (volume * CHALLENGER_REWARD) / 1000_000;`

`volume` comes from `notifyChallengeSucceeded()`:  `uint256 volumeZCHF = _mulD18(pos.price, pos._size);`.

So the `reward` is not related to `minted`

This leads to a problem that a malicious user can generate a malicious `position` and use it to steal rewards, even though the position is in the cooldown period (because the rewards are not related to `minted`, so it doesn't matter)

Note: position can execute `launchChallenge()` and `end()` even though it is in the `cooldown` period.


For example:

1.alice open position  (need 1000 zchf)

collateral = fake erc20  （worthless）
price =  100000* 1e36     (very high for get more volume)
challengePeriod = 0       (0 for can execute end() in same transaction)

although this Position is in the `cooldown` period, it does not affect the execution of the following steps.

2.alice execute `launchChallenge()` , position in `cooldown` is ok when execute `launchChallenge()`

3.alice execute `hub.end()` ,  because `challengePeriod==0`, so it can execute

4.so alice get zchf rewards = `pos.price * pos._size * CHALLENGER_REWARD`

because pos.price and pos._size Can be set at will so can be very large

The following is the demo code:

add
launchChallenge() steal collateral
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
`LaunchChallenge ()` lacks check `price`, the owner of the position can front-run to modify the price and then steal the collateral

## Proof of Concept
use can use `launchChallenge()` to Launch a challenge on a position
The code is as follows:
```solidity
    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;
    }
```

The above implementation does not check the price of the position, which allows a malicious position owner to perform a sandwich attack to steal  collateral from the challenger

For example:

1 . suppose alice has position [1] = {price = 100}, and alice monitors mempool
2 . bob decides to launch the challenge then executes `launchChallenge (pos = 1, _collateralAmount = 100)`, the transaction is still in mempool
3 . alice executes the sandwich attack, the first transaction frontrun bob transaction, changes the price to 0, that is: position [1] = {price = 0}
4 . It is bob's turn to execute the transaction `launchChallenge () `, which it can be executed successfully, but at this time the price of postion == 0
5 . The second transaction of the alice sandwich attack, execute `bid () `, then get bob's collateral for 0 price


Here is the code example:

add to GeneralTest.t.sol

```solidity
    function testSandwich() external {
        // 0. create position
        Position pos = Position(initPosition());
        console.log("0.pos price:",pos.price());
        console.log("0.pos minted:",pos.minted());
        console.log("0.pos collateral:",pos.collateral().balanceOf(address(pos)));
        console.log("0.alice zchf:",zchf.balanceOf(address(alice)));
        uint256 beforeAliceBalance = pos.collateral().balanceOf(address(alice));
        console.log("0.alice collateral:",beforeAliceBalance);

        // 1. alice performs a sandwich attack , frontrun bob
        vm.prank(address(alice));
        pos.adjustPrice(0);
        console.log("1.alice frontrun change price to:",pos.price());

        // 2. bob launchChallenge
        col.mint(address(bob), 100);
        uint256 number = bob.challenge(hub, address(pos), 100);
        (address challenger, IPosition p, uint256 size, uint256 a, address b, uint256 bid) = hub.challenges(number);
        console.log("2.bob challenge challenge size :",size);
A malicious user can become the owner of collateral if there is no bid
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

In `MintingHub.sol` we have `end()` function. Тhis function is public and anyone can call it.

```solidity
function end(uint256 _challengeNumber, bool postponeCollateralReturn) public {
        Challenge storage challenge = challenges[_challengeNumber];
        require(challenge.challenger != address(0x0));
        require(block.timestamp >= challenge.end, "period has not ended");
        // challenge must have been successful, because otherwise it would have immediately ended on placing the winning bid
        returnCollateral(challenge, postponeCollateralReturn);
257:        // notify the position that will send the collateral to the bidder. If there is no bid, send the collateral to msg.sender
258:        address recipient = challenge.bidder == address(0x0) ? msg.sender : challenge.bidder;
        (address owner, uint256 effectiveBid, uint256 volume, uint256 repayment, uint32 reservePPM) = challenge.position.notifyChallengeSucceeded(recipient, challenge.bid, challenge.size);
        if (effectiveBid < challenge.bid) {
            // overbid, return excess amount
            IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);
        }
        uint256 reward = (volume * CHALLENGER_REWARD) / 1000_000;
        uint256 fundsNeeded = reward + repayment;
        if (effectiveBid > fundsNeeded){
            zchf.transfer(owner, effectiveBid - fundsNeeded);
        } else if (effectiveBid < fundsNeeded){
            zchf.notifyLoss(fundsNeeded - effectiveBid); // ensure we have enough to pay everything
        }
        zchf.transfer(challenge.challenger, reward); // pay out the challenger reward
        zchf.burn(repayment, reservePPM); // Repay the challenged part
        emit ChallengeSucceeded(address(challenge.position), challenge.bid, _challengeNumber);
        delete challenges[_challengeNumber];

    }
```
This function ends a challenge successfully after the auction period has ended.

But this function has a serious vulnerability. A malicious user can become the owner of collateral if there is no bid.

## Proof of Concept

Let's look at the following 2 lines of code:

```solidity
257:        // notify the position that will send the collateral to the bidder. If there is no bid, send the collateral to msg.sender
258:        address recipient = challenge.bidder == address(0x0) ? msg.sender : challenge.bidder;
```
This code checks whether there is a bidder for the challenged position. If there is no bidder, then the recipient of the collateral is set to `msg.sender`, which is the address of the caller of the `end` function.

The function assumes that the owner will always be the caller of the `end` function, but this is not necessarily true.
A malicious user could wait for the challenge period to end and then call the `end` function, effectively stealing the collateral if there a
Initial depositor can steal from small investors by restructuring
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Initial depositor can steal from small investors

## Proof of Concept

Restructuring wipes out shares of all addresses on the condition that `zchf.equity() < MINIMUM_EQUITY = 1000e18`.

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

The initial depositor of a 1000 ZCHF can steal from small investors if their sum does not add up to 1000.

For instance, initial depositor mints 1000 shares with 1000 ZCHF.
Next, Alice mints 100 shares with 100 ZCHF.
Bob mints 200 shares with 200 ZCHF.

Initial depositor sees this, calls `redeem()` to redeem 500 shares, as the total shares in the system is 800 now which is less than 1000, and he is also considered qualified since he owns at least 3% of totalSupply(), he can call `restructureCapTable()` to remove shares of Alice and Bob, effectively stealing from them since he would now own 100%.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Consider disallow redemption of shares below 1000.
Malicious Contract Execution through _transfer() Function
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The _transfer() function allows the transfer of tokens from one address to another. However, the function does not check if the recipient address is a contract or not. If the recipient address is a contract, then the contract's fallback function will be called, and the contract may execute arbitrary code, which could be malicious. An attacker could exploit this vulnerability to steal tokens, disrupt the normal functioning of the contract, or even take control of the contract.

## Proof of Concept
To demonstrate this vulnerability, I will assume the role of an attacker named Amrah.

Amrah creates a malicious contract that implements a fallback function that executes arbitrary code 

		pragma solidity ^0.8.0;

		contract MaliciousContract {
		    function() external payable {
			// Malicious code here
		    }
		}

After deploying the malicious contract and obtaining its address, Amrah targets an ERC20 contract that uses the vulnerable _transfer() function and obtains its address. Then, Amrah proceeds to call the _transfer() function of the target ERC20 contract with maliciousContractAddress as the recipient address and a large amount of tokens to transfer.

		function exploit(address erc20ContractAddress) external {

		    ERC20(erc20ContractAddress).transfer(maliciousContractAddress, 1000000);
		}


Once the _transfer() function is called with maliciousContractAddress as the recipient address, the target ERC20 contract transfers the tokens to the malicious contract, causing the fallback function of the malicious contract to be triggered. At this point, the malicious contract can execute any arbitrary code, which could range from stealing the transferred tokens to causing disruption to the entire blockchain network.

## Tools Used
VSCODE
## Recommended Mitigation Steps
 To address this vulnerability, it is recommended to include a check in the _transfer() function to verify that the recipient address is not a contract before any tokens are transferred. This can be achieved by utilizing the isContract() function:

			function _transfer(address sender, address recipient, uint256 amount) internal virtual {
			    require(recipient != address(0));
			   
			    _beforeTokenTransfer(sender, recipient, amount);
			    if (_balances[sender] < amount) revert ERC20InsufficientBalance(sender, _balances[sender], amount);
			    if (isContract(recipient)) revert("ERC20: transfer to contract address");
			    _balances[sender] -= amount;
			    _balances[recipient] += amount;
			    emit Transfer(sender, recipient, amount);
			}

This check ensures that the _transfer() function can only transfer tokens to external addresses that are not contracts. The isContract() function can be implemented as follows:

			function isContract(address addr) internal view returns (bool) {
			    uint256 si
new bid is prioritize even if the new bid is lower than previous bid
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L199-L229


# Vulnerability details

## Impact
new bid is prioritize even if the new bid is lower than previous bid , function does not check if the new bid is higher than the previous bid , attacker can bid less amount than previous bidder .

## Proof of Concept
for example
alice bid for 10 ether 
now if bob bid for 5 ether alice bid will be refunded and bob's 5 ether will be prioritize because there is no restriction whether next bid should be higher then previous bid , now bob can wait for biding to end and he will won the bid.
```
    function bid(uint256 _challengeNumber, uint256 _bidAmountZCHF, uint256 expectedSize) external {
        Challenge storage challenge = challenges[_challengeNumber];
        if (block.timestamp >= challenge.end) revert TooLate();
        if (expectedSize != challenge.size) revert UnexpectedSize();
        if (challenge.bid > 0) {
            zchf.transfer(challenge.bidder, challenge.bid); // return old bid
        }
        emit NewBid(_challengeNumber, _bidAmountZCHF, msg.sender);
        // ask position if the bid was high enough to avert the challenge
        if (challenge.position.tryAvertChallenge(challenge.size, _bidAmountZCHF)) {
            // bid was high enough, let bidder buy collateral from challenger
            zchf.transferFrom(msg.sender, challenge.challenger, _bidAmountZCHF);
            challenge.position.collateral().transfer(msg.sender, challenge.size);
            emit ChallengeAverted(address(challenge.position), _challengeNumber);
            delete challenges[_challengeNumber];
        } else {
            // challenge is not averted, update bid
            if (_bidAmountZCHF < minBid(challenge)) revert BidTooLow(_bidAmountZCHF, minBid(challenge));
            uint256 earliestEnd = block.timestamp + 30 minutes;
            if (earliestEnd >= challenge.end) {
                // bump remaining time like ebay does when last minute bids come in
                // An attacker trying to postpone the challenge forever must increase the bid by 0.5%
                // every 30 minutes, or double it every three days, making the attack hard to sustain
                // for a prolonged period of time.
                challenge.end = earliestEnd;
            }
            zchf.transferFrom(msg.sender, address(this), _bidAmountZCHF);
            challenge.bid = _bidAmountZCHF;
            challenge.bidder = msg.sender;
        }
    }
```

## Tools Used
vs code 
## Recommended Mitigation Steps
require(_bidAmountZCHF > challenge.bid , "bid amount should be higher then previous bid")
When challengePeriod == 0, no bid can be made on the challenge of the position
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
When a challenge is launched on a position, it sets challenge.end == block.timestamp + position.challengePeriod()
```solidity
    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;
    }
```
and when bidding on the challenge, it requires block.timestamp < challenge.end
```solidity
    function bid(uint256 _challengeNumber, uint256 _bidAmountZCHF, uint256 expectedSize) external {
        Challenge storage challenge = challenges[_challengeNumber];
        if (block.timestamp >= challenge.end) revert TooLate();
        if (expectedSize != challenge.size) revert UnexpectedSize();
        if (challenge.bid > 0) {
            zchf.transfer(challenge.bidder, challenge.bid); // return old bid
        }
```
If the position has challengePeriod == 0 (set by the user when opening the position and not denied by the major shareholder), then challenge.end == block.timestamp, i.e. it will end when the challenge is launched and users will not be able to bid on the challenge, i.e. the position will not be affected by challenges.
```solidity
    function openPosition(
        address _collateralAddress, uint256 _minCollateral, uint256 _initialCollateral,
        uint256 _mintingMaximum, uint256 _initPeriodSeconds, uint256 _expirationSeconds, uint256 _challengeSeconds,
        uint32 _mintingFeePPM, uint256 _liqPrice, uint32 _reservePPM) public returns (address) {
        IPosition pos = IPosition(
            POSITION_FACTORY.createNewPosition(
                msg.sender,
                address(zchf),
                _collateralAddress,
                _minCollateral,
                _mintingMaximum,
                _initPeriodSeconds,
                _expirationSeconds,
                _challengeSeconds,
                _mintingFeePPM,
                _liqPrice,
                _reservePPM
            )
        );
        zchf.registerPosition(address(pos));
        zchf.transferFrom(msg.sender, address(zchf.reserve()), OPENING_FEE);
        require(_initialCollateral >= _minCollateral, "must start with min col");
        IERC20(_collateralAddress).transferFrom(msg.sender, address(pos), _initialCollateral);

        return address(pos);
    }
```
A position that cannot be challenged can set any price to mint a large