Sidekick Contracts - SideKick


Prepared by:

Halborn Logo

HALBORN

Last Updated 02/26/2025

Date of Engagement: December 20th, 2024 - December 24th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

21

Critical

0

High

1

Medium

3

Low

7

Informational

10


1. Introduction

SideKick engaged Halborn to conduct a security assessment on their smart contracts beginning on December 20th, 2024 and ending on December 24th, 2024. The security assessment was scoped to the smart contracts provided to Halborn. Commit hashes and further details can be found in the Scope section of this report.


The SideKick codebase in scope consists of a set of smart contracts that allow users to utilize an escrow contract and create transaction bundles for transferring ERC20 tokens.

2. Assessment Summary

Halborn was provided 3 days for the engagement and assigned 1 full-time security engineer to review the security of the smart contracts in scope. The engineer is a blockchain and smart contract security expert with advanced penetration testing and smart contract hacking skills, and deep knowledge of multiple blockchain protocols.

The purpose of the assessment is to:

    • Identify potential security issues within the smart contracts.

    • Ensure that smart contract functionality operates as intended.


In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which were mostly acknowledged by the SideKick team:

    • Add uniqueness validation for challenge IDs to avoid overwriting existing transactions.

    • Modify the createTransaction function to collect the total required amount upfront in a single transaction.

    • Add validations in to ensure that the total of serverAmount + feeForGasUsdt + calculatedFeeForSidekick cannot exceed the transaction amount.

3. Test Approach and Methodology

Halborn performed a combination of manual review of the code and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of smart contracts and can quickly identify items that do not follow security best practices.

The following phases and associated tools were used throughout the term of the assessment:

    • Research into architecture, purpose and use of the platform.

    • Smart contract manual code review and walkthrough to identify any logic issue.

    • Thorough assessment of safety and usage of critical Solidity variables and functions in scope that could led to arithmetic related vulnerabilities.

    • Local testing with custom scripts (Foundry).

    • Fork testing against main networks (Foundry).

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

4. RISK METHODOLOGY

Every vulnerability and issue observed by Halborn is ranked based on two sets of Metrics and a Severity Coefficient. This system is inspired by the industry standard Common Vulnerability Scoring System.
The two Metric sets are: Exploitability and Impact. Exploitability captures the ease and technical means by which vulnerabilities can be exploited and Impact describes the consequences of a successful exploit.
The Severity Coefficients is designed to further refine the accuracy of the ranking with two factors: Reversibility and Scope. These capture the impact of the vulnerability on the environment as well as the number of users and smart contracts affected.
The final score is a value between 0-10 rounded up to 1 decimal place and 10 corresponding to the highest security risk. This provides an objective and accurate rating of the severity of security vulnerabilities in smart contracts.
The system is designed to assist in identifying and prioritizing vulnerabilities based on their level of risk to address the most critical issues in a timely manner.

4.1 EXPLOITABILITY

Attack Origin (AO):
Captures whether the attack requires compromising a specific account.
Attack Cost (AC):
Captures the cost of exploiting the vulnerability incurred by the attacker relative to sending a single transaction on the relevant blockchain. Includes but is not limited to financial and computational cost.
Attack Complexity (AX):
Describes the conditions beyond the attacker’s control that must exist in order to exploit the vulnerability. Includes but is not limited to macro situation, available third-party liquidity and regulatory challenges.
Metrics:
EXPLOITABILITY METRIC (mem_e)METRIC VALUENUMERICAL VALUE
Attack Origin (AO)Arbitrary (AO:A)
Specific (AO:S)
1
0.2
Attack Cost (AC)Low (AC:L)
Medium (AC:M)
High (AC:H)
1
0.67
0.33
Attack Complexity (AX)Low (AX:L)
Medium (AX:M)
High (AX:H)
1
0.67
0.33
Exploitability EE is calculated using the following formula:

E=meE = \prod m_e

4.2 IMPACT

Confidentiality (C):
Measures the impact to the confidentiality of the information resources managed by the contract due to a successfully exploited vulnerability. Confidentiality refers to limiting access to authorized users only.
Integrity (I):
Measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of data stored and/or processed on-chain. Integrity impact directly affecting Deposit or Yield records is excluded.
Availability (A):
Measures the impact to the availability of the impacted component resulting from a successfully exploited vulnerability. This metric refers to smart contract features and functionality, not state. Availability impact directly affecting Deposit or Yield is excluded.
Deposit (D):
Measures the impact to the deposits made to the contract by either users or owners.
Yield (Y):
Measures the impact to the yield generated by the contract for either users or owners.
Metrics:
IMPACT METRIC (mIm_I)METRIC VALUENUMERICAL VALUE
Confidentiality (C)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Integrity (I)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Availability (A)None (A:N)
Low (A:L)
Medium (A:M)
High (A:H)
Critical (A:C)
0
0.25
0.5
0.75
1
Deposit (D)None (D:N)
Low (D:L)
Medium (D:M)
High (D:H)
Critical (D:C)
0
0.25
0.5
0.75
1
Yield (Y)None (Y:N)
Low (Y:L)
Medium (Y:M)
High (Y:H)
Critical (Y:C)
0
0.25
0.5
0.75
1
Impact II is calculated using the following formula:

I=max(mI)+mImax(mI)4I = max(m_I) + \frac{\sum{m_I} - max(m_I)}{4}

4.3 SEVERITY COEFFICIENT

Reversibility (R):
Describes the share of the exploited vulnerability effects that can be reversed. For upgradeable contracts, assume the contract private key is available.
Scope (S):
Captures whether a vulnerability in one vulnerable contract impacts resources in other contracts.
Metrics:
SEVERITY COEFFICIENT (CC)COEFFICIENT VALUENUMERICAL VALUE
Reversibility (rr)None (R:N)
Partial (R:P)
Full (R:F)
1
0.5
0.25
Scope (ss)Changed (S:C)
Unchanged (S:U)
1.25
1
Severity Coefficient CC is obtained by the following product:

C=rsC = rs

The Vulnerability Severity Score SS is obtained by:

S=min(10,EIC10)S = min(10, EIC * 10)

The score is rounded up to 1 decimal places.
SeverityScore Value Range
Critical9 - 10
High7 - 8.9
Medium4.5 - 6.9
Low2 - 4.4
Informational0 - 1.9

5. SCOPE

Files and Repository
(a) Repository: sidekick-contracts
(b) Assessed Commit ID: c116e40
(c) Items in scope:
  • contracts/SideKickTransfer.sol
  • contracts/Escrow.sol
  • contracts/DailyAction.sol
↓ 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

1

Medium

3

Low

7

Informational

10

Security analysisRisk levelRemediation Date
Challenge Ids can be overwrittenHighSolved - 01/14/2025
Insufficient funds collected during transaction creationMediumRisk Accepted - 01/14/2025
Unchecked arithmetic prevents payment processingMediumRisk Accepted - 01/07/2025
Non-standard ERC20 transfer handling could break core functionalityMediumNot Applicable - 12/30/2024
Fee rounding allows fee evasion through small transaction amountsLowRisk Accepted - 12/30/2024
Missing bounds on critical protocol parametersLowRisk Accepted - 12/30/2024
Phantom transactions artificially pollute Escrow contract stateLowRisk Accepted - 12/30/2024
Invalid boosts artificially pollute BoostSideKick contract stateLowRisk Accepted - 12/30/2024
Centralization risksLowRisk Accepted - 12/30/2024
Single step role transfer processLowRisk Accepted - 12/30/2024
Unbounded array iterationsLowSolved - 01/14/2025
Documentation mismatchInformationalAcknowledged - 01/14/2025
Use low level call method instead of transferInformationalAcknowledged - 01/14/2025
Missing eventsInformationalAcknowledged - 01/14/2025
Redundant logic in status handling functionsInformationalAcknowledged - 01/14/2025
Typo in enum stateInformationalAcknowledged - 01/14/2025
Inconsistent use of uint alias instead of uint256InformationalSolved - 01/14/2025
Global variables can be declared as immutableInformationalSolved - 01/14/2025
Unoptimized for loopsInformationalSolved - 01/14/2025
Use of outdated librariesInformationalAcknowledged - 01/14/2025
Use of magic numbersInformationalAcknowledged - 01/14/2025

7. Findings & Tech Details

7.1 Challenge Ids can be overwritten

//

High

Description

The createTransaction() function in the Escrow contract uses a challenge_id mapping to track transactions by their challenge identifier. However, there is no check to prevent duplicate _challengeId values. When a transaction is created with a _challengeId that already exists, the new transaction ID will overwrite the previous mapping entry in challenge_id[_challengeId], making the previous transaction unretrievable through getTransactionByChallengeId().


While the transaction data still exists in the transactions mapping and can be accessed directly by ID, the lack of challenge ID uniqueness breaks the expected one-to-one relationship between challenges and transactions. This could cause issues for external systems relying on challenge IDs to look up specific transactions.


Code Location

function createTransaction(
    address _receiver,
    uint _amount,
    string calldata _challengeId,
    address _serverOwner,
    uint _serverAmount
) external {
    _countTransactions.increment();
    uint _count = _countTransactions.current();

    uint calculatedFeeForSidekick = (_amount * feePercent) / 100;

    transactions[_count] = Transaction({
        id: _count,
        sender: msg.sender,
        receiver: _receiver,
        amount: _amount,
        deadline: block.timestamp + blocktime,
        status: Status.InProgress,
        challengeId: _challengeId,
        serverOwner: _serverOwner,
        serverAmount: _serverAmount,
        feeForSidekick: calculatedFeeForSidekick
    });

    challenge_id[_challengeId] = _count;
    recipients[_receiver].push(_count);
    customers[msg.sender].push(_count);

    usdt.safeTransferFrom(msg.sender, address(this), _amount);

    emit CreateTransactionEvent(
        msg.sender,
        _receiver,
        _amount,
        _count,
        block.timestamp + blocktime,
        _challengeId
    );
}
Proof of Concept

In the following scenario, a user Bob overwrites Alice's challenge ID:

function test_overwriteChallengeId() public {
  uint amount = 0;
  uint serverAmount = 0;
  address receiver = makeAddr("receiver");
  address serverOwner = makeAddr("serverOwner");
  string memory ChallengeForAlice = "ChallengeForAlice";
  
  vm.prank(alice);
  escrow.createTransaction(receiver, amount, ChallengeForAlice, serverOwner, serverAmount);
  Escrow.Transaction memory transaction = escrow.getTransactionByChallengeId(ChallengeForAlice);
  assertEq(transaction.sender, alice);

  vm.prank(bob);
  escrow.createTransaction(receiver, amount, ChallengeForAlice, serverOwner, serverAmount);
  transaction = escrow.getTransactionByChallengeId(ChallengeForAlice);
  assertEq(transaction.sender, bob); // bob overwrites the challenge id for alice
}
Test results
BVSS
Recommendation

Add uniqueness validation for challenge IDs to avoid overwriting existing transactions, e.g.:

if(challenge_id[_challengeId] != 0) revert ChallengeIdNotUnique();
Remediation

SOLVED: The SideKick team solved this finding in commit ff0d2aa by following the mentioned recommendation.

Remediation Hash
References

7.2 Insufficient funds collected during transaction creation

//

Medium

Description

The createTransaction() function in the Escrow contract collects the base amount from users, but fails to account for additional required fees in the initial transfer. While the contract calculates calculatedFeeForSidekick (fee percentage) and tracks serverAmount, the function only transfers the base amount from the sender.


When processPayment() is later called, it attempts to distribute:

  • The base amount minus fees to the recipient.

  • The serverAmount to the server owner.

  • The fees (feeForGasUsdt + feeForSidekick) to the fee address.

This creates a mismatch between collected and distributed funds that will cause the contract to have insufficient balance to process payments, leading to failed transactions and locked funds.


Code Location

function createTransaction(
    address _receiver,
    uint _amount,
    string calldata _challengeId,
    address _serverOwner,
    uint _serverAmount
) external {
    _countTransactions.increment();
    uint _count = _countTransactions.current();

    uint calculatedFeeForSidekick = (_amount * feePercent) / 100;

    transactions[_count] = Transaction({
        id: _count,
        sender: msg.sender,
        receiver: _receiver,
        amount: _amount,
        deadline: block.timestamp + blocktime,
        status: Status.InProgress,
        challengeId: _challengeId,
        serverOwner: _serverOwner,
        serverAmount: _serverAmount,
        feeForSidekick: calculatedFeeForSidekick
    });

    challenge_id[_challengeId] = _count;
    recipients[_receiver].push(_count);
    customers[msg.sender].push(_count);

    usdt.safeTransferFrom(msg.sender, address(this), _amount);

    emit CreateTransactionEvent(
        msg.sender,
        _receiver,
        _amount,
        _count,
        block.timestamp + blocktime,
        _challengeId
    );
}
Proof of Concept

In the following scenario, the transaction cannot be processed due to the contract having insufficient funds, breaking the contract's core functionality:

function _createTx(address sender, address receiver, uint256 amount, uint256 serverAmount, address _serverOwner, string memory challengeId) internal {
  amount = bound(amount, 1, 100 ether);
  deal(address(opBnb_usdt), sender, amount);
  vm.startPrank(sender);
  opBnb_usdt.approve(address(escrow), amount);
  escrow.createTransaction(receiver, amount, challengeId, _serverOwner, serverAmount);
  vm.stopPrank();
}

function test_paymentProcessFails() public {
  uint256 amount = 1 ether;
  uint256 serverAmount = 1;
  _createTx(alice, bob, amount, serverAmount, serverOwner, "aliceChallenge");

  skip(blocktime + 1);

  (, , , uint256 _amount, , Escrow.Status status, , , uint256 _serverAmount, uint256 _feeForSidekick) = escrow.transactions(1);

  uint256 totalExpected = _amount + _serverAmount + _feeForSidekick;
  uint256 currentBalance = opBnb_usdt.balanceOf(address(escrow));
  console.log("totalExpected: ", totalExpected);
  console.log("currentBalance: ", currentBalance);
  assert(totalExpected > currentBalance);

  vm.startPrank(admin);
  vm.expectRevert();
  escrow.processPayment(1);
}
Test results
BVSS
Recommendation

Modify the createTransaction function to collect the total required amount upfront by calculating the total amount needed and transferring it in a single transaction.

Remediation

RISK ACCEPTED: The SideKick team made a business decision to accept the risk of this finding and not alter the contracts, stating:

Agreed. However, this case is very unlikely to happen on our platform because users only need to authorize transactions, and all variables are always calculated by our backend.

References

7.3 Unchecked arithmetic prevents payment processing

//

Medium

Description

In the processPayment() function of the Escrow contract, there's a potential underflow risk when calculating transferAmount = transaction.amount - fee - serverAmount.


While Solidity 0.8.x includes overflow/underflow protection, the function would revert if fee + serverAmount > transaction.amount. This is problematic because these values are set independently - serverAmount during transaction creation, feeForGasUsdt by admin, and feeForSidekick calculated as a percentage. The combination of these values would exceed the transaction amount, causing the payment processing to fail and potentially locking funds in the contract.


Code Location

function processPayment(uint _transactionID) external onlyRole(ADMIN_ROLE) {
    Transaction memory transaction = transactions[_transactionID];
    
    bool isRefund = transaction.status == Status.Refund;
    

    if (!isRefund && transaction.status != Status.InProgress) {
        revert NotStatus();
    }
    if (isRefund && transaction.status != Status.Refund) {
        revert NotStatus();
    }

    if (!isRefund && transaction.deadline >= block.timestamp) {
        revert NotTime();
    }

    if (transaction.amount == 0) {
        revert TransferError();
    }


    address recipient = isRefund ? transaction.sender : transaction.receiver;
    uint fee = isRefund ? 0 : feeForGasUsdt + transaction.feeForSidekick;
    uint serverAmount = isRefund ? 0 : transaction.serverAmount;
    uint transferAmount = transaction.amount - fee - serverAmount;

    
    transaction.amount = 0;
    transaction.status = Status.Paided;
    transactions[_transactionID] = transaction;

    if (!isRefund) {
        if (transaction.serverAmount > 0) {
            usdt.safeTransfer(transaction.serverOwner, transaction.serverAmount);
        }
        if (fee > 0) {
            usdt.safeTransfer(feeAddress, fee);
        }
    }

    usdt.safeTransfer(recipient, transferAmount);

    emit PaymentProcessed(
        transaction.sender,
        recipient,
        transferAmount,
        _transactionID,
        transaction.challengeId,
        isRefund
    );
}
Proof of Concept

In the following scenario, the transaction cannot be processed due to an arithmetic underflow:

function _createTx(address sender, address receiver, uint256 amount, uint256 serverAmount, address _serverOwner, string memory challengeId) internal {
  amount = bound(amount, 1, 100 ether);
  deal(address(opBnb_usdt), sender, amount);
  vm.startPrank(sender);
  opBnb_usdt.approve(address(escrow), amount);
  escrow.createTransaction(receiver, amount, challengeId, _serverOwner, serverAmount);
  vm.stopPrank();
}

function test_paymentProcessFails() public {
  uint256 amount = 1 ether;
  uint256 serverAmount = 1;
  _createTx(alice, bob, amount, serverAmount, serverOwner, "aliceChallenge");

  skip(blocktime + 1);

  (, , , uint256 _amount, , Escrow.Status status, , , uint256 _serverAmount, uint256 _feeForSidekick) = escrow.transactions(1);

  uint256 totalExpected = _amount + _serverAmount + _feeForSidekick;
  uint256 currentBalance = opBnb_usdt.balanceOf(address(escrow));
  console.log("totalExpected: ", totalExpected);
  console.log("currentBalance: ", currentBalance);
  assert(totalExpected > currentBalance);

  vm.startPrank(admin);
  vm.expectRevert();
  escrow.processPayment(1);
}
Test results
BVSS
Recommendation

Add validations in to ensure that the total of serverAmount + feeForGasUsdt + calculatedFeeForSidekick cannot exceed the transaction amount.

Remediation

RISK ACCEPTED: The SideKick team made a business decision to accept the risk of this finding and not alter the contracts.

References

7.4 Non-standard ERC20 transfer handling could break core functionality

//

Medium

Description

The BoostSideKick contract assumes the USDT token implements the standard ERC20 interface, where transfer() returns a boolean value.


While this assumption holds true for the OpBNB's USDT token (deployed at 0x9e5aac1ba1a2e6aed6b32689dfcf62a509ca96f3), some prominent tokens like USDT on Ethereum mainnet don't return a boolean value from transfer(). If deployed with such non-standard tokens, the contract's boost() and payTo() functions would revert on every call due to this incompatibility, effectively breaking the core functionality of the contract.


Code Location

function boost(address boostWallet,address agentWallet, uint256 amount, string calldata boostId) external {
    if(isPause){
        revert Paused();
    }
    uint256 sidekickAmount = amount * sidekickPercentage / 100;
    uint256 leaderboardAmount = amount - sidekickAmount;
    
    totalLeaderboardAmount += leaderboardAmount;
    totalAmount += amount;
    count++;
    boosts[count] = BoostInfo(boostWallet,msg.sender,agentWallet,amount, block.timestamp, boostId);

    if (!(usdt.transferFrom(msg.sender, address(this), leaderboardAmount))) {
        revert TransferError();
    }

    if (!(usdt.transferFrom(msg.sender, sidekickWallet, sidekickAmount))) {
        revert TransferError();
    }
    emit Boost(boostWallet, msg.sender, agentWallet , amount, block.timestamp, sidekickPercentage,boostId);
}

function payTo(address[] calldata recipients, uint256[] calldata amounts) external  onlyAdmin {
    if(recipients.length != amounts.length){
        revert LessInputs();
    }

    for (uint256 i = 0; i < recipients.length; i++) {

    boostsWinners[recipients[i]] += amounts[i];
    if (!(usdt.transfer(recipients[i], amounts[i]))) {
        revert TransferError();
    }
    emit PayTo(recipients[i],amounts[i], block.timestamp);
    }
    
}
Proof of Concept

In the following scenario with Ethereum's USDT contract, the contract's core functionality fails on every call.

function test_payToFailsWithNonCompliantTokens() public {
  vm.createSelectFork("mainnet");
  boostSidekick = new BoostSideKick(sideKickWallet, address(ethereumUsdt));
  deal(address(ethereumUsdt), address(boostSidekick), 100 ether);

  uint256 initialContractBalance = ethereumUsdt.balanceOf(address(boostSidekick));
  uint256 initialRecipientBalance = ethereumUsdt.balanceOf(address(this));

  console.log("balance of contract", initialContractBalance);
  console.log("balance of recipient", initialRecipientBalance);

  address[] memory recipients = new address[](1);
  recipients[0] = address(this);
  uint256[] memory amounts = new uint256[](1);
  amounts[0] = 100;

  vm.startPrank(sideKickWallet);
  vm.expectRevert();
  boostSidekick.payTo(recipients, amounts);
}
Test results
BVSS
Recommendation

Use OpenZeppelin's SafeERC20 library throughout all contracts and for all token transfers to handle non-standard ERC20 implementations.


Remediation

NOT APPLICABLE: The finding has been marked as not applicable as the SideKick team states that:

We are not operating on the ETH chain, only on the opBNB chain (204).

References

7.5 Fee rounding allows fee evasion through small transaction amounts

//

Low

Description

In Escrow, the createTransaction() function calculates calculatedFeeForSidekick as a percentage of the transaction amount using integer division.


Due to integer division rounding down, users can strategically choose small transaction amounts to minimize or completely avoid fees, impacting protocol revenue. For example, with current feePercent = 5, any transaction amount under 20 wei will result in calculatedFeeForSidekick = 0 due to Solidity's integer division truncation.


Code Location

uint calculatedFeeForSidekick = (_amount * feePercent) / 100;
BVSS
Recommendation

Implement a minimum transaction amount that ensures meaningful fee collection and consider using basis points (10000) instead of percentage ( 100 ) for more precise fee calculations.

Remediation

RISK ACCEPTED: The SideKick team made a business decision to accept the risk of this finding and not alter the contracts, stating:

Although we agree on this point, we do not think it is valid for our business. In order to take advantage of our smart contract, users need to use our application, in which we prohibit amounts less than 0.1 USDT.

References

7.6 Missing bounds on critical protocol parameters

//

Low

Description

Throughout the files in scope, several critical admin-controlled parameters lack appropriate validation:


In BoostSideKick contract:

  • ThesidekickPercentage and feePercent >= 100% could take entire transaction amount or fail.

  • The changeSidekickWallet() allows setting zero address.

  • The sidekickWallet could be set to the zero address in the constructor.

  • The usdt could be set to the zero address in the constructor.


In Escrow contract:

  • The feePercent set in the constructor and setFeePercent lacks an upper bound (could be set > 100%).

  • The usdt could be set to the zero address in the constructor.

  • The blocktime set in the constructor and setBlockTime could be set to impractically high values.

  • The feeForGasUsdt set in the constructor and setfeeForGasUsdt has no maximum limit.


In SideKickTransfer contract

  • The usdt could be set to the zero address in the constructor.

  • The adminWallet could be set to the zero address in the constructor.

  • The feePercentage set in the constructor lacks an upper bound, and in setFeePercentage it could be set to 100%.


Misconfigured parameters could disrupt protocol economics and ultimately render the codebase unusable.

BVSS
Recommendation

Validate all constructor parameters, adding reasonable bounds for fees, block time bounds and zero-address checks.

Remediation

RISK ACCEPTED: The SideKick team made a business decision to accept the risk of this finding and not alter the contracts, stating:

Agreed, but we believe that we can ignore this as it is low risk.

References

7.7 Phantom transactions artificially pollute Escrow contract state

//

Low

Description

The createTransaction() function in the Escrow contract allows the creation of transactions with zero and uncapped amounts, zero addresses for receiver and serverOwner, and empty challengeIds. These phantom and invalid transactions can:


1. Increment the global transaction counter ( _countTransactions);

2. Create transactions with invalid receivers (e.g. zero address).

3. Store empty/zero-value entries in the transactions mapping.

4. Update recipient/customer mappings with meaningless IDs.

5. Emit CreateTransactionEvent events with zero values.

6. Consume gas and storage for no legitimate business purpose.

7. If _serverAmount is set higher than _amount, the transaction will be created but will inevitably fail during processPayment() due to arithmetic underflow when calculating transferAmount = transaction.amount - fee - serverAmount.


In conclusion, this allows malicious actors to artificially inflate transaction counts and pollute contract state/event logs, making it harder to track legitimate transactions and potentially impacting external systems relying on this data.

BVSS
Recommendation

Add input validation in createTransaction() to:

1. Require non-zero amounts.

2. Require valid receivers.

3. If _serverAmount > 0, require valid _serverOwner .

4. Require non-empty and unique _challengeId.

5. Add a maximum percentage cap for _serverAmount relative to _amount .

Remediation

RISK ACCEPTED: The SideKick team made a business decision to accept the risk of this finding and not alter the contracts, stating:

Agreed. However, this case is very unlikely to happen on our platform because users only need to authorize transactions, and all variables are always calculated by our backend.

References

7.8 Invalid boosts artificially pollute BoostSideKick contract state

//

Low

Description

The boost() function in BoostSideKick allows users to create boosts with arbitrary amounts with verifying against zero amounts. Additionally, the function accepts boostWallet and agentWallet parameters without any address validation and accepts a boostId parameter as a string without any validation. When this occurs, the following actions still take place:


  • The global count is incremented.

  • A BoostInfo struct is stored in the boosts mapping.

  • A Boost event is emitted.

  • A zero boostWallet would create a boost record with an invalid recipient, affecting reward distribution tracking.

  • A zero agentWallet would incorrectly attribute agent participation in the boost system.

  • Multiple boosts can be created with identical IDs.

  • Empty strings are allowed as valid boostId values.


This allows users to artificially inflate boost counts and emit meaningless events, potentially making it harder to track legitimate boosts. While no funds are at risk, this can alter the contract's state and creates unnecessary data bloat.

BVSS
Recommendation

Add a minimum amount check in the boost() function and add zero-address validation. Additionally, enforce uniqueness for the boostId parameter and require non-empty boostId strings.

Remediation

RISK ACCEPTED: The SideKick team made a business decision to accept the risk of this finding and not alter the contracts, stating:

Agreed. However, this case is very unlikely to happen on our platform because users only need to authorize transactions, and all variables are always calculated by our backend.

References

7.9 Centralization risks

//

Low

Description

The contract implements multiple privileged functions that grant the owner or ADMIN_ROLE excessive control over core protocol functionality. For example:

  • The decisionDeal() function allows the ADMIN_ROLE to unilaterally change the status of a transaction to Refund.

  • The processPayment() , pay() and executeTransaction() functions allows the ADMIN_ROLE to unilaterally process payments, potentially bypassing normal transaction flow.

  • Several crucial values (fees, block times and addresses) and contract states (paused state) can be changed by the ADMIN_ROLE without time constraints or multi-signature requirements.


While administrative functions are necessary for protocol maintenance and emergency response, the current implementation concentrates too much unconstrained power in the admin role. This creates excessive trust requirements and potential for abuse, as a compromised or malicious admin could:

  • Force refunds on valid transactions

  • Manipulate fees to extract excessive value

  • Pause functionality indefinitely

  • Change critical addresses without notice

  • Process transactions out of order

BVSS
Recommendation

Several remedial strategies can be employed, including but not limited to: transitioning control to a multi-signature wallet setup for critical functions, establishing community-driven governance for decision-making on fund management, and/or integrating time locks.

Remediation

RISK ACCEPTED: The SideKick team made a business decision to accept the risk of this finding and not alter the contracts, stating:

This is very important for our business model to act as auditors for every operation between our users. Therefore, we need the ability to decide on every challenge in order to maintain fairness for all users on our platform. Every admin role is granted only to secure and trained employees, which helps us mitigate risks.

References

7.10 Single step role transfer process

//

Low

Description

Throughout the files in scope, several critical admin-controlled parameters lack appropriate validation. For example:


  • In the BoostSideKick contract, the changeSidekickWallet() functions directly updates the admin address. This address controls critical functions like payTo(), resetLeaderboard(), and changeSidekickPercentage().

  • In the Escrow contract, the setfeeForGasUsdt() directly updates fee recipient address.



Regarding this, it is crucial that the address to which the role is transferred is verified to be active and willing to assume the role's responsibilities. Otherwise, the contract could be locked in a situation where it is no longer possible to make administrative changes to it

BVSS
Recommendation

Consider implementing a two-step ownership transfer pattern for both contracts. For critical addresses like sidekickWallet and feeAddress, first store a pending value that must be explicitly accepted by the new address within a timelock period. This pattern significantly reduces risk of accidental lockouts while maintaining administrative flexibility.

Remediation

RISK ACCEPTED: The SideKick team made a business decision to accept the risk of this finding and not alter the contracts, stating:

Agreed. However, only current admin can change those settings, therefore, we pay attention carefully to that.

References

7.11 Unbounded array iterations

//

Low

Description

In the Escrow contract, two view functions contain unbounded iterations that could hit block gas limits as transaction history grows. The getTotal() function iterates through all user transactions via recipients[_user] or customers[_user] arrays with no bounds. Similarly, the getRedeemable() function performs a full iteration of recipient transactions plus array copying operations.


This creates a scaling risk where these functions become unusable when users accumulate many transactions over time and transaction arrays grow beyond gas block limits, possibly failing to query data when hitting the block gas limit.

BVSS
Recommendation

Consider including:

  • Batch Processing: Instead of processing all shares in one transaction, implement a batching mechanism. This would allow processing a fixed number of transactions per call.

  • Pagination: Implement a pagination mechanism that processes a subset of transactions in each call. This could be done by adding start and end parameters to the function, allowing external callers to iterate through the transactions in manageable chunks.

Remediation

SOLVED: The SideKick team solved this finding in commit ff0d2aa by following the mentioned recommendation.

Remediation Hash
References

7.12 Documentation mismatch

//

Informational

Description

The documentation provided for the BoostSideKick contract deployed at 0x0aE03460d31395Fc72be8588fB0756558930e754 is inconsistent with the actual contract code.


For instance, the documentation references affiliatePartner and affiliatePercent parameters in the boost() function, but the actual contract code does not contain these parameters. This inconsistency could lead to confusion for users and developers interacting with the contract.

BVSS
Recommendation

Update the contract documentation to match the actual contract code to avoid confusion and misinterpretation.


Additionally, add NATSPEC comments to all functions and declared variables in the contracts to provide clear explanations of their purpose, inputs, outputs, and any other relevant information. This will help improve code readability, maintainability, and overall quality.

Remediation

ACKNOWLEDGED: The SideKick team made a business decision to acknowledge this finding and not alter the contracts.

References

7.13 Use low level call method instead of transfer

//

Informational

Description

The transferEtherFromAdmin() function from the deployed Escrow contract uses Solidity's transfer method to send native assets (e.g., Ether) from the smart contract. Although the use of transfer has been a standard practice for sending native assets due to its built-in reentrancy protection (since it only forwards 2300 gas, preventing called contracts from performing state changes), it is not considered best practice. The gas cost of EVM instructions may change significantly during hard forks, which may break already deployed contract systems that make fixed assumptions about gas costs.

BVSS
Recommendation

Transfer native assets via the low-level call method instead, e.g:

(bool success, ) = recipient.call{value: msg.value}("");
if (!success) revert TransferError();
Remediation

ACKNOWLEDGED: The SideKick team made a business decision to acknowledge this finding and not alter the contracts.

References

7.14 Missing events

//

Informational

Description

Throughout the contracts in scope, there are instances where administrative functions change contract state by modifying core state variables. However, these changes are not reflected in any event emission.

Instances of this issue can be found in:

  • BoostSideKick.resetLeaderboard()

  • BoostSideKick.changeSidekickPercentage()

  • BoostSideKick.changePause()

  • BoostSideKick.changeSidekickWallet()

  • Escrow.setBlockTime()

  • Escrow.setfeeForGasUsdt()

  • Escrow.setFeePercent()

  • SideKickTransfer.sendUSDT()

  • SideKickTransfer.setFeePercentage()

BVSS
Recommendation

Emit events for all state changes that occur as a result of administrative functions to facilitate off-chain monitoring of the system.

Remediation

ACKNOWLEDGED: The SideKick team made a business decision to acknowledge this finding and not alter the contracts.

References

7.15 Redundant logic in status handling functions

//

Informational

Description

In the Escrow contract, two functions contain redundant or unnecessary logic that increases code complexity and gas costs without providing benefits:


  1. TheprocessPayment() includes a logically impossible condition:

  • isRefund is defined as transaction.status == Status.Refund so the check if (isRefund && transaction.status != Status.Refund) can never be true.


2. ThedecisionDeal() function accepts a Status parameter but enforces only one value because only Status.Refund is accepted as valid input.

BVSS
Recommendation

Simplify the functions by removing or updating the redundant logic accordingly.

Remediation

ACKNOWLEDGED: The SideKick team made a business decision to acknowledge this finding and not alter the contracts.

References

7.16 Typo in enum state

//

Informational

Description

In the Escrow contract, there is a typo in the Status enum, where the word Paid is misspelled as Paided.

While these typo do not affect the functionality of the code, it can make the codebase harder to read and understand.


BVSS
Recommendation

Correct the typo in the Status enum in Escrow.sol.

Remediation

ACKNOWLEDGED: The SideKick team made a business decision to acknowledge this finding and not alter the contracts.

References

7.17 Inconsistent use of uint alias instead of uint256

//

Informational

Description

Throughout the code in scope, it has been noted that there is inconsistent use of uint and uint256 variable declarations, where some variables are declared as uint and others as uint256. While this does not affect the functionality of the code, it is recommended to use explicit size declaration for integers to ensure consistency and readability.

BVSS
Recommendation

Use explicit size declaration for integers. For example, use uint256 instead of uint.


Remediation

SOLVED: The SideKick team solved this finding in commit ff0d2aa by following the mentioned recommendation.

Remediation Hash
References

7.18 Global variables can be declared as immutable

//

Informational

Description

Throughout the contracts, several state variables are set in the constructor and never updated again. They could be declared as immutable to save gas.


The immutable keyword was added in Solidity in 0.6.5, where state variables can be marked with the immutable keyword, which causes them to be read-only, but only assignable in the constructor.

BVSS
Recommendation

Modify the state variables to be immutable if they are not intended to be changed after initialization.

Remediation

SOLVED: The SideKick team solved this finding in commit ff0d2aa by following the mentioned recommendation.

Remediation Hash
References

7.19 Unoptimized for loops

//

Informational

Description

Throughout the code in scope, there are several instances of unoptimized for loop declarations that may incur in higher gas costs than necessary, or some instances where the unnecessary use of a unchecked block is present.

BVSS
Recommendation

Optimize the for loop declarations to reduce gas costs. Best practices include:


  • The non-redundant initialization of the iterator with a default value (declaring simply i is equivalent to i = 0 but more gas efficient),

  • The use of the pre-increment operator (inside an unchecked block if using Solidity >=0.8.0 and <= 0.8.21 :unchecked {++i}, or simply ++i if compiling with Solidity >=0.8.22).


Additionally, when reading from storage variables, it is recommended to reduce gas costs significantly by caching the array to read locally and iterate over it to avoid reading from storage on every iteration. Moreover, if there are several loops in the same function, the i variable can be re-used, to be able to set the value from non-zero to zero and reduce gas costs without additional variable declaration. For example:

uint256[] memory arrayInMemory = arrayInStorage;

uint256 i;
for (; i < arrayInMemory.length ;) {
  // code logic
  unchecked { ++i; }
}

delete i;

uint256[] memory arrayInMemory2 = arrayInStorage2;

for (; i < arrayInMemory2.length ;) {
  // code logic
  unchecked { ++i; }
}
Remediation

SOLVED: The SideKick team solved this finding in commit ff0d2aa by following the mentioned recommendation.

Remediation Hash
References

7.20 Use of outdated libraries

//

Informational

Description

Throughout the codebase, several OpenZeppelin contracts implementations are inherited and used. However, the version of these contracts are outdated (4.9.2) and may contain vulnerabilities that may have been fixed in newer versions (latest stable version is 5.1.0).


For more reference about OpenZeppelin contracts versions and their vulnerabilities, see https://security.snyk.io/package/npm/@openzeppelin%2Fcontracts

BVSS
Recommendation

Consider updating all OpenZeppelin contracts to the latest versions to benefit from the latest security patches and improvements.

Remediation

ACKNOWLEDGED: The SideKick team made a business decision to acknowledge this finding and not alter the contracts.

References

7.21 Use of magic numbers

//

Informational

Description

Throughout the files in scope, particularly in Escrow and BoostSideKick contracts, there are instances where magic numbers are used.


Magic numbers are direct numerical or string values used in the code without any explanation or context. This practice can lead to code maintainability issues, potential errors, and difficulty in understanding the code's logic.

BVSS
Recommendation

To improve the code’s readability and facilitate refactoring, consider defining constants for magic numbers used, giving it clear and self-explanatory names.

Remediation

ACKNOWLEDGED: The SideKick team made a business decision to acknowledge this finding and not alter the contracts.

References

8. 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 repository 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.

The security team assessed all findings identified by the Slither software, however, findings with related to external dependencies are not included in the below results for the sake of report readability.

Output

The findings obtained as a result of the Slither scan were reviewed, and the majority were not included in the report because they were determined as false positives.

Slither results (1)Slither results (2)

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.