Introduction
CoinFabrik has been hired to audit the EasyPool smart contracts.
We start this report writing a summary with the smart contracts provided by the client and a list of the analysis methods used to audit the contracts. Next, we will make a summary of the files we analysed and the public facing functions provided by the ProPool contract. Then we detail our findings ordering the issues by severity, followed by all observations we considered important to add. Furthermore, we ended up this audit report with a conclusion explaining how do the auditors value the code reviewed, and what are the most important things that need to be corrected to it to make it work flawlessly and securely.
Summary
The contracts audited are from the EasyPool repository at https://github.com/gitigs/easypool. The audit is based on the commit 17a1e1ae336a92e3d4d7686aa1cb26aaea3f1f82.
The audited contracts are:
- ProPool.sol: Proxy contract for the investor pool manipulation functions
- common/Affiliate.sol: Affiliate management functions
- common/CMService.sol: Contract that provides functions for deploying a new pool contract and setting the fee service, the pool factory and the registry.
- common/FeeService.sol: Contract that manages the fee in a given pool
- common/PoolFactory.sol: Pool deployment function
- common/PoolRegistry.sol: Pool registering implemented through the emission of events
- common/Restricted.sol: Operator management functions for restricting use to given operators.
- library/ProPoolLib.sol: Code for pool initialization and lifetime functions, managing deposits, whitelist and withdrawal.
- library/QuotaLib.sol: Share claiming functions for manipulating the quota given by the pool
The ProPool contract provides a public interface for managing the pool. The implementation is in the ProPoolLib contract. This latter contract uses another library, defined in QuotaLib, to define an internal data structure within the contract’s representation of an investment pool. There’s also a dependency on a pair of functions from the FeeService contract.
As for CMService, it depends on the interface for PoolRegistry and PoolFactory, which inherit from the Restricted contract, adding operator management functions. PoolFactory also depends on ProPool, since it deploys new instances of the ProPool contract. CMService allows setting fee services, changing the pool factory contract, and such functions. PoolRegistry brings a function to register new pools, emitting an event.
Fortunately, on analysis of these contracts only a few minor issues, mostly with maintainability, were found. We proceed to detail the checks we’ve made, the improvements to the contracts which could help development, and a conclusion.
As for the audit, the following analyses were performed:
- Misuse of the different call methods: call.value(), send() and transfer().
- Integer rounding errors, overflow, underflow and related usage of SafeMath functions.
- Old compiler version pragmas.
- Race conditions such as reentrancy attacks or front running.
- Misuse of block timestamps, assuming anything other than them being strictly increasing.
- Contract softlocking attacks (DoS).
- Potential gas cost of functions being over the gas limit.
- Missing function qualifiers and their misuse.
- Fallback functions with a higher gas cost than the one that a transfer or send call allows.
- Fraudulent or erroneous code.
- Code and contract interaction complexity.
- Wrong or missing error handling.
- Overuse of transfers in a single transaction instead of using withdrawal patterns.
- Insufficient analysis of the function input requirements.
File Summary
ProPool.sol
No vulnerabilities were found in this contract, but some compiler warnings were noted. This is a contract that provides public-facing presale pool management functions, which are implemented in the ProPoolLib library.
Affiliate.sol
A lack of use of SafeMath was noted, but no issues were found. This contract has functions for controlling affiliates within the contract.
CMService.sol
No vulnerabilities were found, as this contract was simple and straightforward. This contract can be called to set the addresses of the fee service, the pool factory, the pool registry and to deploy the ProPool contract by a call to poolFactory. This way the implementation can be changed without redeploying.
FeeService.sol
This contract does not use the SafeMath library consistently, but no major security concerns were found. The objective of this contract is to provide fee settings, which are then attached to the pool through the functions provided in CMService.sol
PoolFactory.sol
No vulnerabilities were found in this contract. Since it provides a simple function for the deploying of a new ProPool contract, no vulnerabilities are considered as possible.
PoolRegistry.sol
No vulnerabilities were found in this contract. It’s a simple contract providing a function that emits an event when called, recording the pool address and details.
Restricted.sol
No vulnerabilities were found here. This is an implementation providing storage and functions that manage operators, which are used to restrict function execution in PoolFactory.sol, PoolRegistry.sol and Affiliate.sol
ProPoolLib.sol
The inconsistent use of SafeMath was noted, but no major issues were found. This contract is the one providing the implementation that ProPool.sol uses.
QuotaLib.sol
Inconsistent SafeMath usage was noted, along with a lack of documentation. While later commits added some, it still wasn’t enough. Other than that, the contract was straightforward, without much complexity. This contract is used to manipulate the refund and token quotas in the ProPoolLib.sol contract.
Function summary
The ProPool contract has many public functions. We will describe how they fit together giving a brief description of what they do.
constructor
Calls init(), initializing the pool fields. It doesn’t initialize the pool state, leaving it in the default of open. It creates the first group of eight calling setGroupSettingsCore.
Once created, the pool accepts deposits. If it’s cancelled, the contributions can be withdrawn together with the remaining balance. If it proceeds, once the presale address is paid, the confirmTokenAddress function can be called, changing the state of the pool to Distribution, and sending the creator and service fees. Elsewise, if the refund sender address is set, the pool goes into FullRefund state, and investors can claim their refund share and tokens.
setGroupSettings
Calls setGroupSettingsCore, and then calls groupRebalance, which balances contributions made to the group.
cancel
Sets the state of the pool to Cancelled.
deposit
Contributes to a certain group. Internally, it calculates the contribution, sets the group as existing and updates the group contribution.
modifyWhitelist
Enables whitelist, includes participants who should get in and excludes those who shouldn’t, as sent by parameter, in a certain group. Afterwards, it rebalances the group.
payToPresale
If presaleAddress isn’t set yet, it does so at this moment. Then sets the pool in PaidToPresale mode, and sets fee-to-token mode, if needed: commission is sent to the presale address. Then the funds are set to the presale address by calling addressCall, which calls the address with the needed value and data, and emits an event.
lockPresaleAddress
Sets presaleAddress and sets the lockPresale boolean, which should be used for verification. This boolean is part of a locking mechanism which should be used, but is never again used in the code.
confirmTokenAddress
Changes state to distribution, saves parameter tokenAddress in the tokenAddresses array if balance > 0.
setRefundAddress
Sets the refund sender for the pool (for use in the fallback function)
Fallback function
Verifies sender is the specified refund address.
withdrawAmount
Withdraws from a group: all of participant remaining balance and some of participant contribution. Then it transfers this amount to the sender.
withdrawAll
What this function does varies according to what state the pool is in:
In FullRefund or Distribution states it calls withdrawRefundAndTokens, which calls withdrawAllRemaining, taking the group’s remaining balance and transferring it to the sender. Then it calls withdrawRefundShare. This is the part that withdraws the refund shares. Then withdrawRefundAndTokens checks if the pool is in fee-to-token mode (where the fee for the creator is sent directly to the presale), and if there was an effective contribution to the token addresses they are transferred.
In cancelled or open states it calls withdrawAllContribution, which takes all of the sender’s contribution and remaining from each and sends him that amount.
In PaidToPresale state it only withdraws the remaining balance of the participant and sends it his way.
tokenFallback
Emits an event for registering. This function comes from ERC223, but this proposal didn’t prosper much.
getPoolDetails1
Getter for pool structure.
getPoolDetails2
Nothing if not in Distribution or FullRefund states.
Else, calculates the balance it will refund and returns the token addresses and balance in each
getParticipantDetails
Gets contribution, remaining and whitelist array.
getParticipantShares
Calculates refund shares and calculates shares (total – claimed) of each tokenAddress in the pool for a certain address
getGroupDetails
Simple getter for group details, returning the respective fields.
getLibVersion
Gets the library version.
Detailed findings
Critical severity
None found.
Medium severity
None found.
Minor severity
Lack of usage of “lockPresale” boolean
The pool has two fields: presaleAddress and lockPresale. These control where the balance will be sent when calling payToPresale and whether the address is already set. On functions init and payToPresale the presaleAddress field can be set via a parameter. However, the lockPresale boolean isn’t set, defaulting to false. This means that the internal state will be inconsistent. Furthermore, checks to see if the presale address was set are made based on whether it is zero or not, and the boolean, when set (which is only done when calling lockPresaleAddress), is never actually used for making the check on if it was locked or not.
The state of the pool is not explicitly initialized
When calling the init function, the pool’s state is never explicitly initialized. This means that it’ll default to the “Open” state, but it still is advised to change this as it can impact when changing the code and versions.
Warnings on compilation
The contracts emit warnings when compiled. It would add to the auditability of the contracts themselves if they were removed. While they may not cause any problem directly, they can occlude other more important warnings. We recommend fixing these before deployment, mostly as a way of ensuring that everything’s going well.
Inconsistent usage of SafeMath
There’s not enough usage of SafeMath in some lines in QuotaLib.sol, yet it is declared as being used in the following statement:
library QuotaLib {
using SafeMath for uint;
We recommend either removing the statement, or following through in the usage consistently, since this could lead to errors and possible future changes could cause bugs. We also recommend the use of SafeMath in the other contracts, which it can be seen that it was considered yet decided against in some commented out parts of the code, which declare
// We could use SafeMath to catch errors in the logic,
// but if there are no such errors we can skip it.
// using SafeMath for uint;
This is something which was found in ProPoolLib.sol, in Affiliate.sol and in FeeService.sol. While it is true that SafeMath has no purpose if the code is correct, it can act as a safeguard if that isn’t the case. It’d be advisable for the team to reconsider this design decision, since it’d mean arithmetical operations are guarded against overflow.
Enhancements
Not enough documentation
In the QuotaLib.sol contract there’s not enough documentation. It’d be advisable for the development team to fix this, as it helps auditability and legibility of the contracts. There are many comments of the form:
/**
* @dev …
*/
Changing these to have meaningful commentary can avoid errors when handling the contract, so it’s suggested to follow through.
Non declarative function naming
There are functions named successively (getPoolDetails1, getPoolDetails2,…, withdrawAllRemaining1, withdrawAllRemaining2,…). These are poor names that don’t reflect what the functions actually do, and can lead to trouble when modifying the code in the future. Clearer names would reflect the differences between them.
Old compiler pragmas
The contracts have the following pragma directive:
pragma solidity ^0.4.24;
We remind the development team that the Solidity compiler has been updated. While this isn’t critical in these particular contracts, there are bugfixes which if absent could’ve caused problems. As usual, it is recommended to stay on top of the new versions, which could avoid problems with things like the ABI encoding, parser issues, et cetera.
Missing deployment scripts
We inferred the deployment order for the contracts based on what they do, since they follow a factory pattern, and we gathered the following inheritance graph for the contracts (taking into account the libraries):
HasNoEther was removed from OpenZeppelin
The contracts CMService, PoolFactory and PoolRegistry inherit from HasNoEther, but it was removed from OpenZeppelin, since it could be misleading, as there’s no actualy way to guarantee a contract won’t have ether. The following discussion is cited: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1254
Observations
We annotate some of the verifications done to the contracts to indicate that they were performed even when no critical issue was found during the audit.
- Misuse of the different call methods: call.value(), send() and transfer().
We found no incorrect use of call() or send(), and transfer(). Calls to these methods were done on checked input, with correct coverage of requirements.
- Integer rounding errors, overflow, underflow and related usage of SafeMath functions.
- Race conditions such as reentrancy attacks or front-running.
- The only call to an external contract is in the addressCall function, but considering it’s private and is only called from payToPresale, which calls to this function with the presaleAddress that’s set. Since this is only callable by the pool administrator and the state change of the pool is done before the call, the pool will have a consistent internal state, so reentrancy can’t cause damage.
- There are many setters for addresses that could if not enough checks are done on input, cause transfers to unwanted addresses. However, these functions have the correct modifiers so only the administrator of the pool can change these.
- Misuse of block timestamps, assuming things other than them being strictly increasing.
- Timestamps aren’t used in the contract at all, so attacking the contract from that angle is impossible.
- Contract softlocking attacks (DoS) / unbounded gas usage.
No function in the contract has a loop that can be abused to cause a soft lock or an unbounded usage of gas.
Conclusion
No critical issues were found, yet some possible legibility issues were considered. Overall the contracts had decent documentation, except for some points, but in general they were simple enough to follow that it wasn’t critical. There is some code which we recommend reviewing, but in general the state of the codebase is good, the only trouble being somewhat inconsistent snippets which reduce legibility, and could also lead to bugs if additions are made.
Disclaimer: This audit report is not a security warranty, investment advice, nor an approval of the EasyPool project since Coinfabrik has not reviewed its platform. Moreover, it does not provide a smart contract code faultlessness guarantee.