GithubHelp home page GithubHelp logo

2023-01-reserve-findings's Introduction

Reserve 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

As a sponsor, you 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. It is possible for issues to be considered 0 (Non-critical).

Feel free to use the question label for anything you would like additional C4 input on.

Please don't change the severity labels; that's up to the judge's discretion.

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.

QA and Gas Reports

For low and non-critical findings (AKA QA), as well as gas optimizations: all warden submissions in these three categories should now be submitted as bulk listings of issues and recommendations:

  • 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, we ask that you:

  • 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 your own GitHub repo, 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. Do not close the issue; simply label it as resolved. If the issue in question has duplicates, please link to your PR from the open/primary issue.

2023-01-reserve-findings's People

Contributors

code423n4 avatar c4-judge avatar kartoonjoy avatar captainmangoc4 avatar

Watchers

Ashok avatar  avatar

Forkers

ololade97

2023-01-reserve-findings's Issues

Insufficient mapping to approve multi private _normalizedIncome

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/AaveLendingPoolMock.sol#L25

Vulnerability details

Impact

This implementation doesn’t support multi private _normalizedIncome

Proof of Concept

At https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/AaveLendingPoolMock.sol#L25

mapping(address => uint256) private _normalizedIncome;

Tools Used

Manual VS Code

Recommended Mitigation Steps

mapping(address => mapping(address => uint256) private _normalizedIncome;

Insufficient mapping to approve multi longFreezes

Multiple Vulnerabilities in 'BrokerP1' Smart Contract - Leading to unauthorized trades, Loss of Functionality, Stolen funds, Front-Running and Trade Manipulation, and Overflow issues.

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L116-L118
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L97-L107
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L51-L61
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L68-L71
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L124-L128
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L97-L98
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L126-L127
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L41
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L85-L87
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L85
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L4
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L21
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L85
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L15

Vulnerability details

Impact

  1. gnosis variable is not protected by the onlyOwner modifier, so it may be possible for a malicious actor to alter the Gnosis contract being used by the smart contract, potentially leading to unauthorized trades or funds being transferred.

If a malicious actor is able to alter the Gnosis contract being used by the smart contract, it could potentially lead to unauthorized trades or funds being transferred, potentially resulting in financial loss for the users of the contract.

Altering the Gnosis contract being used by the smart contract could lead to unauthorized trades or funds being transferred, potentially resulting in a financial loss for the users of the contract, which could be a serious issue.

Proof of Concept

An attacker could potentially exploit the vulnerability of the 'gnosis' variable not being protected by the 'onlyOwner' modifier by calling the 'init' function with an address that they control as the 'gnosis_' parameter. This would allow them to redirect trades and potentially steal funds. Here's an example code snippet that demonstrates the general concept of the attack:

contract Attacker {
    function attack(address gnosis, address broker) public {
        IBroker b = IBroker(broker);
        b.init(..., gnosis, ...);
    }
}

Tools Used

Manual audit, Vs Code

Recommended Mitigation Steps

One way to mitigate the vulnerability of the 'gnosis' variable not being protected by the 'onlyOwner' modifier would be to add the 'onlyOwner' modifier to the 'gnosis' variable and the function that initializes it, as well as changing the 'init' function to check that the caller is the owner before initializing the 'gnosis' variable. Here's an example code snippet that demonstrates the general concept of the mitigation:

contract BrokerP1 is ComponentP1, IBroker {
    address private owner;
    ...
    function init(
        IMain main_,
        IGnosis gnosis_,
        ITrade tradeImplementation_,
        uint48 auctionLength_
    ) external initializer {
        require(address(gnosis_) != address(0), "invalid Gnosis address");
        require(
            address(tradeImplementation_) != address(0),
            "invalid Trade Implementation address"
        );
        require(msg.sender == owner, "only owner can call this function");
        __Component_init(main_);
        ...
    }
    function setOwner() external onlyOwner {
        owner = msg.sender;
    }
    ...
}

Impact

  1. disabled variable can be set to true by the reportViolation function of the ITrade interface, which is implemented by the GnosisTrade contract. However, it is not clear from the provided code what conditions must be met for the reportViolation function to be called, and whether the function is properly restricted to only be callable by the intended parties {such as the contract owner}.

If the disabled variable can be set to true by a malicious actor, it could prevent legitimate trades from being made, leading to loss of functionality for users of the contract. Additionally, if the conditions for calling reportViolation are not properly restricted, a malicious actor may be able to disable the contract arbitrarily.

If the 'disabled' variable can be set to 'true' by a malicious actor, it could prevent legitimate trades from being made, leading to loss of functionality for users of the contract. This could be a serious issue, as it would prevent the intended use of the contract.

Proof of Concept

An attacker could potentially exploit the vulnerability of the 'disabled' variable by calling the reportViolation function on a GnosisTrade contract that they control, which would set the 'disabled' variable to 'true' and prevent legitimate trades from being made. The exact method of exploitation would depend on the specifics of the implementation, such as the conditions for calling reportViolation and the parties that are able to call it.

It would involve creating a new instance of the GnosisTrade contract and calling the reportViolation function on it.

pragma solidity ^0.8.0;

contract Attacker {
    address public attacker;
    GnosisTrade fakeTrade;
    BrokerP1 broker;

    constructor(address _broker) public {
        broker = BrokerP1(_broker);
        attacker = msg.sender;
    }

    function executeAttack() public {
        fakeTrade = new GnosisTrade();
        fakeTrade.reportViolation();
    }
}

Attacker contract creates a new instance of the GnosisTrade contract and calls the reportViolation function on it. The reportViolation function is supposed to be called when there is a violation of the contract, however, since the attacker controls this GnosisTrade instance, the attacker can use it to call the function and set the disabled variable in the BrokerP1 contract to true. This would prevent legitimate trades from being made.

Tools Used

Manual audit, Vs Code

Recommended Mitigation Steps

One way to mitigate the vulnerability of the 'disabled' variable would be to add proper restriction to the conditions for calling 'reportViolation' and the parties that are able to call it, in order to ensure that only authorized parties are able to set the 'disabled' variable to 'true'. Additionally, adding a delay before the effect of the function, that is disabling trading, will give the owner or other responsible party an opportunity to react.

interface ITrade {
    function reportViolation() external onlyOwner;
}
contract GnosisTrade is ITrade {
    function reportViolation() external onlyOwner {
        require(msg.sender == owner, "only owner can call this function");
        require(block.timestamp > startTime + delay);
        ...
    }
}

Impact

  1. openTrade function does not check for a sufficient allowance being set before transferring funds from the caller to the newly created trade contract.

If the contract does not check for a sufficient allowance being set before transferring funds from the caller to the newly created trade contract, then it may be possible for a malicious actor to steal funds by creating a trade with a large sellAmount

Not checking for a sufficient allowance before transferring funds from the caller to the newly created trade contract could allow malicious actors to steal funds, which could be a serious issue.

Proof of Concept

An attacker could potentially exploit the vulnerability of the 'openTrade' function not checking for a sufficient allowance being set before transferring funds by calling the 'openTrade' function with a high 'sellAmount' and a low allowance. This would allow the attacker to steal funds by creating a trade with a large sellAmount. Here's an example code snippet that demonstrates the general concept of the attack:

contract Attacker {
    function attack(address broker) public {
        IBroker b = IBroker(broker);
        b.openTrade(..., 10000000000000000, ...);
    }
}

Tools Used

Manual audit, Vs Code

Recommended Mitigation Steps

To mitigate the vulnerability of the 'openTrade' function not checking for a sufficient allowance being set before transferring funds, the function could be updated to check the caller's allowance before transferring the funds. An example code snippet would look like this:

function openTrade(TradeRequest memory req) external notPausedOrFrozen returns (ITrade) {
    ...
    require(req.sell.allowance(req.trader, address(this)) >= req.sellAmount, "insufficient allowance");
    ...
}

Impact

  1. The contract use SafeERC20Upgradeable from openzeppelin and "Clones" but it not set any upgrade mechanism, so this smart contract could be in a state that no more can be upgraded after deployed.

The contract using 'SafeERC20Upgradeable' and "Clones" but it does not set any upgrade mechanism, so this smart contract could be in a state where no more can be upgraded after deployed. This could lead to the contract becoming outdated and vulnerable over time.

The contract uses 'SafeERC20Upgradeable' and "Clones" but does not set any upgrade mechanism, so this smart contract could be in a state where no more can be upgraded after deployed. This could lead to the contract becoming outdated and vulnerable over time and can be a serious issue.

Proof of Concept

An attacker will take advantage of this by creating a new version of the contract with malicious code, and if it's deployed to the same address as the original contract and then tricking users to interact with it. This could potentially allow the attacker to steal user funds and perform unwanted actions.

An attacker could exploit the vulnerability of the contract using 'SafeERC20Upgradeable' and "Clones" but not set any upgrade mechanism, by creating a new version of the contract with malicious code, and tricking users to upgrade to the new version. This will allow attackers to steal the funds of the users and perform unwanted actions.

For this vulnerability would involve creating a new version of the contract with malicious code, such as a stealFund() function, and then deploying it to the same address as the original contract. An attacker could then trick users into interacting with the malicious contract instead of the original, allowing the attacker to steal funds or perform other unwanted actions.

For example, an attacker could create a malicious version of the contract that looks identical to the original but has added a stealFund() function. They would then have to trick users or exchanges to interact with the malicious version, instead of the original one.

Here is an example of the malicious contract:

pragma solidity ^0.8.0;

contract MaliciousUpgrade {
    address payable public victim;

    constructor() public {
        victim = msg.sender;
    }

    function stealFunds() public {
        require(victim.transfer(address(this).balance));
    }
}

Tools Used

Manual audit, Vs Code

Recommended Mitigation Steps

To mitigate the vulnerability of the contract using 'SafeERC20Upgradeable' and "Clones" but not set any upgrade mechanism, one way is to implement a upgrade mechanism that allow user to upgrade the contract in a safe way. Such as making use of the 'Upgradability Proxy' that OpenZeppelin provides, or another upgrade library with similar functionality. It will allow the contract owner to manage and deploy new version of the contract in a safer way.

Impact

  1. openTrade function does not check the address of the caller is a Trader or not.

If the 'openTrade' function does not check the address of the caller is a Trader or not, it can lead to an attacker opening a trade without having the right to do it, leading to potential financial loss.

Not checking the address of the caller is a Trader or not, could allow malicious actors to open trades without having the right to do it, leading to potential financial loss, which could be a serious issue.

Proof of Concept

An attacker could potentially exploit the vulnerability of the 'openTrade' function not checking the address of the caller is a Trader or not, by calling the 'openTrade' function with an address that they control. This would allow them to open trades without having the right to do it and potentially steal funds.
An attacker would involve creating a new Ethereum address that is not an authorized Trader, and then calling the openTrade function from that address.

pragma solidity ^0.8.0;

contract Attacker {
    address traderAddress;
    address public attacker;
    BrokerP1 broker;

    constructor(address _broker) public {
        broker = BrokerP1(_broker);
        attacker = msg.sender;
    }

    function executeAttack() public {
        TradeRequest memory req;
        req.sell = ERC20(0x...);
        req.buy = ERC20(0x...);
        req.sellAmount = 1 ether;
        req.buyAmount = 2 ether;
        req.startPrice = 1 ether;
        req.endPrice = 2 ether;
        req.duration = 604800;
        broker.openTrade(req);
    }
}

In this, Attacker contract creates a TradeRequest and calls the openTrade function of the BrokerP1 contract, and since the function doesn't have any protection to check if the caller is a trader, the Attacker is able to open trades without being authorized.

Tools Used

Manual audit, Vs Code

Recommended Mitigation Steps

To mitigate the vulnerability of the 'openTrade' function not checking the address of the caller is a Trader or not, the function could be updated to check the caller's role and ensure that only authorized parties are able to open trades. An example code snippet would look like this:

function openTrade(TradeRequest memory req) external notPausedOrFrozen returns (ITrade) {
    ...
    require(traders[msg.sender], "caller must be a trader");
    ...
}

Impact

  1. In the future, if the contract will have more sophisticated choice logic here, probably by trade size, this could lead to a possibility of front-running or trade manipulation.

If in the future the contract will have more sophisticated choice logic here, probably by trade size, this could lead to a possibility of front-running or trade manipulation, meaning that an attacker could gain an unfair advantage and potentially profit at the expense of other users.

The possibility of front-running or trade manipulation could be a serious issue, as it would allow malicious actors to gain an unfair advantage and potentially profit at the expense of other users.

Proof of Concept

An attacker could exploit the possibility of front-running or trade manipulation by monitoring the trade orders in the contract, then quickly placing an order before legitimate traders, allowing them to gain an unfair advantage and potentially profit at the expense of other users.

Would involve creating a new Ethereum address that is not an authorized Trader, and then calling the openTrade function from that address.

pragma solidity ^0.8.0;

contract Attacker {
    address traderAddress;
    address public attacker;
    BrokerP1 broker;

    constructor(address _broker) public {
        broker = BrokerP1(_broker);
        attacker = msg.sender;
    }

    function executeAttack() public {
        TradeRequest memory req;
        req.sell = ERC20(0x...);
        req.buy = ERC20(0x...);
        req.sellAmount = 1 ether;
        req.buyAmount = 2 ether;
        req.startPrice = 1 ether;
        req.endPrice = 2 ether;
        req.duration = 604800;
        broker.openTrade(req);
    }
}

The Attacker contract creates a TradeRequest and calls the openTrade function of the BrokerP1 contract, and since the function doesn't have any protection to check if the caller is a trader, the Attacker is able to open trades without being authorized.

Tools Used

Manual audit, Vs Code

Recommended Mitigation Steps

One way to mitigate the vulnerability of front-running or trade manipulation would be to implement a randomization mechanism or block-based restrictions, to reduce the predictability of the ordering. Additionally, implementing a rate-limiting mechanism prevents a large number of trades from being initiated by a single address within a short period of time. Another option could be implementing a centralized order book, in which the broker will be responsible for matching the trade orders

Impact

  1. There's a large constant GNOSIS_MAX_TOKENS with a fixed value of 7e28 which may be subject to overflow issues or unintended consequences if its value is exceeded in any operation.

The constant GNOSIS_MAX_TOKENS with a fixed value of 7e28 could be subject to overflow issues or unintended consequences if its value is exceeded in any operation, leading to unexpected results and potential errors.

Constant GNOSIS_MAX_TOKENS with a fixed value of 7e28 could be subject to overflow issues or unintended consequences if its value is exceeded in any operation, leading to unexpected results and potential errors, which could be a serious issue.

Proof of Concept

An attacker could potentially exploit the vulnerability of the constant "GNOSIS_MAX_TOKENS" by passing large values to the contract's functions that could lead to overflow issues or unintended consequences if its value is exceeded in any operation.
Passing large values to the functions of the contract that use the GNOSIS_MAX_TOKENS constant

pragma solidity ^0.8.0;

contract Attacker {
    address public attacker;
    BrokerP1 broker;

    constructor(address _broker) public {
        broker = BrokerP1(_broker);
        attacker = msg.sender;
    }

    function executeAttack() public {
        TradeRequest memory req;
        req.sellAmount = 2**200; 
        // This value exceeds the maximum value defined by GNOSIS_MAX_TOKENS
        broker.openTrade(req);
    }
}

Attacker contract creates a TradeRequest with a sellAmount value that exceeds the maximum value defined by GNOSIS_MAX_TOKENS and calls the openTrade function of the BrokerP1 contract. Depending on how the openTrade function uses the GNOSIS_MAX_TOKENS constant, this may cause the function to produce unintended results, such as overflow issues.

Tools Used

Manual audit, vs code

Recommended Mitigation Steps

To mitigate the vulnerability of the constant "GNOSIS_MAX_TOKENS" subject to overflow issues, one solution could be to handle the overflow exception by writing a SafeMath library or using a library like OpenZeppelin's SafeMath library, that will automatically handle overflow and underflow errors.

Impact

  1. The contract doesn't handle any exceptions which may occur.

If the contract doesn't handle any exceptions which may occur, it could lead to unexpected results in which the contract stops working or even funds can be locked, which could negatively impact the users of the contract.
If the contract doesn't handle any exceptions that may occur, it could lead to unexpected results in which the contract stops working or even funds can be locked, which could negatively impact the users of the contract, which could be a serious issue.

Proof of Concept

An attacker could exploit the vulnerability that the contract doesn't handle with any exception by performing actions that would trigger exceptions, such as passing invalid input to the contract. This would cause the contract to fail, leading to unexpected results such as funds being locked, and negatively impacting the users of the contract.

pragma solidity ^0.8.0;

contract Attacker {
    address public attacker;
    BrokerP1 broker;

    constructor(address _broker) public {
        broker = BrokerP1(_broker);
        attacker = msg.sender;
    }

    function executeAttack() public {
        broker.openTrade(0x0);
        //the broker contract expect a TradeRequest as input, passing 0x0 will trigger an exception 
    }
}

Tools Used

Manual audit, vs code

Recommended Mitigation Steps

To mitigate the vulnerability that the contract doesn't handle with any exception, it's important to anticipate and handle with any exception that may occur, such as passing invalid input to the contract. This can be done by including error-handling mechanisms like require() statements, or by using the assert() function to ensure that the preconditions and postconditions of the contract are met.

function myFunction(uint256 _a, uint256 _b) public {
    require(_a > 0, "a must be greater than 0");
    require(_b > 0, "b must be greater than 0");
    uint256 result = _a + _b;
    require(result > _a, "overflow detected");
    ...
}

QA Report

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

Use safe ERC721 mint

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSRVotes.sol#L138
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L694
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L268
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L556
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L559
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L791
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BackingManager.sol#L203
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/aave/StaticATokenLM.sol#L305
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/aave/IAToken.sol#L24

Vulnerability details

Impact

If the token being minted is of the ERC721 type and the _mint() function is not using the safeERC721Mint function, then there could be potential issues related to the uniqueness and non-existance of the tokens being minted.

Without the use of safeERC721Mint, there is a risk that tokens may be minted that are not unique, meaning they have the same token ID as an existing token. This could lead to confusion and errors in the contract's behavior. Additionally, if the minting function doesn't check if the token already exist then multiple copies of token with same ID will be exist which is invalid for ERC721 standard, it could also create issues when trying to transfer or otherwise interact with these tokens.

Proof of Concept

At https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSRVotes.sol#L138

function _mint(address account, uint256 amount) internal override {
        super._safeMint(account, amount);
        _writeCheckpoint(_totalSupplyCheckpoints[era], _add, amount);
    }

Tools Used

Manual code audit

Recommended Mitigation Steps

    function _safeMint(address account, uint256 amount) internal override {
        super._safeMint(account, amount);
        _writeCheckpoint(_totalSupplyCheckpoints[era], _add, amount);
    }

Using the low-level function ".call" leads to no check for contract existence

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/vendor/EasyAuction.sol#L721
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/vendor/EasyAuction.sol#L797

Vulnerability details

Impact

If the recipient address is not a contract address, the call will always fail and throw an exception which will cause the transaction to revert, consuming the gas but not making any change on the state. This will result in wasted gas and a potential loss of funds for the caller of the function.

Additionally, if the recipient address is a contract address, but the contract is not active or has been self-destructed, the call will also fail and throw an exception. This can lead to unexpected behavior and may cause the function to return errors or produce unexpected results.

In summary, not checking for contract existence before using the low-level function ".call" can lead to wasted gas and potential loss of funds if the recipient address is not a contract address and can lead to unexpected behavior and errors if the contract is not active or has been self-destructed

Proof of Concept

At https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/vendor/EasyAuction.sol#L721

    function sendValue(address payable recipient, uint256 amount) internal {
        require(address(this).balance >= amount, "Address: insufficient balance");

        // solhint-disable-next-line avoid-low-level-calls, avoid-call-value
        (bool success, ) = recipient.call{ value: amount }("");
        require(success, "Address: unable to send value, recipient may have reverted");
    }

Tools Used

Manual VS Code review

Recommended Mitigation Steps

add a check that verifies whether the recipient address is a contract address or not.
This can be done by checking the code at the recipient address using the EXTCODESIZE opcode and checking if the returned value is greater than 0. If the value is greater than 0, it means that there is code deployed at that address and it is a contract address.

Additionally, it's also important to check if the contract is active or not, it can be done by calling a method of the contract that check if the contract is active or not, this method should be implemented in the contract.

Example:
``
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");

//Checking recipient address is contract address
bytes4 sig = bytes4(keccak256("isActive()")); 
bool isActive = address(recipient).staticcall(sig); 
require(isActive, "Recipient contract is not active");

//Checking recipient address has code
require(recipient.staticcall(bytes4(keccak256("EXTCODESIZE"))), "Recipient address is not a contract address");

// solhint-disable-next-line avoid-low-level-calls, avoid-call-value
(bool success, ) = recipient.call{ value: amount }("");
require(success, "Address: unable to send value, recipient may have reverted");

}

In this example, first it's checking if the recipient contract is active or not by calling a method `isActive()` which is implemented in the contract, if it's not active it will throw an error message. And then it's checking if the recipient address is a contract address by calling `EXTCODESIZE` opcode, if it's not a contract address it will throw an error message.

QA Report

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

Use safe ERC721 mint

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L230

Vulnerability details

Impact

If the _safeMint() function was not used in the stake() function, it would not provide any guarantees about the safety of the minting process.

The stake() function is taking an argument rsrAmount, which is then used to compute the stakeAmount that the user shall receive. Then it calls _mint(account, stakeAmount) to mint the token for the user.

Without the use of _safeMint() function, there is a risk that tokens may be minted that are not unique, meaning they have the same token ID as an existing token. This could lead to confusion and errors in the contract's behavior. Additionally, if the minting function doesn't check if the token already exist then multiple copies of token with same ID will be exist which is invalid for ERC721 standard, it could also create issues when trying to transfer or otherwise interact with these tokens.

Proof of Concept

At https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L230

_mint(account, stakeAmount);

Tools Used

Manual VS Code review

Recommended Mitigation Steps

_safeMint(account, stakeAmount);

Insufficient mapping to approve multi delegates

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSRVotes.sol#L31
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSRVotes.sol#L38

Vulnerability details

Impact

This implementation doesn’t support multi-longFreezes.

Proof of Concept

At https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSRVotes.sol#L31

mapping(address => address) private _delegates;

Tools Used

Manual VS Code

Recommended Mitigation Steps

mapping(address => mapping(address => address) private _delegates;

currentAllowance all with type(uint256).max in native token (ETH) will always revert

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L745

Vulnerability details

Impact

the transaction will always fail at L122 because IERC20Detailed(amount).decimals() will revert.

Proof of Concept

At https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L745
if (currentAllowance != type(uint256).max) {

Tools Used

Manual VS Code review

Recommended Mitigation Steps

if (amount == type(uint256).max &&  currentAllowance != address(0)) {

if (_amount == type(uint256).max && _asset != address(0)) {

Arbitrary from in transferFrom

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Distributor.sol#L135

Vulnerability details

Impact

manipulate transactions in the distribute function and take over token from anyone who interacts with the distribute function.

Proof of Concept

Wanda approves this contract to her ERC20 tokens. attacker can call distribute and specify Wanda's address as the from parameter in transferFrom, allowing him to transfer Wanda's tokens to himself.

Tools Used

slither

Recommended Mitigation Steps

Use msg.sender as from in transferFrom.

SWC-101 Integer Overflow and Underflow

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/libraries/Fixed.sol#L504-L518

Vulnerability details

Impact

An overflow/underflow happens when an arithmetic operation reaches the maximum or minimum size of a type. For instance if a number is stored in the uint8 type, it means that the number is stored in a 8 bits unsigned number ranging from 0 to 2^8-1. In computer programming, an integer overflow occurs when an arithmetic operation attempts to create a numeric value that is outside of the range that can be represented with a given number of bits – either larger than the maximum or lower than the minimum representable value.

Proof of Concept

PoC

        if (mm > lo) hi -= 1;
        lo -= mm;
        uint256 pow2 = z & (0 - z);
        z /= pow2;
        lo /= pow2;
        lo += hi * ((0 - pow2) / pow2 + 1);
        uint256 r = 1;
        r *= 2 - z * r;
        r *= 2 - z * r;
        r *= 2 - z * r;
        r *= 2 - z * r;
        r *= 2 - z * r;
        r *= 2 - z * r;
        r *= 2 - z * r;
        r *= 2 - z * r;

Tools Used

Remix IDE

Recommended Mitigation Steps

// import safemath.sol and use it to create custom function to apply instead.
balance = add(balance, deposit);
        if (mm > lo) hi = sub(hi, 1);
        lo = sub(lo, mm);
        uint256 pow2 = z & (0 - z);
        z = div(z, pow2);
        lo = div(lo, pow2);
        lo += hi * ((0 - pow2) / pow2 + 1);
        uint256 r = 1;
        r = mul(r, 2) - z * r;
        r = mul(r, 2) - z * r;
        r = mul(r, 2) - z * r;
        r = mul(r, 2) - z * r;
        r = mul(r, 2) - z * r;
        r = mul(r, 2) - z * r;
        r = mul(r, 2) - z * r;
        r = mul(r, 2) - z * r;

use fund may get locked forever

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L310

Vulnerability details

Impact

in StRSR if a user unstaked twice draftQueues will get updated and after some time the user need to call the function withdraw to withdraw his rsr token and the function take two arguments account and endId and if the user passes the the second locked index *firstRemainingDraftfirstRemainingDraft will get set to the second locked index and if he wanted to withdraw the first locked amount and passes the first locked index the will just return because of

        if (endId == 0 || firstId >= endId) return;

becuase firstid is the second locked and endid is the first one which firstid is always Grete than endId

Proof of Concept

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L300

Tools Used

Manual analysis

Recommended Mitigation Steps

check if the first locked is already withdrawn

Insufficient mapping to approve multi balanceOf

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/WETH.sol#L30

Vulnerability details

Impact

This implementation doesn’t support multi-balanceOf

Proof of Concept

At https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/WETH.sol#L30

mapping(address => uint256) public balanceOf;

Tools Used

Manual VS Code

Recommended Mitigation Steps

mapping(address => mapping(address => uint256) public balanceOf;

Multiple Vulnerabilities in 'AssetRegistryP1' Smart Contract. Registering & Swapping Assets, Manipulating Basket, High Gas Consumption, Unupdateable Initial State.

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/AssetRegistry.sol#L61
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/AssetRegistry.sol#L73
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/AssetRegistry.sol#L73-L97
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/AssetRegistry.sol#L46-L51
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/AssetRegistry.sol#L175

Vulnerability details

Impact

  1. register and swapRegistered functions are marked with governance. The functions are intended to be called a governance role, but if a malicious actor were to gain access to this role, they could register or swap assets for their own benefit. This could lead to assets being registered that don't actually exist, or assets being swapped for assets that the attacker controls.

Proof of Concept

The attacker can exploit the vulnerability allowing them to register or swap assets for their own benefit by first gaining access to the governance role. Once they have this role, they could call the register or swapRegistered functions and provide the contract with assets that they control. Here is an example PoC how the attacker could exploit.

// Attacker gains access to the governance role
contract.grantRole(msg.sender, "governance")

// Attacker creates an ERC20 token that they control
address attackerToken = new AttackerToken()

// Attacker creates an IAsset object that references the attacker-controlled ERC20 token
IAsset attackerAsset = new AttackerAsset(attackerToken)

// Attacker calls the register function and provides the contract with the attacker-controlled IAsset
contract.register(attackerAsset)

Tools Used

Manual audit, Vs code

Recommended Mitigation Steps

The vulnerability that allows a malicious actor to register or swap assets for their own benefit, the governance role should be restricted to only trusted and verified actors. This can be achieved by implementing a whitelisting mechanism or a multi-signature mechanism that requires multiple trusted actors to sign off on any changes to the asset's registry.

// Implement a whitelist of approved addresses
mapping(address => bool) public whitelist;

// Restrict the governance role to addresses on the whitelist
function isGovernanceRole(address _address) internal view returns (bool) {
    return whitelist[_address];
}

function grantRole(address _address, string memory _role) internal {
    require(isGovernanceRole(msg.sender), "Only whitelisted addresses can grant roles");
    require(isGovernanceRole(_address), "Only whitelisted addresses can be granted roles");
    // ... existing role management code ...
}

Impact

  1. disableBasket() in swapRegistered function, If an asset is already in the basketHandler's basket, the swapRegistered function calls basketHandler.disableBasket(). It should be verified that this action does not allow an attacker to manipulate the basketHandler in any unintended way, such as allowing an attacker to remove assets from the basket that they do not own or prevent the addition of assets to the basket that they do not want.

Proof of Concept

An attacker could potentially exploit the vulnerability that the attacker could manipulate the basketHandler in any unintended way by creating an IAsset object that references an ERC20 token that is already in the basketHandler's basket, and then calling the swapRegistered function to register that IAsset object.
This would cause the basketHandler.disableBasket() to be called, which could disable the basket, allowing the attacker to remove assets from the basket that they do not own, or prevent the addition of assets to the basket that they do not want.

// Attacker creates an IAsset object that references an ERC20 token already in the basket
IAsset attackerAsset = new AttackerAsset(basketErc20)

// Attacker calls the swapRegistered function, providing the contract with the attacker-controlled IAsset
contract.swapRegistered(attackerAsset)

In this PoC, the attacker has used the swapRegistered function to register an IAsset object that references an ERC20 token already in the basket, which would cause the basketHandler to disable the basket. This could allow the attacker to manipulate the contents of the basket to their advantage.

Tools Used

Manual Audit, VS Code

Recommended Mitigation Steps

To eliminate the vulnerability that allows an attacker to manipulate the basketHandler in any unintended way, an additional access control mechanism should be implemented that prevents unauthorized actors from modifying the basket.
This could be done by implementing an additional access control mechanism that checks that the caller of the swapRegistered function is the owner of the ERC20 token that is already in the basket.

function swapRegistered(IAsset asset) external governance returns (bool swapped) {
    require(_erc20s.contains(address(asset.erc20())), "no ERC20 collision");

    // Check that the caller of the function is the owner of the ERC20 token
    require(asset.erc20().isOwner(msg.sender), "Only the owner of the ERC20 token can swap it");

    // ... existing swapRegistered code ...
}

Impact

  1. refresh function, this calls the refresh() function on all assets in the registry, which may cause a large amount of gas to be spent if there are a large number of assets in the registry. This could lead to the contract running out of gas and being unable to complete its intended function. It could also make the contract too expensive for regular users to interact with. Alternative solutions such as calling the refresh function on a subset of assets should be considered.

Proof of Concept

Bad actors could exploit the vulnerability that a large amount of gas could be spent if there are a large number of assets in the registry by calling the refresh function multiple times when there are a large number of assets in the registry.
Here's a code snippet that demonstrates the general concept of this attack.

// Attacker calls the refresh function repeatedly
while(true) {
    contract.refresh()
}

Tools Used

Manual audit, Vs Code

Recommended Mitigation Steps

In this case, to mitigate the vulnerability that a large amount of gas could be spent if there are a large number of assets in the registry, an alternative solution that allows the contract to refresh a subset of assets should be implemented. One way of doing this is to implement an additional function that allows users to call the refresh() function on specific assets, rather than on all assets in the registry. this is how it will be implemented.

function refreshAsset(IERC20 _erc20) external {
    require(_erc20s.contains(address(_erc20)), "Asset is not registered");
    assets[_erc20].refresh();
}

Centralization risk: contract have a single point of control

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/EasyAuction.sol#L129

Vulnerability details

Impact

Centralization risks are weaknesses that malevolent project creators as well as hostile outside attackers can take advantage of. They may be used in several forms of attacks, including rug pulls and infinite minting vulnerabilities.

Proof of Concept

Finding

function setFeeParameters(uint256 newFeeNumerator, address newfeeReceiverAddress)
        public
        onlyOwner
    {

Tools Used

  • Private self-made tool for static analysis
  • Manual Review, Remix IDE

Recommended Mitigation Steps

Some solutions include:

  • implementing timelocks
  • multi signature custody

See also What is Centralization Risk?

approveERC20() max uses unlimited approval and has risk

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BackingManager.sol#L76
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/aave/StaticATokenLM.sol#L101

Vulnerability details

Impact

Granting an unlimited allowance of tokens by setting the value to type(uint256).max can be risky. Since this allow the main.rToken() contract to potentially spend all the caller's tokens, and it could lead to a complete loss of funds if the main.rToken() contract is malicious

Proof of Concept

At https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BackingManager.sol#L76
IERC20Upgradeable(address(erc20)).safeApprove(address(main.rToken()), type(uint256).max);

Tools Used

Manual VS Code review

Recommended Mitigation Steps

function grantRTokenAllowance(IERC20 erc20, uint256 amount) external notPausedOrFrozen {
    require(assetRegistry.isRegistered(erc20), "erc20 unregistered");
    require(amount > 0, "Amount should be greater than 0");
    // == Interaction ==
    IERC20Upgradeable(address(erc20)).safeApprove(address(main.rToken()), 0);
    IERC20Upgradeable(address(erc20)).safeApprove(address(main.rToken()), amount);
}

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.

Multiple Security Vulnerabilities in "Deployer.sol" Smart Contract leading to unauthorized contract deployments, stolen assets, and unexpected behaviors

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Deployer.sol#L109-L114
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Deployer.sol#L46
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Deployer.sol#L120
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Deployer.sol#L206-L216
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Deployer.sol#L7-L17
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Deployer.sol#L25-L29

Vulnerability details

  1. Lack of a mechanism to limit the number of contract deployments
  2. Lack of proper verification of passed in contract addresses
  3. Not declaring memory keyword for Implementations structs components
  4. Lack of a mechanism for upgrading the implementation contracts
  5. Lack of implementation of imported libraries
  6. Lack of a mechanism to pause or stop the contract in case of emergency
  7. Lack of an explicit check for the msg.sender to have permission to call the constructor function.

Impact

  1. No mechanism in place to limit the number of contract deployments. An attacker could repeatedly deploy the contract and exhaust all available gas.

The absence of any mechanism to limit the number of contract deployments can lead to a potential denial-of-service attack If an attacker repeatedly deploys the contract, they can exhaust all available gas, causing the network to become unable to process any new transactions. This can disrupt the functionality of the entire system and make it inaccessible to other users.

Proof of Concept

The attacker could create a script that repeatedly calls the constructor function of the DeployerP1 contract, effectively preventing any other transactions from being processed on the network.

function attack() public {
    while (true) {
        DeployerP1 deployer = new DeployerP1(...); // constructor call
    }
}

Tools Used

Manual audit, Vs Code

Recommended Mitigation Steps

In this way, to mitigate the risk of a denial-of-service attack a mechanism should be implemented to limit the number of contract deployments. For example, by adding a limit to the number of times the constructor function can be called or by adding a whitelist of addresses that are allowed to deploy the contract.

uint public deploymentCounter;

constructor() public {
    require(deploymentCounter < MAX_DEPLOYMENTS, "Too many deployments");
    deploymentCounter++;
}
address[] public whitelist;

constructor() public {
    require(whitelist[msg.sender], "Address not whitelisted");
}

Impact

  1. require statement in the constructor only checks that the addresses passed as arguments are non-zero. It does not check that they are the correct addresses of deployed contracts. in this way, an attacker could pass in a malicious contract that "masquerades" as one of the expected contracts and potentially steal assets or disrupt the functionality of the system.

By passing in a malicious contract address that "masquerades" as one of the expected contracts, an attacker could potentially steal assets or disrupt the functionality of the system. The attacker could, for example, use a malicious contract that poses as the rsr contract to steal assets from the system or disrupt the normal functioning of the system. It is important that the contracts passed as arguments are properly verified to be the correct, expected contracts.

Proof of Concept

An attacker could pass in a malicious contract address that masquerades as one of the expected contracts. This could allow the attacker to steal assets or disrupt the normal functioning of the system.

Here is how an attacker could pass in a malicious contract.

// Malicious Contract
contract Attacker {
    mapping(address => uint) private balances;

    function steal() public {
        balances[msg.sender] = address(this).balance;
    }
}

// Attacker code
function attack() public {
    Attacker attacker = new Attacker();
    DeployerP1 deployer = new DeployerP1(attacker, ...); // constructor call with attacker address
    attacker.steal();
}

Tools Used

Manual Audit, VS Code

Recommended Mitigation Steps

To prevent an attacker from passing in a malicious contract address, the contracts passed as arguments should be properly verified to be the correct, expected contracts. This could be done by, for example, checking that the contract is deployed at a known, expected address or by checking that the contract has a specific known codehash.

address expectedAddress = 0x1234...;

constructor(address _rsr) public {
    require(_rsr == expectedAddress, "Invalid contract address");
}
bytes32 expectedCodeHash = 0xabcd...;

constructor(address _rsr) public {
    bytes32 codeHash = keccak256(abi.encodePacked(address(_rsr).code));
    require(codeHash == expectedCodeHash, "Invalid contract codehash");
}

Impact

  1. Implementations struct is using a memory keyword, but it does not declare memory keyword for any of the components in the struct. This means that if any of the component contracts are deployed before this contract, they will be overwritten by the component addresses passed as arguments.

Not declaring the memory keyword for the Implementations struct's components means that if any of the component contracts are deployed before this contract, they will be overwritten by the component addresses passed as arguments. This means that there could be unintended consequences if this contract is deployed before other contracts in the system, potentially resulting in loss of data or incorrect functionality.

Proof of Concept

Not declaring the memory keyword for Implementations struct's components, an attacker could deploy contracts with the same name and exploit this vulnerability by calling them before this contract deployment. This could cause unexpected behaviors in the system.

// Attacker contract
contract Attacker {
    address owner;
    function setOwner() public {
        owner = msg.sender;
    }
}

// Attacker Code
function attack() public {
    Attacker attacker = new Attacker();
    attacker.setOwner();
    DeployerP1 deployer = new DeployerP1(..., address(attacker));
    require(attacker.owner() != msg.sender, "Unexpected Owner");
}

Tools Used

Manual audit, vs code.

Recommended Mitigation Steps

To prevent an any attack from an "attacker" exploiting the vulnerability caused by not declaring memory keyword for Implementations struct's components. The memory keyword should be added to ensure that the component addresses are correctly copied over and that they don't point to pre-existing deployed contracts.

struct Implementations {
    address memory main;
    address memory trade;
    struct Components {
        address memory assetRegistry;
        address memory backingManager;
        address memory basketHandler;
        address memory broker;
        address memory distributor;
        address memory furnace;
        address memory rsrTrader;
        address memory rTokenTrader;
        address memory rToken;
        address memory stRSR;
    }
    Components memory components;
}

Impact

  1. No mechanism for upgrading the implementation contracts. Once deployed, the system will be stuck with the initial implementation contracts and cannot be updated to fix bugs or improve the system.

The lack of a mechanism for upgrading the implementation contracts means that the system will be stuck with the initial "implementation" contracts and cannot be updated to fix bugs or improve the system. This can limit the ability of the system to adapt to changing circumstances and potentially make it vulnerable to exploits that take advantage of known bugs or vulnerabilities.

Proof of Concept

lack of a mechanism for upgrading the implementation contracts, an attacker can use this exxploit.

contract Main {
    address public implementation;

    constructor(address _implementation) public {
        implementation = _implementation;
    }

    function doSomething() public {
        IMyInterface(implementation).myFunction();
    }
}
// Initial implementation
contract InitialImpl is IMyInterface {
    function myFunction() public {
        // do something
    }
}
// Attacker contract
contract Attacker {
    function attack() public {
        Main main = Main(msg.sender);
        IMyInterface impl = IMyInterface(main.implementation);

        // exploit a known vulnerability in the initial implementation
        impl.doSomethingElse();
    }
}

This exploit demonstrates how an attacker creates and deploys a new contract that implements IMyInterface and assigns it to the Main contract by calling the Main constructor function. This allows the attacker to take advantage of known vulnerabilities in the initial implementation of the contract, this vulnerability could be mitigated by using a proxy contract that implements the ERC-1967 standard that allows for upgrading the implementation contracts without disrupting the normal functioning of the system.

Tools Used

Manual audit, vs code

Recommended Mitigation Steps

To fix the lack of an upgrading mechanism, a proxy contract that implements the ERC-1967 standard can be used. This will allow the implementation contracts to be upgraded without disrupting the normal functioning of the system.

contract Main is ERC1967 {
    constructor(address _implementation) ERC1967Proxy(_implementation) {}

    function upgrade(address _implementation) public {
        upgradeTo(_implementation);
    }
}

Impact

  1. Some of the imported libraries like IAsset, IAssetRegistry, IBackingManager, IBasketHandler, IBroker, IDeployer, IDistributor, IFurnace, IRevenueTrader, IRToken, IStRSR contracts are not provided in the shared code snippet, so it's difficult to know if they are implemented correctly.

Lack of implementation of imported libraries can result in a system that is not fully functional or that behaves unexpectedly. It is difficult to know if these libraries are implemented correctly, and any errors in their implementation could have a significant impact on the overall functionality of the system.

Proof of Concept

With no implementation of imported libraries could be exploited by an attacker who can create a malicious implementation of the library and pass it as an argument in the contract deployment, this could cause unexpected behaviors of the system.

// Attacker Library
contract AttackerLib {
    function steal() public {
        msg.sender.transfer(address(this).balance);
    }
}

// Attacker Code
function attack() public {
    AttackerLib attacker = new AttackerLib();
    DeployerP1 deployer = new DeployerP1(..., address(attacker));
    attacker.steal();
}

Tools Used

Manual audit, vs code

Recommended Mitigation Steps

In order to prevent the no implementation of imported libraries, it's important to ensure that the libraries are properly implemented and that their functionality is tested before deployment. It's a good practice to use well-established, audited libraries to minimize the risk of unexpected behavior. Additionally, it's important to ensure that the imported libraries are being used throughout the codebase and that they are not just imported but not used.

Impact

  1. DeployerP1 contract not having any mechanism to pause or stop the contract in case of emergency, this can be a security risk in case some unexpected event happens.

The lack of a mechanism to pause or stop the contract in case of emergency can make it difficult to respond quickly to critical issues or exploits. An attacker could potentially take advantage of such a situation to steal assets or disrupt the system.

Proof of Concept

No mechanism to pause or stop the contract in case of emergency can make it difficult to respond quickly to critical issues or exploits. An attacker could potentially take advantage of such a situation to steal assets or disrupt the system. An attacker could try to exploit this vulnerability by executing a malicious transaction or calling a malicious function during a critical issue.

Tools Used

Manual Audit, Vs Code.

Recommended Mitigation Steps

In other to prevent the risk of not being able to stop or pause the contract in case of emergency, a mechanism to pause or stop the contract should be implemented. This could be done by adding a pause() and unpause() function that can be called by a trusted address or by adding a kill() function that can be called by the contract owner.

bool public paused = false;
address public owner;

constructor() public {
    owner = msg.sender;
}

function pause() public {
    require(msg.sender == owner, "Sender must be the contract owner");
    paused = true;
}

function unpause() public {
    require(msg.sender == owner, "Sender must be the contract owner");
    paused = false;
}

function kill() public {
    require(msg.sender == owner, "Sender must be the contract owner");
    selfdestruct(owner);
}

Impact

  1. There is no explicit check that the msg.sender has permission to call the constructor function. Anyone who can call the constructor function can deploy a new instance of the entire system which may have unintended consequences.

The lack of an explicit check for the msg.sender to have permission to call the constructor function. This means that anyone who can call the constructor function can deploy a new instance of the entire system. Without proper access control, an attacker could potentially deploy a malicious instance of the system and steal assets or disrupt the normal functioning of the system.

Proof of Concept

The lack of an explicit check for the msg.sender to have permission to call the constructor function could allow anyone to call the constructor function, potentially deploying a malicious instance of the entire system. An attacker could create a malicious contract and call the constructor function of the DeployerP1 contract, passing in the malicious contract as one of the expected contracts. This could potentially allow the attacker to steal assets or disrupt the normal functioning of the system.

// Attacker Contract
contract Attacker {
    function steal() public {
        msg.sender.transfer(address(this).balance);
    }
}

// Attacker Code
function attack() public {
    Attacker attacker = new Attacker();
    DeployerP1 deployer = new DeployerP1(..., address(attacker));
    attacker.steal();
}

In the general concept of the attack, there could be many variations of these attacks, and it's also possible that the actual attack would need to be more complex than this. In addition, an attacker with more sophisticated capabilities would likely use more complex methods to exploit these vulnerabilities.

Tools Used

Manual audit, vs code.

Recommended Mitigation Steps

To Eliminate the vulnerability of not having an explicit check for the msg.sender to have permission to call the constructor function, it's recommended to add a check that the msg.sender is a specific, trusted address or that it has a specific role.

address public owner;

constructor() public {
    require(msg.sender == owner, "Sender must be the contract owner");
    // constructor code
}
address public deployer;

constructor() public {
    require(msg.sender == deployer, "Sender must be the deployer");
    // constructor code
}

Insufficient mapping to approve multi trades

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L44
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L95

Vulnerability details

Impact

This implementation doesn’t support multi-trades.

Proof of Concept

At https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Broker.sol#L44

mapping(address => bool) private trades;

Tools Used

Manual VS Code review

Recommended Mitigation Steps

mapping(address => mapping(address => bool) private trades;

Distributor: Furnace and stRSR can be registered twice and receive non-zero values of tokens

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Distributor.sol#L163-L164

Vulnerability details

Impact

The Distributor contract (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Distributor.sol#L12) is used to distribute rsr and rToken to different destinations. The governance can configure which share each destination receives.

There are two special destination addresses that have aliases configured.

The furnace is referenced with the alias address(1), and stRSR is referenced with the alias address(2) (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Distributor.sol#L31-L32).

It is important for the furnace to not receive any rsr because it cannot monetize the rsr. Likewise it is important for the stRSR to not receive any rToken.

There are also checks in Distributor._setDistribution to ensure this (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Distributor.sol#L163-L164).

The issue is that both furnace and stRSR cannot only be registered via their aliases (in which case the checks are implemented) but also a second time via their actual addresses.

When furnace and stRSR are registered via their actual addresses, there are no checks in place to ensure that furnace does not receive rsr and stRSR does not receive rToken.

This causes a loss in rToken appreciation (in case of rToken sent to stRSR) and / or a loss in insurance (in case of rsr sent to furnace).

Proof of Concept

I show how furnace can be set up to receive a non-zero share of rsr. It works similary when setting up stRSR to receive a non-zero share of rToken.

Distributor.setDistribution (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Distributor.sol#L61) is called with dest being the actual furnace address (not its alias) and share having a non-zero value for rsr.

The Share struct (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/interfaces/IDistributor.sol#L7-L10) just has two values. One for the share of rToken to receive, the other for the share of rsr to receive:

struct RevenueShare {
    uint16 rTokenDist; // {revShare} A value between [0, 10,000]
    uint16 rsrDist; // {revShare} A value between [0, 10,000]
}

In the downstream Distributor._setDistribution function (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Distributor.sol#L161) which is called by Distributor.setDistribution there is no check to ensure that the rsr share is zero.

The checks are only made when furnace is referenced via its alias.

Tools Used

VSCode

Recommended Mitigation Steps

In the Distributor._setDistribution function, it should be checked that the dest parameter is not the actual furnace or stRSR address such that they can only be registered once via their aliases (in which case the zero-checks are implemented correctly).

Change the Distributor._setDistribution function like this:

diff --git a/contracts/p1/Distributor.sol b/contracts/p1/Distributor.sol
index 53322973..b4db67d1 100644
--- a/contracts/p1/Distributor.sol
+++ b/contracts/p1/Distributor.sol
@@ -164,6 +164,8 @@ contract DistributorP1 is ComponentP1, IDistributor {
         if (dest == ST_RSR) require(share.rTokenDist == 0, "StRSR must get 0% of RToken");
         require(share.rsrDist <= 10000, "RSR distribution too high");
         require(share.rTokenDist <= 10000, "RToken distribution too high");
+        require(dest != furnace, "Furnace must be registered via alias);
+        require(dest != stRSR, "StRSR must be registered via alias);
 
         if (share.rsrDist == 0 && share.rTokenDist == 0) {
             destinations.remove(dest);

QA Report

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

SWC-101 Integer Overflow and Underflow

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/libraries/Fixed.sol#L104

Vulnerability details

Impact

An overflow/underflow happens when an arithmetic operation reaches the maximum or minimum size of a type. For instance if a number is stored in the uint8 type, it means that the number is stored in a 8 bits unsigned number ranging from 0 to 2^8-1. In computer programming, an integer overflow occurs when an arithmetic operation attempts to create a numeric value that is outside of the range that can be represented with a given number of bits – either larger than the maximum or lower than the minimum representable value.

Proof of Concept

URL

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/libraries/Fixed.sol#L104

Case

Check for -= or += or *= or /=

Description

An overflow/underflow happens when an arithmetic operation reaches the maximum or minimum size of a type. For instance if a number is stored in the uint8 type, it means that the number is stored in a 8 bits unsigned number ranging from 0 to 2^8-1. In computer programming, an integer overflow occurs when an arithmetic operation attempts to create a numeric value that is outside of the range that can be represented with a given number of bits – either larger than the maximum or lower than the minimum representable value.

PoC

shiftLeft += 18;

Tools Used

Remix IDE

Recommended Mitigation Steps

// import safemath.sol and use it to create custom function to apply instead.
shiftLeft = add(shiftLeft , 18);

QA Report

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

Insufficient mapping to approve multi trades

A potential reentrancy if address(rsr) is updated to ERC777

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L230

Vulnerability details

Impact

Proof of Concept

The staking function on StRSR contract make the state update before the transfer of RSR token occurs,

 _mint(account, stakeAmount);
...
 IERC20Upgradeable(address(rsr)).safeTransferFrom(account, address(this), rsrAmount);

if the address(rsr) is updated to ERC777 which hooks transfers it will cause a potential reentrancy which an attacker reenter the " function stake(uint256 rsrAmount) external {" function, with a single transferFrom a user can get stakeAmount minted to his account multiple times as the mint is made before the transfer call is made

Tools Used

Manual review

Recommended Mitigation Steps :

Make this line in the top of the function :

IERC20Upgradeable(address(rsr)).safeTransferFrom(account, address(this), rsrAmount);

then make the state changing logics after that call

QA Report

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

Insufficient mapping to approve multi distributions

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Distributor.sol#L18

Vulnerability details

Impact

This implementation doesn’t support multi-distributions.

Proof of Concept

At https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Distributor.sol#L18

mapping(address => RevenueShare) public distribution;

Tools Used

Manual VS Code review

Recommended Mitigation Steps

mapping(address => mapping(address => RevenueShare)) public distribution;

QA Report

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

SWC-109 - RewardableLibP1 have no initial storage may allows for storage collision

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/mixins/RewardableLib.sol#L29

Vulnerability details

Impact

Proof of Concept

The contract makes a direct delegatecalls (via functionDelegateCall) and it has no storage used for the initial slots, a contract that inherits from it (as RTokenP1) when calls the RewardableLibP1.claimRewards it may override it's storage, that contract at the slot0 stores the mandate string, if the RewardableLibP1.claimRewards call to main.assetRegistry address edits the slot0, this will overrides the mandate string on RTokenP1 !

Deploy that contract

contract test {
  string public t;
  fallback() external {
     t = "something else";
  }
}

On the RTokenP1 contract set the test contract as assetRegistry using the init function
Call the claimRewards
See the RTokenP1.mandate string you will see it has changed to "something else"

Tools Used

Manual review

Recommended Mitigation Steps

Make an initial storage on the RewardableLibP1 lib

BackingManager: rTokens might not be redeemable when protocol is paused due to missing token allowance

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L439-L514
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BackingManager.sol#L72-L77

Vulnerability details

Impact

The Reserve protocol allows redemption of rToken even when the protocol is paused.

The docs/system-design.md documentation describes the paused state as:

all interactions disabled EXCEPT RToken.redeem + RToken.cancel + ERC20 functions + StRSR.stake

Redemption of rToken should only ever be prohibited when the protocol is in the frozen state.

The issue is that the RToken.redeem function (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L439-L514) relies on the BackingManager.grantRTokenAllowance function (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BackingManager.sol#L72-L77) to be called before redemption.

Also the only function that relies on BackingManager.grantRTokenAllowance to be called before is RToken.redeem.

Therefore BackingManager.grantRTokenAllowance can be called at any time before a specific ERC20 needs first be transferred from the BackingManager for the purpose of redemption of rToken.

The issue is that the BackingManager.grantRTokenAllowance function has the notPausedOrFrozen modifier. This means it cannot (in contrast to RToken.redeem) be called when the protocol is paused.

Therefore if rToken is for the first time redeemed for a specific ERC20 in a paused protocol state, BackingManager.grantRTokenAllowance might not have been called before.

This effectively disables redemption of rToken as long as the protocol is paused and is clearly against the usability / economic considerations to allow redemption in the paused state.

Proof of Concept

For simplicity assume there is an rToken backed by a single ERC20 called AToken

  1. rToken is issued and AToken is transferred to the BackingManager.
  2. The protocol goes into the paused state before any redemptions have occurred. So the BackingManager.grantRTokenAllowance function might not have been called at this point.
  3. Now the protocol is paused which should allow redemption of rToken but it is not possible because the AToken allowance cannot be granted since the BackingManager.grantRTokenAllowance function cannot be called in the paused state.

Another scenario is when the basket of a RToken is changed to include an ERC20 that was not included in the basket before. If the protocol now goes into the paused state without BackingManager.grantRTokenAllowance being called before, redemption is not possible.

Tools Used

VSCode

Recommended Mitigation Steps

The BackingManager.grantRTokenAllowance function should use the notFrozen modifier instead of the notPausedOrFrozen modifier such that allowance can be granted in the paused state:

diff --git a/contracts/p1/BackingManager.sol b/contracts/p1/BackingManager.sol
index 431e0796..7dfa29e9 100644
--- a/contracts/p1/BackingManager.sol
+++ b/contracts/p1/BackingManager.sol
@@ -69,7 +69,7 @@ contract BackingManagerP1 is TradingP1, IBackingManager {
     // checks: erc20 in assetRegistry
     // action: set allowance on erc20 for rToken to UINT_MAX
     // Using two safeApprove calls instead of safeIncreaseAllowance to support USDT
-    function grantRTokenAllowance(IERC20 erc20) external notPausedOrFrozen {
+    function grantRTokenAllowance(IERC20 erc20) external notFrozen {
         require(assetRegistry.isRegistered(erc20), "erc20 unregistered");
         // == Interaction ==
         IERC20Upgradeable(address(erc20)).safeApprove(address(main.rToken()), 0);

SWC-101 Integer Overflow and Underflow

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/libraries/Fixed.sol#L537-L556

Vulnerability details

Impact

An overflow/underflow happens when an arithmetic operation reaches the maximum or minimum size of a type. For instance if a number is stored in the uint8 type, it means that the number is stored in a 8 bits unsigned number ranging from 0 to 2^8-1. In computer programming, an integer overflow occurs when an arithmetic operation attempts to create a numeric value that is outside of the range that can be represented with a given number of bits – either larger than the maximum or lower than the minimum representable value.

Proof of Concept

PoC

        if (mm > 0) result += 1;
    } else {
        if (mm > ((z - 1) / 2)) result += 1; // z should be z-1
    }
    return result;
}

/// Return (x*y) as a "virtual uint512" (lo, hi), representing (hi*2**256 + lo)
///   Adapted from sources:
///   https://medium.com/wicketh/27650fec525d, https://medium.com/coinmonks/4db014e080b1
/// @dev Intended to be internal to this library
/// @return hi (hi, lo) satisfies  hi*(2**256) + lo == x * y
/// @return lo (paired with `hi`)
function fullMul(uint256 x, uint256 y) pure returns (uint256 hi, uint256 lo) {
    unchecked {
        uint256 mm = mulmod(x, y, uint256(0) - uint256(1));
        lo = x * y;
        hi = mm - lo;
        if (mm < lo) hi -= 1;
    }

Tools Used

Remix IDE

Recommended Mitigation Steps

// import safemath.sol and use it to create custom function to apply instead.
        if (mm > 0) result = add(result, 1);
    } else {
        if (mm > ((z - 1) / 2)) result = add(result, 1); // z should be z-1
    }
    return result;
}

/// Return (x*y) as a "virtual uint512" (lo, hi), representing (hi*2**256 + lo)
///   Adapted from sources:
///   https://medium.com/wicketh/27650fec525d, https://medium.com/coinmonks/4db014e080b1
/// @dev Intended to be internal to this library
/// @return hi (hi, lo) satisfies  hi*(2**256) + lo == x * y
/// @return lo (paired with `hi`)
function fullMul(uint256 x, uint256 y) pure returns (uint256 hi, uint256 lo) {
    unchecked {
        uint256 mm = mulmod(x, y, uint256(0) - uint256(1));
        lo = x * y;
        hi = mm - lo;
        if (mm < lo) hi = sub(hi, 1);
    }

Owner change leads to user loss

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/EasyAuction.sol#L129
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/vendor/EasyAuction.sol#L914
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/vendor/EasyAuction.sol#L926
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/vendor/EasyAuction.sol#L935

Vulnerability details

Impact

If the owner was malicious owner the flexibility of the contract, as it limits the ability to change the fee parameters to only the owner. If the contract needs to be updated or modified in the future, only the owner will be able to do so, which could make it more difficult to make changes to the contract if the owner is unavailable or unwilling to do so.

Proof of Concept

At https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/EasyAuction.sol#L129

    function setFeeParameters(uint256 newFeeNumerator, address newfeeReceiverAddress)
        public
        onlyOwner
    {
        require(newFeeNumerator <= 15, "Fee is not allowed to be set higher than 1.5%");
        // caution: for currently running auctions, the feeReceiverUserId is changing as well.
        feeReceiverUserId = getUserId(newfeeReceiverAddress);
        feeNumerator = newFeeNumerator;
    }

Tools Used

Manual VS Code review

Recommended Mitigation Steps

Set a multisig as the owner and use a timelock.

QA Report

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

QA Report

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

Use solc v0.8.13

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Main.sol#L2

Vulnerability details

Impact

Should consider using solc v0.8.13

p1/Main.sol#L2

Proof of Concept

Taking into account these considerations :

  1. Reserve contracts currently using v0.8.9
pragma solidity 0.8.9;
  1. solidity team recommands to use latest version, i.e. v0.8.17
    When deploying contracts, you should use the latest released version of Solidity
  2. latest version may potentially have compiler bugs, mainly in the optimizer, so maybe latest version is not safest choice
  3. these 4 following versions v0.8.10 (13 fixes), v0.8.11 (5 fixes), v0.8.12 (14 fixes), v0.8.13 (7 fixes) contains a total of 39 fixes
  4. version v0.8.13 is dated on 16 march 2022, 9 months from now, without any bug found in this version related to compilation and optimization

we recommand to use solc v0.8.13, to benefits of the 39 bugs fixed after v0.8.9

Tools Used

Manuel review

Recommended Mitigation Steps

Use solc version v0.8.13

- pragma solidity 0.8.9;
+ pragma solidity 0.8.13;

BasketHandler: Users might not be able to redeem their rToken when protocol is paused due to refreshBasket function

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L439-L514
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L448
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BasketHandler.sol#L183-L192

Vulnerability details

Impact

The Reserve protocol allows redemption of rToken even when the protocol is paused.

The docs/system-design.md documentation describes the paused state as:

all interactions disabled EXCEPT RToken.redeem + RToken.cancel + ERC20 functions + StRSR.stake

Redemption of rToken should only ever be prohibited when the protocol is in the frozen state.

You can see that the RToken.redeem function (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L439-L514) has the notFrozen modifier so it can be called when the protocol is in the paused state.

The issue is that this function relies on the BasketHandler.status() to not be DISABLED (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L448).

The BasketHandler.refreshBasket function (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BasketHandler.sol#L183-L192) however, which must be called to get the basket out of the DISABLED state, cannot be called by any user when the protocol is paused.

When the protocol is paused it can only be called by the governance (OWNER) address.

So in case the basket is DISABLED and the protocol is paused, it is the governance that must call refreshBasket to allow redemption of rToken.

This is dangerous because redemption of rToken should not rely on governance to perform any actions such that users can get out of the protocol when there is something wrong with the governance technically or if the governance behaves badly.

Proof of Concept

The RToken.redeem function has the notFrozen modifier so it can be called when the protocol is paused (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L439).

The BasketHandler.refreshBasket function can only be called by the governance when the protocol is paused:

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BasketHandler.sol#L186-L190

require(
    main.hasRole(OWNER, _msgSender()) ||
        (status() == CollateralStatus.DISABLED && !main.pausedOrFrozen()),
    "basket unrefreshable"
);

Therefore the situation exists where rToken redemption should be possible but it is blocked by the BasketHandler.refreshBasket function.

Tools Used

VSCode

Recommended Mitigation Steps

The BasketHandler.refreshBasket function should be callable by anyone when the status() is DISABLED and the protocol is paused.

So the above require statement can be changed like this:

diff --git a/contracts/p1/BasketHandler.sol b/contracts/p1/BasketHandler.sol
index f74155b1..963e29de 100644
--- a/contracts/p1/BasketHandler.sol
+++ b/contracts/p1/BasketHandler.sol
@@ -185,7 +185,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler {
 
         require(
             main.hasRole(OWNER, _msgSender()) ||
-                (status() == CollateralStatus.DISABLED && !main.pausedOrFrozen()),
+                (status() == CollateralStatus.DISABLED && !main.frozen()),
             "basket unrefreshable"
         );
         _switchBasket();

It was discussed with the sponsor that they might even allow rToken redemption when the basket is DISABLED.

In other words only disallow it when the protocol is frozen.

This however needs further consideration by the sponsor as it might negatively affect other aspects of the protocol that are beyond the scope of this report.

QA Report

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

RToken.sol: Denial of Service attack can prohibit users from issuing rToken

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L186-L340
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L344-L370
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L406-L418
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L72

Vulnerability details

Impact

rToken can be issued via the RToken.issue function (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L186-L340).

There is a limit to how many rToken can be minted within one block (a percentage of the rToken supply).

If RToken.issue is called but the rToken cannot be minted immediately, the issuance is added to a queue.

The time when the issuance can be vested (i.e. when the rToken can be actually minted) is determined by the RToken.whenFinished function (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L344-L370) which is called internally by RToken.issue (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L243).

If there is an issuance in the queue it can be cancelled by the recipient of the issuance by calling RToken.cancel (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L406-L418).

All the tokens from the basket are then refunded.

So when a user calls RToken.issue and the issuance is queued and immediately after that he calls RToken.cancel, there is no cost to the user except the Gas.

The issue is that by performing this issue -> cancel roundtrip, the allVestAt variable (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L72) is increased (https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L369). This means that other users that now call RToken.issue need now to wait longer to vest their rToken.

So an attacker can repeatedly call issue and cancel to essentially increase the allVestAt to a very large block number (maybe years in the future).

The amount of blocks by which allVestAt is increased depends on the capital the attacker uses to perform one issue -> cancel roundtrip (this capital is not lost, just used for some amount of time) and the number of roundtrips (which costs a little bit of Gas).

Assume the attacker uses enough capital to increase allVestAt by 5 blocks per roundtrip (i.e. approximately 60 seconds) and one roundtrip costs $1 in Gas.

It then costs $525,600 to increase allVestAt by 1 year:

1 year is 31,536,000 seconds

31,536,000 seconds / 12 seconds per block = 2,628,000 blocks

2,628,000 blocks / 5 blocks per roundtrip = 525,600 roundtrips

525,600 roundtrips * $1 per roundtrip = $525,600

This is definitely within reach of a determined attacker. Also Gas costs might decrease in the future which makes this attack even more dangerous.

The vision of Reserve is to provide a protocol to create currencies that are used worldwide with a TVL of possibly trillions of dollars.

The attack described above requires little capital in comparison to the TVL and can be very effective in denying the protocol to operate.

Thereby this attack, although it does not pose a risk to the users' assets, makes it impossible for the protocol to satisfy its ambitions. I therefore rate this issue to be of "High" severity.

Proof of Concept

  1. Assume allVestAt is a block number in the future. This means that by calling RToken.issue, the rToken are not issued immediately but added to the issuance queue instead. So the issuance can be canceled.
    This is not much of a constraint however. In the case that allVestAt is not a block number in the future, the attacker must issue some amount of rToken immediately. This amount can the be redeemed again. So there is just an added need for capital to perform the attack.
  2. The attacker calls RToken.issue to increase allVestAt and immediately after that he calls RToken.cancel. All collateral tokens are returned to him again.
  3. The attacker repeats step 2 until allVestAt is as large as he wants.
  4. rToken that are issued from now on can only be vested when block.timestamp has reached allVestAt.

Tools Used

VSCode

Recommended Mitigation Steps

There is no easy fix for this. Any change to the issuance mechanism changes the economic incentives of the protocol.

A possible solution can be to introduce a cancellation fee such that the attack becomes too expensive or to "recharge" the issuance capacity when an issuance is cancelled.

However the sponsor should explore possible mechanisms and make sure they don't introduce any bad economic incentives.

Use safe ERC721 mint

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/ComptrollerMock.sol#L27

Vulnerability details

Impact

if "safeMint" is not used, the code will still execute and mint the specified amount of tokens to the provided holder address. However, it does not include any safety checks to ensure that the operation is authorized or that the total supply of tokens will not be exceeded by the minting operation. This means that anyone who can call this function can mint an unlimited amount of tokens to any address

Proof of Concept

At https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/mocks/ComptrollerMock.sol#L27

    function claimComp(address holder) external {
        // Mint amount and update internal balances
        if (address(compToken) != address(0)) {
            uint256 amount = compBalances[holder];
            compBalances[holder] = 0;
            compToken.mint(holder, amount);
        }
    }

Tools Used

Manual VS Code audit

Recommended Mitigation Steps

'''
function claimComp(address holder) external {
// Mint amount and update internal balances
if (address(compToken) != address(0)) {
uint256 amount = compBalances[holder];
compBalances[holder] = 0;
compToken.safeMint(holder, amount);
}
}

QA Report

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

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.