Solidified ‘Solid’ Token Audit
By: New Alchemy
Tags: Solidity, Security, Ethereum, Smart Contracts, Token Sale

Moderate Issues

1. Sale Stage May Stall

The TokenSale contract implements logic involving the enforcement of minimum and maximum participation levels, the process of returning change under certain purchase conditions and token crowdsale fundraising caps. There is a logic bug at the intersection of these concerns in the _preValidatePurchase function. This bug prevents the early switching from one sale stage to the next BREAK or FINALAIZED as is supposed to happen when no more contributions can be made to the current stage.

This logic is shown in the code excerpt below:

uint256 acceptedValue = _weiAmount;

if(contributions[_beneficiary].add(acceptedValue) > maximumContribution){

changeDue = (contributions[_beneficiary].add(acceptedValue)).sub(maximumContribution);

acceptedValue = acceptedValue.sub(changeDue);

}

uint256 currentCap = getCurrentCap();

uint256 raised = getRaisedForCurrentStage();

if(raised.add(acceptedValue) > currentCap){

changeDue = changeDue.add(raised.add(acceptedValue).sub(currentCap));

acceptedValue = _weiAmount.sub(changeDue);

capReached = true;

}

require(acceptedValue >= minimumContribution, "Contribution below minimum");

Consider the following situation:

An incoming transaction arrives with a value less than the maximumContribution and equal to or larger than the minimumContribution .

and equal to or larger than the . The difference between the current fundraising cap and the current fundraising amount is less than the minimumContribution . In other words, the current fundraising amount is very, very close to the cap.

. In other words, the current fundraising amount is very, very close to the cap. The current fundraising cap will require that change be made and the accepted value reduced.

That change forces the accepted contribution to be below the minimumContribution .

. The last require statement will 'fail', reverting all the changes from that transaction.

This can happen when successive contributions have brought the amount of ether raised for the current stage to within less than minimumContribution amount from the current sale cap.

This can be solved by implementing the following modification to the second part of the _preValidatePurchase function as shown below:

uint256 currentCap = getCurrentCap();

uint256 raised = getRaisedForCurrentStage();

if(raised.add(acceptedValue) > currentCap.sub(minimumContribution)){

if(raised.add(acceptedValue) > currentCap){

changeDue = changeDue.add(raised.add(acceptedValue).sub(currentCap));

acceptedValue = _weiAmount.sub(changeDue);

}

capReached = true;

}

require(acceptedValue >= minimumContribution, "Contribution below minimum");

This will effectively set the capReached if no other contributions can be made to the current sale stage (because no contribution lower than minimumContribution can be made). This would also remove the need for the updateStage function to ever be called, allowing it to be entirely removed from the contract (the timed transition can still be forced through a direct call to the saleOpen function).

Re-test v2.0: Solidified reports this issue as fixed in the repository provided for re-testing. New Alchemy concurs and considers the risk mitigated.

2. Inconsistent/Confusing Critical Constant Declaration and Behavior

Critical constants are those values which are necessary for a user to clearly understand the implementation and behavior prior to making a decision regarding participation. New Alchemy strongly believes that high clarity and transparency standards directly correspond to the trust and confidence users place in the contracts and their owners. As they stand, the contracts demonstrate three different implementations of critical constants and two different behaviors.

Some constants are effectively hardcoded into the source code during development (albeit in the constructor) and not subject to change. An example is shown below:

minimumContribution = 0.5 ether;

maximumContribution = 100 ether;

Some constants are passed as parameters to the constructor during deployment and may be changed on a per deployed-instance basis. An example is shown below:

constructor(uint256 _rate, address _wallet, ERC20 _token, uint256 _presaleCap,

uint256 _mainCap) public Crowdsale(_rate,_wallet,_token) {

presale_TokenCap = _presaleCap.div(rate).mul(1250); // .. (cap/rate*1000)*1.25

mainSale_TokenCap = _mainCap.div(rate).mul(1000);

presale_Cap = _presaleCap;

mainSale_Cap = _mainCap;

Some constants are buried within complex logic. While these are obviously placed during development and not subject to change, finding and understanding them may be challenging. Their definition is placed amongst complex code execution logic. An example is shown below:

function distributeTokens() public onlyOwner atStage(Stages.FINALAIZED) {

require(!distributed);

require(checkPercentages(40)); // Magic number -> Only 60 will be sold,

// therefore all other must be less than 40

uint256 totalTokens = (presale_TokesSold.add(mainSale_TokesSold)).mul(10).div(6);

for(uint i = 0; i < partners.length; i++){

uint256 amount = percentages[partners[i]].mul(totalTokens).div(1000);

_deliverTokens(partners[i], amount);

}

require(SolidToken(token).finishMinting());

distributed = true;

}

Some constants appear to be placeholders for later use as shown by the excerpt from Distributable.sol below. The source code comments seem to confirm this interpretation. The percentages contract state variable is never set or modified outside of the constructor (even within the whole package of contracts). The same is true for the partners address array.

bool distributed;

//Not actual addresses

address[] public partners = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10]; mapping(address => uint) public percentages; constructor(){

//Not actual percentages

percentages[0x01] = 10;

percentages[0x02] = 10;

percentages[0x03] = 10;

percentages[0x04] = 10;

percentages[0x05] = 10;

New Alchemy strongly recommends to clearly hardcode as many critical constants as possible directly into the contract as constant state variables with a distinctive convention (such as in SNAKE_CASE). This should be done at development time. This makes it easy for the user to find, understand and have confidence in.

Second, New Alchemy recommends to minimize the number of critical constants passed as parameters to the contract constructor and to clarify if/how these constants may change.

Third, eliminate ‘magic numbers’. Declare them as constant state variables.

In addition to the above, OpenZeppelin functionality such as (changeable) ownership and pause capabilities should be documented. As it stands, the user needs to research this inherited functionality and determine if important aspects are overridden.

Re-test v2.0: Solidified reports this issue as fixed in the repository provided for re-testing. New Alchemy concurs and considers the risk mitigated.

Minor Issues

3. Two Functions Missing onlyOwner

Several functions can benefit from the onlyOwner modifier. Specifically,

The enableTransfer() function on line 32 of SolidToken.sol .

function on line 32 of . The updateStage() function on line 130 of TokenSale.sol .

While this doesn’t create a security concern as these functions are only supposed to help trigger timing based functionality (at a pre-determined point in time), changing the contract internal state is usually only done by the contract owner.

Re-test v2.0: Solidified has indicated their philosophy “is that if there are no security/functionality concerns with allowing a function to be called by anyone, then that function should not have the onlyOwner modifier”. While New Alchemy generally takes a much stricter (and perhaps paranoid) approach, it is agreed that Solidified’s approach is reasonable in this instance as these functions are more operational than ownership related. This risk is accepted.

4. Unnecessary Loss of Precision

When division is done prior to multiplication there is an unnecessary loss of precision in the result.

The constructor for TokenSale is shown below (with adapted formatting). There are two examples of division prior to multiplication resulting in an unnecessary loss of precision.

constructor(uint256 _rate, address _wallet, ERC20 _token, uint256 _presaleCap,

uint256 _mainCap) public Crowdsale(_rate,_wallet,_token) {

presale_TokenCap = _presaleCap.div(rate).mul(1250); // <removed for space>

mainSale_TokenCap = _mainCap.div(rate).mul(1000);

presale_Cap = _presaleCap;

mainSale_Cap = _mainCap; minimumContribution = 0.5 ether;

maximumContribution = 100 ether;

}

The loss of precision can accumulate and become significant when a large number of sequential operations are performed and/or when the magnitude of operands is similar and small. It is believed that neither condition applies in this instance, hence a minor issue.

Since multiplication and division are associative operations their ordering is theoretically unimportant. However, in practice performing the multiplication prior to division will result in less loss of precision.

Re-test v2.0: Solidified reports this issue as fixed in the repository provided for re-testing. New Alchemy concurs and considers the risk mitigated.

5. Pause Functionality Incorporated but not Used

The TokenSale.sol contract imports Pausable functionality from OpenZeppelin and inherits it on the contract declaration. However, the contract never makes use of this functionality. Specifically, Pausable provides function modifiers named whenPaused() and whenNotPaused() that are intended to block and unblock activity. Pausable capabilities are not used within the additional functionality imported from OpenZeppelin. The TokenSale.sol contract currently has extensive sale-stage management functionality that may present challenges to make interwork correctly.

Re-test v2.0: Solidified reports this issue as fixed in the repository provided for re-testing. However, upon re-inspection of the code, New Alchemy identified only a single instance of modifier usage on the TokenSale.sol::saleOpen() function and this function does not change state. Pausable functionality is intended to be an "emergency stop" mechanism for use on all significant state-changing functions. New Alchemy considers this issue to be not fixed.

Re-test v2.1: During further discussion, Solidified Ltd. correctly pointed out that the TokenSale.sol::saleOpen() function is very strategically located in that it prevents multiple state-changing operations from a single place. Accepting this, New Alchemy would like to see Pausable functionality also applied to the distrubuteTokens() and setupSale() functions. The former is considered a significant state changing operation and the latter is simply based upon the principle of overabundance of caution. Solidified Ltd. considers both functions to be an acceptable risk as-is, and thus the proposed mitigations extraneous. Ultimately, New Alchemy and Solidity Ltd. remain partially unaligned on the ideal resolution to distributeTokens() .

6. Lack of Short-Address Attack Protections

Some Ethereum clients may create malformed messages if a user is persuaded to call a function 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 function 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 affects functions that transfer tokens to destination addresses, where the function parameters include a destination address followed immediately by a value. In the Solidified contracts, such functions include:

SolidToken.sol:57 function transfer(address _to, uint256 _value)

SolidToken.sol:70 function transferFrom(address _from, address _to, uint256 _value)

SolidToken.sol:46 function mint(address _to, uint256 _amount)

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 function should verify that msg.data.length is at least the minimum length of the function's expected arguments (for instance, msg.data.length for an external call to 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 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 function hash. If they don't match, then the call is internal and no short-address check is necessary.

against the function hash. If they don't match, then the call is internal and no short-address check is necessary. Avoid creating public functions that may be subject to short-address attacks; instead create only external functions that check for short addresses as described above. Public functions can be simulated by having the external functions call private or internal functions 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, Solidified would not stand out from the community if they also choose not to do so.

Re-test v2.0: Solidified has indicated that this risk is accepted. New Alchemy considers this approach technically and commercially reasonable.

7. Lack of two-phase ownership transfer

In contracts that inherit the common Ownable contract from the OpenZeppelin project^2a, 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 a 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: Solidified has indicated that this risk is accepted. New Alchemy considers this approach technically and commercially reasonable.

8. 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^12. 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, 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 are available athttps://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/ and https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729.

Due to this flaw, the safe use of the 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. Requiring 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 change allowances, New Alchemy recommends providing increaseApproval and decreaseApproval functions that add or subtract to the existing allowances rather than overwriting them. This effectively moves the check of whether Bob has spent its 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 ; using approve remains open to abuse. This is approach is implemented by the current version of the OpenZeppelin library^13.

Alternatively, only allow approve to change allowances between zero and non-zero and don't 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.

Since both approaches are outside of the ERC20 standard, both approaches require user cooperation to work properly. Accordingly, Solidified should provide documentation advising its users on how they should manage other users’ allowances.

Re-test v2.0: The last line of this informational issue text recommends providing documentation advising users on proper allowance management. As such, the fix itself goes beyond the scope of contract source code review. New Alchemy accepts Solidified’s assertion.