A CONSENSYS DILIGENCE Audit Report

rICO

1 Executive Summary

This report presents the results of our engagement with Lukso rICO to review the Reversible Initial Coin Offering, a version of an ICO that gives investors the ability to reverse their investment in different stages.

The review was conducted over the course of two weeks, from April 13th, 2020 to April 27th, 2020 by Shayan Eskandari and Gonçalo Sá. A total of 15 person-days were spent.

During the first week, we reviewed the documentation and attended several code walkthrough sessions with the developers. Initial issues were discussed and resulted in a new commit to be the base of the audit by mid-week. In an effort to understand the system, we produced several ancillary visualizations (that can be seen throughout the audit report) over the course of the week.

During the second week we reviewed the codebase with the aid of the aforementioned visualizations and looked attentively for breaches of the invariants described in the Security Properties section.

2 Scope

Our review focused on the commit hash dc6b22ba8991d77560e574eac7f4f1e17f643115 77517a4dceed53ff7c5a7f7580cb805831a7f8d5 (tree/audit). The list of files in scope can be found in the Appendix.

2.1 Documentations

The following documentation was provided by the client:

2.2 Objectives

Together with the Lukso rICO team, we identified the following priorities for our review

  1. Ensure that the system is implemented consistently with the intended functionality, and without unintended edge cases, according to the specification derived from the documentation that was provided to us.
  2. Identify known vulnerabilities particular to smart contract systems, as outlined in our Smart Contract Best Practices, and the Smart Contract Weakness Classification Registry.
  3. The implementation of the mathematical relationships in the rICO smart contract corresponds to the specification in the documentation.
  4. The flow of funds occurs as specified in the documentation. No undocumented flow of native or ERC20 tokens exists.

3 System Overview

The Reversible Initial Coin Offering, or rICO, for short, has two main contracts:

  • ReversibleICO
    • Main functionality for swapping ETH with Token, and the other way around
  • RicoToken
    • ERC777 with modified functions to consider the available unlocked balance in the rICO

Bellow you can see the visualization of the rICO system.

alt text

UPDATE: The above chart has been updated to reflect the new changes in the mitigation phase to the Token contract. However, it might lack some details, such as proper visualization of freezing functionality and the new roles.

More details about the Actors and their permissions can be found in Actors.

4 Security Specification

This section describes, from a security perspective, the expected behavior of the system under audit. It is not a substitute for documentation. The purpose of this section is to identify specific security properties that were validated by the audit team.

4.1 Actors

The relevant actors are listed below with their respective abilities:

alt text

rICO

  • deployingAddress : Only this address is allowed to set all other addresses and stage details when initializing the rICO contract. after the initial setup the details of the rICO cannot be changed by any actor.

  • whitelistingAddress : Only this address can whitelist or blacklist participants in the rICO.

  • freezerAddress: Freezer address is designed for emergency scenarios, when the rICO must be frozen. This address can:

    • Freeze the contract to stop all functionalities in the contract, such as:
      • Receiving Eth or Tokens
      • canceling pending contributions
      • accepting pending contributions
      • withdrawing any tokens or contributions by either participants or project address
      • whitelisting addresses
    • Unfreeze the contract to resume all functionalities
      • As mentioned in issue 6.2 this results in extension to the rICO time frame
    • Disable Escape hatch: to remove freezerAddress and rescuerAddress from the system. This is design to be called when the smart contract is presumably secure. The smart contract cannot be frozen if this function is used.
  • rescuerAddress : This address based on client discussion, will be held by a trusted third party, and only will be used in case of emergency. *After the contract has been frozen for 3 days *(18000 blocks), this address can transfer all the funds and tokens to the specified address.

  • Participant : Any entity sending more than minContribution (0.001 ether) to the rICO smart contract, while the rICO is running, will be added as participants. The purchase of the committed tokens, however, depends if the participant is whitelisted or not.

    • Participants can also withdraw their contributions by returning the purchased tokens
  • projectAddress: The project wallet

    • Can withdraw ETH contributions (Unlocked ETH)
    • Add tokens to tokenSupply of the rICO by sending tokens to rICO contract
    • Holds all the initial balance of the token
  • tokenAddress: The address of the ERC777 token used in the rICO

    • manager: is the address deploying the RicoToken (LYXeToken) contract.
      • UPDATE:: TokenManager has been removed and its permissions has been separated into the new roles, described below.

Token

  • deployingAddress : Only this address is allowed to set all other addresses when initializing the token contract.

  • freezerAddress: Freezer address is designed for emergency scenarios, when the token must be frozen. This address can:

    • Freeze the contract to stop all functionalities in the contract, such as:
      • Burn Tokens
      • Move Tokens All token transfers will be frozen
    • Unfreeze the contract to resume all functionalities
      • As mentioned in issue 6.2 this results in extension to the rICO time frame
    • Remove Freezer Address: to remove freezerAddress from the system. This is design to be called when the smart contract is presumably secure. The smart contract cannot be frozen if this function is used.
  • rescuerAddress : This address based on client discussion, will be held by a trusted third party, and only will be used in case of emergency. This address can change the rICO address when the token is frozen. No grace period is implemented for this functionality.

Note: The addresses with the same name in rICO and Token contract can be different entities. However, as for Lukso rICO, it is assumed that they will be deployed and initialized for the same addresses.

4.2 Trust Model

In any smart contract system, it’s important to identify what trust is expected/required between various actors. For this audit, we established the following trust model:

  • deployingAddress is initially in full control of setting the actors in the system. However after the initialization, the deployer does not have any special access.
  • freezerAddress has the most control over the rICO system, although no ability to withdraw or steal funds. freezerAddress can freeze and unfreeze the contract, resulting in total system halt or restore.
    • It should be noted that this entity can completely deny itself and rescuerAddress the opportunity to withdraw funds.
  • rescuerAddress after the contract has been frozen for more than 3 days (18000 blocks), rescuerAddress can withdraw the funds and tokens to any address of choosing.
  • Manager of the token (ERC777), can also freeze the underlying token.
  • Due to ERC777 callbacks (e.g. tokenReceived) must be verified in order to consider the rICO to be safe to be used in DeFi.

4.3 Important Security Properties

The following is a non-exhaustive list of security properties that were verified in this audit.

Rico Token Flow

  • During the commit and buy phases of the reversible ICO, locked tokens cannot be transferred by participants unless the receiver is the Reversible ICO contract address itself.
  • With the exception of the privileged actors described above, no other actor should be able to withdraw ETH from the Reversible ICO contract.

ETH Flow

  • No participant can withdraw other participant’s committed ETH.
  • With the exception of the privileged actors described above, no other actor should be able to withdraw ETH from the Reversible ICO contract.

Lockup Conditions

  • No lockup conditions arise from incorrect usage of SafeMath.
    • Note: The obvious exception to this being the issue reported regarding the, incorrectly, unchecked subtraction of the frozen period, which the audit team expects to be resolved ASAP. (issue 6.4)
  • No lockup condition arises from the incorrect calculation of a stage number.

Reentrancy Instances

  • Both the reentrancy instances accessible by participants pose no problem to the correct functioning of the rICO. The only and obvious exception to this being the transfer of tokens present in the escapeHatch() method (this last one is called by a privileged actor that has the ability to drain the contract at any point in time as per the specification).

5 Recommendations

5.1 Sanity check for addresses

Even though the init function is called by the address deployer and possibly using scripts, it is recommended to have sanity checks inside the function to prevent some common mistakes, such as :

 require(tokenAddress != address(0));
 require(whitelistingAddress != address(0));
 require(projectAddress != address(0));
 require(freezerAddress != address(0));
 require(rescuerAddress != address(0));

These checks can be extended to other security specifications such as to prevent projectAddress and freezerAddress to be the same, and so on.

Update: The proper checks were added in lukso-network/rICO-smart-contracts@edb880c.

5.2 Separate currentBlock from currentEffectiveBlock

In rICO contract, the current block number is gotten from getCurrentBlockNumber() and the context it is used might mean different block numbers.

It is used to get actual current block in the following functions:

  • init()
  • The first time freeze() and unfreeze() are called

However, it is used to get the effective block number (currentBlock - frozenPeriod) in the following functions:

  • getCurrentStage() (adds frozenPeriod for fixing the math)
  • getCurrentPrice() (adds frozenPeriod for fixing the math)
  • The second+ time freeze() and unfreeze() are called
  • Other functions

The point is, even though, the mathematics behind the stages (e.g. multiple frozen periods) works out, it adds unnecessary complexity to the code and makes future updates and modifications tricky. It is suggested (similar to https://github.com/ConsenSys/lukso-rico-audit-2020-04/issues/1), to define two different functions, for example getCurrentBlockNumber() for actual current block number, and getCurrentEffectiveBlockNumber() for effective block number (deducting frozenPeriod).

UPDATE: The new function getCurrentEffectiveBlockNumber() was added in lukso-network/rICO-smart-contracts@e4c9ed5.

5.3 Shadowed variable stages

In the acceptContributions() a variable is defined as stages that shadows a global variable with the same name. It is verified that within the scope of this function, there are no issues with this shadowing, however it might result in confusion or possible bugs in future updates. It is suggested to use a new name for the variable to prevent shadowing global variables.

mapping(uint8 => Stage) public stages;
ParticipantStageDetails storage stages = participantStats.stages[stageId];

UPDATE: The shadowed variable was renamed to byStage in lukso-network/rICO-smart-contracts@e4c9ed5.

5.4 Limit the length of the stages

Currently there are no limits in how many stages can there be in a given rICO instance. Given that any participant can contribute in every stage, and there are many functions that iterate through the stages each participant has contributed in (e.g. cancelPendingContributions() and acceptContributions()), there must be an upper limit to the number of stages before it reaches the gasBlockLimit. It is recommended to calculate and add such a limit to init() function.

UPDATE: This limitation has been acknowledged by Lukso team. The number of stages are limited for Lukso rICO, however for future reference a note was added to the README file and an inline comment (in lukso-network/rICO-smart-contracts@e4c9ed5) as a warning for future deployemnets.

**NOTE** Its not recommended to choose more than 50 stages!
9 stages require ~650k GAS when whitelisting contributions,
the whitelisting function could run out of gas with a high number of stages, preventing accepting contributions.

Test before using the `/test/solc_tests/flows/random_tests.js`

5.5 Usage of variables under 32 bytes in size

Variable types smaller than 32 bytes in size are almost always (and also counterintuitively!) more gas intensive than 32-bytes-sized ones.

The audit team therefore recommends the sole use of 32-byte-sized variables (i.e. uint256) except in the situations where these can be tightly packed, like in the Participant or Stage struct, illustrated below.

//ReversibleICO.sol#L139-L140
    struct Stage {
        uint128 startBlock;
        uint128 endBlock;
        uint256 tokenPrice;
    }

6 Issues

Each issue has an assigned severity:

  • Minor issues are subjective in nature. They are typically suggestions around best practices or readability. Code maintainers should use their own judgment as to whether to address such issues.
  • Medium issues are objective in nature but are not security vulnerabilities. These should be addressed unless there is a clear reason not to.
  • Major issues are security vulnerabilities that may not be directly exploitable or may require certain conditions in order to be exploited. All major issues should be addressed.
  • Critical issues are directly exploitable security vulnerabilities that need to be fixed.

6.1 Test code present in the code base Medium ✓ Fixed

Description

Test code are present in the code base. This is mainly a reminder to fix those before production.

Examples

rescuerAddress and freezerAddress are not even in the function arguments.

code/contracts/ReversibleICO.sol:L243-L247

whitelistingAddress = _whitelistingAddress;
projectAddress = _projectAddress;
freezerAddress = _projectAddress; // TODO change, here only for testing
rescuerAddress = _projectAddress; // TODO change, here only for testing

Recommendation

Make sure all the variable assignments are ready for production before deployment to production.

6.2 FreezerAddress has more power than required Medium  Acknowledged

Resolution

This issue is acknowledged by the client and the behaviour has been documented in security measurements.

Description

FreezerAddress is designed to have the ability of freezing the contract in case of emergency. However, indirectly, there are other changes in the system that can result from the freeze.

Examples

  1. FreezerAddress can extend the rICO time frame. Given that the frozenPeriod is deducted from the blockNumber in stage calculations, the buyPhaseEndBlock is technically equals to buyPhaseEndBlock + frozenPeriod

  2. FreezerAddress can call disableEscapeHatch(), which disables the escape hatch and rendering RescuerAddress useless.

Recommendation

If these behaviors are intentional they should be well documented and specified. If not, they should be removed.

In the case they are, indeed, intentional the audit team believes that, for Example 1., there should be some event fired to serve as notification for the participants (possibly followed by off-chain infrastructure to warn them through email or other communication channel).

6.3 frozenPeriod is subtracted twice for calculating the current price Medium ✓ Fixed

Resolution

Found in parallel to the audit team and has been mitigated in lukso-network/rICO-smart-contracts@ebc4bce . The issue was further simplified by adding getCurrentEffectiveBlockNumber() in lukso-network/rICO-smart-contracts@e4c9ed5 to remove ambiguity when calculating current block number.

Description

If the contract had been frozen, the current stage price will calculate the price by subtracting the frozenPeriod twice and result in wrong calculation.

getCurrentBlockNumber() subtracts frozenPeriod once, and then getStageAtBlock() will also subtract the same number again.

Examples

code/contracts/ReversibleICO.sol:L617-L619

function getCurrentStage() public view returns (uint8) {
    return getStageAtBlock(getCurrentBlockNumber());
}

code/contracts/ReversibleICO.sol:L711-L714

function getCurrentBlockNumber() public view returns (uint256) {
    return uint256(block.number)
    .sub(frozenPeriod); // make sure we deduct any frozenPeriod from calculations
}

code/contracts/ReversibleICO.sol:L654-L656

function getStageAtBlock(uint256 _blockNumber) public view returns (uint8) {

    uint256 blockNumber = _blockNumber.sub(frozenPeriod); // adjust the block by the frozen period

Recommendation

Make sure frozenPeriod calculation is done correctly. It could be solved by renaming getCurrentBlockNumber() to reflect the calculation done inside the function.

e.g. :

  • getCurrentBlockNumber() : gets current block number
  • getCurrentEffectiveBlockNumber() : calculates the effective block number deducting frozenPeriod

6.4 Lockup condition in getStageAtBlock() Minor ✓ Fixed

Resolution

Even though the freeze pattern does indeed create a lot of additional complexity to the protocol, the particular require mentioned in the issue corpus by the audit team was found to never be triggered in a harmful way by rICO’s development team.

In the light of this new discovery, we are greatly reducing the severity of the issue to “Minor”. The reason why it is still kept as an issue is that the implementation of the freezing mechanism could still be greatly improved as we saw in the presented fixes here:

lukso-network/rICO-smart-contracts@e4c9ed5

The changes resulted in a much more resilient rICO implementation.

Description

Given that the contract has been frozen at least once, if the frozenPeriod is longer than the period before the freeze event (starting from commitPhaseStartBlock till the freezeStart), the following require in getStageAtBlock() will revert due to the fact that blockNumber < commitPhaseStartBlock:

uint256 blockNumber = _blockNumber.sub(frozenPeriod); // adjust the block by the frozen period

require(blockNumber >= commitPhaseStartBlock && blockNumber <= buyPhaseEndBlock, "Block outside of rICO period.");

Note that the issue here is also related to the way currentBlockNumber is calculated (See issue 6.3 and Separate currentBlock from currentEffectiveBlock.

getCurrentStage() is called for every accept or cancelation of contributions and this lockup can result in total system halt.

Recommendation

Given that in the init function, the following condition is checked:

require(_commitPhaseStartBlock > getCurrentBlockNumber(), "Start block cannot be set in the past.");

The check in the getStageAtBlock() can be removed. However this is assuming that the correct calculation of the currentEffectiveBlockNumber is used.

6.5 emit events for significant state changes Minor ✓ Fixed

Resolution

This issue was discussed in the code walk through meeting and was fixed, by adding proper events to the code base in lukso-network/rICO-smart-contracts@77517a4, before the end of the audit.

Description

Events are useful for UI changes and user notifications. The code base overall can use more use of events to update the UI and participants.

One of the most important aspects that must emit events, are when system state and functionality are changed. These functions require to emit events for better visibility to the participants:

  • freeze()
  • unfreeze()
  • disableEscapeHatch()
  • escapeHatch()

Recommendation

emit events when system state is changed.

Appendix 1 - Agent-based Tests

Agent-based testing of the platform based on a modified version of the pre-existing random tests produced by the development team was ran. The results were adapted into graphs constructed with d3.js and were used to validate both the implementation of the mathematical models being used and their implementations, and the presence of subtle and nuanced nefarious effects coming from the interactions in an environment with many non-rational actors.

Presented below is a summarized version of the full graph. Please find the full, interactive version here.

The data presented in the charts stems from a simulation with the following parameters:

  • Total participants: 20
  • Blocks per day: 25
  • Number of days of the Commit stage: 3
  • Number of days of each Buy Phase stage: 5
  • Total number of stages (including the Commit stage): 10
  • Price of token in the Commit stage: 0.002 ETH
  • Price increment per stage: 0.0001 ETH

The project address agent withdraws ETH as often as it cans and the whitelister agent whitelists and blacklists randomly.

The participant agents have a total random strategy within the domain of valid actions (i.e., valid in this context means a transaction that won’t revert). There are also two flavors of the commit ETH action being randomized. Sending the full ETH balance or sending half of it.

The code was adapted from the, already well-constructed, random tests present in the rICO repository.

A second test, with a different strategy for participants, was ran and can be found here.

In this version, the participants can commit any amount of their available balance and not just half or all of it. The number of days per stage also changed from 5 to 3.


Note: The chart is zoomable. If there are ratio problems with the iframe below, please refresh the page.

7 Document Change Log

Version Date Description
1.0 2020-04-27 Initial report
1.1 2020-05-09 Reflect fixes

Appendix 2 - Files in Scope

This audit covered the following files:

File SHA-1 hash
contracts/ReversibleICO.sol 3d5bf2c18b1ffa10b50eaac4cc62eaf43a40b6c2
contracts/RicoToken.sol 7d500809f2d14e4ea728ae126d4711239dffc422

Appendix 3 - Artifacts

This section contains some of the artifacts generated during our review by automated tools, the test suite, etc. If any issues or recommendations were identified by the output presented here, they have been addressed in the appropriate section above.

A.3.1 MythX

MythX is a security analysis API for Ethereum smart contracts. It performs multiple types of analysis, including fuzzing and symbolic execution, to detect many common vulnerability types. The tool was used for automated vulnerability discovery for all audited contracts and libraries. More details on MythX can be found at mythx.io.

Below is the raw output of the MythX vulnerability scan:

A.3.2 Ethlint

Ethlint is an open source project for linting Solidity code. Only security-related issues were reviewed by the audit team.

Below is the raw output of the Ethlint vulnerability scan:

A.3.3 Surya

Surya is a utility tool for smart contract systems. It provides a number of visual outputs and information about the structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts.

Below is a complete list of functions with their visibility and modifiers:

Sūrya’s Description Report

A.3.4 Tests Suite

Below is the output generated by running the test suite:

Appendix 4 - Disclosure

ConsenSys Diligence (“CD”) typically receives compensation from one or more clients (the “Clients”) for performing the analysis contained in these reports (the “Reports”). The Reports may be distributed through other means, including via ConsenSys publications and other distributions.

The Reports are not an endorsement or indictment of any particular project or team, and the Reports do not guarantee the security of any particular project. This Report does not consider, and should not be interpreted as considering or having any bearing on, the potential economics of a token, token sale or any other product, service or other asset. Cryptographic tokens are emergent technologies and carry with them high levels of technical risk and uncertainty. No Report provides any warranty or representation to any Third-Party in any respect, including regarding the bugfree nature of code, the business model or proprietors of any such business model, and the legal compliance of any such business. No third party should rely on the Reports in any way, including for the purpose of making any decisions to buy or sell any token, product, service or other asset. Specifically, for the avoidance of doubt, this Report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. CD owes no duty to any Third-Party by virtue of publishing these Reports.

PURPOSE OF REPORTS The Reports and the analysis described therein are created solely for Clients and published with their consent. The scope of our review is limited to a review of Solidity code and only the Solidity code we note as being within the scope of our review within this report. The Solidity language itself remains under development and is subject to unknown risks and flaws. The review does not extend to the compiler layer, or any other areas beyond Solidity that could present security risks. Cryptographic tokens are emergent technologies and carry with them high levels of technical risk and uncertainty.

CD makes the Reports available to parties other than the Clients (i.e., “third parties”) – on its website. CD hopes that by making these analyses publicly available, it can help the blockchain ecosystem develop technical best practices in this rapidly evolving area of innovation.

LINKS TO OTHER WEB SITES FROM THIS WEB SITE You may, through hypertext or other computer links, gain access to web sites operated by persons other than ConsenSys and CD. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such web sites’ owners. You agree that ConsenSys and CD are not responsible for the content or operation of such Web sites, and that ConsenSys and CD shall have no liability to you or any other person or entity for the use of third party Web sites. Except as described below, a hyperlink from this web Site to another web site does not imply or mean that ConsenSys and CD endorses the content on that Web site or the operator or operations of that site. You are solely responsible for determining the extent to which you may use any content at any other web sites to which you link from the Reports. ConsenSys and CD assumes no responsibility for the use of third party software on the Web Site and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.

TIMELINESS OF CONTENT The content contained in the Reports is current as of the date appearing on the Report and is subject to change without notice. Unless indicated otherwise, by ConsenSys and CD.