=================
- 1. Introduction
- 2. Summary of Findings
- 2.1 Major Issues
- 2.2 Implementation Recommendations
- 3. Design Clarifications and Recommendations
- 4. Detailed report
- 4.1 General notes
- 4.2 Per file notes
- 4.2.1 CoreStorage.sol
- 4.2.2 TokenInterface.sol
- 4.2.3 TokenLoggerCallback.sol
- 4.2.4 TokenInfoController.sol
- 4.2.5 TokenApprovalController.sol
- 4.2.6 TokenRecastController.sol
- 4.2.7 TokenTransferController.sol
- 4.2.8 MathUtils.sol
- 4.2.9 AssetDisplay.sol
- 4.2.10 AssetExplorerInterface.sol
- 4.2.11 Controller.sol
- 4.2.12 AssetStatus.sol
- 4.2.13 ACOOwned.sol
- 4.2.14 ACGroups.sol
- 4.2.15 ContractResolver.sol
- 4.2.16 ResolverClient.sol
- 4.2.17 AssetIndexStorage
- 4.2.18 AssetInfoStorage.sol
- 4.2.19 AssetCustodianController.sol
- 4.2.20 LibraryDLL.sol
In the dates of May 21st 2017 to June 18th 2017, Digix engaged Yaron Velner and Loi Luu to perform security audit for Digix smart contracts. The audited code was taken from three private github repositories:
core2
repository: commit3ef8bd454322c8133b4686efcaa9543bb16dfe05
.cacp-contracts
repository: commitb190ec94af728d4c973dd9b8f03008ea5ee035b9
.solidity-core-libraries
repository: commit9a787b3502d2aa9c70b43dd3bb855a3018b74fac
.
Digix is an asset-tokenisation platform built on Ethereum. It allows users to purchase gold backed tokens, which they can transfer and convert to gold.
The Digix platform collects demurrage fees from its users in return of storing the physical gold bars. Hence, its token contract requires sophisticated implementation for the demurrage fees and cannot reuse existing standard ERC20 token implementation.
In the audit we reviewed the smart contracts that implement the token mechanism, which includes an implementation for all ER20 token operations as well as conversion (recast) of a token into gold (to be precise, a registration on blockchain of the recast operation).
In particular the following files were reviewed:
CoreStorage.sol
TokenInterface.sol
TokenLoggerCallback.sol
TokenInfoController.sol
TokenApprovalController.sol
TokenRecastController.sol
TokenTransferController.sol
AssetDisplay.sol
AssetExplorerInterface.sol
Controller.sol
AssetStatus.sol
MathUtils.sol
ACOOwned.sol
ACGroups.sol
ContractResolver.sol
ResolverClient.sol
AssetIndexStorage.sol
AssetInfoStorage.sol
AssetCustodianController.sol
LibraryDLL.sol
This audit uses the following terminology. Note that we only rank the likelihood, impact and
How likely a bug is to be encountered or exploited in the wild, as specified by the OWASP risk rating methodology.
The impact a bug would have if exploited, as specified by the OWASP risk rating methodology.
How serious the issue is, derived from Likelihood and Impact as specified by the OWASP risk rating methodology.
Overall, the code is clearly written, and demonstrates effective use of abstraction, separation of concerns, and modularity. Digix development team demonstrated high technical capabilities, both in the design of the architecture and in the implementation.
We found one critical issue and several additional issues that require the attention of the Digix team. Given the subjective nature of some assessments, it will be up to the Digix team to decide whether any changes should be made.
- Likelihood: high
- Impact: high
This is a critical bug that allows user to create tokens out of thin air.
We recommend blocking the option of sending tokens to self by checking if msg.sender == _to
.
Blocking can be done either by a throw
or an error return, or simply by a return
that skips the operation.
The bug is applicable for both transfer
and transferFrom
functions. Making the validation at TokenTransferController.sol
would take care of both cases.
In addition, when msg.sender == _to
, the demurrage fees are not properly collected.
- Likelihood: high
- Impact: low
User can send 0 tokens to one of the collectors addresses (Collectors::demurrage
, Collectors::recast
, Collectors::transfer
).
It will make them pay demurrage fees.
Severity is low as the demurrage collectors can later compensate themselves.
The bug is easy to fix. Especially, the transfer
and transferFrom
functions should verify that destination addresses are not Collectors::demurrage
, Collectors::recast
and Collectors::transfer
.
Given the upgradability mechanism in the code, an additional security can be obtained by ensuring the source address is not either
Collectors::demurrage
, Collectors::recast
or Collectors::transfer
.
- Likelihood: medium
- Impact: medium
The potential importance of this bug is high, but it could just be a misunderstanding from our side. We identify two issues in the implementation:
-
calculate_demurrage_time
does not calculate next payment date properly. If in the middle of the day a user performs two transactions, he will be charged for a full day fee. The core reason for the bug is_next_payment_date = subtract(add(_last_payment_date, _demurrage_seconds), _remainder);
which to our understanding should be
_next_payment_date = add(_last_payment_date, _demurrage_seconds);
-
calculate_demurrage_fee
value is too high when paid for long durations. In the current implementation the fees areamount * daily_fees * num_days
. To our understanding it should besum_{i=1}^{num_days} amount * (1-daily_fees)^{i-1}*daily_fees
.
The current implementation suggests that daily fees are the same over time (rather than exponentially reduced). Thus, the current demurrage fees for a given amount and days could be different depending on user actions. In particular, suppose the daily demurrage fees are 1% and a user has 100 tokens. With the current implementation, the honest user will have to pay 2 tokens for the fees after 2 days. However, a more rational user can exploit this demurrage fees scheme to get less fees. Specially, the rational user will choose to pay fees every day, so the total fees he pays after 2 days is 100 * 0.01 + 99*0.01 = 1.99 tokens. This could be significant over long periods. As a result, the current implementation suggests the user to strategically perform dummy operations, in order to pay lower fees.
- Likelihood: high
- Impact: medium
In file TokenTransferController.sol
, the value of _d.recipient.demurrage.fee
is never set.
A bug in line 141 set _d.sender.demurrage.fee
instead.
At the time of the audit, there were not tests for the token operations. This is a major issue since it is important to test all the code before deployment.
- Likelihood: low
- Impact: medium
The Digix team self-implemented linked list in solidity code in order to store and display existing gold bars to users. Implementing linked list from scratch is notoriously error prone.
We recommend the Digix team to implement a set (rather then list) using a simple array. The main advantage is that set is a data structure that has much easier (and thus safer) implementation. Another advantage is that the iteration over a linked list could return inconsistent results when traversing backwards. On the other hand, in an array-based implementation for the set, elements can be accessed quickly and the set returns consistent results. The main disadvantage of the set data structure is that the order of fetched elements is arbitrary, while linked list maintains a FIFO order of gold tokens.
In addition, the current implementation is not scalable, and simple append function will run out of gas if list length is in the order of 10,000. We recommend a solution for the scaling problem in Section 5.
2.2.2 Implementation of token approve
function does not mitigate known attack
- Likelihood: medium
- Impact: low
The current implementation does not mitigate the problem.
We recommend changing line 35 in TokenApprovalController
to
if (_a.pre > 0 && amount > 0 ) throw;
or alternatively, just returns false
.
We have discussed the following design details with the Digix team.
- Likelihood: unknown
- Impact: medium
Digix has confirmed that they are aware of the fact that transaction fees could be avoided by creating a token backed by Digix gold token.
Although there are always demurrage fees that Digix will charge for storage and maintenance of the physical gold bars, the tx fees could be collected by a competing platform, which offers lower or even zero fees. We understand that DGD holders (including Digix) share all the transaction fees, and only part of the demurrage fees. Hence, its bad for DGD holders if the tx fees are collected by competing platforms.
Users should be aware that fees are reduced from sent amount, and they should take it into account when using the token for payment. This behavior is non-standard in ERC20 token contracts, however is needed to support Digix business model.
The contract is fully upgradeable and as a result Digix administrators can in anytime change the behavior of its token, including increasing or decreasing user balances at will.
- Likelihood: medium
- Impact: low
A race condition between users who recast their tokens is unavoidable. As a result, the recast operation could fail even in a normal scenarios. For example, two users choose which asset (i.e. gold bar) they want to recast, and they both will likely choose the first gold bar to recast. If someone recasts first, the other will fail. It is because the asset explorer needs to iterate through all assets. This problem could be mitigated by a front end implementation that will try to present different orders of assets to different users.
History suggests that users mistakenly transfer tokens or
Ether to token contracts.
Consider having owner_transfer(address tokenAddress, amount)
function that drains token contract from tokens and Ether it holds.
In this section we report issues, notes and some improvement suggestions of lower importance.
Strings like "i:token"
should be replaced by
string constant I_TOKEN = "i:token";
Using precise string prevents compiler from catching typos. As these strings directly dictates the permission level of a contract, it would be beneficial to replace them with constants.
A better practice is to replace code like:
function TokenInterface(address _resolver)
{
resolver = _resolver;
init("i:token");
}
with a code where resolver
is initialized in the parent class constructor.
In general, it is a better practice if objects do not access the variables of their parent directly.
A more elegant way is to have uint256 counter
variable in storage and return address(counter++)
.
create(0,0,0)
creates a blockchain bloat and is more expensive in gas.
For example in:
function read_demurrage_data()
constant
returns (uint256 _collector_balance,
uint256 _base,
uint256 _rate,
address _collector)
{
_collector_balance = system.users[system.collectors.demurrage].raw_balance;
_base = system.fees.demurrage.base;
_rate = system.fees.demurrage.rate;
_collector = system.collectors.demurrage;
return (_collector_balance,
_base,
_rate,
_collector);
}
The last part, namely,
return (_collector_balance,
_base,
_rate,
_collector);
can be omitted. This can shorten some of the info functions by half and prevent potential bugs.
Using uint8
is more expensive then using uint256
, and solidity compiler had at least one bug in the past for wrong handling of non 256-bit types.
In your code you mainly use uint8
to store an enumeration value.
We recommend to change it to uint256
.
Declaring a function as public
is already done by default. There is no problem doing it, but in most files you don't explicitly define functions as public
while in some places you do.
- The name
total_supply
could be renamed tototal_token_supply
in order to be consistent withtotal_grams
.
_demurrage_collector := create(0,0,0);
_recast_collector := create(0,0,0);
_transfer_collector := create(0,0,0);
_minter := create(0,0,0);
could be replaced by
_demurrage_collector = address(0);
_recast_collector = address(1);
_transfer_collector = address(2);
_minter = address(3);
We recommend to implement a drain function.
In log_mint
: logging the event as a Transfer event is not a standard.
On the other hand, there are no standard approaches for this.
At the very least, log a dummy from
address.
Not sure why you bother to define BalanceInfoData
and Transformable
structures, as they are only used once.
- Not clear why you define an
Allowance
structure. You can just use stack variables. log_approve
should log_a.post
, rather thenamount
.- Your implementation does not mitigate the problem from https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit#
Consider logging demurrage and recast fees as transfer events
DemurrageCollectorData
andTransferCollectorData
structures are the same (not a bug, but consider defining only a single structure).- When sending tokens, fees are reduced from sent amount (rather than from sender balance). This is different behavior in comparison to sending ether or other ERC20 tokens. Though it is similar behavior to a bitcoin transaction.
d.sender.demurrage.fee
is set twice. This is a bug! second time should bed.recipient.demurrage.fee
(line 141).- Consider logging demurrage fees as transfer events
- Block sending to self. Your implementation increases the sender's balance if he transfers to himself, this is a bug. In addition demurrage is calculated wrongly when sending to self.
- Block sending to collectors. Consider blocking sending from collectors.
- We suspect
calculate_demurrage_fee
andcalculate_demurrage_time
implementations are different from what was explained to us as aforementioned. rate_of
function is never used in your code. It is hard to evaluate if it is implemented correctly.
Casting to uint8
can and should be avoided.
All info functions should be defined as constant
.
No issues were found.
No issues were found.
No issues were found.
No issues were found.
The role of locked
variable is not clear. It is read in ResolverClient.sol
file, but its role there is also not clear, as all the functions that are reading it are not in use anywhere in the code.
lines 13-14:
/// Make our own address available to us as a constant
address public CONTRACT_ADDRESS;
The variable is not a constant and its naming convention is not consistent with other variables.
Moreover, this variable is always set to this
. Hence it is not clear why it is needed
- Use of
create(0,0,0)
could be avoided. update_swap_between_collections
andupdate_vendor_put_asset
performs two operations on linked list. In both functions the second operation should be avoided if first operation failed. And first operation should be roll-backed when second operation fails.- In
read_next_in_collection
andread_previous_in_collection
you should first check that current element is valid before calling the linked list functions.
- Elements are only added to
documents
, hence a simple array could replace the linked list. - In
read_asset_previous_document
andread_asset_next_document
you should verify that current document hash is valid.
- In
put_asset_mint
: Ifcore_storage().update_asset_mint
fails thenmarketplace_storage().delete_custodian_delivery_after_accept
should not be called. Similarly, ifinfo_storage().update_mint_asset
fails then don't callindex_storage().update_custodian_mint_asset
. get_asset
should be defined asconstant
.
- We recommend to exclude this file from the project and implement set instead.
uint
,bytes32
andaddress
, all takes the same amount of memory (32 bytes). Implementing the same functionality three times (one for each data type) could be avoided by defining all of them asuint
(orbytes32
) and require the callee to cast his data touint
(bytes32
). Or have some encapsulation insideLibraryDLL.sol
code.- If called on-chain,
find
could quickly run out of gas. Consider implementfind
with a simple mapping between value and index. append
would run out of gas when callingfind
if list is too long.- In
remove
function: consider verify that_index
is valid. Currently if_index
is invalid then the entire list can be corrupted. Alternatively, make it aninternal
function that is only called byremove_item
. In addition, consider adding a sanity check thatself.count
is not already0
. remove_item
: again,find
could run out of gas if list is too long._item_index != 0
. Replace0
withNONE
.- In
valid
: you explicitly rely on overflow to happen if_index == NONE
. It is not very pretty. - In
previous_item
andnext_item
: why do you use0
instead ofNONE
? - Same comments are applicable for
address
andbytes32
implementation.