MAKE CSPR.name - Casper Association


Prepared by:

Halborn Logo

HALBORN

Last Updated 07/03/2025

Date of Engagement: May 29th, 2025 - June 17th, 2025

Summary

100% of all REPORTED Findings have been addressed

All findings

18

Critical

0

High

0

Medium

3

Low

5

Informational

10


1. Introduction

MAKE engaged Halborn to conduct a security assessment of their CSPR.name contracts on the Casper Network, from May 29th, 2025, to June 16th, 2025. The scope of this assessment was limited to the repository identified by a specific commit hash, with additional details provided in the Scope section of this report.


CSPR.name is a Web3 naming service built on the Casper Network. It enables users to replace complex hexadecimal account identifiers with human-readable names (e.g., smith.cspr), functioning similarly to how Web2 DNS translates IP addresses into domain names.

2. Assessment Summary

The team at Halborn assigned a dedicated, full-time security engineer to evaluate the security of the smart contracts. The security engineer possesses advanced expertise in blockchain and smart contract security, with extensive skills in penetration testing, smart-contract hacking, and comprehensive knowledge of multiple blockchain protocols.


The objectives of this assessment are to:

    • Verify that the contract functionalities operate as intended

    • Identify potential security vulnerabilities within the contracts


Overall, Halborn identified several areas for improvement to reduce risks and their potential impact, which have been addressed by the MAKE team. The primary recommendations include:

    • Increment the minted_tokens_count variable within the mint function after each successful token minting.

    • Enforce strict subdomain validation by verifying that the extracted token_name matches a DNS-label pattern using a regex.

    • Ensure that the resolved address of the primary name matches the caller’s address to guarantee that only the legitimate owner can set reverse resolution for their address.

    • Enable overflow checks in release mode to safeguard the expiration logic from potential integer overflows.

3. Test Approach and Methodology

Halborn employed a combination of manual code review and automated security testing to ensure a comprehensive, efficient evaluation of the smart contracts. Manual testing aimed to identify logical, procedural, and implementation flaws, while automated testing enhanced coverage and rapidly detected deviations from security best practices. The assessment employed the following phases and tools:

    • Research into the architecture, purpose, and usage of the protocol.

    • Manual code review and walkthrough.

    • Manual assessment of critical Rust variables and functions to identify potential arithmetic vulnerabilities.

    • Evaluation of cross-contract call controls.

    • Logical control review based on the overall architecture.

    • Scanning Rust files for known vulnerabilities using cargo audit.

    • Integration testing within a local testing environment.


4. 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
Our penetration tests use the industry standard Common Vulnerability Scoring System (CVSS) to calculate the severity of our findings.

5. SCOPE

Files and Repository
(a) Repository: cspr-name-contracts
(b) Assessed Commit ID: 3a14be5
(c) Items in scope:
  • src/data_structures.rs
  • src/contracts/registrar.rs
  • src/contracts/name_token.rs
↓ Expand ↓
Out-of-Scope: Third party dependencies and economic attacks.
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

3

Low

5

Informational

10

Security analysisRisk levelRemediation Date
Missing Token Count Increment Bypasses Max Supply LimitMediumSolved - 06/30/2025
Lack of Subdomain Name ValidationMediumSolved - 06/30/2025
Users Can Set Unowned Names as Primary in Reverse ResolutionMediumSolved - 06/30/2025
Integer Overflow in Expiration Logic May Cause Incorrect Reverts or Premature ExpirationsLowSolved - 06/30/2025
Missing Refund Mechanism For Excess CSPRLowSolved - 06/30/2025
Lacking Pausability MechanismLowSolved - 06/30/2025
Missing Error Definitions in Contract SchemaLowSolved - 06/30/2025
Incorrect Access Control in Whitelist RevocationLowSolved - 06/30/2025
Double Cleanup of Default Resolver During Token TransferInformationalSolved - 06/30/2025
Unnecessary Storage Write on Revoke for Non-Whitelisted AddressesInformationalSolved - 06/30/2025
Reverse Resolver Allows Unnecessary Primary Name UpdatesInformationalSolved - 06/30/2025
Missing Label Validation Allows Registration of Structurally Invalid and Unusable DomainsInformationalSolved - 06/30/2025
Missing Validation Of Contract AddressesInformationalSolved - 06/30/2025
Unneeded Admin Role Assignment in Registrar DeploymentInformationalSolved - 06/30/2025
Incorrect Naming Can Lead to Logic Errors or MisuseInformationalSolved - 07/02/2025
Lacking Event EmissionInformationalSolved - 06/30/2025
Inconsistencies In DocumentationInformationalSolved - 06/30/2025
Unnecessary CloneInformationalSolved - 06/30/2025

7. Findings & Tech Details

7.1 Missing Token Count Increment Bypasses Max Supply Limit

//

Medium

Description

In the name_token module, the mint function enforces a maximum supply limit using the minted_tokens_count and max_supply variables. However, it fails to increment minted_tokens_count after successfully minting a token. This flaw allows the contract to mint an unlimited number of tokens, effectively bypassing the intended supply cap of 1,000,000 tokens. Because minted_tokens_count remains unchanged, the max_supply check becomes ineffective and does not reflect the actual number of tokens minted. This issue can lead to uncontrolled token issuance, compromising scarcity guarantees and potentially disrupting the token's economic model and trust assumptions.

Code Location

Below is the implementation of the mint function:

pub fn mint(
    &mut self,
    recipient: Address,
    token_id: U256,
    token_metadata: Vec<(String, String)>,
) {
    let caller = self.env().caller();
    self.assert_whitelisted(&caller);

    if self.minted_tokens_count.get_or_default() >= self.max_supply.get_or_default() {
        self.revert(NameTokenError::TokenSupplyDepleted);
    }
    if self.token.exists(&token_id) {
        self.revert(NameTokenError::InvalidTokenIdentifier);
    }
    // mint the token
    self.token.mint(recipient, token_id, token_metadata);
}

BVSS
Recommendation

It is recommended to increment the minted_tokens_count variable within the mint function each time a token is successfully minted.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID.

Remediation Hash

7.2 Lack of Subdomain Name Validation

//

Medium

Description

The resolver contract allows domain owners to create subdomains without any validation on the subdomain name format. This lack of validation enables the creation of problematic subdomains such as:

  • Empty strings (e.g., "..cspr")

  • Spaces (e.g., " .cspr")

  • Special characters (e.g., "$.cspr")

  • No length restrictions

The issue originates from the set_resolution function, which only validates:

  • Whether the domain ends with ".cspr"

  • Whether the token ID is valid

  • Whether the caller is the owner of the token ID

The lack of subdomain validation creates a significant security and usability concern. Malicious actors can create confusing or deceptive subdomains that closely resemble legitimate ones, enabling sophisticated phishing attacks. For instance, an attacker could create subdomains with subtle variations or misleading characters that appear legitimate to users. This not only compromises the security of the naming service but also creates technical challenges for client applications that need to implement proper domain name resolution. Additionally, the absence of validation rules leads to inefficient storage usage as the system must accommodate any arbitrary string as a valid subdomain. This combination of security risks, usability issues, and technical inefficiencies makes the current implementation vulnerable to abuse and potentially harmful to the ecosystem's integrity.

Code Location

Below is the implementation of the set_resolution function:

pub fn set_resolution(&mut self, full_domain: Domain, address: Option<Address>) {
    let env = self.env();
    let token_id = self
        .calculate_token_id(&full_domain)
        .unwrap_or_revert_with(self, ResolverError::InvalidDomain);
    let caller = env.caller();

    if !self.name_token.is_token_valid(token_id) {
        env.revert(ResolverError::ResolutionSetWithInvalidToken);
    }

    if self.owner_of(token_id) != Some(caller) {
        env.revert(ResolverError::ResolutionSetByInvalidOwner);
    }

    let nonce = self.nonce(&token_id);
    self.resolutions
        .set(&(token_id, full_domain.clone(), nonce), address);

    env.emit_event(ResolutionChanged {
        full_domain,
        address,
    });
}

Below is the implementation of the calculate_token_id function:

fn calculate_token_id(&self, full_domain: &str) -> Option<TokenId> {
    let token_name = utils::extract_token_name(&full_domain)?;
    let hash = self.env().hash(token_name);
    Some(U256::from(hash))
}

Below is the implementation of the extract_token_name function within the utils module, which only ensures that the domain ends with CSPR_DOMAIN, which is ".cspr":

pub fn extract_token_name(full_domain: &str) -> Option<String> {
    if full_domain.ends_with(CSPR_DOMAIN) {
        let token_name = full_domain.trim_end_matches(CSPR_DOMAIN);
        token_name.split('.').last().map(|s| s.to_string())
    } else {
        None
    }
}
const CSPR_DOMAIN: &str = ".cspr";

BVSS
Recommendation

Enforce strict subdomain validation in set_resolution by verifying that the extracted token_name matches a DNS-label regex pattern (e.g., allowing only alphanumeric characters and hyphens, 1–63 characters long, with no empty or special characters). If validation fails, return a custom InvalidSubdomainFormat error.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID.

Remediation Hash

7.3 Users Can Set Unowned Names as Primary in Reverse Resolution

//

Medium

Description

Users can set any primary name in the reverse resolution without verifying ownership, enabling them to set names they do not control. This can lead to misleading or incorrect reverse records, compromising the trust and integrity of address-to-name mappings.

Code Location

Below is the implementation of the set_primary_name function:

pub fn set_primary_name(&mut self, primary_name: String) {
    // Load currently set primary name.
    let caller = self.env().caller();
    let current_primary_name = self.get_primary_name(&caller);
    // Update primary name.
    self.primary_names.set(&caller, primary_name.clone());
    // Emit event.
    self.env().emit_event(PrimaryNameChanged {
        address: caller,
        old_primary_name: current_primary_name,
        new_primary_name: primary_name,
    });
}

BVSS
Recommendation

It is recommended to verify that the resolved address of the primary name matches the caller’s address to ensure that only the legitimate owner can set the reverse resolution for their address.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID. The set_primary_name function now ensures that only a valid token owner can set a primary name, and that the name exists in the resolver. Additionally, the get_primary_name function now checks if the token has been invalidated in the resolver and invalidates reverse resolution accordingly.

Remediation Hash

7.4 Integer Overflow in Expiration Logic May Cause Incorrect Reverts or Premature Expirations

//

Low

Description

In computer programming, an overflow occurs when an arithmetic operation attempts to create a numeric value that is outside the range that can be represented with a given number of bits – either larger than the maximum or lower than the minimum representable value. By default, overflow checks are disabled in release mode. Enabling overflow-checks in the workspace ensures that overflow errors are caught even in release builds.

The identified unchecked operations are listed below:

casper-name-contracts/src/contracts/registrar.rs
L176: if block_time > expiration + grace_period {
L231: block_time > token_expiration + grace_period + PENDING_DELETE_PERIOD

The expression block_time > expiration + grace_period is vulnerable to an integer overflow if grace_period is set to an excessively high value. Since overflow checks are disabled by default in release mode, this could cause the sum to wrap around the u64 limit, effectively reversing the comparison logic. As a result, valid prolongation attempts may incorrectly revert with a GracePeriodExpired error.

Similarly, the condition block_time > token_expiration + grace_period + PENDING_DELETE_PERIOD is susceptible to the same overflow issue. This could result in unintended behavior during domain registration or expiration, such as prematurely expiring domains that are still within their valid or grace period—particularly since the expire entry point is publicly accessible.

Although the likelihood of an overflow is low, since it depends on a misconfigured grace_period, enabling overflow checks will prevent such edge-case vulnerabilities.

BVSS
Recommendation

It is recommended to enable overflow checks for release mode by setting the overflow-checks = true flag in the [profile.release] section of the Cargo configuration to ensure arithmetic operations are checked for overflows even in optimized builds.

Additionally, consider using safe arithmetic methods like checked_add instead of +, and make sure to handle overflows by returning an error or reverting with a clear message.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID. The team implemented a maximum grace period of one year to mitigate the risk of u64 overflow.

Remediation Hash

7.5 Missing Refund Mechanism For Excess CSPR

//

Low

Description

The payable functions within the controller and secondary market contracts accept all attached CSPR funds without refunding any excess amount when the attached payment exceeds the amount specified in the voucher. This behavior results in users unintentionally overpaying, as any surplus funds are retained by the contract instead of being reimbursed.

Code Location

Below is the implementation of the collect_cspr_payment function, which is responsible for collecting the attached CSPR tokens in payable functions:

fn collect_cspr_payment<P: Payment>(&self, voucher: &P) {
    let fee_collector = self
        .treasury
        .get_or_revert_with(ControllerError::FeeCollectorNotSet);
    let payment_info = voucher.payment_info();
    if self.env().attached_value() < payment_info.amount {
        self.revert(ControllerError::InsufficientPayment);
    }
    self.env()
        .transfer_tokens(&fee_collector, &payment_info.amount);
    self.env().emit_event(PaymentFulfilled {
        payment_id: payment_info.payment_id.clone(),
        buyer: payment_info.buyer,
        amount: payment_info.amount,
    });
}

BVSS
Recommendation

It is recommended to refund any excess CSPR to the user to ensure fair handling of funds and prevent unintended overpayments.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID. The transaction logic now reverts if the provided amount does not match the domain price.

Remediation Hash

7.6 Lacking Pausability Mechanism

//

Low

Description

The controller and registrar contracts currently lack a pausability mechanism. This feature is crucial for smart contracts, especially those handling significant assets or performing critical operations. A pause functionality allows the contract owner or designated parties to temporarily halt contract operations in case of emergencies, such as discovered vulnerabilities, unexpected behavior, or during upgrades.

BVSS
Recommendation

It is recommended to implement a pausability mechanism for the aforementioned contracts.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID. The involved contracts now enforce that all mutable, non-admin actions can only be performed when the contract is in an unpaused state.

Remediation Hash

7.7 Missing Error Definitions in Contract Schema

//

Low

Description

In the Odra framework, contract schemas are crucial for documenting contract behavior and enabling proper client integration. The framework uses the #[odra::module] attribute macro to define which errors and events should be included in the contract schema. Errors are defined with #[odra::odra_error] and events with #[odra::event], but they must be explicitly included in the module declaration to be part of the schema.

Each contract is missing its corresponding error declaration in the module:

  • Controller: Missing ControllerError

  • NameToken: Missing NameTokenError

  • Resolver: Missing ResolverError

  • Registrar: Missing RegistrarError

The incomplete data in the contract schema may result in the following issues:

  • The contract schema will be incomplete, missing important error definitions

  • Generated documentation will not include all possible error cases

  • External clients may not be able to properly handle all error scenarios


BVSS
Recommendation

It is recommended to include all contract-specific errors in the contract schema to ensure completeness and improve tooling support, developer experience, and documentation accuracy.

For example, in the case of the registrar contract, errors can be explicitly defined in the contract schema as follows:

#[odra::module(errors = [RegistrarError])]
pub struct Registrar

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID.

Remediation Hash

7.8 Incorrect Access Control in Whitelist Revocation

//

Low

Description

The revoke_whitelist function allows any whitelisted account to revoke the whitelist status of any other address. This is a violation of the principle of least privilege, as whitelisted users should not have administrative capabilities to manage other users' whitelist status. While the contract owner can re-whitelist themselves (since they control the whitelist function), this design flaw could be exploited by malicious whitelisted users to revoke other users' whitelist status, potentially causing disruption to legitimate users and to the availability of critical contract operations.

Code Location

Below is the implementation of the revoke_whitelist function:

pub fn revoke_whitelist(&mut self, address: Address) {
    let caller = self.env().caller();
    self.assert_whitelisted(&caller);
    self.whitelist.set(&address, false);
}

BVSS
Recommendation

It is recommended to restrict access to the revoke_whitelist function so that only the contract owner can invoke it — mirroring the access control already applied to the whitelist function.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID.

Remediation Hash

7.9 Double Cleanup of Default Resolver During Token Transfer

//

Informational

Description

When transferring tokens via admin_transfer or transfer_from by a whitelisted operator where the token's resolver is set to the default resolver, the contract performs two cleanup operations on the same token ID. This occurs because the function first calls self.cleanup(token_id), which performs a cleanup if the token's resolver is the default resolver, and then immediately calls self.default_resolver.cleanup(token_id) again after the transfer. With each cleanup operation, the nonce for the transferred token ID is incremented in the resolver contract. These redundant cleanup operations result in unnecessary gas consumption and cause the nonce to be incremented twice instead of once.

Code Location

Below is a code snippet of the admin_transfer function:

// if called by an operator
if caller != owner {
    self.cleanup(token_id);
    self.token.raw_transfer_from(owner, recipient, token_id);
    // make sure there were no previous records for the new owner
    self.default_resolver.cleanup(token_id);
} else {
    self.token.raw_transfer_from(owner, recipient, token_id);
}

Below is a code snippet of the transfer_from function:

 // if called by an operator
 if caller != owner {
    self.cleanup(token_id);
    self.token.transfer_from(from, to, token_id);
    self.default_resolver.cleanup(token_id);
} else {
    self.token.transfer_from(from, to, token_id);
}

Below is the implementation of the cleanup function:

fn cleanup(&mut self, token_id: U256) {
    let mut metadata = self.wrapped_metadata(token_id);
    let resolver = metadata.resolver().unwrap_or_revert(self);
    if resolver == Some(*self.default_resolver.address()) {
        self.default_resolver.cleanup(token_id);
    } else {
        let default_resolver = *self.default_resolver.address();
        metadata.set_resolver(default_resolver);
        self.set_token_metadata(token_id, metadata.to_vec());
    }
}

Below is the implementation of the resolver::cleanup function:

pub fn cleanup(&mut self, token_id: TokenId) {
    let env = self.env();
    let caller = env.caller();

    if !self.has_role(&DEFAULT_ADMIN_ROLE, &caller) && self.owner_of(token_id) != Some(caller) {
        self.env().revert(ResolverError::UnauthorizedCleanup);
    }
    self.nonces.add(&token_id, 1);

    env.emit_event(ResolutionCleared { token_id });
}

BVSS
Recommendation

It is recommended to:

  • modify the cleanup function to handle the default resolver cleanup in a single operation, and

  • remove the redundant self.default_resolver.cleanup(token_id) calls from both admin_transfer and transfer_from functions

The cleanup function should be updated to always perform the default resolver cleanup after handling the metadata updates, ensuring that the cleanup operation is performed exactly once per token transfer.


fn cleanup(&mut self, token_id: U256) {
    let mut metadata = self.wrapped_metadata(token_id);
    let resolver = metadata.resolver().unwrap_or_revert(self);

    if resolver != Some(*self.default_resolver.address()) {
        let default_resolver = *self.default_resolver.address();
        metadata.set_resolver(default_resolver);
        self.set_token_metadata(token_id, metadata.to_vec());
    }
    
    self.default_resolver.cleanup(token_id);
}

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID.

Remediation Hash

7.10 Unnecessary Storage Write on Revoke for Non-Whitelisted Addresses

//

Informational

Description

The current implementation does not check whether an address is actually whitelisted before attempting to revoke it. While not a security issue, this results in unnecessary gas consumption due to a redundant storage write (self.whitelist.set(&address, false))—writing the default false value for a non-whitelisted address. This could also lead to confusion, as users may expect the function to revert when revoking a non-whitelisted address.

Code Location

Below is the implementation of the revoke_whitelist function:

pub fn revoke_whitelist(&mut self, address: Address) {
    let caller = self.env().caller();
    self.assert_whitelisted(&caller);
    self.whitelist.set(&address, false);
}

BVSS
Recommendation

It is recommended to ensure that the address is whitelisted before revoking it, otherwise revert with a custom error, such as AddressNotWhitelisted.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID.

Remediation Hash

7.11 Reverse Resolver Allows Unnecessary Primary Name Updates

//

Informational

Description

The reverse resolver allows users to reassign their primary name even when the new value is identical to the existing one. This results in unnecessary state changes and event emissions, increasing gas costs without any functional benefit.

Code Location

Below is the implementation of the set_primary_name function:

pub fn set_primary_name(&mut self, primary_name: String) {
    // Load currently set primary name.
    let caller = self.env().caller();
    let current_primary_name = self.get_primary_name(&caller);
    // Update primary name.
    self.primary_names.set(&caller, primary_name.clone());
    // Emit event.
    self.env().emit_event(PrimaryNameChanged {
        address: caller,
        old_primary_name: current_primary_name,
        new_primary_name: primary_name,
    });
}

BVSS
Recommendation

It is recommended to restrict primary name updates to only when the new name differs from the currently set name.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID. The function now exits early with a return statement if the primary name is unchanged, avoiding unnecessary operations.

Remediation Hash

7.12 Missing Label Validation Allows Registration of Structurally Invalid and Unusable Domains

//

Informational

Description

The registrar contract lacks internal validation to reject structurally invalid domain labels—specifically, labels equal to "cspr" (the top-level domain) or those containing a dot ("."). While the responsibility for providing valid labels lies with the user, and either the controller signer is expected to approve only well-formed names or the admin is expected to register them correctly, there are no safeguards at the smart contract level to enforce this validation.


If either the controller inadvertently signs such a label or the admin mistakenly provides one, the contract will register it, resulting in domains that are technically valid on-chain but unusable within the system’s resolution logic. This includes failure to manage subdomains or resolve the domain as expected. Consequently, users may spend funds on domains that cannot be used functionally, leading to a poor experience and potential loss.

Code Location

Below is the implementation of the register function:

fn register(&mut self, names: Vec<NameMintInfo>) {
    let block_time = self.env().get_block_time();
    for info in names {
        self.assert_token_expires_in_future(info.token_expiration, block_time);

        // Compute token hash.
        let token_id = self.compute_namehash(&info.label);

        // Check if token already exists.
        let token_exists = self.name_token.token_exists(token_id);

        // If token exists and is expired and grace period is over, burn it.
        if token_exists {
            let metadata = self.wrapped_metadata(token_id);
            self.assert_token_expired(metadata.expiration().unwrap_or_revert(self), block_time);
            self.name_token.burn(token_id);
        }

        // Mint token.
        let metadata = NameTokenMetadata::with_resolver(
            &info.label,
            info.token_expiration,
            self.name_token.get_default_resolver(),
        );
        self.name_token
            .mint(info.owner, token_id, metadata.to_vec());
    }
}

BVSS
Recommendation

It is recommended to enforce strict validation of domain labels before completing the registration process, ensuring that only structurally sound domains can be registered.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID.

Remediation Hash

7.13 Missing Validation Of Contract Addresses

//

Informational

Description

Multiple contract initialization and setup functions lack validation to ensure address parameters are valid contract addresses:

  • marketplace::init(name_token)

  • controller::init(registrar)

  • registrar::init(name_token)

  • resolver::init(name_token)

  • resolver::set_name_token(name_token)

Without proper validation, these functions could be initialized with regular account addresses instead of contract addresses. This could lead to severe issues as the contracts attempt to call functions on non-contract addresses, resulting in failed transactions and potential protocol disruption. The contracts should implement address validation to ensure that only valid contract addresses are accepted during initialization and setup.

BVSS
Recommendation

It is recommended to verify that the specified addresses are indeed contract addresses by utilizing the is_contract function from the Address type. If an address is not a contract address, the transaction should be reverted. An example implementation is as follows:

if !address.is_contract() {
    self.revert(NameTokenError::InvalidContractAddress);
}

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID.

Remediation Hash

7.14 Unneeded Admin Role Assignment in Registrar Deployment

//

Informational

Description

When deploying the registrar contract, the set_admin_role function from the access_control module is invoked to configure the admin role for the controller role (CONTROLLER_ROLE). This admin role determines which entity can grant or revoke the controller role. However, the function is called with the default admin role (DEFAULT_ADMIN_ROLE) as the admin — which is already the default behavior if no admin is explicitly set.

As a result, this invocation has no practical effect and introduces unnecessary code.

Code Location

Below is a code snippet of the init function from the registrar module:

// Setup roles.
self.access_control
    .unchecked_grant_role(&DEFAULT_ADMIN_ROLE, &caller);
self.access_control
    .set_admin_role(&CONTROLLER_ROLE, &DEFAULT_ADMIN_ROLE);

Below is the implementation of the set_admin_role function from the access_control module:

pub fn set_admin_role(&mut self, role: &Role, admin_role: &Role) {
    let previous_admin_role = self.get_role_admin(role);
    self.role_admin.set(role, *admin_role);
    self.env().emit_event(RoleAdminChanged {
        role: *role,
        previous_admin_role,
        new_admin_role: *admin_role
    });
}

Below is the implementation of the get_role_admin function from the access_control module, which is used within grant_role and revoke_role functions:

/// Returns the admin role that controls `role`.
///
/// The admin role may be changed using [set_admin_role()](AccessControl::set_admin_role).
pub fn get_role_admin(&self, role: &Role) -> Role {
    let admin_role = self.role_admin.get(role);
    if let Some(admin) = admin_role {
        admin
    } else {
        DEFAULT_ADMIN_ROLE
    }
}

Score
(0.0)
Recommendation

It is recommended to remove the unnecessary set_admin_role function invocation.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID.

Remediation Hash

7.15 Incorrect Naming Can Lead to Logic Errors or Misuse

//

Informational

Description

In the registrar module, the compute_namehash function hashes the provided token name and derives a U256 token ID from it. However, the returned value within the resolve function is named token_hash, which is misleading — the variable is not a hash but a U256 representation derived from one. This inaccurate naming can cause confusion for developers and auditors, potentially leading to incorrect assumptions about the type or semantics of the value.

Additionally, in the resolver module, the cleanup function is misleadingly named. Despite implying state-clearing behavior, the function merely increments a nonce and does not remove or modify any stored resolution data. The previous resolutions remain on-chain and are simply rendered inaccessible. This may lead to misunderstanding of the function’s purpose, and could result in overestimating the contract’s ability to reclaim or prune storage.

Code Location

Below is a code snippet of the resolve function from the registrar module:

let token_hash = self.compute_namehash(&token_name);

Below is the implementation of the cleanup function from the resolver module:

pub fn cleanup(&mut self, token_id: TokenId) {
    let env = self.env();
    let caller = env.caller();

    if !self.has_role(&DEFAULT_ADMIN_ROLE, &caller) && self.owner_of(token_id) != Some(caller) {
        self.env().revert(ResolverError::UnauthorizedCleanup);
    }
    self.nonces.add(&token_id, 1);

    env.emit_event(ResolutionCleared { token_id });
}

Score
(0.0)
Recommendation

It is recommended to rename the token_hash variable to token_id to accurately reflect its type and purpose, aligning with the naming convention already used in the register function. Additionally, consider renaming the cleanup function to something more descriptive, such as invalidate_resolutions, to better convey its actual behavior.

Remediation Comment

SOLVED: The MAKE team solved this issue in the following commit IDs:


Remediation Hash

7.16 Lacking Event Emission

//

Informational

Description

The registrar, resolver and controller contracts implement admin-only functions that modify critical state variables without emitting events. The affected functions are the following:

  • registrar::set_grace_period

  • resolver::set_name_token

  • controller::set_signer_public_key

  • controller::set_treasury

The absence of events prevents external systems or users from tracking critical administrative changes via event logs. This lack of transparency diminishes visibility into admin actions that impact the protocol's behavior.

Score
(0.0)
Recommendation

It is recommended to emit an event whenever important contract state variables are updated.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID.

Remediation Hash

7.17 Inconsistencies In Documentation

//

Informational

Description

Several inconsistencies and issues were identified in the project’s inline documentation and comments:

Typographical Errors

The terms "preffered" and "controy" were found in comments within the reverse_resolver and controller modules, and should be corrected to "preferred" and "control", respectively. These typos diminish the perceived quality and professionalism of the codebase.

Misleading or Unclear Comments

In the registrar module, a comment stating "Consider removing this line." was found immediately before granting the controller role to the caller during installation. This suggests uncertainty or a lack of clarity regarding whether the caller should be granted the controller role — especially since the caller already holds the DEFAULT_ADMIN_ROLE, which allows them to grant the controller role themselves later. This could mislead future developers or auditors about the intended behavior.

Also in the registrar module, when reading a token_id from input, the comment reads "Compute token hash". However, no computation occurs at this stage — the value is simply being read.

Additionally, in the name_token module, the following comments incorrectly references the CEP78 standard instead of CEP95:

  • "NameToken contract. It is a CEP78 token with additional functionalities."

  • "Initializes CEP78 with the given name and symbol."


Score
(0.0)
Recommendation

It is recommended to correct the identified typographical errors and to remove the comment regarding the hash computation.

Additionally, review the logic used to assign the controller role during registrar installation. If role assignment is intentional, consider removing the ambiguous comment ("Consider removing this line."); if the assignment is redundant, remove the assignment itself.

Finally, replace all references to CEP78 with CEP95.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID.

Remediation Hash

7.18 Unnecessary Clone

//

Informational

Description

It was identified that the resolver address is being unnecessarily cloned each time it is accessed via its dedicated getter function. Since the resolver address is of type Option<Address> and implementing the Copy trait, cloning it introduces unnecessary overhead without providing any functional benefit. This results in minor inefficiencies and may indicate a misunderstanding of the type's intended semantics.

Code Location

Below is the implementation of the resolver function found in the data_structures file:

pub fn resolver(&self) -> OdraResult<Option<Address>> {
    Ok(self.resolver.clone())
}

Score
(0.0)
Recommendation

It is recommended to remove the unnecessary clone.

Remediation Comment

SOLVED: The MAKE team solved this issue in the specified commit ID.

Remediation Hash

8. Automated Testing

Static Analysis Report

Description

Halborn employed automated security scanners to aid in the detection of known security issues and vulnerabilities. One of the tools used was cargo audit, a security scanner that checks for vulnerabilities reported to the RustSec Advisory Database. This database stores all published vulnerabilities found on https://crates.io. The cargo audit tool provides a human-readable report based on this advisory database, scanning the Cargo.lock file. The scope of this analysis is limited to security detections only. To assist developers in maintaining the code, the auditors included the output of the dependencies tree alongside the scan results. This additional context helps identify which dependencies are affected by unmaintained or vulnerable crates.

 

ID

Package

Short Description

RUSTSEC-2024-0370

proc-macro-error 1.0.4

The proc-macro-error crate is unmaintained


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.

© Halborn 2025. All rights reserved.