The CoinCircle team asked us to review and audit their UnikoinGold token and distribution contracts. We looked at the code and now publish our results.
The audited contract is in the unikoingold/UnikoinGold-UKG-Contract Github repository. The version used for this report is the commit 8b38f30039c2d13383c416fd6143f6bd0f091404
.
Good job using OpenZeppelin to write minimal extra code. Excellent automated tests code coverage, with 96% for TokenDistribution
and 95% for ParticipantAdditionProxy
.
Here’s our assessment and recommendations, in order of importance.
No critical severity issues were found.
Contributors participate in the sale by transferring their balance to a set of trusted addresses managed by Unikrn, who then distribute the tokens in a later step via the TokenDistribution
contract. Token allocations are set for each contributor, who must then claim the tokens to have them added to their balance.
The documentation indicates that this is done “because if a participant were to lose their key, there would never be 1B tokens created”. This is incorrect, since tokens can still be created and allocated to their addresses. This way, there will be indeed 1B tokens, even if some participants cannot move them afterwards.
Furthermore, if a participant did misplace their keys, tokens are locked in the TokenDistribution contract (as per L136) and there are not methods to take them out of the contract, so the tokens are created but are unusable. A participant may misplace their keys after the distribution, so it makes no sense to try to prevent allocation to addresses with lost keys.
Consider removing the claim mechanic altogether, and allocate the tokens to the contributors directly. The whole ParticipantAdditionProxy
contract seems to be unnecessary. This reduces the complexity of the distribution process, simplifies the code, and reduces the hassle for end users to access their purchased tokens.
Update: The CoinCircle team indicated that this was motivated by the fact that the proxy contract will be deployed before the distribution contract, in order to store the future owners of the token before the final implementation is finalized:
The design decision for the contract was to separate concerns: the proxy contract will be deployed prior to the distribution contract. The proxy contract will be populated by Unikrn, and not be made known to the public until the contract is verified, locked and unchangeable
The TokenDistribution contract relies on three timestamps:
freezeTimestamp
, which indicates up to which point it is possible to cancel the distribution contractdistributionStartTimestamp
, that signals when the distribution startslockupTimestamp
, which indicates when locked tokens may be releasedThough it is assumed that freezeTimestamp
should be before the distributionStartTimestamp
, this is not checked in the code. If this precondition is not respected, an owner may stop the contract mid-distribution.
Also, for the lockup to have any effect, it is necessary for lockupTimestamp
to occur after the distributionStartTimestamp
. Otherwise, locked tokens can be claimed as soon as distribution starts.
Consider adding a require
guard for freezeTimestamp < distributionStartTimestamp
, and another for distributionStartTimestamp < lockupTimestamp
, in the TokenDistribution constructor.
Update: First guard added in this commit. Second one is unnecessary as per this commit.
There’s several ways in which the UKG token is not ERC20 compliant.
totalSupply
variable is not updated correctly. It should always contain the total token supply. For example, token balance is given to the Unikrn team in line 138 of TokenDistribution.sol and totalSupply
is left unchanged. Consider updating totalSupply
every time tokens are created or destroyed.Update: Fixed in this commit.
name
, symbol
and decimal
properties to the Token contract. Though not mandatory, they are recommended as part of the ERC20 specification. See OpenZeppelin’s SimpleToken as an example.Update: Fixed in this and this commits. Remember also to bump version
field to 1.0 and mark it as constant once the final code is frozen.
TokenDistribution
constructor L137 and L139.Update: Fixed in this commit.
Consider making these changes to make the token respect the ERC20 standard.
The token Distribution process is built into the same Token contract in TokenDistribution.sol. Since the distribution is not part of the token intrinsic mechanics, it is strongly recommended to use a standard Token contract and a separate Distribution contract. This contract would start with all the tokens to distribute among users, and invoke the transfer method of the Token when needed.
This simplifies the logic of the token contract, separates different concerns among different contracts, and also reduces the attack surface for the token contract.
Update: Fixed in this, this, this and this commits.
Though some math operations are safe, there are others that are unchecked in TokenDistribution lines 162, 236, 294. It’s always better to be safe and perform checked operations.
In particular, line 235 subtracts the current phase allocation from the sender’s remaining allowance, without checking that the minuend is greater or equal than the subtrahend. Even though the distribution logic should not allow for this to happen, it’s highly recommended to always check that a user has enough balance when subtracting from the allowance, which is done automatically when using SafeMath.
Update: Fixed in this commit.
In the spirit of failing as promptly as possible in all methods, consider adding checks in functions allocatePresaleBalances
,allocateSaleBalances
, andallocateLockedBalances
of ParticipantAdditionProxy to ensure that both arrays passed in as parameters have the same length. If the first array passed in is shorter than the second by mistake (ie if an address is missing for an allocation), then the code will silently continue.
Update: Fixed in this commit.
Code from SafeMath
, Ownable
, ERC20Basic
, ERC20
, BasicToken
, and StandardToken
was copied from OpenZeppelin into the files SafeMath, Ownable, and Token. This violates OpenZeppelin’s MIT license, which requires the licence and copyright notice to be included if its code is used, and makes it difficult to update to a more recent version.
Consider following the recommended way to use OpenZeppelin contracts, which is via the zeppelin-solidity npm package. This allows for any bugfixes to be easily integrated into the codebase, such as issues 375 and 400 which are fixed in the latest release and affect the Token contract.
Update: Fixed in this commit.
For both the sale and presale participants, the ParticipantAdditionProxy keeps track of whether the entire addition process is complete (presaleAdditionDone
,lockedAdditionDone
, and saleAdditionDone
), which is the balance for each address(presaleBalances
,lockedBalances
, and saleBalances
), and whether an address has already been allocated (presaleParticipantAllocated
,lockedParticipantAllocated
, and saleParticipantAllocated
).
Assuming that each participant has a non-negative allocation, and since once a participant is allocated it cannot be modified, then the flag stating whether a participant has been allocated can be replaced by a check of whether the balance for the address is zero or not. This allows the threeParticipantAllocated
variables to be removed.
Furthermore, since it is not possible to allocate balance over the pre-defined cap, and the process can only be marked as complete when the cap is reached, the flags and methods for ending the addition process (presaleAdditionDone
/lockedAdditionDone
/saleAdditionDone
and endPresaleParticipantAddition
/endLockedParticipantAddition
/endSaleParticipantAddition
) are not needed, and can be replaced by a check on whether the cap was reached.
Removing redundant state variables not only reduces gas costs due to reduced storage used, but also greatly simplifies the code. Consider doing so.
Update: Fixed in this commit, though endParticipantAddition
functions were kept for clarity.
Current code specifies version pragma ^0.4.11
. We recommend changing the solidity version pragma to the latest version (pragma solidity ^0.4.17
) to enforce the use of an up to date compiler.
Update: Updated to 0.4.15 in this commit, latest version of solidity supported by the used Truffle version.
Throughout the code, in many functions a temp variable with a calculation is defined, an assertion is performed over the temp value, and if it succeeds, then a state variable is assigned with the value temp
. See as an example ParticipantAdditionProxy L70:
`uint256 tempPresaleTotalSupply = presaleAllocationTokenCount.add(approvedPresaleParticipantsAllocations[i]);` `require(tempPresaleTotalSupply <= PRESALE_TOKEN_ALLOCATION_CAP);` `presaleAllocationTokenCount = tempPresaleTotalSupply;`
The same happens in ParticipantAdditionProxy L92 and L114, and TokenDistribution L158 and L290.
Since a failed assertion via require will revert all state changes within the transaction, it is not necessary to use this temp variable. Assigning directly to the state variable and then checking the assertion will have the same effect, and make the code shorter and easier to read. Consider removing these unneeded temporal variables.
Update: Fixed in this and this commits.
The total number of phases in the TokenDistribution is repeated as a magic constant throughout the contract code, in L142, L189, L214, L215 and L239.
Use of magic constants reduces code readability and makes it harder to understand code intention. We recommend extracting magic constants into contract constants.
Update: Fixed all except for L142 in this and this commits.
State variable phasesClaimable
in TokenDistribution
is unused. Moreover, there is a function with the same name in L198 which shadows the getter generated by the public
modifier, and has entirely different semantics. Consider removing the phasesClaimable
field.
Update: Fixed in this commit.
State variable isVesting
in TokenDistribution
is defined as a map from addresses into uint256
, though only 0 and 1 values are used as a proxy to represent boolean values. Futhermore, 0 is used to represent true
and 1 is used to represent false
, which is the opposite as to how boolean values are represented in the ABI, . Consider changing the type of the map values from uint256
to bool
, or removing the field altogether, as explained below.
Update: Fixed by removing the field (as suggested below) in this commit._
State variable isVesting
in TokenDistribution
is set to true for an address if and only if a participant reached the last phase in claimPresaleTokens
. As such, it is straightforward to infer whether a participant is vesting or not just by checking if phasesClaimed
for the address is equal to 10. Consider removing the redundant state variable (isVesting
) if it will not have any other uses.
Update: Fixed in this commit.
State variables claimed
and phasesClaimed
in TokenDistribution
hold the same information: whether an address has claimed a particular phase for the presale. The former keeps track of both through a mapping phase => address => isClaimed
, while the latter stores the last phase claimed for a given address. Given that phases can only be claimed incrementally (i.e. phase N cannot be claimed unless phase N-1 was claimed), the following equivalence holds:
`claimed[aPhase][anAddress] == (phasesClaimed[anAddress] >= aPhase)`
Consider removing either of the two (we suggest removing claimed
) in favour of the other to remove redundant state from the contract.
Update: Both variables were decided to be kept to reduce complexity of the phasesClaimable
function implementation.
PRESALE_TOKEN_ALLOCATION_CAP
is defined in both TokenDistribution and ParticipantAdditionProxy. (Update: kept for readability).presaleTokensStillAvailable
defined in L102 of TokenDistribution.sol is only used in L255 of the same file. Consider having that as a regular function precondition. (Update: fixed in this commit).public
. Solidity automatically creates getter functions for these fields, so it is not necessary to redefine functions to access them from outside the contract. Examples of these functions arebalanceOfPresaleParticipants
, balanceOfLockedParticipants
and balanceOfSaleParticipants
in ParticipantAdditionProxy; as well as all the test functions (presaleParticipantAllowedAllocationTest
, allocationPerPhaseTest
,remainingAllowanceTest
, saleParticipantCollectedTest
, and isVestingTest
) defined in TokenDistribution L314–L332. (Update: fixed in thesetwo commits).balanceOf
from BasicToken is repeated in TokenDistribution L310, which inherits from BasicToken. Consider removing the redundant code. (Update: fixed in this commit).ParticipantAdditionProxy
is very similar and contains a lot of very similar code. Lots of functions related to that could be generalized, or abstracted, to reduce code repetition. See for example allocateSaleBalances
, allocateLockedBalances
, and allocatePresaleBalances
, which are almost exact copies of the same code, with different variable names. The same happens with claimLockedTokens
and claimSaleTokens
in TokenDistribution. (Update: locked logic was removed in thiscommit, reducing the number of copies of the same code)._min
in L180 of TokenDistribution
is already implemented in the Math library of OpenZeppelin. Consider reusing the available function from OpenZeppelin rather than copying it into the contract. (Update: fixed in this commit).claimed
in L77 of TokenDistribution
to clarify that the mapping is indexed by the presale phase. (Update: fixed in this commit).cancelDistribution
flag, along with the modifier and method to set it, to a separate Cancelable
contract, similar to OpenZeppelin’s Pausable contract, to improve separation of concerns._endOfPhaseTimestamp
in L142 of TokenDistribution
is skipped and executed manually outside the loop. Consider executing it within the loop to reduce code duplication. (Update: fixed in this commit).endOfPhaseTimestamp
altogether in order to reduce gas costs from unnecessary storage, since this value can be easily calculated on the fly whenever needed._After the original audit, the Unikrn team asked us to review two additional changes:
We have reviewed these changes and found no issues with the updated code. We include a few comments below:
removeParticipant
method, it is now possible to remove or change the number of tokens for a participant, as long as the sale is not marked as done via the saleAdditionDone
flag. In order to prevent any modifications during the distribution, consider adding a check in distributeSaleTokens
to ensure that saleAdditionDone
is set in the participantData ProxyContract
. This prevents a distribution from taking place using a ParticipantAdditionProxy
that might still change. Consider doing the same for the claimPresaleTokens
method and the presaleAdditionDone
flag.balanceOfPresaleParticipants
and balanceOfSaleParticipants
in the ProxyContract
interface L32–33 are not needed and can be removed to prevent confusions, since they are unimplemented by the ParticipantAdditionProxy
contract.No critical severity issues were found. Some changes were proposed to follow best practices and reduce potential attack surface.
Note that as of the date of publishing, the above review reflects the current understanding of known security patterns as they relate to the UKG token and distribution contracts. We have not reviewed the related UnikoinGold project. The above should not be construed as investment advice. For general information about smart contract security, check out our thoughts here.