GithubHelp home page GithubHelp logo

2023-04-frankencoin-findings's Introduction

Frankencoin Contest

Unless otherwise discussed, this repo will be made public after contest completion, sponsor review, judging, and two-week issue mitigation window.

Contributors to this repo: prior to report publication, please review the Agreements & Disclosures issue.


Contest findings are submitted to this repo

Sponsors have three critical tasks in the contest process:

  1. Weigh in on severity.
  2. Respond to issues.
  3. Share your mitigation of findings.

Let's walk through each of these.

High and Medium Risk Issues

Please note: because wardens submit issues without seeing each other's submissions, there will always be findings that are duplicates. For all issues labeled 3 (High Risk) or 2 (Medium Risk), these have been pre-sorted for you so that there is only one primary issue open per unique finding. All duplicates have been labeled duplicate, linked to a primary issue, and closed.

Weigh in on severity

Judges have the ultimate discretion in determining severity of issues, as well as whether/how issues are considered duplicates. However, sponsor input is a significant criteria.

For a detailed breakdown of severity criteria and how to estimate risk, please refer to the judging criteria in our documentation.

If you disagree with a finding's severity, leave the severity label intact and add the label disagree with severity, along with a comment indicating your opinion for the judges to review. You may also add questions for the judge in the comments.

Respond to issues

Label each open/primary High or Medium risk finding as one of these:

  • sponsor confirmed, meaning: "Yes, this is a problem and we intend to fix it."
  • sponsor disputed, meaning either: "We cannot duplicate this issue" or "We disagree that this is an issue at all."
  • sponsor acknowledged, meaning: "Yes, technically the issue is correct, but we are not going to resolve it for xyz reasons."

(Note: please don't use sponsor disputed for a finding if you think it should be considered of lower or higher severity. Instead, use the label disagree with severity and add comments to recommend a different severity level -- and include your reasoning.)

Add any necessary comments explaining your rationale for your evaluation of the issue.

Note that when the repo is public, after all issues are mitigated, wardens will read these comments; they may also be included in your C4 audit report.

QA and Gas Reports

For low and non-critical findings (AKA QA), as well as gas optimizations: wardens are required to submit these as bulk listings of issues and recommendations. They may only submit a single, compiled report in each category:

  • QA reports should include all low severity and non-critical findings, along with a summary statement.
  • Gas reports should include all gas optimization recommendations, along with a summary statement.

For QA and Gas reports, sponsors are not required to weigh in on severity or risk level. We ask that sponsors:

  • Leave a comment for the judge on any reports you consider to be particularly high quality. (These reports will be awarded on a curve.)
  • Add the sponsor disputed label to any reports that you think should be completely disregarded by the judge, i.e. the report contains no valid findings at all.

Once labelling is complete

When you have finished labelling findings, drop the C4 team a note in your private Discord backroom channel and let us know you've completed the sponsor review process. At this point, we will pass the repo over to the judge to review your feedback while you work on mitigations.

Share your mitigation of findings

Note: this section does not need to be completed in order to finalize judging. You can continue work on mitigations while the judge finalizes their decisions and even beyond that. Ultimately we won't publish the final audit report until you give us the OK.

For each finding you have confirmed, you will want to mitigate the issue before the contest report is made public.

As you undertake that process, we request that you take the following steps:

  1. Within a repo in your own GitHub organization, create a pull request for each finding.
  2. Link the PR to the issue that it resolves within your contest findings repo.

This will allow for complete transparency in showing the work of mitigating the issues found in the contest. If the issue in question has duplicates, please link to your PR from the open/primary issue.

2023-04-frankencoin-findings's People

Contributors

c4-judge avatar code423n4 avatar kartoonjoy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

2023-04-frankencoin-findings's Issues

Incorrect implementation of restructure capitalization

Lines of code

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

Vulnerability details

Impact

Detailed description of the impact of this finding.
Equity.restructureCapTable() function is incorrectly implemented. In the loop of the function, while the function iterates through elements passed in the array, while trying to burn the FPS, it always attempts to burn only the tokens at 0 index.

It is a bug in the logic.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Incorrect Implementaion

for (uint256 i = 0; i<addressesToWipe.length; i++){
address current = addressesToWipe[0];
_burn(current, balanceOf(current));
}

Correct Implementation

for (uint256 i = 0; i<addressesToWipe.length; i++){
address current = addressesToWipe[i]; <==========Fix
_burn(current, balanceOf(current));
}

Tools Used

Manual review

Recommended Mitigation Steps

This is a logical error and should be fixed by correcting the way the current address is read from the addressesToWipe array. Instead of a hard code 0 index, use i to retrieve the current address element.

Missing input validation in `splitChallenge` function (MintingHub.sol)

Lines of code

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

Vulnerability details

Impact

There is no validation on the splitOffAmount parameter. An attacker could potentially provide a malicious value, causing unexpected behavior in the contract.

Proof of Concept

There is no validation for splitOffAmount parameter

Tools Used

VS Code

Recommended Mitigation Steps

Implement input validation such as require()
e.g
require(splitOffAmount > 0 && splitOffAmount < challenge.size, "Invalid splitOffAmount value");

launch a challenge with out proper collateral

Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L134#L148
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L170#L171

Vulnerability details

Impact

Detailed description of the impact of this finding.
MintingHub contract allows anyone to launch a challenge against a position with out proper collateral.

there is no check in the function to ensure that the challenger as transfer the required minimum collateral before such challenge is accepted.

there should be a condition like in the below code before accepting the challenge
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L170#L171

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Example,
mintingHub.launchChallenge(address(0xya...),1);

Even when 1 is not enough as collateral, the challenge is accepted and inserted into the challenge collection.

Tools Used

Manual review

Recommended Mitigation Steps

Implement the below logic
In the launchChallenge() function, before accepting the challenge, the function should check for minimum
collateral required and only if the collateral passed by the caller is greater than the minimum require
the challenge should be accepted. Else, the call should be reverted.

uint256 min = IPosition(challenge.position).minimumCollateral();

# Whale would brick stablecoinbridge

Lines of code

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

Vulnerability details

Impact

A whale could reach the limit provided by stablecoinbridge, this is not the case provided by the previous audit which is not practical since no sensible attacker would lock his/her tokens to the contract in order to cause a DOS.
In contrary, a whale could reach the limit in a single transaction causing DOS to other users.
Another scenario is where a flashloaner can arbitrage (take advantage of the slight price difference between frankencoin and the other stablecoin with a free flash-loan). Although the flashloaner won't cause a DOS since the loan is repaid in the same transaction, the flashloaner can keep on minting and burning the limit amount to make more profit, keeping the two scenario here because of the same underlying issue.

Proof of Concept

place the following test in GeneralTest.t.sol

function test_whaleOrFlashloaner() public {
        // A whale a could break the bridge contract
        address whale = makeAddr("whale");
        vm.startPrank(whale);
        // collects a flashloan or a whale
        TestToken(address(xchf)).mint(whale, 1_000_000e36);
        TestToken(address(xchf)).approve(address(swap), 1_000_000e36);
        swap.mint(whale, 1_000_000 ether);
        assertEq(IERC20(zchf).balanceOf(whale), 1_000_000 ether);
        vm.stopPrank();

        vm.startPrank(address(bob));
        // Lets say bob has 10 ether of xchf to bridge zchf
        // bob is DOSed
        TestToken(address(xchf)).mint(address(bob), 10e36);
        TestToken(address(xchf)).approve(address(swap), 10e36);
        vm.expectRevert(bytes("limit"));
        swap.mint(10 ether);
        vm.stopPrank();

        // In a case of a flashloaner, would burn and sell on the market and repeat
        vm.prank(whale);
        swap.burn(whale, 1_000_000 ether);
        assertEq(TestToken(address(xchf)).balanceOf(whale), 1_000_000e36);//TestToken has zero decimals
    }

Tools Used

Manuel Review, VSCode

Recommended Mitigation Steps

Add a maxAmount check to the mint function in stablecoinbridge which would be the maximum amount a single user can bridge(this would give each user a fair chance). this would increase the cost of this attack if a malicious user has to split the funds into different account.
Note: the smaller the maximum amount the more expensive the attack.

Frankencoin.sol suggestMinter

Lines of code

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

Vulnerability details

Impact

suggestMinter can be called with period 0 and application fee 0.

If blockchian notice the MinterApplied Event then the attacker can call the function
before there is any mint, the attacker can register addresses and it cannot deny

Proof of Concept

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

If totalSupply() == 0 then any body can register directly minter without pay and pending.

Tools Used

Recommended Mitigation Steps

Deploy stablecoinBridge like reserve while deploy zchf contract and setMinter directly the bridge
remove totalSupply check in suggestMinter function

[H-01] usage of transferFrom with no return checks

Lines of code

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

Vulnerability details

Impact

Users would launch challenge without making a real transferFrom on position.collateral() at MintingHub contract when this position.collateral() token returns bool instead of reverting

Proof of Concept

The usual transferFrom() function returns a boolean value indicating success of the transaction. This parameter needs to be checked after the call to see if the transfer has been successful. the MintingHub contract however on the launchChallenge function makes a transferFrom on the position.collateral() without a check,

Some tokens like EURS (0xdb25f211ab05b1c97d595516f45794528a807ad8) and BAT (0x0d8775f648430679a709e98d2b0cb6250d2887ef) will not revert if the transfer failed but return false instead. if this one token is used as collateral for the position given, this will makes users launch a challenge without performing a real transferFrom call, thus the user will be able to bypass the payment check

PoC (used truffle and ganache) :

deploy a position and '0x0d8775f648430679a709e98d2b0cb6250d2887ef' is it's collateral :


contract protoPosition {
  ...
  function collateral() external returns(address) {
    return address(0x0d8775f648430679a709e98d2b0cb6250d2887ef);
  }
}
  1. Make the call (after validating the above position as valid pos in the zchf) :
const me = "address that have funds"
const position_ = "protoPosition address";

await Mintinghub.launchchallenge(position_,1000,{from:me});
  1. the call will succeed without a real token transfer on the 0x0d8775f648430679a709e98d2b0cb6250d2887ef by the msg.sender (me)

Note : the same issue can occurs on the following lines :

108 : zchf.transferFrom(msg.sender, address(zchf.reserve()), OPENING_FEE);
110 : zchf.transferFrom(msg.sender, address(zchf.reserve()), OPENING_FEE);
129 : existing.collateral().transferFrom(msg.sender, address(pos), _initialCollateral);
210 : zchf.transferFrom(msg.sender, challenge.challenger, _bidAmountZCHF);
225 : zchf.transferFrom(msg.sender, address(this), _bidAmountZCHF);

Tools Used

Truffle / ganache

Recommended Mitigation Steps

Check the success boolean of all transferFrom() calls. Alternatively, use OZ’s SafeERC20’s safeTransferFrom() function.

Minter can burn any user’s funds

Lines of code

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

Vulnerability details

Impact

The minter should not be able to burn any user's funds in the contract without proper authorization.
If the contract allows the minter to have unrestricted access to all user funds, then the minter could potentially burn any user's funds.
It can lead to the loss of funds and also damage the trust of users in the contract.

Proof of Concept

here s the function that allow the minter to get with the user's fund and burn his tokens with any amount he wants.

  function burnFrom(address payer, uint256 targetTotalBurnAmount, uint32 _reservePPM) external override minterOnly returns (uint256) {
  uint256 assigned = calculateAssignedReserve(targetTotalBurnAmount, _reservePPM);
  _transfer(address(reserve), payer, assigned); // send reserve to owner
  _burn(payer, targetTotalBurnAmount); // and burn the full amount from the owner's address
  minterReserveE6 -= targetTotalBurnAmount * _reservePPM; // reduce reserve requirements by original ratio
  return assigned;
   }

Tools Used

Manual

Recommended Mitigation Steps

It should be kept to this:

  function burn(uint256 _amount) external {
        _burn(msg.sender, _amount);
  }

Missing input validation in `redeem` function (Equity.sol)

Lines of code

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

Vulnerability details

Impact

The redeem function in Equity.sol does not check if the provided shares parameter is greater than zero.

Proof of Concept

There is no checking for uint256 shares, it accept zero value in this parameter.

Tools Used

VS Code

Recommended Mitigation Steps

Implement input validation such as require(shares > 0, "Shares must be greater than zero");

Unsafe call to ERC20::transfer can result in stuck funds

Lines of code

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

Vulnerability details

Impact

If an ERC20::transfer call fails it will lead to stuck funds for a user.

Proof of Concept

In the returnPostponedCollateral method we have the following code for transferring ERC20 tokens

IERC20(collateral).transfer(target, amount);

The problem is that the transfer function from ERC20 returns a bool to indicate if the transfer was a success or not

Here is a scenario case:
1 - User (e.g Alice) try to withdraw collateral.
2 - If the smart contract's balance of the collateral is less than amount that needs to be transfer, the transfer function fall.
3 - The transfer fails and since the token does not revert but returns false this is not accounted for by the protocol and transaction completes successfully
4 - The tokens are now stuck and cannot withdraw by Alice anymore

Tools Used

VS Code

Recommended Mitigation Steps

Use OpenZeppelin’s SafeERC20 library and change transfer to safeTransfer

Incorrect implementation of ERC-677

Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/f86279e76fd9f810d2a25243012e1be4191a547e/contracts/ERC20.sol#L162

Vulnerability details

Impact

The implementation of ERC677 leads to the following issues:

  • It is impossible to use transferAndCall to transfer tokens to a regular user.
  • If the recipient returns false, which means they failed to process the token reception, the tokens will still end up on their contract since there is no check for the result of the onTokenTransfer call, which leads to a loss of the very essence of using this standard.

Proof of Concept

The full implementation of the contract is as follows:
https://github.com/code-423n4/2023-04-frankencoin/blob/f86279e76fd9f810d2a25243012e1be4191a547e/contracts/ERC20.sol#L162

File: contracts\ERC20.sol

162:     function transferAndCall(address recipient, uint256 amount, bytes calldata data) external override returns (bool) {
163:         bool success = transfer(recipient, amount);
164:         if (success){
165:             success = IERC677Receiver(recipient).onTokenTransfer(msg.sender, amount, data);
166:         }
167:         return success;
168:     }

Here we can see that there is no possibility of transferring tokens through this method to a regular user(since a simple user cannot handle the onTokenTransfer call), and the result of the 'onTokenTransfer' call is not checked, leading to a situation where tokens are sent in L163 but not accepted in L165, which may result in their being locked on the recipient's contract.

Links on the resource side:

Tools Used

  • Manual review
  • Hardhat

Recommended Mitigation Steps

  • Fix the implementation or replace it with the current actual standard

Division before multiplication operation on cubic root calculation

Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MathUtil.sol#L24-#L26

Vulnerability details

Impact

It is important to be mindful about the order of operations. If a calculation requires the use of both multiplication and division operations at once, then the multiplication operation should always be performed before the division operation.

If a contract uses floating-point numbers for calculations, there is a risk of precision loss if those calculations are not handled properly.

Proof of Concept

The order of operation plays a vital role. Always prefer to perform the multiplication operation first, followed by division operations at last. Change the order of the operations from:

x = _mulD18(x, _divD18( (powX3 + 2 * _v) , (2 * powX3 + _v)));

to:

x = _divD18(_mulD18((powX3 + 2 * _v), x), (2 * powX3 + _v));

Tools Used

Manual Review

Recommended Mitigation Steps

Perform all multiplication operations first. Division operations should be done at last.

Potential issues of minting Frankencoin using the StablecoinBridge contract.

Lines of code

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

Vulnerability details

Impact

Detailed description of the impact of this finding.
StablecoinBridge contract's mint function does not validate for 0 address for target parameter. As such,if the minting is done with zero address, the FrankenCoins minted will be lost for ever. This will also break the pricing model pegged to stable coin.

Burn function:

Even Burn function is not checking for the target address to be not a 0x0 address and presents the same risk. Burn function should also validate a target address like mentioned above for minting.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Refer to the below call.
StablecoinBridge.mint(0x0,5000);
This will transfer transfer coin from the sender to StablecoinBridge contract. But since the target address passed it 0x0, in the mintInternal, after validating the conditions, the below function will mint 5000 coins to a 0x0 address and hence the coins are lost breaking the supply relationship between Stablecoin and frankenCoin.

zchf.mint(target, amount);

Also, there is one more potential issue with mint function, where stable coin is printed immediately, but for printing frankenCoin, the below two conditions should be true. The time horizon has not passed and balance of stablecoin is less than limit configured.

When the contract hits the situation where these two limits are hit, the scenario where only stable coin is being minted, but not the frankenCoin could lead to imbalance in their supplies

Tools Used

Manual review

Recommended Mitigation Steps

a) validate for target address to be not 0x0 address as below by adding the require statement.
function mint(address target, uint256 amount) public {
require(target!=address(0x0);
chf.transferFrom(msg.sender, address(this), amount);
mintInternal(target, amount);
}

b) moving the below two lines to a modifier, and attaching them to both mint and mintInternal() to ensure that both the pair tokens are maintained in relation while minting and burning.

require(block.timestamp <= horizon, "expired");
require(chf.balanceOf(address(this)) <= limit, "limit");

c) Burn function should also be checked for 0x0 address so that tokens are not lost.

QA Report

See the markdown file with the details of this report here.

splitChallenge has division of zero exception if challenge's size is 0

Lines of code

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

Vulnerability details

Impact

splitChallenge has division of zero exception if challenge's size is 0

Proof of Concept

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

        Challenge memory copy = Challenge(
            challenge.challenger,
            challenge.position,
            splitOffAmount,
            challenge.end,
            challenge.bidder,
            (challenge.bid * splitOffAmount) / challenge.size
        );
        return address(pos);
    }

Tools Used

Recommended Mitigation Steps

either check challenge.size in splitChallenge or launchChallenge

QA Report

See the markdown file with the details of this report here.

User can open position with no init period

Lines of code

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

Vulnerability details

Impact

The documentation shows that when a user opens a new Position, there should be an initial time period of 7 days in which the position can not mint tokens and in which qualified pool share holders can deny the position.
It is assumed this check is in place to detect and prevent malicious positions that could hurt the system, for example if the provided collateral is not acceptable.

In MintingHub.sol, there are two public functions openPosition(). The first one enforces _initPeriodSeconds to be set to 7 days, the second one allows the user to set _initPeriodSeconds to any value, for example 0.

This allows a freshly created position to immediately start minting tokens and making it impossible to deny the position.

Proof of Concept

Public openPosition function, _initPeriodSeconds can be set to 0

 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) {

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

Constructor in Position.sol sets start to block.timestamp + initPeriod and cooldown to the same value, in this case the current timestamp

uint256 public immutable start; // timestamp when minting can start
...
start = block.timestamp + initPeriod; // one week time to deny the position
cooldown = start;

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

It is now impossible to deny the position

      function deny(address[] calldata helpers, string calldata message) public {
        if (block.timestamp >= start) revert TooLate(); ...}

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

Position owner can now mint, as there is no active cooldown period

function mint(address target, uint256 amount) public onlyOwner noChallenge noCooldown alive {...}

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

Tools Used

Recommended Mitigation Steps

The second openPosition function in MintingHub.sol should be marked internal.

function setOwner is vulnerable with no params control

Lines of code

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

Vulnerability details

Impact

1.there is no limitation for newOwner, it can be 0 address, it can be the same address as old one
2.if malicious attacker inherits contract Ownable and call setOwner to make owner himself, all capital will be unsafe

Proof of Concept

https://github.com/code-423n4/2023-04-frankencoin/blob/1022cb106919fba963a89205d3b90bf62543f68f/contracts/Ownable.sol#L39-L43
no access control for function, no limitation for params

Tools Used

manual

Recommended Mitigation Steps

use onlyOwner modifier, and add
require(newOwner != address(0), "0 address not allowed")
require(newOwner != oldOwner, "same owner after transfer")

Impact of fiat world economics on this project

Lines of code

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

Vulnerability details

Impact

Detailed description of the impact of this finding.
Swiss franc is a safe haven in the fiat world today. Often in the scenarios of economic turmoil or where and when uncertainty looms around the world, most of the fait based economies/investors lean towards moving their large chunks of investment funds into holding swiss franc as a safe heaven currency. These kinds of switch or migration of funds happen very fast in fait world.

The concern is about how franken coin is going to respond to such swings in the fiat market and will frankencoin be able to cope with sudden demand for it using the auction system. As this needs manual exploration on price, there seems to be a possibility that the price difference between swiss franc(fait) and frankenCoin might get widen very quickly defeating its in principle role as a peg.

The main area of concern is the locking/cooling and waiting periods in the range of 7 days and 3 days applied in the contracts. The lock and waiting periods are implemented with intention to prevent manipulation and an develop a responsible role for minters who play the role to preserve FPS value in the frankencoin ecosystem.

But, these economic swings have time and again showed the swiss franc is safe haven currency during this unexpected times and also that such switches happens very very fast, will frankencoin be able to hold as peg to swiss franc under the current implementation.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
This risk is not related to a specific line in code, but general preparation for sudden demand where response to that demand is restricted by design due to cooling periods and could be very slow.

Under current circumstances, minters are not enabled with an option to address this demand. This might
How can minters be enabled to keep the peg for swiss franc(token) to frankencoin at 1:1 in the case of these real market swings.

Tools Used

Manual review

Recommended Mitigation Steps

Currently there does not seem to be a mechanism to help address this sudden demand for frankencoin under a short time window and hence frankencoin will not remain pegged at 1:1 to swiss franc in those extreme circumstance.

The solution needs to be thought out. Like in the case of a sudden erode of faith in the coin, there is a provision for minters to put in some funds and take over the coin fps, there should be a way to handle this sudden demand which might last for a time window and fall back.

One approach could be to use the voting mechanism to update the locking/cooling periods based on consensus with very high vote percentage and update these time windows. The votes are based on FPS share like how it is implemented in the framework. The votes can be enabled based on threshold in relation to swiss franc.

Usage of idea proposal EIP-677

Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/f86279e76fd9f810d2a25243012e1be4191a547e/contracts/ERC20.sol#L161
https://github.com/code-423n4/2023-04-frankencoin/blob/f86279e76fd9f810d2a25243012e1be4191a547e/contracts/StablecoinBridge.sol#L75
https://github.com/code-423n4/2023-04-frankencoin/blob/f86279e76fd9f810d2a25243012e1be4191a547e/contracts/Equity.sol#L241

Vulnerability details

Impact

According to https://eips.ethereum.org/erc, the ERC-677 standard used in the project is not included in the Final, Last Call list of standards (This standard is an Idea and not a standard), which leads to the fact that a standard is used which has not been approved/deprecated or removed

Consequences:

  • this is not a standard
  • it can change
  • there are no clear requirements
  • truncated functionality as transferFromAndCall

Proof of Concept

Links to the code:

Tools Used

  • Manual review

Recommended Mitigation Steps

QA Report

See the markdown file with the details of this report here.

end should delete `challenges[_challengeNumber];` before execution to avoid potential re-entrency.

Lines of code

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

Vulnerability details

Impact

end should delete challenges[_challengeNumber]; before execution to avoid potential re-entrency.

Proof of Concept

while the function only interacts with zchf and position proxy, both of which are deployed by the protocol and should be deemed safe, there is no re-entrency protection on the end function. It is advised to practice the "Checks-Effects-Interactions Pattern", by deleting the challenges[_challengeNumber] first than at then end.

Tools Used

Recommended Mitigation Steps

retrieve the challenge from memory and delete to avoid reentrancy. It also works as a gas optimisation since accessing memory variable is much cheaper than accessing storage.

function end(uint256 _challengeNumber, bool postponeCollateralReturn) public {
        Challenge memory challenge = challenges[_challengeNumber]; @> audit
        delete challenges[_challengeNumber]; @> audit
        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);
        // notify the position that will send the collateral to the bidder. If there is no bid, send the collateral to msg.sender
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);
    }

QA Report

See the markdown file with the details of this report here.

ERC20's transferAndCall is reentrant with significant impact

Lines of code

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

Vulnerability details

Impact

Contracts that uses frankencoin's ERC20.transferAndCall can get their funds lost due to existence of reentrancy

Proof of Concept

The ERC20.transferAndCall function calls makes a call on the recipient address when making a transfer (line 165) :

   success = IERC677Receiver(recipient).onTokenTransfer(msg.sender, amount, data);

the function does not check for any reentrancy, which as result a client contract that relies on frankencoin's ERC20.transferAndCall can get it's funds lost, consider the following scnario :

  1. Example contract relies on frankencoin's ERC20.transferAndCall to make withdrawals
// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.0;
interface IERC20 {
    function onTokenTransfer(address sender, uint256 amount, bytes calldata data) external returns (bool);
}

contract Example {
    address private _tokenAddress; // address of frankencoin's ERC20
    mapping(address => uint256) private _balances; // User balances
    
    constructor(address tokenAddress) {
        _tokenAddress = tokenAddress;
    }
    
    function deposit(uint256 amount) external {
         require(IERC20(_tokenAddress).transferFrom(msg.sender, address(this), amount), "Transfer failed");
        _balances[msg.sender] += amount;
    }
    
    
    function withdraw(address recipient, uint256 amount, bytes calldata data) external returns (bool) {
        
        bool success = IERC20(_tokenAddress).onTokenTransfer(recipient, amount, data);
        require(success, "Transfer failed");
        return true;
    }
    
    function balanceOf(address user) external view returns (uint256) {
        return _balances[user];
    }
}
  1. Alice is attacker, that have 100 tokens on frankencoin's ERC20
  2. Bob and Eve have 500 tokens on Example contract
  3. Alice create an deploy this contract :

interface vuln {
   function withdraw(address sender, uint256 amount, bytes calldata data) external;
}

contract exploit {
  vuln public v;
  function set(address _vuln) external {
     v = _vuln;
  }
  function dep() external {
     vuln(v).deposit(50);
  }
  function exp() external {
     vuln(v).withdraw(50);
  }
  fallback() {
    exp();
  }
}
  1. Alice sends her 100 tokens of frankencoin's ERC20 to exploit contract
  2. she sets vuln (v) as the example contract
  3. now she calls the exp function
  4. Stats will be exploit have 100 token, example have 600,
  5. while Alice is only allowed to withdraw 100 she will withdraw 600 from example since the frankencoin's ERC20.transferAndCall have no reentrancy protection
  6. the exp function will make a withdrawal using frankencoin's ERC20.transferAndCall:
    a. the exploit contract will receive 50
    b. it gets a call from frankencoin's ERC20
    c. the the exploit fallback will re-call the exp, which this will makes another transfer (example.withdraw)
    d. the example contract will make another 50 transfer to exploit, during the 2nd withdrawal the frankencoin's ERC20 will hook a call to exploit
    e. this will make another withdrawal which results for another 50 tokens from Example ...etc
    f. and so on
  7. The Example contract that relies on frankencoin's ERC20.transferAndCall now had it's funds lost via the reentrancy existence

Tools Used

Remix IDE, truffle and ganache

Recommended Mitigation Steps

Consider using OZ's reentrancyGuard on the ERC20.transferAndCall function

QA Report

See the markdown file with the details of this report here.

restructureCapTable does not enumerate each addressToWipe

Lines of code

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

Vulnerability details

Impact

restructureCapTable does not enumerate each addressToWipe

Proof of Concept

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

    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]; @> audit
            _burn(current, balanceOf(current));
        }
    }

Tools Used

Recommended Mitigation Steps

Recommendation

    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[i]; 
            _burn(current, balanceOf(current));
        }
    }

Position accidentally inherits `Ownable` instead of `Owner` which removes an important safeguard without sponsor knowledge

Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L15, https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/PoolsUtility.sol#L62

Vulnerability details

Vulnerability details

Impact

Position may accidentally transfer ownership to inoperable address due to perceived safeguard that doesn't exist

Proof of concept

contracts/utilities/poolsUtility/Position.sol

15:   contract Position is Ownable, DSMath {

Position.sol#L15
Position inherits from Ownable rather than Owner, which is the intended contract.
Owner overwrites the critical Ownable#transferOwnership function to make the ownership transfer process a two step process.
This adds important safeguards because in the event that the target is unable to accept for any reason (input typo, incompatible multisig/contract, etc.) the ownership transfer process will fail because the pending owner will not be able to accept the transfer.

contracts/utilities/poolsUtility/PoolsUtility.sol

62:   position.transferOwnership(msg.sender);

PoolsUtility.sol#L62
To make matters worse, since it only overwrites the transferOwnership() function present in the PoolsUtility contract, the Position contract will function as intended just without this safeguard. It is likely that the owner won't even realize until too late and the safeguard has failed.
A perceived safeguard where there isn't one is more damaging than not having any safeguard at all.

Tools Used

Manual Review

Recommended mitigation steps

-  15:   contract Position is Ownable, DSMath {
+  15:   contract Position is Owner, DSMath {

suggestMinter() function allows attacker to submit many proposals and drain reserve funds

Lines of code

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

Vulnerability details

Impact

The _applicationFee is transferred before the proposal is vetted, which may allow a malicious user to submit a large number of proposals and drain funds from the reserve address.

Proof of Concept

_transfer(msg.sender, address(reserve), _applicationFee);

This line of code transfers the _applicationFee from the caller's address (msg.sender) to the reserve address. The transfer occurs before the proposal is added to the minters mapping, which means that the fee seems to be transferred before the proposal is vetted.

Scenario cases:

  1. If the _applicationFee is small, the malicious user could submit many proposals, each with a small fee. This could drain funds from the reserve address if the number of proposals submitted is large enough.
  2. If the contract does not has limit on the number of proposals that can be submitted by a single address, malicious user could submit multiple proposals using different addresses. This could allow the malicious user to submit many proposals without triggering the limit imposed by the contract.

Tools Used

VS Code

Recommended Mitigation Steps

To prevent this, implement a mechanism that only allows proposals to be submitted if the reserve address has sufficient funds to cover the _applicationFee.
Also limit to prevent an attacker from submitting multiple proposals and draining funds from the reserve address.

notifyLoss can be frontrun by redeem

Lines of code

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

Vulnerability details

Impact

notifyLoss can be frontrun by redeem

Proof of Concept

notifyLoss immediately transfer zchf from reserve to the minter, reducing the amount of reserve and hence the equity and zchf to claim pershare.

While the deposit has 90 days cooldown before depositor can withdraw, current depositor that passed this cooldown can take advantage of a notifyLoss event by first frontrunning the notifyLoss by redeeming, then re-depositing into the protocol to take advantage of the reducedvalue per share.

notifyLoss can only be called by MintingHub::end, current depositor can bundle redeem + end + deposit, when they see a challenge that is ending in loss for the reserve.

Tools Used

Recommended Mitigation Steps

This is a re-current issue for most defi strategy to account loss in a mev-resistent way, a few possible solutions:

  1. create an additional window for MintingHub::end to be called by a whitelist, before it opens up to the public. The whitelist is trusted bot that will call end through private mempool.
  2. amortised the loss in the next coming period of time instead in 1 go with a MAX_SPEED.
  3. create an withdrawal queue such that the final withdrawal price is dependent on the upcoming equity change(s)

denyMinter() function could allow invalid _minter addresses

Lines of code

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

Vulnerability details

Impact

The malicious user could potentially deny a non-existent or invalid minter address, which could lead to unexpected behavior in the contract.

Proof of Concept

reserve.checkQualified(msg.sender, _helpers);
delete minters[_minter];

The denyMinter() function is set as external. In this function, there is no checking to ensure that the proposed _minter address is a valid contract address before deleting it from the minters mapping.

Tools Used

VS Code

Recommended Mitigation Steps

Implement checking to ensure that the proposed _minter address is a valid contract address before proceeding with the denial.

Equity redeem function to loss tokens

Lines of code

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

Vulnerability details

Impact

Detailed description of the impact of this finding.
The redeem function in Equity contract accepts a target without validation for 0x0 address.
As such, after burning the share of the caller, the frankencoin tokens are sent to 0x0 address and can be permanently lost.

This is high risk as this could lead to imbalance between frankencoin and FPS share in the equity.
A bad player could hit of this equilibrium mechanism of the frankencoin tokenomics.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
equity.redeem(address(0x0), 5000);

By the above call, the FPS shares are burnt and corresponding tokens are sent to 0x0 address and cannot be recovered. As long as the holding time is met, this function can be called by any one and could be used for manipulation by effecting the supplies for FPS and Franken coin.

Tools Used

Manual review

Recommended Mitigation Steps

add require statement validating the target address

QA Report

See the markdown file with the details of this report here.

MintingHub.end() attempts to return collateral twice, which can fail and lock collateral

Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/fc86a4ada6dfaae3990609e3cc531f5c85a8f785/contracts/MintingHub.sol#L257
https://github.com/code-423n4/2023-04-frankencoin/blob/fc86a4ada6dfaae3990609e3cc531f5c85a8f785/contracts/MintingHub.sol#L260
https://github.com/code-423n4/2023-04-frankencoin/blob/fc86a4ada6dfaae3990609e3cc531f5c85a8f785/contracts/Position.sol#L352

Vulnerability details

Impact

Challenger collateral may become locked. MintingHub.end() attempts to return collateral twice, which can cause revert and make the collateral irretrievable.

Proof of Concept

MintingHub.end() makes two calls to return the same collateral value:

1)

MintingHub.returnCollateral() is invoked directly within end() [here[(https://github.com/code-423n4/2023-04-frankencoin/blob/fc86a4ada6dfaae3990609e3cc531f5c85a8f785/contracts/MintingHub.sol#L257):

        returnCollateral(challenge, postponeCollateralReturn);

2)

Subsequently within end() a call is made to notifyChallengeSucceeded() here:

        (address owner, uint256 effectiveBid, uint256 volume, uint256 repayment, uint32 reservePPM) = challenge.position.notifyChallengeSucceeded(recipient, challenge.bid, challenge.size);

This in turn invokes a collateral return here:

        internalWithdrawCollateral(_bidder, _size); // transfer collateral to the bidder and emit update

Tools Used

Documentation review, visual inspection

Recommended Mitigation Steps

Correct logic to only return collateral once.

Non-compliance with the `EIP-2612/EIP-712` standard

Lines of code

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

Vulnerability details

Impact

Non-compliance with the EIP-2612/EIP-712 standard.

When generating DOMAIN_SEPARATOR, a truncated EIP712Domain is used, in which the following fields are omitted: name, version. Which leads to the generation of a DOMAIN_SEPARATOR that does not correspond to what is expected according to the standard.

Proof of Concept

We can see what's in the contract ERC20PermitLight.sol when generating DOMAIN_SEPARATOR, the name and version fields are missing, as well as the hash for EIP712Domain does not correspond to what is in the standard

  • Non-compliance with the standard
  • The absence of these fields makes it easier to reuse signatures
  • Standard permit libraries will not be able to generate them and will have to rely on the return from the DOMAIN_SEPARATOR method
File: contracts\ERC20PermitLight.sol

61:     function DOMAIN_SEPARATOR() public view returns (bytes32) {
62:         return
63:             keccak256(
64:                 abi.encode(
65:                     //keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");
66:                     bytes32(0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218),
67:                     block.chainid,
68:                     address(this)
69:                 )
70:             );
71:     }

Links on the resource side:

Tools Used

  • Manual review

Recommended Mitigation Steps

  • Correct according to the standard

Failed challenges can incorrectly return collateral to highest failing bidder

Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/fc86a4ada6dfaae3990609e3cc531f5c85a8f785/contracts/MintingHub.sol#L259-L260

Vulnerability details

Impact

Calling MintingHub.end() can treat a failed challenge as successful and incorrectly return the collateral to the highest failing bidder.

Proof of Concept

All calls to end() will invoke this code which will always return collateral to either the challenge.bidder or msg.sender, even for failed challenges:

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

In the cases of failing bids, recipient will be the highest failing challenge.bidder.

Tools Used

Documentation. Visual inspection.

Recommended Mitigation Steps

Update end() logic to only call notifyChallengeSucceeded() if the challenge succeeded.

MintingHub.end() logic enables front-runner to steal collateral when un-bid challenge ends

Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/fc86a4ada6dfaae3990609e3cc531f5c85a8f785/contracts/MintingHub.sol#L259

Vulnerability details

Impact

Challenge collateral can reliably be stolen by a front-runner if:

  • the challenge has ended
  • there are no bids

Proof of Concept

The following lines will set the recipient as msg.sender if there are no bidders:

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

This allows any front-runner to wait for called to end(), and front-run them their own call to end() with the expectation of stealing collateral.

Tools Used

Documentation, visual inspection

Recommended Mitigation Steps

Consider updating code to send collateral back to the one who provided it, if no bids were made.

QA Report

See the markdown file with the details of this report here.

Implementation of IERC677Receiver doesn't follow the standard

Lines of code

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/IERC677Receiver.sol#L4-#L8
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/StablecoinBridge.sol#L75-#L84

Vulnerability details

Impact

The receiver in IERC677 implementation should be properly checked to ensure if the address to be passed is a contract or not. The current implementation of functions doesn't cover that.

Moreover, there is no support to or implementation of ERC677 standard by Ethereum. The proposal is still in the draft stage.

Proof of Concept

Include a check to see if the address is a contract.

function isContract(address addr) internal view returns (bool) {
    uint size;
    assembly {
      size := extcodesize(addr)
    }
    return (size > 0);
  }

& revert accordingly.

Tools Used

Manual Review

Recommended Mitigation Steps

Comply with the standard, and include a check to see if the address is a contract address.
Reference Implementation

Non-compliance with the EIP-2 & (BIP 62/146) standard, s < n/2 + 1 validation is miss

Lines of code

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

Vulnerability details

Impact

  • Non-compliance with the standard EIP-2
  • Possibility to change s, r by places, as well as v by 27-28 to get the same signature

Proof of Concept

The contract ERC20PermitLight.sol use ecrecover without s < n/2 + 1 requirement.
According to the standard described in

the contract violates the requirement

All transaction signatures whose s-value is greater than secp256k1n/2 are now considered invalid. The ECDSA recover precompiled contract remains unchanged and will keep accepting high s-values; this is useful e.g. if a contract recovers old Bitcoin signatures.

The requirement that 0 < s < secp256k1n ÷ 2 + 1 is imposed in order to avoid attacks on ECDSA, such as extraneous canonical curve attacks. In addition, it facilitates the verification of signatures and improves the overall security of the system.


Tools Used

  • Manual review

Recommended Mitigation Steps

  • Add the appropriate check

Shareholders will never be able to restructure capital due to a loop iteration error.

Lines of code

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

Vulnerability details

Impact

The restructureCapTable() function in Equity.sol is designed to allow FPS shareholders to be able to restructure capital by burning the tokens of an array of addressesToWipe.

Because of an error in the loop of this function, only the 0th index in the array, address[] calldata addressesToWipe will be targeted for restructuring rather than addressesToWipe.length as intended by the protocol.

Proof of Concept

Let us examine the restructureCapTable() function.


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

As you can see, the current address targeted will always be addressesToWipe[0]; this means, all other addresses in the array address[] calldata addressesToWipe will never be targeted, therefore their tokens will never be fully restructured.

Tools Used

Manual Review

Recommended Mitigation Steps

Correct the function to iterate through all addresses in addressesToWipe like so:

   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[i];
            _burn(current, balanceOf(current));
        }
    }

Approve and transferFrom functions of ERC20 implementation are subject to front-run attack.

Lines of code

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

Vulnerability details

Impact

approve and transferFrom methods of ERC20 implementation are subject to front-running attacks because the approve method overwrites the current allowance regardless of whether the spender has already used it or not. In case the spender spends the amount, the approve function will approve a new amount. As a result, the user loses funds.

This is a standard issue for ERC20 implementations

Proof of Concept

According to NatSpec for ERC20.sol functions were added to the implementation to prevent the possibility of an attack, but in fact, the contract ERC20.sol does not contain decreaseAllowance, increaseAllowance functions that would mitigate the possibility of front-run on approve.

Links on the resource side:

Tools Used

Recommended Mitigation Steps

  • Add methods to mitigate the front-run approve issue

The initialization period for position is different in the code and documentation.

Lines of code

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

Vulnerability details

Impact

The initialization period for position is different in the code and documentation.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
According to the docs

When opening a position, it is already provided with some collateral, but nothing is minted yet. Minting is not possible until the initialization period of seven days has passed.

However, this statement does not hold true in the code, as there is only a 3-day restriction before the owner can start minting.
require(initPeriod >= 3 days);

    constructor(address _owner, address _hub, address _zchf, address _collateral, 
        uint256 _minCollateral, uint256 _initialLimit, uint256 initPeriod, uint256 _duration,
        uint256 _challengePeriod, uint32 _mintingFeePPM, uint256 _liqPrice, uint32 _reservePPM) {
        require(initPeriod >= 3 days); // must be at least three days, recommended to use higher values
        setOwner(_owner);
        original = address(this);
        hub = _hub;
        price = _liqPrice;
        zchf = IFrankencoin(_zchf);
        collateral = IERC20(_collateral);
        mintingFeePPM = _mintingFeePPM;
        reserveContribution = _reservePPM;
        minimumCollateral = _minCollateral;
        challengePeriod = _challengePeriod;
        start = block.timestamp + initPeriod; // one week time to deny the position
        cooldown = start;
        expiration = start + _duration;
        limit = _initialLimit;
        
        emit PositionOpened(_owner, original, _zchf, address(collateral), _liqPrice);
    }

contracts/Position.sol#L53

Tools Used

Manualr

Recommended Mitigation Steps

Change docs to 3 days or make it 7 days in the code.

As protocol relies heavily on admin actions, single-step ownership transfer pattern is dangerous

Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L15

Vulnerability details

Vulnerability details

Description

contracts/utilities/poolsUtility/Position.sol

15:   contract Position is Ownable, DSMath {

Inheriting from OpenZeppelin's Ownable contract means you are using a single-step ownership transfer pattern. If an admin provides an incorrect address for the new owner this will result in none of the onlyOwner marked methods being callable again. The better way to do this is to use a two-step ownership transfer approach, where the new owner should first claim its new rights before they are transferred.

Tools Used

Manual Review

Recommended mitigation steps

Use OpenZeppelin's Ownable2Step instead of Ownable

QA Report

See the markdown file with the details of this report here.

delegate can further delegate votes

Lines of code

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

Vulnerability details

Impact

delegate can further delegate votes

Proof of Concept

canVoteFor recursively checks, so if A delegate to B, B then delegate to C. then C can vote on-behalf of A + B + C.

    function canVoteFor(address delegate, address owner) internal view returns (bool) {
        if (owner == delegate){
            return true;
        } else if (owner == address(0x0)){
            return false;
        } else {
            return canVoteFor(delegate, delegates[owner]);
        }
    }

Tools Used

Recommended Mitigation Steps

MintingHub bid function can fire invalid bid notification to external world

Lines of code

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

Vulnerability details

Impact

Detailed description of the impact of this finding.
MintingHub.bid function will fire events to the Dapps above a bid, while the same has actually failed in blockchain. Any Dapps monitoring the status of challenges and bids will be misguided.

example:
MintingHub.bid(validChallengeId, validAmount,0);

The attacker can call this function with out sufficient balance of FrankenCoins in his account. As such, based on the conditions in the function, the below event will be fired for the dapps that the new user has a bid.

emit NewBid(validChallengeId, validAmount, msg.sender);

But, the function will fail due to lack of proper balance in the account. So, Onchain knows about the failure, while offchain world would get false notifications.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

example:
MintingHub.bid(validChallengeId, validAmount,0);
The caller of bid function does not have sufficient balance of FrankenCoins to claim the position.

Tools Used

Manual review

Recommended Mitigation Steps

Checks Effects Interaction Pattern is an approach to minimise the partial execution of contract.Until all the conditions for the input data is valid, dont let the program flow enter the business functionality.

https://medium.com/returnvalues/smart-contract-security-patterns-79e03b5a1659

Agreements & Disclosures

Agreements

If you are a C4 Certified Contributor by commenting or interacting with this repo prior to public release of the contest report, you agree that you have read the Certified Warden docs and agree to be bound by:

To signal your agreement to these terms, add a 👍 emoji to this issue.

Code4rena staff reserves the right to disqualify anyone from this role and similar future opportunities who is unable to participate within the above guidelines.

Disclosures

Sponsors may elect to add team members and contractors to assist in sponsor review and triage. All sponsor representatives added to the repo should comment on this issue to identify themselves.

To ensure contest integrity, the following potential conflicts of interest should also be disclosed with a comment in this issue:

  1. any sponsor staff or sponsor contractors who are also participating as wardens
  2. any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
  3. any wardens who have a relationship with a judge that would typically fall in the category of potential conflict of interest (family, employer, business partner, etc)
  4. any other case where someone might reasonably infer a possible conflict of interest.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.