Halborn Logo

Genesis - CoreDAO


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/26/2024

Date of Engagement by: September 4th, 2022 - November 29th, 2022

Summary

98% of all REPORTED Findings have been addressed

All findings

48

Critical

0

High

1

Medium

7

Low

14

Informational

26


Table of Contents

1. INTRODUCTION

CoreDAO engaged Halborn to conduct a security audit on their smart contracts beginning on 2022-09-04 and ending on 2022-11-29. The security assessment was scoped to the smart contracts provided to the Halborn team.

2. AUDIT SUMMARY

The team at Halborn was provided eight weeks for the engagement and assigned a three full-time security engineers to audit the security of the smart contract. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.

The purpose of this audit is to:

    • Ensure that smart contract functions operate as intended.

    • Identify potential security issues with the smart contracts.

In summary, Halborn identified some security risks that were mostly addressed by the CoreDAO team.

3. TEST APPROACH & METHODOLOGY

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this audit. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the code and can quickly identify items that do not follow the security best practices. The following phases and associated tools were used during the audit:

    • Research into architecture and purpose.

    • Smart contract manual code review and walkthrough.

    • Graphing out functionality and contract logic/connectivity/functions. (solgraph)

    • Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes.

    • Manual testing by custom scripts.

    • Scanning of solidity files for vulnerabilities, security hot-spots or bugs. (MythX)

    • Static Analysis of security for scoped contract, and imported functions. (Slither)

    • Testnet deployment. (Brownie, Remix IDE)

5. RISK METHODOLOGY

Vulnerabilities or issues observed by Halborn are ranked based on the risk assessment methodology by measuring the LIKELIHOOD of a security incident and the IMPACT should an incident occur. This framework works for communicating the characteristics and impacts of technology vulnerabilities. The quantitative model ensures repeatable and accurate measurement while enabling users to see the underlying vulnerability characteristics that were used to generate the Risk scores. For every vulnerability, a risk level will be calculated on a scale of 5 to 1 with 5 being the highest likelihood or impact.
RISK SCALE - LIKELIHOOD
  • 5 - Almost certain an incident will occur.
  • 4 - High probability of an incident occurring.
  • 3 - Potential of a security incident in the long term.
  • 2 - Low probability of an incident occurring.
  • 1 - Very unlikely issue will cause an incident.
RISK SCALE - IMPACT
  • 5 - May cause devastating and unrecoverable impact or loss.
  • 4 - May cause a significant level of impact or loss.
  • 3 - May cause a partial impact or loss to many.
  • 2 - May cause temporary impact or loss.
  • 1 - May cause minimal or un-noticeable impact.
The risk level is then calculated using a sum of these two values, creating a value of 10 to 1 with 10 being the highest level of security risk.
Critical
High
Medium
Low
Informational
  • 10 - CRITICAL
  • 9 - 8 - HIGH
  • 7 - 6 - MEDIUM
  • 5 - 4 - LOW
  • 3 - 1 - VERY LOW AND INFORMATIONAL

6. SCOPE

Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

0

High

1

Medium

7

Low

14

Informational

26

Impact x Likelihood

HAL-04

HAL-01

HAL-03

HAL-06

HAL-07

HAL-09

HAL-10

HAL-11

HAL-12

HAL-14

HAL-17

HAL-19

HAL-20

HAL-22

HAL-23

HAL-02

HAL-05

HAL-08

HAL-27

HAL-32

HAL-33

HAL-34

HAL-40

HAL-45

HAL-46

HAL-13

HAL-15

HAL-16

HAL-21

HAL-18

HAL-24

HAL-26

HAL-28

HAL-29

HAL-30

HAL-31

HAL-35

HAL-37

HAL-38

HAL-39

HAL-42

HAL-43

HAL-47

HAL-25

HAL-36

HAL-41

HAL-44

HAL-48

Security analysisRisk levelRemediation Date
UNDELEGATED COINS ARE NOT CONSIDERED ON THE REWARD DISTRIBUTIONHighSolved - 11/28/2022
PROPOSAL CAN BE DEFEATED IF THERE IS NO MEMBERMediumSolved - 11/28/2022
ISCONTRACT MODIFIER CAN BE BYPASSED THROUGH CONSTRUCTORMediumSolved - 11/28/2022
BURN ADDRESS SHOULD BE DEFINED AS DIFFERENT THAN SYSTEM CONTRACTSMediumRisk Accepted
MISSING CHECK TO IF THE AGENT IS MSG.SENDER WHEN TRANSFERRING POWERMediumSolved - 11/28/2022
CANDIDATES ARE NOT LIMITED ON THE REGISTRATIONMediumSolved - 11/28/2022
MISSING ONLY INIT MODIFIERMediumSolved - 11/28/2022
DUST IS ADDED INTO THE FIRST MINERMediumSolved - 11/28/2022
EXPIRED PROPOSALS ARE NOT CHECKEDLowSolved - 11/28/2022
REQUIRE STATEMENTS ARE NOT COMPATIBLE WITH THE COMMENTSLowSolved - 11/28/2022
REWARDS DO NOT HAVE UPPER BOUNDLowSolved - 11/28/2022
REQUIRECOINDEPOSIT SHOULD BE CARRY TO OUT OF IF-CASELowRisk Accepted
MISSING ADDRESS CHECK ON THE FEEADDR AND CONSENSUSADDRLowSolved - 11/28/2022
INCORRECT EVENT IS EMITTED DURING THE REWARD CLAIMLowRisk Accepted
POTENTIAL OVERFLOW/UNDERFLOW ON THE SEVERAL LOCATIONSLowSolved - 11/28/2022
UNSAFE CASTS UINT256 TO INT256 AND INT256 TO UINT256LowSolved - 11/28/2022
CONSIDER USING CALL INSTEAD OF TRANSFER IN THE CONTRACTSLowSolved - 11/28/2022
MISSING/INCOMPLETE NATSPEC COMMENTSLowSolved - 11/28/2022
PREVIOUS HEIGHT IS NOT INITIALIZED ON THE SLASH INDICATOR CONTRACTLowRisk Accepted
MISSING REENTRANCY GUARDLowSolved - 11/28/2022
PROPOSALS WITH IDENTICAL TRANSACTIONS ARE ALLOWEDLowRisk Accepted
MISSING VALIDATOR EXISTENCE CHECKLowSolved - 11/28/2022
CHAINDID IS NOT COMPATIBLE WITH CORE CHAINInformational-
REVERT STRING SIZE OPTIMIZATIONInformationalSolved - 11/28/2022
EMPTY BLOCKS SHOULD BE REMOVED OR EMIT SOMETHINGInformationalSolved - 11/28/2022
CALLDATA IS CHEAPER THAN MEMORYInformationalSolved - 11/28/2022
FLOATING PRAGMA IS SETInformationalSolved - 11/28/2022
OPTIMIZE UNSIGNED INTEGER COMPARISONInformationalSolved - 11/28/2022
CACHE ARRAY LENGTHInformationalSolved - 11/28/2022
NO NEED TO INITIALIZE VARIABLES WITH DEFAULT VALUESInformationalAcknowledged
INCREMENT/DECREMENT FOR LOOP VARIABLE IN AN UNCHECKED BLOCKInformationalSolved - 11/28/2022
DEPRECATED IMPLEMENTATION USEDInformationalSolved - 11/28/2022
ROUNDING LOSS ARE NOT CONSIDERED ON THE REWARD CALCULATIONInformationalAcknowledged
OPEN TO-DOSInformationalSolved - 11/28/2022
DELETE - ABI CODER V2 FOR GAS OPTIMIZATIONInformationalSolved - 11/28/2022
SAFEMATH IS IMPORTED BUT NOT USEDInformationalSolved - 11/28/2022
UNUSED IMPORT FILESInformationalSolved - 11/28/2022
DECLARATION NAMING CONVENTIONInformationalSolved - 11/28/2022
EVENT IS MISSING INDEXED FIELDSInformationalSolved - 11/28/2022
MISSING ABSTAIN OPTION ON THE PROPOSALSInformationalAcknowledged
INCREASE OPTIMIZER RUNSInformationalSolved - 11/28/2022
USE CUSTOM ERRORS INSTEAD OF REVERT STRINGS TO SAVE GASInformationalSolved - 11/28/2022
REDUCE BOOLEAN COMPARISONInformationalSolved - 11/28/2022
UPGRADE PRAGMA TO AT LEAST 0.8.4InformationalSolved - 11/28/2022
REWARDS CANNOT BE DISTRIBUTED IF THERE IS NO ENOUGH BALANCE ON THE CONTRACTInformationalAcknowledged
BINDING HASH IS NOT IMPLEMENTED ON THE CODE BASEInformationalAcknowledged
NO NEED TO RETURN VARIABLE NAME ON THE VIEWSInformationalAcknowledged
ROUND LIMIT CAN BE DEFINED AS GOVERNANCE PARAMETERInformationalAcknowledged

8. Findings & Tech Details

8.1 UNDELEGATED COINS ARE NOT CONSIDERED ON THE REWARD DISTRIBUTION

// High

Description

The distributePowerReward function is used to distribute delegated hash power rewards to the candidate validator. The method is designed to be called at the beginning of the turn round workflow. One potential issue with the contract is that undelegated coin rewards are not considered. This can lead to inaccurate or incomplete calculations and potentially cause issues with rewards distribution. In order to address this issue, the contract should be updated to consider undelegated coin rewards and incorporate them into the calculations for reward distribution. This will help ensure the accuracy and reliability of the reward distribution process and can help prevent potential issues or errors. With the workflow, remaining rewards directly compared to the miner's total rewards.

Code Location

/contracts/PledgeAgent.sol#L204

PledgeAgent.sol

  function distributePowerReward(address candidate, address[] calldata miners) external override onlyCandidate {
    // if no hash power is delegated in the round, return
    RoundState storage rs = stateMap[roundTag];
    if (rs.power == 1) {
      return;
    }
    // distribute rewards to every miner
    // note that the miners are represented in the form of reward addresses
    // and they can be duplicated because everytime a miner delegates a BTC block
    // to a validator on Core blockchain, a new record is added in BTCLightClient
    Agent storage a = agentsMap[candidate];
    uint256 l = a.rewardSet.length;
    if (l == 0) {
      return;
    }
    Reward storage r = a.rewardSet[l-1];
    if (r.totalReward == 0 || r.round != roundTag) {
      return;
    }
    uint256 reward = rs.coin * POWER_BLOCK_FACTOR * rs.powerFactor / 10000 * r.totalReward / r.score;
    uint256 minerSize = miners.length;

    uint256 totalReward = reward * minerSize;
    uint256 remainReward = r.remainReward;
    require(remainReward >= totalReward, "there is not enough reward");

    for (uint256 i = 0; i < minerSize; i++) {
      rewardMap[miners[i]] += reward;
    }

    if (r.coin == 0) {
      delete a.rewardSet[l-1];
      if (minerSize == 0) {
        payable(FOUNDATION_ADDR).transfer(remainReward);
      } else {
        uint256 dust = remainReward - totalReward;
        if (dust != 0) {
          rewardMap[miners[0]] += dust;
        }
      }
    } else if (totalReward != 0) {
      r.remainReward -= totalReward;
    }
  }

    def test_delegate2validator(self, pledge_agent, candidate_hub, validator_set):
        operator = accounts[1]
        consensus_address = register_candidate(operator=operator)
        candidate_hub.turnRound()
        assert validator_set.isValidator(consensus_address)
        tx = pledge_agent.delegateCoin(operator, {"value": MIN_INIT_DELEGATE_VALUE})
        expect_event(tx, "delegatedCoin", {
            "agent": operator,
            "delegator": accounts[0],
            "amount": MIN_INIT_DELEGATE_VALUE,
            "totalAmount": MIN_INIT_DELEGATE_VALUE
        })
Score
Impact: 5
Likelihood: 3
Recommendation

SOLVED: The CoreDAO team solved the issue in this commit d2dfe11 by changing the flow in the distributePowerReward function.

8.2 PROPOSAL CAN BE DEFEATED IF THERE IS NO MEMBER

// Medium

Description

GovHub is the governance contract for CoreDAO, a contract that allows any member to submit proposals with the initiator address stored in the proposer field. On the GovHub contract, the proposal is marked as DEFEATED If forVotes is smaller than totalVotes/2. However, if all the members are deleted during the genesis with governance, all proposals can be defeated in the system. The system should be ensured that member's length have a minimum threshold.

Code Location

/contracts/GovHub.sol#L139

GovHub.sol

  function removeMember(address member) external onlyInit onlyGov {
    uint256 index = members[member];
    require(index > 0, "member does not exist");
    if (index != memberSet.length) {
      address addr = memberSet[memberSet.length - 1];
      memberSet[index - 1] = addr;
      members[addr] = index;
    }
    memberSet.pop();
    delete members[member];
    emit MemberDeleted(member);
  }
    address public user1 = 0x9fB29AAc15b9A4B7F17c3385939b007540f4d791;
    address public user2 = 0x96C42C56fdb78294F96B0cFa33c92bed7D75F96a;

    function testMinimumThresholdOnTheRemoveMember() public {
        vm.startPrank(govHubAddress);
        gov_hub.removeMember(user1);
        gov_hub.removeMember(user2);
        gov_hub.members;
        vm.stopPrank();
    }
poc1.jpg
Score
Impact: 3
Likelihood: 3
Recommendation

SOLVED: The CoreDAO team solved the issue in commit f7a91316 by adding the minimum requirement.

8.3 ISCONTRACT MODIFIER CAN BE BYPASSED THROUGH CONSTRUCTOR

// Medium

Description

The Validator can register as a Core validator candidate by calling register in CandidateHub contract. However, feeAddress and consensus address are checked with isContract modifier. the isContract function that uses EXTCODESIZE was discovered to be bypassable. The function will return false if it is invoked from a contract's constructor. Because the contract has not been deployed yet.

Code Location

/contracts/CandidateHub.sol#L221

CandidateHub.sol

  function register(address consensusAddr, address payable feeAddr, uint32 commissionThousandths)
    external payable
    onlyInit
  {
    require(operateMap[msg.sender] == 0, "candidate already exists");
    require(int256(msg.value) >= requiredMargin, "deposit is not enough");
    require(commissionThousandths > 0 && commissionThousandths < 1000, "commissionThousandths should in range (0, 1000)");
    require(consensusMap[consensusAddr] == 0, "consensus already exists");
    require(!isContract(consensusAddr), "contract is not allowed to be consensus address");
    require(!isContract(feeAddr), "contract is not allowed to be fee address");
    // check jail status
    require(jailMap[msg.sender] < roundTag, "it is in jail");

    uint status = SET_CANDIDATE;
    candidateSet.push(Candidate(msg.sender, consensusAddr, feeAddr, commissionThousandths, int256(msg.value), status, roundTag, commissionThousandths));
    uint256 index = candidateSet.length;
    operateMap[msg.sender] = index;
    consensusMap[consensusAddr] = index;

    emit registered(msg.sender, consensusAddr, feeAddr, commissionThousandths, int256(msg.value));
  }
contract Hack {
    bool public isContract;
    address public addr;

    // When contract is being created, code size (extcodesize) is 0.
    // This will bypass the isContract() check
    constructor(address _target) {
        isContract = Target(_target).isContract(address(this));
        addr = address(this);
        // This will work
        Target(_target).protected();
    }
}
Score
Impact: 4
Likelihood: 2
Recommendation

SOLVED: The CoreDAO team solved the issue in this commit b51c87f8 by deleting the isContract check.

8.4 BURN ADDRESS SHOULD BE DEFINED AS DIFFERENT THAN SYSTEM CONTRACTS

// Medium

Description

The CoreDAO chain implements a number of dedicated built-in system contracts. Unlike smart contracts deployed by blockchain users, built-in system contracts were deployed at genesis time. The system contracts are increasing one by one. Instead of using the burn address as an address, the address can be defined different from the system contract addresses.

Code Location

contracts/System.sol#L20

System.sol

  address public constant VALIDATOR_CONTRACT_ADDR = 0x0000000000000000000000000000000000001000;
  address public constant SLASH_CONTRACT_ADDR = 0x0000000000000000000000000000000000001001;
  address public constant SYSTEM_REWARD_ADDR = 0x0000000000000000000000000000000000001002;
  address public constant LIGHT_CLIENT_ADDR = 0x0000000000000000000000000000000000001003;
  address public constant RELAYER_HUB_ADDR = 0x0000000000000000000000000000000000001004;
  address public constant CANDIDATE_HUB_ADDR = 0x0000000000000000000000000000000000001005;
  address public constant GOV_HUB_ADDR = 0x0000000000000000000000000000000000001006;
  address public constant PLEDGE_AGENT_ADDR = 0x0000000000000000000000000000000000001007;
  address public constant BURN_ADDR = 0x0000000000000000000000000000000000001008;
  address public constant FOUNDATION_ADDR = 0x0000000000000000000000000000000000001009;
  • System Burn contract should not be updated during the upgrades.
  • All burned funds will be located on the Burn address.
  • In the BSC, BurnContract address is deleted from the constant contract addresses.
Score
Impact: 5
Likelihood: 1
Recommendation

RISK ACCEPTED: After some internal discussion, the CoreDAO team has decided to maintain the current implementation. Both the BSC implementation and the Core implementation require software updates to change the way in which the burn address works.

8.5 MISSING CHECK TO IF THE AGENT IS MSG.SENDER WHEN TRANSFERRING POWER

// Medium

Description

Delegators can call transferPower in PledgeAgent to transfer BTC hash power delegate to a different validator. The transferPower method is a simpler version of delegateHashPower since the delegator has already proved to be a valid BTC miner when calling delegateHashPower previously. During the code review, It has been noticed that when power is transferred, agent is set to target. However, the function can be called by multiple time because of there is no check If the m.agent is msg.sender.

Code Location

/contracts/PledgeAgent.sol#L296

  function transferPower(address targetAgent) external {
    require(ICandidateHub(CANDIDATE_HUB_ADDR).canDelegate(targetAgent), "agent is inactivated");
    BtcDelegator storage m = btcDelegatorsMap[msg.sender];
    require(m.agent != address(0x00), "delegator does not exist");
    address sourceAgent = m.agent;
    require(sourceAgent!=targetAgent, "source agent and target agent are the same one");
    m.agent = targetAgent;
    m.power = 0;
    emit transferredPower(sourceAgent, targetAgent, msg.sender);
  }

  1. transferPower differs from transferCoin which clearly combines undelegate and delegate steps. A BTC miner can only delegate hash power to one validator, and the rewards in each round are automatically distributed to them when the system executes a turn round. As a result, transferPower is actually similar to delegateHashPower instead of transferCoin.
  2. However, when the power is transferred to targetAgent, there is no check If source agent power is zero.
  3. Even if power is zero, the delegator can call a function multiple times.
Score
Impact: 3
Likelihood: 3
Recommendation

SOLVED: According to the Satoshi Plus consensus algorithm, BTC hash power is measured using blocks produced on the BTC network. In the new design, mining pools no longer need to use their private keys to sign transactions on the Core blockchain. Instead, they can include delegate information directly in the blocks they generate on the BTC network. Calculations for distributing rewards to hash delegators are now performed in a dedicated function, which is called directly by the CandidateHub.turnRound method. Additionally, rewards are no longer automatically sent to hash delegators, but must be claimed in the new workflow. For that reason, the CoreDAO team has solved the issue thanks to the new improved design. As a result, the function was deleted due to the new design.

8.6 CANDIDATES ARE NOT LIMITED ON THE REGISTRATION

// Medium

Description

One can register as a Core validator candidate by calling register in CandidateHub contract. The calling address will be used as the operator address, which represents the validator identity. However, there is no max number of candidates is defined by the system. In the CandidateHub.turnRound function calls distributeReward in ValidatorSet to distribute rewards to all roles, expect BTC hash delegators. If the candidateSet is large enough, turnRound for loops in this contract runs over an array, which can be artificially inflated If there are many registrations. An attacker can create many candidates, making the candidate Set array large. In principle, this can be done such that the gas required to execute the for loop exceeds the block gas limit, essentially making the turnRound() function inoperable.

Code Location

contracts/CandidateHub.sol#L213

  function register(address consensusAddr, address payable feeAddr, uint32 commissionThousandths)
    external payable
    onlyInit
  {
    require(operateMap[msg.sender] == 0, "candidate already exists");
    require(int256(msg.value) >= requiredMargin, "deposit is not enough");
    require(commissionThousandths > 0 && commissionThousandths < 1000, "commissionThousandths should in range (0, 1000)");
    require(consensusMap[consensusAddr] == 0, "consensus already exists");
    require(!isContract(consensusAddr), "contract is not allowed to be consensus address");
    require(!isContract(feeAddr), "contract is not allowed to be fee address");
    // check jail status
    require(jailMap[msg.sender] < roundTag, "it is in jail");

    uint status = SET_CANDIDATE;
    candidateSet.push(Candidate(msg.sender, consensusAddr, feeAddr, commissionThousandths, int256(msg.value), status, roundTag, commissionThousandths));
    uint256 index = candidateSet.length;
    operateMap[msg.sender] = index;
    consensusMap[consensusAddr] = index;

    emit registered(msg.sender, consensusAddr, feeAddr, commissionThousandths, int256(msg.value));
  }
    function testRegister() public {
        for (uint i=0; i<addrSet.length; i++){
        address newCandidate = addrSet[i];
        vm.startPrank(newCandidate, newCandidate);
        candidateHub.register{value: 10000 ether}(newCandidate, payable(newCandidate), 10);
        vm.stopPrank();
        }
    }
poc2.jpg
Score
Impact: 4
Likelihood: 2
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 6d456cf2 by adding CANDIDATE_COUNT_LIMIT.

8.7 MISSING ONLY INIT MODIFIER

// Medium

Description

The onlyInit modifier throws an error If the source contract is not initialized. In the SystemReward contract, receiveRewards function is missing onlyInit modifier. If the function is called without initialization, all address balance will be burned in the contract.

Code Location

/contracts/SystemReward.sol#L40

  function receiveRewards() external payable override {
    if (msg.value > 0) {
      if (address(this).balance > incentiveBalanceCap) {
        IBurn(BURN_ADDR).burn{value:address(this).balance - incentiveBalanceCap}();
      }
      emit receiveDeposit(msg.sender, msg.value);
    }
  }

/contracts/BtcLightClient.sol#L200

  function claimRelayerReward(address relayerAddr) external {
     uint256 reward = relayerRewardVault[relayerAddr];
     require(reward != 0, "no relayer reward");
     relayerRewardVault[relayerAddr] = 0;
     address payable recipient = payable(relayerAddr);
     ISystemReward(SYSTEM_REWARD_ADDR).claimRewards(recipient, reward);
  }
    function testReceiveRewardsMissingModifier() public {
        for (uint i=0; i<addrSet.length; i++){
        address newCandidate = addrSet[i];
        vm.startPrank(newCandidate, newCandidate);
        systemReward.receiveRewards{value: 10000 ether}();
        vm.stopPrank();
        }
    }
Score
Impact: 4
Likelihood: 2
Recommendation

SOLVED: The CoreDAO team solved the issue in the following commits b283001 and 9e290d8 by adding the onlyInit modifier.

8.8 DUST IS ADDED INTO THE FIRST MINER

// Medium

Description

distributePowerReward function is used to distribute rewards for delegated hash power on the candidate validator. The method is designed to be called at the beginning of the turn round workflow. However, the remaining dust is always added to the first miner of the system.

Code Location

/contracts/PledgeAgent.sol#L240

  function distributePowerReward(address candidate, address[] calldata miners) external override onlyCandidate {
    // if no hash power is delegated in the round, return
    RoundState storage rs = stateMap[roundTag];
    if (rs.power == 1) {
      return;
    }
    // distribute rewards to every miner
    // note that the miners are represented in the form of reward addresses
    // and they can be duplicated because everytime a miner delegates a BTC block
    // to a validator on Core blockchain, a new record is added in BTCLightClient
    Agent storage a = agentsMap[candidate];
    uint256 l = a.rewardSet.length;
    if (l == 0) {
      return;
    }
    Reward storage r = a.rewardSet[l-1];
    if (r.totalReward == 0 || r.round != roundTag) {
      return;
    }
    uint256 reward = rs.coin * POWER_BLOCK_FACTOR * rs.powerFactor / 10000 * r.totalReward / r.score;
    uint256 minerSize = miners.length;

    uint256 totalReward = reward * minerSize;
    uint256 remainReward = r.remainReward;
    require(remainReward >= totalReward, "there is not enough reward");

    for (uint256 i = 0; i < minerSize; i++) {
      rewardMap[miners[i]] += reward;
    }

    if (r.coin == 0) {
      delete a.rewardSet[l-1];
      if (minerSize == 0) {
        payable(FOUNDATION_ADDR).transfer(remainReward);
      } else {
        uint256 dust = remainReward - totalReward;
        if (dust != 0) {
          rewardMap[miners[0]] += dust;
        }
      }
    } else if (totalReward != 0) {
      r.remainReward -= totalReward;
    }
  }
    def test_undelegate_with_reward(self, pledge_agent):
        operator = accounts[1]
        consensus = register_candidate(operator=operator)
        pledge_agent.delegateCoin(operator, {"value": MIN_INIT_DELEGATE_VALUE + 1e19})
        turn_round([consensus])

        pledge_agent.undelegateCoin(operator)
Score
Impact: 3
Likelihood: 3
Recommendation

SOLVED: The CoreDAO team solved the issue in commit d2dfe1155f by sending dust to the system reward contract.

8.9 EXPIRED PROPOSALS ARE NOT CHECKED

// Low

Description

GovHub is the governance contract for CoreDAO, a contract that allows any member to submit proposals with the initiator address stored in the proposer field. In the implementation, expired proposals are not check on the state function. Expired proposals still can be executed by the governance.

Code Location

/contracts/GovHub.sol#L208

  function state(uint256 proposalId) public view returns (ProposalState) {
    require(proposalCount >= proposalId && proposalId > 0, "state: invalid proposal id");
    Proposal storage proposal = proposals[proposalId];
    if (proposal.canceled) {
      return ProposalState.Canceled;
    } else if (block.number <= proposal.startBlock) {
      return ProposalState.Pending;
    } else if (block.number <= proposal.endBlock) {
      return ProposalState.Active;
    } else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes <= proposal.totalVotes / 2) {
      return ProposalState.Defeated;
    } else if (proposal.executed) {
      return ProposalState.Executed;
    } else {
      return ProposalState.Succeeded;
    }
  }
Score
Impact: 3
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 82e9fad19c7 by adding the expired option in proposals.

8.10 REQUIRE STATEMENTS ARE NOT COMPATIBLE WITH THE COMMENTS

// Low

Description

In the two require statements, Comment mentioned as opposite version of implementation. Ensure that require statements are compatible with the implementation.

Code Location

BtcLightClient.sol#L477

      require(value.length == 32, "length of callerCompensationMolecule mismatch");
      uint256 newCallerCompensationMolecule = BytesToTypes.bytesToUint256(32, value);
      require(newCallerCompensationMolecule <= 10000, "new callerCompensationMolecule shouldn't be in range [0,10000]");

/contracts/BtcLightClient.sol#L482

BtcLightClient.sol

      require(value.length == 32, "length of roundSize mismatch");
      uint256 newRoundSize = BytesToTypes.bytesToUint256(32, value);
      require(newRoundSize >= maximumWeight, "new newRoundSize shouldn't be greater than maximumWeight");
Score
Impact: 3
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in the following commits dbc50679e and e88fe73 by editing the comments.

8.11 REWARDS DO NOT HAVE UPPER BOUND

// Low

Description

During the code review, It has been noticed that claimRewards function do not have upper-bound on the reward parameter. A possible systemic vulnerability can be resulted with stolen rewards of contract balance. On the remediation, Reward can have max bound like a BSC.

Code Location

/contracts/SystemReward.sol#L56

  function claimRewards(address payable to, uint256 amount)
    external
    override(ISystemReward)
    onlyInit
    onlyOperator
    returns (uint256)
  {
    uint256 actualAmount = amount < address(this).balance ? amount : address(this).balance;
    if (actualAmount > 0) {
      to.transfer(actualAmount);
      emit rewardTo(to, actualAmount);
    } else {
      emit rewardEmpty();
    }
    return actualAmount;
  }

Score
Impact: 3
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in the following commits e037eb33 and de91c9c by adding upper bounds to the reward.

8.12 REQUIRECOINDEPOSIT SHOULD BE CARRY TO OUT OF IF-CASE

// Low

Description

delegateCoin function has been used for delegating coin to active agents. However, requiredCoinDeposit is only checked when the newDeposit equals to deposit. This implementation accepts that newDeposit will be zero and for the first time deposit will be equal to newDeposit. If this check fails, a user can deposit lower than requiredCoinDeposit. The system should ensure that deposits are more than requiredCoinDeposit.

Code Location

PledgeAgent.sol#L353

  function delegateCoin(
    address agent,
    address payable delegator,
    uint256 deposit
  ) internal returns (uint256) {
    Agent storage a = agentsMap[agent];
    uint256 newDeposit = a.cDelegatorMap[delegator].newDeposit + deposit;
    if (newDeposit == deposit) {
      require(deposit >= requiredCoinDeposit, "deposit is too small");
      uint256 rewardIndex = a.rewardSet.length;
      a.cDelegatorMap[delegator] = CoinDelegator(0, deposit, roundTag, rewardIndex);
    } else {
      require(deposit != 0, "deposit value is zero");
      CoinDelegator storage d = a.cDelegatorMap[delegator];
      (uint256 rewardAmount, uint dust) = collectCoinReward(a, d, 0x7FFFFFFF);
      distributeReward(delegator, rewardAmount, dust);
      if (d.changeRound < roundTag) {
        d.deposit = d.newDeposit;
        d.changeRound = roundTag;
      }
      d.newDeposit = newDeposit;
    }
    a.totalDeposit += deposit;
    return newDeposit;
  }
Score
Impact: 3
Likelihood: 1
Recommendation

RISK ACCEPTED: The CoreDAO team accepted the risk of the issue. The team claims that this is by design that they allow an arbitrary value after the first deposit.

8.13 MISSING ADDRESS CHECK ON THE FEEADDR AND CONSENSUSADDR

// Low

Description

Both of the variables are missing address check. Every address should be validated and checked that it is different from zero.

Code Location

CandidateHub.sol#L213

  function register(address consensusAddr, address payable feeAddr, uint32 commissionThousandths)
    external payable
    onlyInit
  {
    require(operateMap[msg.sender] == 0, "candidate already exists");
    require(int256(msg.value) >= requiredMargin, "deposit is not enough");
    require(commissionThousandths > 0 && commissionThousandths < 1000, "commissionThousandths should in range (0, 1000)");
    require(consensusMap[consensusAddr] == 0, "consensus already exists");
    require(!isContract(consensusAddr), "contract is not allowed to be consensus address");
    require(!isContract(feeAddr), "contract is not allowed to be fee address");
    // check jail status
    require(jailMap[msg.sender] < roundTag, "it is in jail");

    uint status = SET_CANDIDATE;
    candidateSet.push(Candidate(msg.sender, consensusAddr, feeAddr, commissionThousandths, int256(msg.value), status, roundTag, commissionThousandths));
    uint256 index = candidateSet.length;
    operateMap[msg.sender] = index;
    consensusMap[consensusAddr] = index;

    emit registered(msg.sender, consensusAddr, feeAddr, commissionThousandths, int256(msg.value));
  }
Score
Impact: 2
Likelihood: 2
Recommendation

SOLVED: The CoreDAO team solved the issue in commit b51c87f8 by adding validation on addresses.

8.14 INCORRECT EVENT IS EMITTED DURING THE REWARD CLAIM

// Low

Description

When rewards claimed by delegation, depends on the dust limit, rewards are calculated with dust. However, If the delegator transfer is failed, an event is emitted with 0. All reward and dust is sent GovHub contract. The event can be emitted when the transfer is succeeded.

Code Location

PledgeAgent.sol#L344

  function distributeReward(address payable delegator, uint256 reward, uint256 dust) internal {
    if (reward > 0) {
      if (dust <= DUST_LIMIT) {
        reward += dust;
        dust = 0;
      } else {
        reward += DUST_LIMIT;
        dust -= DUST_LIMIT;
      }
      bool success = delegator.send(reward);
      emit claimedReward(delegator, msg.sender, reward, success);
      if (!success) {
        totalDust += reward + dust;
      } else if (dust > 0) {
        totalDust += dust;
      }
    }
  }
Score
Impact: 3
Likelihood: 1
Recommendation

RISK ACCEPTED: The CoreDAO team accepted the risk of the issue. The team claims that they will always emit an event, regardless of success or failure.

8.15 POTENTIAL OVERFLOW/UNDERFLOW ON THE SEVERAL LOCATIONS

// Low

Description

Integer overflow and underflow might happen in integer operations if the Solidity version is lower than 0.8.0.

Code Location

ValidatorSet.sol#L84

ValidatorSet.sol#L91

  function deposit(address valAddr) external payable onlyCoinbase onlyInit onlyZeroGasPrice {
    uint256 value = msg.value;
    if (address(this).balance >= totalInCome + value + blockReward) {
      value += blockReward;
    }
    uint256 index = currentValidatorSetMap[valAddr];
    if (index > 0) {
      Validator storage validator = currentValidatorSet[index - 1];
      totalInCome = totalInCome + value;
      validator.income = validator.income + value;
      emit validatorDeposit(valAddr, value);
    } else {
      emit deprecatedDeposit(valAddr, value);
    }
  }

/contracts/ValidatorSet.sol#L124

  function distributeReward() external override onlyCandidate {
    address payable feeAddress;
    uint256 validatorReward;

    uint256 incentiveSum = 0;
    for (uint256 i = 0; i < currentValidatorSet.length; i++) {
      Validator storage v = currentValidatorSet[i];
      uint256 incentiveValue = (v.income * blockRewardIncentivePercent) / 100;
      incentiveSum += incentiveValue;
      v.income -= incentiveValue;
    }
    ISystemReward(SYSTEM_REWARD_ADDR).receiveRewards{ value: incentiveSum }();

    address[] memory operateAddressList = new address[](currentValidatorSet.length);
    uint256[] memory rewardList = new uint256[](currentValidatorSet.length);
    uint256 rewardSum = 0;
    for (uint256 i = 0; i < currentValidatorSet.length; i++) {
      Validator storage v = currentValidatorSet[i];
      operateAddressList[i] = v.operateAddress;
      if (v.income > 0) {
        feeAddress = v.feeAddress;
        validatorReward = (v.income * v.commissionThousandths) / 1000;
        if (v.income > validatorReward) {
          rewardList[i] = v.income - validatorReward;
          rewardSum += rewardList[i];
        }

        bool success = feeAddress.send(validatorReward);
        if (success) {
          emit directTransfer(v.operateAddress, feeAddress, validatorReward, v.income);
        } else {
          emit directTransferFail(v.operateAddress, feeAddress, validatorReward, v.income);
        }
        v.income = 0;
      }
    }

    IPledgeAgent(PLEDGE_AGENT_ADDR).addRoundReward{ value: rewardSum }(operateAddressList, rewardList);
    totalInCome = 0;
  } 
Score
Impact: 2
Likelihood: 2
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 9b147c3 by updating pragma to 0.8.4.

8.16 UNSAFE CASTS UINT256 TO INT256 AND INT256 TO UINT256

// Low

Description

Solidity will not perform automatic checks when down casting, and it's possible for some fields to overflow while adding tiers.

Code Location

CandidateHub.sol#L218

CandidateHub.sol#L227

  function register(address consensusAddr, address payable feeAddr, uint32 commissionThousandths)
    external payable
    onlyInit
  {
    require(operateMap[msg.sender] == 0, "candidate already exists");
    require(int256(msg.value) >= requiredMargin, "deposit is not enough");
    require(commissionThousandths > 0 && commissionThousandths < 1000, "commissionThousandths should in range (0, 1000)");
    require(consensusMap[consensusAddr] == 0, "consensus already exists");
    require(!isContract(consensusAddr), "contract is not allowed to be consensus address");
    require(!isContract(feeAddr), "contract is not allowed to be fee address");
    // check jail status
    require(jailMap[msg.sender] < roundTag, "it is in jail");

    uint status = SET_CANDIDATE;
    candidateSet.push(Candidate(msg.sender, consensusAddr, feeAddr, commissionThousandths, int256(msg.value), status, roundTag, commissionThousandths));
    uint256 index = candidateSet.length;
    operateMap[msg.sender] = index;
    consensusMap[consensusAddr] = index;

    emit registered(msg.sender, consensusAddr, feeAddr, commissionThousandths, int256(msg.value));
  }
Score
Impact: 2
Likelihood: 2
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 6d456cf by changing the integer type.

8.17 CONSIDER USING CALL INSTEAD OF TRANSFER IN THE CONTRACTS

// Low

Description

The transfer function is not recommended for sending ETH due to its, 2300 gas unit limit. Instead, call can be used to circumvent the gas limit.

Code Location

  Burn.sol::32 => msg.sender.transfer(remain);
  CandidateHub.sol::253 => if (value > 0)  msg.sender.transfer(value);
  CandidateHub.sol::254 => systemPayable.transfer(uint256(dues));
  PledgeAgent.sol::274 => msg.sender.transfer(deposit);
  PledgeAgent.sol::497 => govhub.transfer(totalDust);
  RelayerHub.sol::65 => msg.sender.transfer(r.deposit.sub(r.dues));
  RelayerHub.sol::67 => systemPayable.transfer(r.dues);
  SystemReward.sol::58 => to.transfer(actualAmount);
Score
Impact: 3
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 06da9d3 by changing transfer with the call. They replaced transfer/send with call on PledgeAgent.sol when users claimed their rewards.

8.18 MISSING/INCOMPLETE NATSPEC COMMENTS

// Low

Description

The functions are missing @param for some of their parameters. Given that NatSpec is an important part of code documentation, this affects code comprehension, auditability, and usability.

Code Location

Contracts

Score
Impact: 3
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in the following commits 2292855 & 9eb8d1 by adding natspec in the functions.

8.19 PREVIOUS HEIGHT IS NOT INITIALIZED ON THE SLASH INDICATOR CONTRACT

// Low

Description

CandidateHub.turnRound function calls clean in the SlashIndicator. The function has a modifier named as oncePerBlock. The modifier checks If the block.height is bigger than previousHeight. But, the previousHeight is not set in the init function. When the turnRound function is called firstly by the CandidateHub contract, previousHeight always will be 0.

Code Location

SlashIndicator.sol#L28

  modifier oncePerBlock() {
    require(block.number > previousHeight, "can not slash twice in one block");
    _;
    previousHeight = block.number;
  }
Score
Impact: 3
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team claims this is by design, they are good with the default value 0.

8.20 MISSING REENTRANCY GUARD

// Low

Description

To protect against cross-function re-entrancy attacks, it may be necessary to use a mutex. By using this lock, an attacker can no longer exploit the withdrawal function with a recursive call. OpenZeppelin has its own mutex implementation called ReentrancyGuard which provides a modifier to any function called nonReentrant that guards the function with a mutex against re-entrancy attacks.

Code Location

/contracts/ValidatorSet.sol#L127

  function distributeReward() external override onlyCandidate {
    address payable feeAddress;
    uint256 validatorReward;

    uint256 incentiveSum = 0;
    for (uint256 i = 0; i < currentValidatorSet.length; i++) {
      Validator storage v = currentValidatorSet[i];
      uint256 incentiveValue = (v.income * blockRewardIncentivePercent) / 100;
      incentiveSum += incentiveValue;
      v.income -= incentiveValue;
    }
    ISystemReward(SYSTEM_REWARD_ADDR).receiveRewards{ value: incentiveSum }();

    address[] memory operateAddressList = new address[](currentValidatorSet.length);
    uint256[] memory rewardList = new uint256[](currentValidatorSet.length);
    uint256 rewardSum = 0;
    for (uint256 i = 0; i < currentValidatorSet.length; i++) {
      Validator storage v = currentValidatorSet[i];
      operateAddressList[i] = v.operateAddress;
      if (v.income > 0) {
        feeAddress = v.feeAddress;
        validatorReward = (v.income * v.commissionThousandths) / 1000;
        if (v.income > validatorReward) {
          rewardList[i] = v.income - validatorReward;
          rewardSum += rewardList[i];
        }

        bool success = feeAddress.send(validatorReward);
        if (success) {
          emit directTransfer(v.operateAddress, feeAddress, validatorReward, v.income);
        } else {
          emit directTransferFail(v.operateAddress, feeAddress, validatorReward, v.income);
        }
        v.income = 0;
      }
    }

    IPledgeAgent(PLEDGE_AGENT_ADDR).addRoundReward{ value: rewardSum }(operateAddressList, rewardList);
    totalInCome = 0;
  } 

/contracts/RelayerHub.sol#L67

  function  unregister() external exist onlyInit{
    Relayer memory r = relayers[msg.sender];
    msg.sender.transfer(r.deposit.sub(r.dues));
    address payable systemPayable = address(uint160(SYSTEM_REWARD_ADDR));
    systemPayable.transfer(r.dues);
    delete relayersExistMap[msg.sender];  
    delete relayers[msg.sender];
    emit relayerUnRegister(msg.sender);
  }
Score
Impact: 2
Likelihood: 2
Recommendation

SOLVED: The CoreDAO team solved the issue in the following commits b31380 & 064004 by adding the check effect interaction pattern.

8.21 PROPOSALS WITH IDENTICAL TRANSACTIONS ARE ALLOWED

// Low

Description

Proposals that contain identical transactions (target, value, signature, data values) can be proposed. The GovHub contract allow duplicate transactions.

Code Location

GovHub.sol#L97

  function propose(
    address[] memory targets,
    uint256[] memory values,
    string[] memory signatures,
    bytes[] memory calldatas,
    string memory description
  ) public onlyInit onlyMember returns (uint256) {
    require(
      targets.length == values.length && targets.length == signatures.length && targets.length == calldatas.length,
      "proposal function information arity mismatch"
    );
    require(targets.length != 0, "must provide actions");
    require(targets.length <= proposalMaxOperations, "too many actions");

    uint256 latestProposalId = latestProposalIds[msg.sender];
    if (latestProposalId != 0) {
      ProposalState proposersLatestProposalState = state(latestProposalId);
      require(
        proposersLatestProposalState != ProposalState.Active,
        "one live proposal per proposer, found an already active proposal"
      );
      require(
        proposersLatestProposalState != ProposalState.Pending,
        "one live proposal per proposer, found an already pending proposal"
      );
    }

    uint256 startBlock = block.number + 1;
    uint256 endBlock = startBlock + votingPeriod;

    proposalCount++;
    Proposal memory newProposal = Proposal({
      id: proposalCount,
      proposer: msg.sender,
      targets: targets,
      values: values,
      signatures: signatures,
      calldatas: calldatas,
      startBlock: startBlock,
      endBlock: endBlock,
      forVotes: 0,
      againstVotes: 0,
      totalVotes: memberSet.length,
      canceled: false,
      executed: false
    });

    proposals[newProposal.id] = newProposal;
    latestProposalIds[newProposal.proposer] = newProposal.id;

    emit ProposalCreated(
      newProposal.id,
      msg.sender,
      targets,
      values,
      signatures,
      calldatas,
      startBlock,
      endBlock,
      memberSet.length,
      description
    );
    return newProposal.id;
  }
Score
Impact: 3
Likelihood: 1
Recommendation

RISK ACCEPTED: The CoreDAO team accepted the risk of the issue. By being open to any stakeholder and using proposals, the governance process is more decentralized.

8.22 MISSING VALIDATOR EXISTENCE CHECK

// Low

Description

SlashIndicator.slash function updates the slash count for the misbehaving validator. If this validator meets the felony threshold, SlashIndicator.slash calls the felony method in ValidatorSet to forfeit rewards of the validator in the round and distribute them to other honest validators. And the validator is kicked out of the validator set immediately. However, at the beginning of slash function, validator existence is not checked.

Code Location

SlashIndicator.sol#L66

  function slash(address validator) external onlyCoinbase onlyInit oncePerBlock onlyZeroGasPrice{
    Indicator memory indicator = indicators[validator];
    if (indicator.exist) {
      indicator.count++;
    } else {
      indicator.exist = true;
      indicator.count = 1;
      validators.push(validator);
    }
    indicator.height = block.number;
    if (indicator.count % felonyThreshold == 0) {
      indicator.count = 0;
      IValidatorSet(VALIDATOR_CONTRACT_ADDR).felony(validator, felonyRound, felonyDeposit);
    } else if (indicator.count % misdemeanorThreshold == 0) {
      IValidatorSet(VALIDATOR_CONTRACT_ADDR).misdemeanor(validator);
    }
    indicators[validator] = indicator;
    emit validatorSlashed(validator);
  }
Score
Impact: 3
Likelihood: 2
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 9145da64 by adding the validator existence check.

8.23 CHAINDID IS NOT COMPATIBLE WITH CORE CHAIN

// Informational

Description
Finding description placeholder
Score
Impact:
Likelihood:

8.24 REVERT STRING SIZE OPTIMIZATION

// Informational

Description

Shortening revert strings to fit in 32 bytes will decrease deploy time gas and will decrease runtime gas when the revert condition has been met.

Code Location

  BtcLightClient.sol::81 => require(INIT_CHAIN_HEIGHT % DIFFICULTY_ADJUSTMENT_INTERVAL == 0, "The initial block must be the first block of the adjustment cycle.");
  BtcLightClient.sol::470 => require(value.length == 32, "length of rewardForSyncHeader mismatch");
  BtcLightClient.sol::472 => require(newRewardForSyncHeader > 0, "the newRewardForSyncHeader out of range");
  BtcLightClient.sol::475 => require(value.length == 32, "length of callerCompensationMolecule mismatch");
  BtcLightClient.sol::477 => require(newCallerCompensationMolecule <= 10000, "new callerCompensationMolecule shouldn't be in range [0,10000]");
  BtcLightClient.sol::482 => require(newRoundSize >= maximumWeight, "new newRoundSize shouldn't be greater than maximumWeight");
  BtcLightClient.sol::487 => require(newMaximumWeight != 0 && roundSize >= newMaximumWeight, "the newMaximumWeight must not be zero and no less than newRoundSize");
  CandidateHub.sol::140 => require(roundTimestamp > roundTag, "can not turn round twice in one round");
  CandidateHub.sol::219 => require(commissionThousandths > 0 && commissionThousandths < 1000, "commissionThousandths should in range (0, 1000)");
  CandidateHub.sol::221 => require(!isContract(consensusAddr), "contract is not allowed to be consensus address");
  CandidateHub.sol::222 => require(!isContract(feeAddr), "contract is not allowed to be fee address");
  CandidateHub.sol::239 => require(c.margin >= dues, "margin is not enough to cover dues");
  CandidateHub.sol::261 => require(commissionThousandths > 0 && commissionThousandths < 1000, "commissionThousandths should in range (0, 1000)");
  CandidateHub.sol::262 => require(!isContract(consensusAddr), "contract is not allowed to be consensus address");
  CandidateHub.sol::263 => require(!isContract(feeAddr), "contract is not allowed to be fee address");
  CandidateHub.sol::272 => "commissionThousandths out of adjustment range"
  CandidateHub.sol::381 => require(value.length == 32, "length of requiredMargin mismatch");
  CandidateHub.sol::391 => require(value.length == 32, "length of validatorCount mismatch");
  CandidateHub.sol::393 => require(newValidatorCount > 5 && newValidatorCount < 42, "the newValidatorCount out of range");
  CandidateHub.sol::396 => require(value.length == 32, "length of maxCommissionChange mismatch");
  CandidateHub.sol::398 => require(newMaxCommissionChange > 0, "the newMaxCommissionChange out of range");
  GovHub.sol::19 => bytes public constant INIT_MEMBERS = hex"f83f9491fb7d8a73d2752830ea189737ea0e007f999b949448bfbc530e7c54c332b0fae07312fba7078b878994de60b7d0e6b758ca5dd8c61d377a2c5f1af51ec1";
  GovHub.sol::81 => require(members[msg.sender] > 0, "only member is allowed to call the method");
  GovHub.sol::106 => "proposal function information arity mismatch"
  GovHub.sol::116 => "one live proposal per proposer, found an already active proposal"
  GovHub.sol::120 => "one live proposal per proposer, found an already pending proposal"
  GovHub.sol::190 => require(state(proposalId) == ProposalState.Succeeded, "proposal can only be executed if it is succeeded");
  GovHub.sol::258 => require(value.length == 32, "length of proposalMaxOperations mismatch");
  GovHub.sol::260 => require(newProposalMaxOperations > 0, "the proposalMaxOperations out of range");
  PledgeAgent.sol::135 => require(agentList.length == rewardList.length, "the length of agentList and rewardList should be equal");
  PledgeAgent.sol::168 => require(miners.length == powers.length, "the length of miners and powers should be equal");
  PledgeAgent.sol::290 => require(sourceAgent!=targetAgent, "source agent and target agent are the same one");
  PledgeAgent.sol::301 => require(sourceAgent!=targetAgent, "source agent and target agent are the same one");
  PledgeAgent.sol::479 => require(value.length == 32, "length of requiredCoinDeposit mismatch");
  PledgeAgent.sol::481 => require(newRequiredCoinDeposit > 0, "the requiredCoinDeposit out of range");
  RelayerHub.sol::57 => require(msg.value == requiredDeposit, "deposit value does not match requirement");
  RelayerHub.sol::76 => require(value.length == 32, "length of requiredDeposit mismatch");
  SlashIndicator.sol::91 => require(items1[0].toUintStrict() == items2[0].toUintStrict(),"parent of two blocks must be the same");
  SlashIndicator.sol::97 => require(validator1 == validator2, "validators of the two blocks must be the same");
  SlashIndicator.sol::203 => require(value.length == 32, "length of misdemeanorThreshold mismatch");
  SlashIndicator.sol::205 => require(newMisdemeanorThreshold >= 1 && newMisdemeanorThreshold < felonyThreshold, "the misdemeanorThreshold out of range");
  SlashIndicator.sol::208 => require(value.length == 32, "length of felonyThreshold mismatch");
  SlashIndicator.sol::213 => require(value.length == 32, "length of rewardForReportDoubleSign mismatch");
  SlashIndicator.sol::215 => require(newRewardForReportDoubleSign != 0, "the rewardForReportDoubleSign out of range");
  System.sol::26 => require(msg.sender == block.coinbase, "the message sender must be the block producer");
  System.sol::49 => require(msg.sender == SLASH_CONTRACT_ADDR, "the msg sender must be slash contract");
  System.sol::54 => require(msg.sender == GOV_HUB_ADDR, "the msg sender must be governance contract");
  System.sol::59 => require(msg.sender == CANDIDATE_HUB_ADDR, "the msg sender must be candidate contract");
  System.sol::64 => require(msg.sender == VALIDATOR_CONTRACT_ADDR, "the msg sender must be validatorSet contract");
  System.sol::74 => require(!isContract(msg.sender), "contract is not allowed to be a relayer");
  SystemReward.sol::25 => require(operators[msg.sender], "only operator is allowed to call the method");
  SystemReward.sol::73 => require(value.length == 32, "length of incentiveBalanceCap mismatch");
Score
Impact: 1
Likelihood: 1
Recommendation

ACKNOWLEDGED: The CoreDAO team acknowledged this issue.

8.25 EMPTY BLOCKS SHOULD BE REMOVED OR EMIT SOMETHING

// Informational

Description

Empty blocks should do something or be removed.

Code Location

BtcLightClient.sol#L78

  constructor() public {}
Score
Impact: 1
Likelihood: 2
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 5969c8 by deleting the empty block.

8.26 CALLDATA IS CHEAPER THAN MEMORY

// Informational

Description

When a function with a memory array is called externally, the abi.decode() step has to use a for-loop to copy each index of the calldata to the memory index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * .length). Using calldata directly, obviates the need for such a loop in the contract code and runtime execution.

If the array is passed to an internal function which passes the array to another internal function where the array is modified and therefore memory is used in the external call, it's still more gas-efficient to use calldata when the external function uses modifiers, since the modifiers may prevent the internal functions from being called. Some gas savings if function arguments are passed as calldata instead of memory. Note that in older Solidity versions, changing some function arguments from memory to calldata may cause “unimplemented feature error”. This can be avoided by using a newer (0.8.*) Solidity compiler.

Code Location

ValidatorSet.sol#L141

  function updateValidatorSet(
    address[] memory operateAddrList,
    address[] memory consensusAddrList,
    address payable[] memory feeAddrList,
    uint256[] memory commissionThousandthsList
  ) {}
// 30481 GAS
contract C {
    function add(uint[] memory arr) external returns (uint sum) {
        uint length = arr.length;
        for (uint i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
    }
}
// 28670 GAS
contract C {
    function add(uint[] calldata arr) external returns (uint sum) {
        uint length = arr.length;
        for (uint i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
    }
}
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 12daef4 by using calldata.

8.27 FLOATING PRAGMA IS SET

// Informational

Description

The current pragma Solidity directive is ˆ0.6.4. It is recommended to specify a specific compiler version to ensure that the byte code produced does not vary between builds. Contracts should be deployed using the same compiler version/flags with which they have been tested. Locking the pragma ensures that contracts do not accidentally get deployed using an older compiler version with known compiler bugs.

Code Location

  BtcLightClient.sol::1 => pragma solidity ^0.6.4;
  Burn.sol::1 => pragma solidity ^0.6.4;
  CandidateHub.sol::1 => pragma solidity ^0.6.4;
  Foundation.sol::1 => pragma solidity ^0.6.4;
  GovHub.sol::1 => pragma solidity ^0.6.4;
  Migrations.sol::1 => pragma solidity ^0.6.4;
  PledgeAgent.sol::1 => pragma solidity ^0.6.12;
  RelayerHub.sol::1 => pragma solidity ^0.6.4;
  SlashIndicator.sol::1 => pragma solidity ^0.6.4;
  System.sol::1 => pragma solidity ^0.6.4;
  SystemReward.sol::1 => pragma solidity ^0.6.4;
  ValidatorSet.sol::1 => pragma solidity ^0.6.4;
  interface/IBurn.sol::1 => pragma solidity ^0.6.4;
  interface/ICEP20.sol::1 => pragma solidity ^0.6.4;
  interface/ICandidateHub.sol::1 => pragma solidity ^0.6.4;
  interface/ILightClient.sol::1 => pragma solidity ^0.6.4;
  interface/IParamSubscriber.sol::1 => pragma solidity ^0.6.4;
  interface/IPledgeAgent.sol::1 => pragma solidity ^0.6.4;
  interface/IRelayerHub.sol::1 => pragma solidity ^0.6.4;
  interface/ISlashIndicator.sol::1 => pragma solidity ^0.6.4;
  interface/ISystemReward.sol::1 => pragma solidity ^0.6.4;
  interface/IValidatorSet.sol::1 => pragma solidity ^0.6.4;
  lib/BytesLib.sol::9 => pragma solidity ^0.6.4;
  lib/BytesToTypes.sol::1 => pragma solidity ^0.6.4;
  lib/Memory.sol::1 => pragma solidity ^0.6.4;
  lib/Ownable.sol::1 => pragma solidity ^0.6.4;
  lib/RLPDecode.sol::1 => pragma solidity ^0.6.4;
  lib/RLPEncode.sol::1 => pragma solidity ^0.6.4;
  lib/SafeMath.sol::1 => pragma solidity ^0.6.4;
Score
Impact: 2
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 1d7917a by locking the pragma.

8.28 OPTIMIZE UNSIGNED INTEGER COMPARISON

// Informational

Description

The check != 0 costs less gas compared to > 0 for unsigned integers in require statements with the optimizer enabled. While it may seem that > 0 is cheaper than !=0, this is only true without the optimizer enabled and outside a require statement. If the optimizer is enabled at 10k, and it is in a require statement, that would be more gas efficient.

Code Location

  BtcLightClient.sol::184 => require(reward > 0, "no relayer reward");
  BtcLightClient.sol::472 => require(newRewardForSyncHeader > 0, "the newRewardForSyncHeader out of range");
  BtcLightClient.sol::528 => if (count > 0) {
  Burn.sol::34 => if (v > 0) emit burned(msg.sender, v);
  CandidateHub.sol::71 => require(operateMap[msg.sender] > 0, "candidate does not exist");
  CandidateHub.sol::201 => if (jailedRound > 0 && jailedRound <= roundTag) {
  CandidateHub.sol::219 => require(commissionThousandths > 0 && commissionThousandths < 1000, "commissionThousandths should in range (0, 1000)");
  CandidateHub.sol::253 => if (value > 0)  msg.sender.transfer(value);
  CandidateHub.sol::261 => require(commissionThousandths > 0 && commissionThousandths < 1000, "commissionThousandths should in range (0, 1000)");
  CandidateHub.sol::304 => require(msg.value > 0, "value should be not nil");
  CandidateHub.sol::370 => if (d > 0) {
  CandidateHub.sol::388 => require(newDues > 0 && newDues < requiredMargin, "the dues out of range");
  CandidateHub.sol::398 => require(newMaxCommissionChange > 0, "the newMaxCommissionChange out of range");
  CandidateHub.sol::415 => return operateMap[operateAddr] > 0;
  CandidateHub.sol::419 => return consensusMap[consensusAddr] > 0;
  Foundation.sol::10 => if (msg.value > 0) {
  GovHub.sol::81 => require(members[msg.sender] > 0, "only member is allowed to call the method");
  GovHub.sol::209 => require(proposalCount >= proposalId && proposalId > 0, "state: invalid proposal id");
  GovHub.sol::227 => if (msg.value > 0) {
  GovHub.sol::241 => require(index > 0, "member does not exist");
  GovHub.sol::260 => require(newProposalMaxOperations > 0, "the proposalMaxOperations out of range");
  PledgeAgent.sol::328 => require(rewardSum > 0, "no pledge reward");
  PledgeAgent.sol::335 => if (reward > 0) {
  PledgeAgent.sol::347 => } else if (dust > 0) {
  PledgeAgent.sol::383 => require(newDeposit > 0, "delegator does not exist");
  PledgeAgent.sol::389 => if (a.rewardSet.length > 0) {
  PledgeAgent.sol::481 => require(newRequiredCoinDeposit > 0, "the requiredCoinDeposit out of range");
  PledgeAgent.sol::495 => if (totalDust > 0) {
  RelayerHub.sol::83 => require(newDues > 0 && newDues < requiredDeposit, "the dues out of range");
  System.sol::82 => return size > 0;
  SystemReward.sol::35 => if (msg.value>0) {
  SystemReward.sol::41 => if (msg.value > 0) {
  SystemReward.sol::57 => if (actualAmount > 0) {
  ValidatorSet.sol::78 => return currentValidatorSetMap[addr] > 0;
  ValidatorSet.sol::88 => if (index > 0) {
  ValidatorSet.sol::119 => if (v.income > 0) {
  ValidatorSet.sol::263 => require(newBlockRewardIncentivePercent > 0 && newBlockRewardIncentivePercent < 100, "the blockRewardIncentivePercent out of range");
  ValidatorSet.sol::310 => bool success = items.length > 0;
  lib/BytesToTypes.sol::36 => if gt(mod(size,32),0) {// if size%32 > 0
  lib/RLPDecode.sol::123 => require(item.len > 0 && item.len <= 33);
  lib/RLPDecode.sol::158 => require(item.len > 0);
  lib/SafeMath.sol::114 => require(b > 0, errorMessage);

Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 85d15fd by changing the integer comparison.

8.29 CACHE ARRAY LENGTH

// Informational

Description

In a for loop, the length of an array can be put in a temporary variable to save some gas. This has been done already in several other locations in the code.

In the above case, the solidity compiler will always read the length of the array during each iteration. That is,

  • if it is a storage array, this is an extra sload operation (100 additional extra gas (EIP-2929) for each iteration except for the first),
  • if it is a memory array, this is an extra mload operation (3 additional gas for each iteration except for the first),
  • if it is a calldata array, this is an extra calldataload operation (3 additional gas for each iteration except for the first)

Code Location

CandidateHub.sol::408 => for (uint256 i = 0; i < candidateSet.length; i++) 
GovHub.sol::89 => for (uint256 i = 0; i < items.length; i++) 
GovHub.sol::193 => for (uint256 i = 0; i < proposal.targets.length; i++) 
PledgeAgent.sol::171 => for (uint256 i = 0; i < lastMiners.length; i++) 
PledgeAgent.sol::188 => for (uint256 i = 0; i < miners.length; i++) 
ValidatorSet.sol::67 => for (uint256 i = 0; i < validatorSet.length; i++) 
ValidatorSet.sol::291 => for (uint256 i = 0; i < consensusAddrList.length; i++) 
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit e22f52 by caching array lengths.

8.30 NO NEED TO INITIALIZE VARIABLES WITH DEFAULT VALUES

// Informational

Description

Initialization to 0 or false is not necessary, as these are the default values in Solidity.

Code Location

  BtcLightClient.sol::170 => for(uint i = 0; i < CONFIRM_BLOCK; ++i){
  BtcLightClient.sol::193 => uint256 totalWeight=0;
  BtcLightClient.sol::196 => for (uint256 index = 0; index < relayers.length; index++) {
  BtcLightClient.sol::214 => for (uint256 index = 0; index < relayers.length; index++) {
  BtcLightClient.sol::438 => uint32 length = 0;
  BtcLightClient.sol::445 => uint32 compact = 0;
  BtcLightClient.sol::518 => for (uint i = 0; i < count; ++i){
  BtcLightClient.sol::530 => for (uint i = 0; i < count; ++i){
  CandidateHub.sol::149 => uint validCount = 0;
  CandidateHub.sol::151 => for (uint256 i = 0; i < candidateSize; i++) {
  CandidateHub.sol::156 => uint j = 0;
  CandidateHub.sol::157 => for (uint256 i = 0; i < candidateSize; i++) {
  CandidateHub.sol::176 => for (uint i = 0; i < totalCount; ++i) {
  CandidateHub.sol::198 => for (uint256 i = 0; i < candidateSize; i++) {
  CandidateHub.sol::207 => for (uint256 i = 0; i < candidateSize; i++) {
  CandidateHub.sol::332 => uint256 l = 0;
  CandidateHub.sol::333 => uint256 r = 0;
  CandidateHub.sol::408 => for (uint256 i = 0; i < candidateSet.length; i++) {
  GovHub.sol::89 => for (uint256 i = 0; i < items.length; i++) {
  GovHub.sol::193 => for (uint256 i = 0; i < proposal.targets.length; i++) {
  PledgeAgent.sol::137 => for (uint256 i = 0; i < agentList.length; ++i) {
  PledgeAgent.sol::170 => uint256 reward = 0;
  PledgeAgent.sol::171 => for (uint256 i = 0; i < lastMiners.length; i++) {
  PledgeAgent.sol::183 => for (uint256 i = 0; i < candidateSize; ++i) {
  PledgeAgent.sol::188 => for (uint256 i = 0; i < miners.length; i++) {
  PledgeAgent.sol::204 => for (uint256 i = 0; i < candidateSize; ++i) {
  PledgeAgent.sol::213 => for (uint256 i = 0; i < candidateSize; ++i) {
  PledgeAgent.sol::230 => for (uint256 i = 0; i < validators.length; ++i) {
  PledgeAgent.sol::312 => uint256 rewardSum = 0;
  PledgeAgent.sol::313 => uint256 dustSum = 0;
  PledgeAgent.sol::315 => for (uint256 i = 0; i < agentList.length; ++i) {
  SlashIndicator.sol::108 => for(uint i = 0;i < 15;++i){
  SlashIndicator.sol::152 => uint i = 0;
  SlashIndicator.sol::155 => bool findLeft = false;
  SlashIndicator.sol::156 => bool findRight = false;
  ValidatorSet.sol::67 => for (uint256 i = 0; i < validatorSet.length; i++) {
  ValidatorSet.sol::104 => uint256 incentiveSum = 0;
  ValidatorSet.sol::105 => for (uint256 i = 0; i < currentValidatorSet.length; i++) {
  ValidatorSet.sol::115 => uint256 rewardSum = 0;
  ValidatorSet.sol::116 => for (uint256 i = 0; i < currentValidatorSet.length; i++) {
  ValidatorSet.sol::177 => for (uint256 i = 0; i < currentValidatorSet.length; i++) {
  ValidatorSet.sol::210 => for (uint256 i = 0; i < index; i++) {
  ValidatorSet.sol::246 => for (uint256 i = 0; i < n; i++) {
  ValidatorSet.sol::291 => for (uint256 i = 0; i < consensusAddrList.length; i++) {
  ValidatorSet.sol::292 => for (uint256 j = 0; j < i; j++) {
  ValidatorSet.sol::303 => for (uint256 j = 0; j < items.length; j++) {
  ValidatorSet.sol::317 => bool success = false;
  lib/RLPDecode.sol::68 => for (uint i = 0; i < items; i++) {
  lib/RLPDecode.sol::176 => uint count = 0;
Score
Impact: 1
Likelihood: 1
Recommendation

ACKNOWLEDGED: The CoreDAO team acknowledged this issue.

8.31 INCREMENT/DECREMENT FOR LOOP VARIABLE IN AN UNCHECKED BLOCK

// Informational

Description

i++ involves checked arithmetic, which is not required. This is because the value of i is always strictly less than length <= 2**256 - 1. Therefore, the theoretical maximum value of i to enter the for-loop body is 2**256 - 2. This means that the i++ in the for loop can never overflow. Regardless, the compiler performs the overflow checks.

Code Location

  BtcLightClient.sol::196 => for (uint256 index = 0; index < relayers.length; index++) {
  BtcLightClient.sol::214 => for (uint256 index = 0; index < relayers.length; index++) {
  CandidateHub.sol::151 => for (uint256 i = 0; i < candidateSize; i++) {
  CandidateHub.sol::157 => for (uint256 i = 0; i < candidateSize; i++) {
  CandidateHub.sol::176 => for (uint i = 0; i < totalCount; ++i) {
  CandidateHub.sol::198 => for (uint256 i = 0; i < candidateSize; i++) {
  CandidateHub.sol::207 => for (uint256 i = 0; i < candidateSize; i++) {
  CandidateHub.sol::408 => for (uint256 i = 0; i < candidateSet.length; i++) {
  GovHub.sol::89 => for (uint256 i = 0; i < items.length; i++) {
  GovHub.sol::193 => for (uint256 i = 0; i < proposal.targets.length; i++) {
  PledgeAgent.sol::171 => for (uint256 i = 0; i < lastMiners.length; i++) {
  PledgeAgent.sol::188 => for (uint256 i = 0; i < miners.length; i++) {
  ValidatorSet.sol::67 => for (uint256 i = 0; i < validatorSet.length; i++) {
  ValidatorSet.sol::105 => for (uint256 i = 0; i < currentValidatorSet.length; i++) {
  ValidatorSet.sol::116 => for (uint256 i = 0; i < currentValidatorSet.length; i++) {
  ValidatorSet.sol::177 => for (uint256 i = 0; i < currentValidatorSet.length; i++) {
  ValidatorSet.sol::210 => for (uint256 i = 0; i < index; i++) {
  ValidatorSet.sol::246 => for (uint256 i = 0; i < n; i++) {
  ValidatorSet.sol::291 => for (uint256 i = 0; i < consensusAddrList.length; i++) {
  ValidatorSet.sol::292 => for (uint256 j = 0; j < i; j++) {
  ValidatorSet.sol::303 => for (uint256 j = 0; j < items.length; j++) {
Score
Impact: 1
Likelihood: 1
Recommendation

ACKNOWLEDGED: The CoreDAO team acknowledged this issue.

8.32 DEPRECATED IMPLEMENTATION USED

// Informational

Description

Using .value(...) is deprecated. Use {value: ...} instead.

Code Location

GovHub.sol#L201

  function execute(uint256 proposalId) public payable onlyInit {
    require(state(proposalId) == ProposalState.Succeeded, "proposal can only be executed if it is succeeded");
    Proposal storage proposal = proposals[proposalId];
    proposal.executed = true;
    for (uint256 i = 0; i < proposal.targets.length; i++) {
      bytes memory callData;
      if (bytes(proposal.signatures[i]).length == 0) {
        callData = proposal.calldatas[i];
      } else {
        callData = abi.encodePacked(bytes4(keccak256(bytes(proposal.signatures[i]))), proposal.calldatas[i]);
      }

      (bool success, bytes memory returnData) = proposal.targets[i].call.value(proposal.values[i])(callData);
      require(success, "Transaction execution reverted.");
      emit ExecuteTransaction(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i]);
    }
    emit ProposalExecuted(proposalId);
  }
Score
Impact: 2
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 82e9fad by changing the implementation.

8.33 ROUNDING LOSS ARE NOT CONSIDERED ON THE REWARD CALCULATION

// Informational

Description

On the round reward calculation, rounding (lower) is not considered on the implementation. ABDK libraries or Compound Protocol libraries can be used. Reference

Code Location

PledgeAgent.sol#L154

  function addRoundReward(address[] memory agentList, uint256[] memory rewardList)
    external
    payable
    override
    onlyValidator
  {
    require(agentList.length == rewardList.length, "the length of agentList and rewardList should be equal");
    RoundState memory rs = stateMap[roundTag];
    for (uint256 i = 0; i < agentList.length; ++i) {
      Agent storage a = agentsMap[agentList[i]];
      if (a.rewardSet.length == 0) {
        continue;
      }
      Reward storage r = a.rewardSet[a.rewardSet.length - 1];
      uint256 rIntegral = r.totalIntegral;
      if (rIntegral == 0) {
        delete a.rewardSet[a.rewardSet.length - 1];
        continue;
      }
      if (rewardList[i] == 0) {
        continue;
      }
      r.totalReward = rewardList[i];
      r.remainReward = rewardList[i];
      uint256 coinReward = rewardList[i] * a.coin * rs.power / rIntegral;
      uint256 powerReward = rewardList[i] * a.power * rs.coin / 10000 * rs.powerFactor / rIntegral;
      emit roundReward(agentList[i], coinReward, powerReward);
    }
  }

Score
Impact: 2
Likelihood: 1
Recommendation

ACKNOWLEDGED: The CoreDAO team acknowledged this issue.

8.34 OPEN TO-DOS

// Informational

Description

Open To-dos can point to architecture or programming issues that still need to be resolved. Often these kinds of comments indicate areas of complexity or confusion for developers. This provides value and insight to an attacker who aims to cause damage to the protocol.

Code Location

./PledgeAgent.sol:162:  // TODO bad naming, should use `hybrid score` instead of integral
./CandidateHub.sol:165:    // TODO bad naming; should use `hybrid score` instead of integral
Score
Impact: 2
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 6b44bf by resolving to-dos.

8.35 DELETE - ABI CODER V2 FOR GAS OPTIMIZATION

// Informational

Description

From Pragma 0.8.0, ABI coder v2 is activated by default. The pragma abicoder v2 can be deleted from the repository. That will provide gas optimization.

Code Location

./PledgeAgent.sol:2:pragma experimental ABIEncoderV2;
./GovHub.sol:2:pragma experimental ABIEncoderV2;
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 99f9e7ea by resolving to-dos.

8.36 SAFEMATH IS IMPORTED BUT NOT USED

// Informational

Description

In the CandidateHub contract, SafeMath library is imported, but It is not used in the implementation.

Code Location

CandidateHub.sol#L13

contract CandidateHub is ICandidateHub, System, IParamSubscriber {
  using SafeMath for uint256;
}
Score
Impact: 1
Likelihood: 2
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 9b147c3da by enabling the overflow protection with pragma.

8.37 UNUSED IMPORT FILES

// Informational

Description

The linked import statements include a contract or library which is not used in the file that imported such files.

Code Location

System.sol#L3

import "./interface/ISystemReward.sol";
import "./interface/ILightClient.sol";
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 22a0e89 by deleting unused imports.

8.38 DECLARATION NAMING CONVENTION

// Informational

Description

The linked declarations do not conform to the Solidity style guide in regard to its naming convention.

camelCase : Should be applied to function names, argument names, local and state variable names, modifiers.
UPPER_CASE : Should be applied to constant variables.
CapWords : Should be applied to contract names, struct names, event names and enums.

Code Location

RelayerHub.sol#L45

  event relayerRegister(address _relayer);
  event relayerUnRegister(address _relayer);
  event paramChange(string key, bytes value);
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 4f6eb2c by changing the naming convention.

8.39 EVENT IS MISSING INDEXED FIELDS

// Informational

Description

Index event fields make the field more quickly accessible to off-chain tools that parse events. However, note that each index field costs extra gas during emission, so it’s not necessarily best to index the maximum allowed per event (three fields).

Code Location

RelayerHub.sol#L45

  event relayerRegister(address _relayer);
  event relayerUnRegister(address _relayer);

GovHub.sol#L46

  event MemberAdded(address member);
  event MemberDeleted(address member);
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit dba15c9 by adding index on events.

8.40 MISSING ABSTAIN OPTION ON THE PROPOSALS

// Informational

Description

Governor contracts allow the deployment of on-chain voting protocols, but The GovHub contract is missing abstain voting option.

Code Location

/contracts/GovHub.sol#L168

  function castVote(uint256 proposalId, bool support) public onlyInit onlyMember {
    require(state(proposalId) == ProposalState.Active, "voting is closed");
    Proposal storage proposal = proposals[proposalId];
    Receipt storage receipt = proposal.receipts[msg.sender];
    require(receipt.hasVoted == false, "voter already voted");
    if (support) {
      proposal.forVotes += 1;
    } else {
      proposal.againstVotes += 1;
    }

    receipt.hasVoted = true;
    receipt.support = support;
    emit VoteCast(msg.sender, proposalId, support);
  }
Score
Impact: 2
Likelihood: 1
Recommendation

ACKNOWLEDGED: The CoreDAO team acknowledged this issue. The team claims that they might consider adding the abstain option in future versions.

8.41 INCREASE OPTIMIZER RUNS

// Informational

Description

Optimizer variable can optimize costs by increasing the number of runs to something like 2000 at least in the config. This value is a tuning parameter for deploy v/s runtime costs. Higher values optimize for lower runtime cost.

Code Location

truffle-config.js#L99

{
  compilers: {
    solc: {
      version: "0.6.12",    // Fetch exact version from solc-bin (default: truffle's version)
      docker: false,        // Use "0.5.1" you've installed locally with docker (default: false)
      settings: {          // See the solidity docs for advice about optimization and evmVersion
       optimizer: {
         enabled: true,
         runs: 200
       }
      }
    }
  },
   plugins: ["solidity-coverage"]
}
Score
Impact: 1
Likelihood: 2
Recommendation

ACKNOWLEDGED: The CoreDAO team acknowledged this issue.

8.42 USE CUSTOM ERRORS INSTEAD OF REVERT STRINGS TO SAVE GAS

// Informational

Description

Custom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they’re hit by avoiding having to allocate and store the revert string. Not defining the strings also save deployment gas.

Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit e88fe7 by changing revert messages with custom errors.

8.43 REDUCE BOOLEAN COMPARISON

// Informational

Description

In the GovHub contract, a boolean (return) value is compared to false. Unnecessary comparison to a boolean constant, only using the boolean value, is slightly cheaper on gas.

Code Location

GovHub.sol#L166

  function castVote(uint256 proposalId, bool support) public onlyInit onlyMember {
    require(state(proposalId) == ProposalState.Active, "voting is closed");
    Proposal storage proposal = proposals[proposalId];
    Receipt storage receipt = proposal.receipts[msg.sender];
    require(receipt.hasVoted == false, "voter already voted");
    if (support) {
      proposal.forVotes += 1;
    } else {
      proposal.againstVotes += 1;
    }

    receipt.hasVoted = true;
    receipt.support = support;
    emit VoteCast(msg.sender, proposalId, support);
  }
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The CoreDAO team solved the issue in commit 440cfef by changing the operator.

8.44 UPGRADE PRAGMA TO AT LEAST 0.8.4

// Informational

Description

Using newer compiler versions and the optimizer gives gas optimizations and additional safety checks for free.

The advantages of versions =0.8.*= over =<0.8.0= are:

Safemath by default from 0.8.0 (can be more gas efficient than some library based safemath).
Low level inliner from 0.8.2, leads to cheaper runtime gas. Especially relevant when the contract has small functions. For example, OpenZeppelin libraries typically have a lot of small helper functions and if they are not inlined, they cost an additional 20 to 40 gas because of 2 extra jump instructions and additional stack operations needed for function calls.
Optimizer improvements in packed structs: Before 0.8.3, storing packed structs, in some cases used an additional storage read operation. After EIP-2929, if the slot was already cold, this means unnecessary stack operations and extra deploy time costs. However, if the slot was already warm, this means additional cost of 100 gas alongside the same unnecessary stack operations and extra deploy time costs.
Custom errors from 0.8.4, leads to cheaper deploy time cost and run time cost. Note: the run time cost is only relevant when the revert condition is met. In short, replace revert strings by custom errors.
Score
Impact: 1
Likelihood: 2
Recommendation

SOLVED: The CoreDAO team solved the issue in the commit by changing the pragma version.

8.45 REWARDS CANNOT BE DISTRIBUTED IF THERE IS NO ENOUGH BALANCE ON THE CONTRACT

// Informational

Description

CandidateHub.turnRound calls distributeReward in ValidatorSet to distribute rewards to all roles, expect BTC hash delegators. However, in the distributeReward function, address balance is not checked during the sending of rewards to fee address.

Code Location

/contracts/ValidatorSet.sol#L127

  function distributeReward() external override onlyCandidate {
    address payable feeAddress;
    uint256 validatorReward;

    uint256 incentiveSum = 0;
    for (uint256 i = 0; i < currentValidatorSet.length; i++) {
      Validator storage v = currentValidatorSet[i];
      uint256 incentiveValue = (v.income * blockRewardIncentivePercent) / 100;
      incentiveSum += incentiveValue;
      v.income -= incentiveValue;
    }
    ISystemReward(SYSTEM_REWARD_ADDR).receiveRewards{ value: incentiveSum }();

    address[] memory operateAddressList = new address[](currentValidatorSet.length);
    uint256[] memory rewardList = new uint256[](currentValidatorSet.length);
    uint256 rewardSum = 0;
    for (uint256 i = 0; i < currentValidatorSet.length; i++) {
      Validator storage v = currentValidatorSet[i];
      operateAddressList[i] = v.operateAddress;
      if (v.income > 0) {
        feeAddress = v.feeAddress;
        validatorReward = (v.income * v.commissionThousandths) / 1000;
        if (v.income > validatorReward) {
          rewardList[i] = v.income - validatorReward;
          rewardSum += rewardList[i];
        }

        bool success = feeAddress.send(validatorReward);
        if (success) {
          emit directTransfer(v.operateAddress, feeAddress, validatorReward, v.income);
        } else {
          emit directTransferFail(v.operateAddress, feeAddress, validatorReward, v.income);
        }
        v.income = 0;
      }
    }
Score
Impact: 2
Likelihood: 1
Recommendation

ACKNOWLEDGED: The deposit method in ValidatorSet.sol contains checks to ensure that there are always enough funds available to distribute. The CoreDAO team acknowledged this issue.

8.46 BINDING HASH IS NOT IMPLEMENTED ON THE CODE BASE

// Informational

Description

On the spec, it is mentioned as Blockhash is defined as the latest blockhash of Core blockchain. Although, this field is optional, The BTC miner/mining pool who adds this information brings extra security to the Core network and will receive extra rewards. However, the parameter is not implemented in the codebase.

Code Location

/contracts/BtcLightClient.sol#L130

...
    assembly {
      // call precompiled contract contracts_lightclient.go 
      // contract address: 0x64
      if iszero(staticcall(not(0), 0x64, input, length, result, 128)) {
        revert(0, 0)
      }
      candidateAddr := mload(add(result, 0))
      rewardAddr := mload(add(result, 0x20))
      bindingHash := mload(add(result, 0x40))
    }
...
Score
Impact: 2
Likelihood: 1
Recommendation

ACKNOWLEDGED: The CoreDAO team acknowledged this issue. The team will enable the feature in the future.

8.47 NO NEED TO RETURN VARIABLE NAME ON THE VIEWS

// Informational

Description

Using both named returns and a return statement isn't necessary. Removing unused named return variables can reduce gas usage and improve code clarity.

Code Location

/contracts/BtcLightClient.sol#L586

  function getRoundMiners(uint256 roundTimeTag, address candidate) external override view returns (address[] memory miners) {
    return roundPowerMap[roundTimeTag].powerMap[candidate].miners;
  }

  /// Get BTC blocks delegated to a given candidate in a specific round
  /// @param roundTimeTag The specific round time
  /// @param candidate The given candidate to get its blocks
  /// @return blocks The blocks delegated to the candidate in the round
  function getRoundBlocks(uint256 roundTimeTag, address candidate) external view returns (bytes32[] memory blocks) {
    return roundPowerMap[roundTimeTag].powerMap[candidate].btcBlocks;
  }

  /// Get candidates of a specific round
  /// @param roundTimeTag The specific round time
  /// @return candidates The valid candidates in the round
  function getRoundCandidates(uint256 roundTimeTag) external override view returns (address[] memory candidates) {
    return roundPowerMap[roundTimeTag].candidates;
  }
Score
Impact: 1
Likelihood: 1
Recommendation

ACKNOWLEDGED: The CoreDAO team acknowledged this issue.

8.48 ROUND LIMIT CAN BE DEFINED AS GOVERNANCE PARAMETER

// Informational

Description

During the code review, It has been noticed that roundLimit is hardcoded into claimRewards function. It is recommended to activate round limit as governance parameter.

Code Location

/contracts/PledgeAgent.sol#L287

  function claimReward(address[] calldata agentList) external returns (uint256, bool) {
    // limit round count to control gas usage
    int256 roundLimit = 500;
    uint256 reward;
    uint256 rewardSum = rewardMap[msg.sender];
    if (rewardSum != 0) {
      rewardMap[msg.sender] = 0;
    }

    uint256 agentSize = agentList.length;
    for (uint256 i = 0; i < agentSize; ++i) {
      Agent storage a = agentsMap[agentList[i]];
      if (a.rewardSet.length == 0) continue;
      CoinDelegator storage d = a.cDelegatorMap[msg.sender];
      if (d.newDeposit == 0) continue;
      int256 roundCount = int256(a.rewardSet.length - d.rewardIndex);
      reward = collectCoinReward(a, d, roundLimit);
      roundLimit -= roundCount;
      rewardSum += reward;
      // if there are rewards to be collected, leave them there
      if (roundLimit < 0) break;
    }
    if (rewardSum != 0) {
      distributeReward(payable(msg.sender), rewardSum);
    }
    return (rewardSum, roundLimit >= 0);
  }

Score
Impact: 1
Likelihood: 2
Recommendation

ACKNOWLEDGED: The CoreDAO team acknowledged this issue. The team claims that this is very similar to the limit set on candidate size. It is more of a technical limitation rather than something they want to expose to governance.

9. Automated Testing

STATIC ANALYSIS REPORT

Description

Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contracts in scope. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified the smart contracts in the repositories and was able to compile them correctly into their ABIs and binary format, Slither was run against the contracts. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base.

Slither results

slither1.jpgslither2.jpgslither3.jpgslither4.jpgslither5.jpgslither6.jpgslither7.jpgslither8.jpg
  • No major issues were found by Slither.

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.

// Download the full report

* Use Google Chrome for best results

** Check "Background Graphics" in the print settings if needed

© Halborn 2024. All rights reserved.