ICOVO Smart Contract Audit
By: New Alchemy
Tags: Solidity, Security, Token Sale, Ethereum, Smart Contracts

Critical Issues

1. Fixed: Negative-Outcome Vote Stalls Voting System

In the Voting.sol contract, the finalizeVoting() function is used to finalize a vote that is past its end_time and will call the isPassed() function to determine the outcome of the vote. The relevant code is excerpted below.

function finalizeVoting () external {

uint256 pid = this.getCurrentVoting();

require(proposals[pid].end_time <= block.timestamp);

require(!proposals[pid].isFinalized); proposals[pid].isFinalized = true; if (isPassed(pid)) {

if (isSubjectRaiseTap(pid)) {

DaicoPool(poolAddr).raiseTap(proposals[pid].tapMultiplierRate);

queued[uint(Subject.RaiseTap)] = false;

} else if (isSubjectDestruction(pid)) {

DaicoPool(poolAddr).selfDestruction();

queued[uint(Subject.Destruction)] = false;

} else {

revert();

}

}

} function isPassed (uint256 pid) public constant returns(bool) {

require(proposals[pid].isFinalized);

uint256 ayes = getAyes(pid);

uint256 nays = getNays(pid);

uint256 absent = ERC20Interface(votingTokenAddr).totalSupply().sub(ayes).sub(nays);

return (ayes.sub(nays).add(absent.div(6)) > 0);

}

At the end of the isPassed() function, the return statement tests for the difference between ayes and nays using a subtraction performed with safeMath . If the number of nays is greater than the number of ayes the call to sub() will assert an error which will revert the whole transaction. This reverting of the transaction will undo all the prior changes that were performed in the transaction, including setting isFinalized to true. Since no more votes can be cast and the outcome won't change, the contract enters an implicit stalled state where the proposal can not be finalized, the next proposal cannot be put to a vote and the tokens used to vote on this specific proposal cannot be returned to their owners (and are stuck forever in this contract).

Separately, in the case where the sub() function does not assert, the outcome of the return statement will almost always be positive since it tests for an unsigned integer ( uint256 ) value to be greater than 0 (e.g. the only case where this returns false is where this value is exactly 0).

To prevent stalling the contract, the isPassed function return statement needs to be changed to the following:

return (ayes.add(absent.div(6)) > nays);

Re-test v2.0: New Alchemy has inspected the new code and has determined that this issue has been fixed and the associated risk mitigated. The isPassed() function has been recast as shown below:

function isPassed (uint256 pid) public view returns(bool) {

require(proposals[pid].isFinalized);

uint256 ayes = getAyes(pid);

uint256 nays = getNays(pid);

uint256 absent = ERC20Interface(votingTokenAddr).totalSupply().sub(ayes).sub(nays);

return (ayes > nays.add(absent.div(6)));

}

Moderate Issues

2. Fixed: Loss of Precision Defeats raiseTap Granularity

Upon a positive outcome to a vote on a proposal to “raise tap” from the Voting.sol contract, a call to raiseTap on the DaicoPool.sol contract will be made to increase the tap by a value between 1.01 and 2.00 (by allowing the _tapMultiplierRate value from Voting.sol to range from 101 to 200 included).

function raiseTap(uint256 tapMultiplierRate) external onlyVoting {

updateReleasedBalance();

updateTap(tap.mul(tapMultiplierRate.div(100)));

}

However because of the order of operation and truncating integer math, only two actual multiplier values will ever be used. Either a multiply value of 1 if tapMultiplierRate is a value ranging from 101 to 199 or a multiply value of 2 if tapMultiplierRate is exactly 200. This defeats the purpose of the vote to increase the tap for any value of tapMultiplierRate different from 200.

To fix this issue, the order of operations needs to be shifted in the following manner to minimize the intermediate loss of precision from integer math:

updateTap(tap.mul(tapMultiplierRate).div(100));

Re-test v2.0: New Alchemy has inspected the new code and has determined that this issue has been fixed and the associated risk mitigated. The operation code now matches the recommendation shown above.

3. Fixed: WhitepaperVersioning.sol Stall

An attacker may be able to stall this contract through a carefully crafted malicious post transaction.

Consider the scenario where:

WhitepaperVersioning.sol is freshly deployed and the whitepapers structure is empty.

is freshly deployed and the whitepapers structure is empty. An attacker submits a post(_ipfsHash="HEAD", _version=255, _prev="HEAD") transaction. The logic bug revolves around _ipfsHash="HEAD" which may be an unexpected (or undesired) input with a version parameter equal to MAX_INT8 .

transaction. The logic bug revolves around which may be an unexpected (or undesired) input with a version parameter equal to . This transaction creates a whitepapers["HEAD"] entry with version equal to 255.

The code pertinent to this scenario is excerpted below:

function post (string _ipfsHash, uint8 _version, string _prev) public ... {

// Check if the IPFS hash doesn't exist already.

require(!whitepapers[_ipfsHash].initialized); // Check if the specified version is counted up

require(_version > whitepapers[_prev].version);



// Check if previous whitepaper's author is identical to posting whitepaper's

// author or the posting whitepaper is the initial version (HEAD version)

require(keccak256(_prev) == keccak256("HEAD") ||

whitepapers[_prev].author == msg.sender);

// Check if there is no fork from the previous version

require(bytes(whitepapers[_prev].next).length == 0);



whitepapers[_prev].next = _ipfsHash;

whitepapers[_ipfsHash] = Whitepaper(_version, msg.sender, _prev, "", true);

emit Post(_ipfsHash, _version, msg.sender);

return true;

}

Any subsequent post transaction seeking to create a legitimate fresh entry (which necessitates specifying _prev="HEAD" ) may fail the second require statement meant to ensure versions are strictly incrementing. It may also fail the fourth require statement meant to prevent forks. This means the contract is closed to new entries. New Alchemy recommends this contract be more thoroughly tested (including this scenario) and the logic around "HEAD" revisited.

Re-test v2.0: New Alchemy has inspected the new code and has determined that this issue has been fixed and the associated risk mitigated. ICOVO has effectively rewritten the algorithm to manage whitepaper tracking.

4. Not Fixed: Unbounded Loops

A number of contracts include for loops that iterate over each element in a data structure of indeterminate size. As each iteration consumes additional gas, a large data structure may cause function execution to be impacted by block gas limits. This may ultimately result in an unusable contract. Scenario risk is heightened if an attacker is able to directly increase the data structure length and/or if each loop iteration involves significant gas cost.

The following table shows instances of contract, line and loop specifics for this issue:

IndividuallyCapped...sol:39: for (uint256 i = 0; i < _beneficiaries.length; i++)

WhitelistedCrowdsale.sol:42: for (uint256 i = 0; i < _beneficiaries.length; i++)

TokenSaleManager.sol:126: for (uint256 i = 0; i < tokenSales.length; i++ )

TokenSaleManager.sol:133: for (uint256 i = 0; i < tokenSales.length; i++ )

TokenSaleManager.sol:160: for (uint256 i = 0; i < tokenSales.length; i++ )

TimeLockPool.sol:90: for (uint256 i = 0; i < lockedBalances[account]

[tokenAddr].length; i++)

TimeLockPool.sol:128: for (uint256 i = 0; i < lockedBalances[account]

[tokenAddr].length; i++)

TimeLockPool.sol:148: for (uint256 i = 0; i < lockedBalances[account]

[tokenAddr].length; i++)

TimeLockPool.sol:168: for (uint256 i = 0; i < lockedBalances[account]

[tokenAddr].length; i++)

Voting.sol:106: for (uint256 pid = 0; pid < this.getCurrentVoting();

pid++)

Voting.sol:119: for (uint256 i = 0; i < accounts.length; i++)

Voting.sol:125: for (uint256 i = 0; i < proposals.length; i++)

In some cases it may be preferable to keep running counts during operation such that a summary function does not have to loop over an entire data structure to calculate total counts. In other cases it may be preferable to invert control by exposing helper functions to support external operations. In rare cases, an additional ‘helper’ data structure may be required.

Re-test v2.0: New Alchemy has inspected the new code and has determined that this issue has not been fixed. A specific instance is described in the following paragraph.

Re-test v2.0: An attacker can greatly expand the lockedBalances data structure in TimeLockPool.sol by making a large number of small deposits via the depositERC20() function with distinct releaseTime values. When the victim subsequently attempts to call the getAvailableBalanceOf() function, its unbounded loop on line 131 may run out of gas before finishing the walk through the lockedBalances data structure. As a result, this function effectively no longer works and any other functionality that depends on this function (such as a withdrawal process) may no longer work either.

5. Informational: Inadequate Dependency Management

The primary contracts make excellent use of standardized functionality. However, a robust strategy for version control and dependency management is lacking. As a result, it is difficult to definitively determine file versions, heritage and freshness. This will impact vulnerability awareness and resolution.

The crowdsale directory provides an illustrative example. These files contain standardized functionality sourced from the OpenZeppelin project. Since some details have been injected into the files themselves as comments, it is now no longer possible to compare file hashes with the source project. Further, it is difficult to track the emergence of new vulnerabilities and their applicability. Many of the line-by-line comments noted in a later section of this report have already been fixed in OpenZeppelin source but have not been incorporated into this code base.

An attractive way forward is to consider the use of Git submodules. Github provides useful documentation on this topic^gh1.

Re-test v2.0: New Alchemy has inspected the new code and has determined that this issue has not been addressed. However, it has been marked as informational because it relates to best practices and not to a specific security bug.

6. Fixed: Unused Contract Code

The MintedCrowdsale.sol and IndividuallyCappedCrowdsale.sol source files are included in the project but are never used. While these are likely the result of normal development refactoring work, unused files provide the potential for confusion and mistakes.

Re-test v2.0: New Alchemy has inspected the new code and has determined that this issue has been fixed and the associated risk mitigated. The two source files noted above have been removed.

Minor Issues

7. Fixed: Potential for Tokens and ETH Lock-up

In TimeLockPool.sol , both deposit functions ( depositERC20() and depositETH() ) allow the destination account to be set to an address of 0x0 . However, the withdraw() function requires account to be different from 0x0 for the transaction to succeed. This means that any tokens or ether sent to the 0x0 address will stay locked in the contract forever with no way to recover them. New Alchemy recommends adding a requirement to both deposit functions so that they check that account is different from the 0x0 address to avoid loss of funds or tokens.

require(account != address(0x0));

Re-test v2.0: New Alchemy has inspected the new code and has determined that this issue has been fixed and the associated risk mitigated. The two functions noted above now check for the 0x0 address via the require statement specified above.

8. Not Fixed: Constant Return Values

The contracts include many functions with hardcoded return values (generally equal to true ). This typically indicates incorrectly specified functions and/or missing error checking. For example, TimeLockPool::depositERC20() function is hardcoded to return true and it is only called via a require statement in the TokenSaleManager.sol::mintTimeLocked() function, which is also hardcoded to always return true . This may be inadvertently misleading or confusing when read or used out of context.

Examples of contracts, line numbers and actual excerpt are shown below:

MintableToken.sol:42: return true;

MintableToken.sol:52: return true;

ERC223Standard.sol:74: return true;

ERC20Standard.sol:37: return true;

ERC20Standard.sol:55: return true;

ERC20Standard.sol:71: return true;

ERC20Standard.sol:113: return true;

ERC20Standard.sol:134: return true;

TokenController.sol:79: return true;

TokenController.sol:90: return true;

WhitepaperVersioning.sol:52: return true;

DaicovoStandardToken.sol:35: return true;

TokenSaleManager.sol:104: return true;

TokenSaleManager.sol:121: return true;

TimeLockPool.sol:61: return true;

TimeLockPool.sol:79: return true;

TimeLockPool.sol:107: return true;

TimeLockPool.sol:112: return true;

New Alchemy recommends avoiding constant return values by checking both positive and negative conditions and returning the appropriate value. Subsequently, ensure all return values are checked and handled. Where this is not possible, the function should not specify a return value.

Re-test v2.0: New Alchemy has inspected the new code and has determined that this issue has not been fixed. Many functions continue to return hardcoded values as shown, including the scenario in the introductory paragraph above.

9. Not Fixed: ERC20 double-spend attack

The standard ERC20 interface, implemented in ERC20Token , has a design flaw: if some user Alice wants to change the allowance that it grants to some other user Bob, then Alice can only check if Bob has already spent its allowance before issuing the transaction to change Bob's allowance. However, Bob can still spend the original allowance before the transaction changing the allowance is mined, allowing Bob to spend both the pre-change and post-change allowances. In order to have a high probability of successfully spending the pre-change allowance after the victim has verified that it is not yet spent, it may be necessary for the attacker to wait until the transaction to change the allowance is issued, and then issue a spend transaction with an unusually high gas price to ensure that the spend transaction is mined before the allowance change. More detail on this flaw is available at https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/ and https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729.

Due to this flaw, safe usage of the standard ERC20 approve() interface to change allowances requires that:

allowances only change between zero and non-zero allowances change at most once in a block

These restrictions allow Alice to safely change Bob’s allowance by first setting it to zero, waiting for that transaction to be mined, verifying that Bob didn’t spend its original allowance, then finally setting the allowance to the new value. Strictly requiring only this sequence of operations in approve() would violate the ERC20 standard, though users can still take this approach even without changes in the approve() implementation.

To make it more convenient to safely change allowances, New Alchemy recommends providing increaseApproval() and decreaseApproval() functions that add or subtract to the existing allowances rather than overwriting them. These functions are indeed included in the ERC20Standard.sol contract -- which is excellent. This effectively moves the check of whether Bob has spent his allowance to the time that the transaction is mined, removing Bob's ability to double-spend. Clients that are aware of this non-standard interface can use it rather than approve() .

However, the contracts under review do not utilize increaseApproval() nor decreaseApproval() but instead continue use of approve() in TokenSaleManager.sol .

Separately, one can only allow approve() to change allowances between zero and non-zero and not allow multiple changes to a user's allowance in the same block. In order to change Bob's allowance, Alice must first set it to zero, wait until that transaction is mined, verify that the original allowance was not spent, then finally set the allowance to the new value. Requiring this sequence of operations by implementing restrictions in approve() would violate the ERC20 standard, though users can still take this approach even without changes in the approve() implementation. This logic has not been implemented in the source contracts under review.

Since both approaches are outside of the ERC20 standard, both approaches require user cooperation to work properly. Accordingly, ICOVO should provide documentation advising its users on how ERC20 allowances are safely managed.

Re-test v2.0: New Alchemy has inspected the new code and has determined that this issue has not been fixed. The TokenSaleManager.sol contract continues to use the approve() function. New Alchemy reviewed the latest available whitepapers and was unable to located specific directions around ERC20 allowances.

10. Not Fixed: Lack of two-phase ownership transfer

In contracts that inherit the common Ownable contract from the OpenZeppelin project, a contract has a single owner. That owner can unilaterally transfer ownership to a different address. However, if the owner of a contract makes a mistake in entering the address of an intended new owner, then the contract can become irrecoverably unowned.

In order to preclude this, New Alchemy recommends implementing two-phase ownership transfer. In this model, the original owner designates a new owner, but does not actually transfer ownership. The new owner then accepts ownership and completes the transfer. This can be implemented as follows:

contract Ownable {

address public owner;

address public newOwner



event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);



function Ownable() public {

owner = msg.sender;

newOwner = address(0);

}



modifier onlyOwner() {

require(msg.sender == owner);

_;

}



function transferOwnership(address _newOwner) public onlyOwner {

require(address(0) != _newOwner);

newOwner = _newOwner;

}



function acceptOwnership() public {

require(msg.sender == newOwner);

OwnershipTransferred(owner, msg.sender);

owner = msg.sender;

newOwner = address(0);

}

}

Re-test v2.0: New Alchemy has inspected the new code and has determined that this issue has not been fixed. However, ICOVO does not stand out from the community for choosing not to fix this.

11. Not Fixed: Lack of short-address attack protections

Some Ethereum clients may create malformed messages if a user is persuaded to call a method on a contract with an address that is not a full 20 bytes long. In such a “short-address attack”, an attacker generates an address whose last byte is 0x00, then sends the first 19 bytes of that address to a victim. When the victim makes a contract method call, it appends the 19-byte address to msg.data followed by a value. Since the high-order byte of the value is almost certainly 0x00, reading 20 bytes from the expected location of the address in msg.data will result in the correct address. However, the value is then left-shifted by one byte, effectively multiplying it by 256 and potentially causing the victim to transfer a much larger number of tokens than intended. msg.data will be one byte shorter than expected, but due to how the EVM works, reads past its end will just return 0x00.

This attack effects methods that transfer tokens to destination addresses, where the method parameters include a destination address followed immediately by a value. In the DAICOVO contracts, such methods include the following:

ERC223ReceivingC...sol:21: function tokenFallback(address _from, uint _value, ...

ERC223Standard.sol:30: function transfer(address _to, uint256 _value, byte...

ERC223Standard.sol:58: function transfer(address _to, uint256 _value) exte...

ERC20Standard.sol:29: function transfer(address _to, uint256 _value) exte...

ERC20Standard.sol:46: function transferFrom(address _from, address _to, u...

ERC20Standard.sol:68: function approve(address _spender, uint256 _value) ...

ERC20Standard.sol:96: function allowance(address _owner, address _spender...

ERC20Standard.sol:110: function increaseApproval(address _spender, uint _a...

TokenController.sol:50: function mint (address to, uint256 amount) external...

DaicovoStandardT...sol:28: function forceTransfer(address _to, uint _value) ex...

TokenSaleManager.sol:101: function mint (address _beneficiary, uint256 _token...

While the root cause of this flaw is buggy serializers and how the EVM works, it can be easily mitigated in contracts. When called externally, an affected method should verify that msg.data.length is at least the minimum length of the method's expected arguments. For instance, msg.data.length for an external call to ERC20Standard::transfer() should be at least 68: 4 for the hash, 32 for the address including 12 bytes of padding, and 32 for the value. Some clients may add additional padding to the end. This test can be implemented in a modifier. External calls can be detected in the following ways:

Compare the first four bytes of msg.data against the method hash. If they don't match, then the call is internal and no short-address check is necessary.

against the method hash. If they don't match, then the call is internal and no short-address check is necessary. Avoid creating public methods that may be subject to short-address attacks; instead create only external methods that check for short addresses as described above. public methods can be simulated by having the external methods call private or internal methods that perform the actual operations and that do not check for short-address attacks.

Whether or not it is appropriate for contracts to mitigate the short-address attack is a contentious issue among smart-contract developers. Many, including those behind the OpenZeppelin project, have explicitly chosen not to do so. While it is New Alchemy’s position that there is value in protecting users by incorporating low-cost mitigations into likely target functions, ICOVO would not stand out from the community if they also choose not to do so.

Re-test v2.0: New Alchemy has inspected the new code and has determined that this issue has not been fixed. As stated above, ICOVO does not stand out from the community because they have chosen not to fix this.

12. Informational: Incorporate Assertions

The contracts do not incorporate any assertions (outside of the SafeMath library) to check contract invariants. New Alchemy recommends incorporating assertions near the end of functions to confirm expected contract state wherever possible, particularly when balances are being adjusted.

One example is line 100 of DaicoPool.sol::withdraw() where several calculations are made involving balances followed by a transfer. An assertion relating to the expected final state could potentially be incorporated here. The TimeLockPool.sol contract has multiple opportunities along the same lines.

Re-test v2.0: New Alchemy has inspected the new code and has determined that this issue has not been addressed. However, it has been marked as informational because it relates to best practices and not to a specific security bug.

Smart Contract ABI Reference

This section provides the fully-elaborated application binary interface (ABI) for four of the primary contracts as seen at runtime. The ABI defines how the contracts may be interacted with while running inside the EVM. The tables make for very easy inspection of top-level specifics including:

The definitive reference of all exposed functions

Correct input and output function signatures

Presence of constructors and fallbacks as expected

Correct state mutability (pure, view, nonpayable, payable)

Extraneous functions which should be private

From inspecting the tables below it becomes clear that many return values have no name. New Alchemy recommends naming all return values wherever possible to maximize clarity and minimize cognitive loading. Further, the absence of some function return (output) values may also represent missed opportunities to report then check/handle status above and beyond simple transaction failures.

OVOToken.sol ABI Reference

DaicoPool.sol ABI Reference

TokenSale.sol ABI Reference