Prepared by:
HALBORN
Last Updated 11/27/2024
Date of Engagement by: October 30th, 2024 - November 15th, 2024
100% of all REPORTED Findings have been addressed
All findings
9
Critical
0
High
1
Medium
3
Low
2
Informational
3
Bellum Exchange
engaged Halborn to conduct a security assessment on smart contracts beginning on October 30th, 2024 and ending on November 15th, 2024. The security assessment was scoped to the smart contracts provided to the Halborn team. Commit hashes and further details can be found in the Scope section of this report.
The team at Halborn dedicated 2 weeks and 3 days for the engagement and assigned one full-time security engineer to evaluate 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 assessment is to:
Ensure that smart contract functions operate as intended.
Identify potential security issues with the smart contracts.
In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which were mostly addressed by the Bellum Exchange team
. The main ones were the following:
Mitigate risks related to reentrancy.
Remove unlimited token approval to external contracts.
Allow emergency withdraw in case of contract compromise.
Follow industry best practices while developing and deploying smart contracts.
Halborn performed a combination of manual, semi-automated and automated security testing to balance efficiency, timeliness, practicality, and accuracy regarding 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 the code 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 and purpose.
Smart contract manual code review and walk-through.
Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any vulnerability classes
Manual testing by custom scripts.
Static Analysis of security for scoped contract, and imported functions. (Slither
)
Local deployment and testing ( Foundry
)
EXPLOITABILITY METRIC () | METRIC VALUE | NUMERICAL 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 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL 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 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
1
Medium
3
Low
2
Informational
3
Security analysis | Risk level | Remediation Date |
---|---|---|
Unfair Reward Distribution in BellumNursery | High | Risk Accepted - 11/26/2024 |
Potential Reentrancy Attack Vector in Token Creation and Launch Flow | Medium | Solved - 11/19/2024 |
Excessive Token Approvals to External DEX Routers | Medium | Solved - 11/19/2024 |
Emergency Withdrawal Function Restricted by Timelock | Medium | Solved - 11/19/2024 |
Lack of Two-Step Ownership Transfer | Low | Solved - 11/19/2024 |
Centralization Risk in Fee Distribution | Low | Solved - 11/19/2024 |
Missing Zero Address Validation | Informational | Solved - 11/19/2024 |
Hardcoded Constructor Constants | Informational | Solved - 11/19/2024 |
Missing Event Emissions for Critical State Changes | Informational | Acknowledged - 11/26/2024 |
// High
The reward distribution mechanism in BellumNursery unfairly allocates rewards based on current share ratio rather than historical stake duration, leading to reward dilution for long-term stakers.
Current implementation issues:
A user who stakes just before rewards calculation gets the same share as long-term stakers
Enables "reward sniping" where users can:
Monitor large reward deposits
Stake right before
Claim disproportionate rewards
Discourages long-term staking
Makes protocol vulnerable to reward manipulation
Example Scenario:
User A stakes 100 tokens for 30 days
Protocol accumulates 100 reward tokens
User B stakes 900 tokens just before reward distribution
Rewards are distributed:
User A gets: 10 tokens (despite staking for 30 days)
User B gets: 90 tokens (despite just staking)
Add the following function to provided foundry test file
Run with forge test --mt "test_staking" -vvvv
function test_staking() public {
deal(address(token), creator, 100e18);
deal(address(token), buyer, 100e18);
deal(owner, 500e18);
vm.prank(owner);
wavax.deposit{value: 500e18}();
vm.prank(owner);
wavax.transfer(address(nursery), 100e18);
vm.startPrank(creator);
token.approve(address(nursery), 50e18);
nursery.deposit(50e18);
vm.stopPrank();
skip(100 days);
nursery.getUnpaidEarnings(creator);
vm.startPrank(buyer);
token.approve(address(nursery), 50e18);
nursery.deposit(50e18);
vm.stopPrank();
skip(200 days);
nursery.getUnpaidEarnings(creator);
nursery.getUnpaidEarnings(buyer);
}
Output:
The new staker receives 50% of the rewards
It is suggested to implement reward checkpoints
struct Checkpoint {
uint256 timestamp;
uint256 totalShares;
uint256 rewardPerShare;
}
mapping(address => Checkpoint[]) public userCheckpoints;
function updateCheckpoint(address user) internal {
userCheckpoints[user].push(Checkpoint({
timestamp: block.timestamp,
totalShares: totalShares,
rewardPerShare: currentRewardPerShare
}));
}
RISK ACCEPTED: The Bellum Exchange team accepted the risk related to this finding.
// Medium
In BellumFactory
contract, the createToken()
function combined with the buy()
functionality exposes a potential reentrancy attack vector that could allow an attacker to manipulate conditions or drain AVAX from the contract.
The vulnerability exists because:
Contract holds AVAX balances during token creation and launch
External call pattern in buy() -> buy() -> launchToken()
flow that returns excess AVAX
No reentrancy guard on createToken()
State changes occur after external calls
Attack Scenario:
Attacker creates token with carefully calculated parameters
During buy()
execution, reaches final bin triggering _launchToken()
Before state updates, excess AVAX is returned via sendValue()
Attacker contract receives AVAX and reenters
Original execution continues with a compromised state
Attacker potentially extracts additional AVAX or manipulates launch conditions
While currently mitigated by token pricing mechanics, changes to pricing parameters or discovery of specific attack parameters could make this exploitable in the future.
Add ReentrancyGuard to BellumFactory::createToken()
:
function createToken(...) external payable nonReentrant returns (address) {
SOLVED: The suggested mitigation was implemented by the Bellum Exchange team.
// Medium
The BellumFactory
contract approves an unlimited amount (i.e.: type(uint256).max
) of newly created tokens to external DEX routers (TJ_ROUTER
or PHARAOH_ROUTER
). While this is done for convenience, it creates a security risk:
If either router contract is compromised through:
Implementation vulnerabilities
Upgradeable proxy attacks
Admin key compromise
Future bugs
The attacker could:
Drain all tokens held by the BellumFactory
Manipulate token launches
Extract value from the protocol
Impact multiple tokens simultaneously
Replace infinite approvals with exact amount approvals during token launch.
SOLVED: The suggested mitigation was implemented by the Bellum Exchange team.
// Medium
The BellumNursery.emergencyWithdrawal
function incorrectly includes the waitForWithdrawal
modifier, which enforces a 6-epoch waiting period before withdrawals. This fundamentally contradicts the purpose of an emergency withdrawal mechanism:
Current Implementation Issues:
Users must wait 6 epochs (36 hours) even in emergencies
Cannot quickly respond to:
Smart contract vulnerabilities
Market crashes
Protocol exploits
Network issues
Emergency Function Requirements:
Should provide immediate exit route
Critical for user fund safety
Standard DeFi safety mechanism
Expected behavior by users
Remove timelock from emergency withdrawal:
function emergencyWithdrawal() external nonReentrant
SOLVED: The function was removed from the contract.
// Low
The BellumFactory contract inherits from OpenZeppelin's Ownable but doesn't implement a two-step ownership transfer process. With the current single-step transfer, if the owner accidentally transfers ownership to an incorrect address, all owner-privileged functions will be permanently lost. This is particularly risky given the contract's critical fee management and pause functionality.
Implement a two-step ownership transfer pattern:
First transaction: Current owner indicates intended new owner
Second transaction: New owner accepts ownership
This can be achieved by using OpenZeppelin's Ownable2Step
instead of Ownable
.
SOLVED: The suggested mitigation was implemented by the Bellum Exchange team.
// Low
In BellumFactory contract, the token launch function violates the Checks-Effects-Interactions (CEI) pattern and introduces centralization risks through fee distribution:
1. CEI Pattern Violation:
External call (sendValue
) is made before important state changes
Token ownership is renounced but contract state isn't fully updated
LP creation happens after potentially failed fee transfer
2. Centralization Risks:
Single feeReceiver
address can block token launches
No fallback mechanism if fee transfer fails
Could be used maliciously to prevent specific tokens from launching
Implement pull pattern for fees which sends the fees to feeReceiver
:
// Add state tracking
uint256 public pendingFees;
function _launchToken(address token) internal {
// Calculate fee
uint256 fee = avaxToLP * migrationFee / BASIS;
// Update state first
if (fee > 0) {
pendingFees += fee;
}
// Complete token launch
IOwnable(token).renounceOwnership();
// ... rest of launch logic ...
emit TokenLaunched(token, fee);
}
// Separate fee collection function
function collectFees() external{
uint256 amount = pendingFees;
require(amount > 0, "No fees to collect");
pendingFees = 0;
(bool success, ) = feeReceiver.call{value: amount}("");
require(success, "Fee transfer failed");
emit FeesCollected(feeReceiver, amount);
}
SOLVED: All the instances, wherein the fees were sent to feeReceiver
, are now replaced with a pull pattern:
- feeReceiver.sendValue(createFee_);
+ pendingFees += createFee_;
// Informational
In BellumFactory
contract, the constructor accepts critical addresses (owner_
, tjRouter_
, pharaohRouter_
, incentivizer_
) without validating that they are not the zero address (0x0). If any of these parameters are accidentally set to the zero address, it could lead to locked functionality or loss of funds since:
Zero address owner would make the contract permanently ownerless
Invalid router addresses would break core trading functionality
Zero address incentivizer would lead to lost token incentives
Add zero address validation checks in the constructor:
require(owner_ != address(0), "Zero address not allowed for owner");
require(tjRouter_ != address(0), "Zero address not allowed for TJ router");
require(pharaohRouter_ != address(0), "Zero address not allowed for Pharaoh router");
require(incentivizer_ != address(0), "Zero address not allowed for incentivizer");
SOLVED: The suggested mitigation was implemented by the Bellum Exchange team.
// Informational
In BellumFactory
contract, the critical protocol parameters like tradingFee
, migrationFee
, ETHER
, BASIS
, BIN_WIDTH
, COEF
, MIN_IN
, MIN_OUT
, incentivizerFee
, createFee
, and percentPerEpoch
are hardcoded in the constructor.
Move constant values to constructor parameters. The other way is to define truly constant values (like BASIS = 10_000) as immutable state variables outside the constructor.
SOLVED: Constant values were defined for the given variables:
uint256 private constant ETHER = 1 ether;
uint256 private constant BIN_WIDTH = 2000;
uint256 private constant BASIS = 10000;
uint256 private constant COEF = 100;
uint256 private constant MIN_IN = 0.1 ether;
uint256 private constant MIN_OUT = 0.02 ether;
// Informational
In BellumFactory
contract, none of the admin functions that modify critical protocol parameters emit events. This includes changes to:
Fee receiver address
Trading fees
Pause state
Migration fee
Epoch percentage
Incentivizer fee
Creation fee
Without events, it becomes difficult to:
Track historical changes to important parameters
Build monitoring systems
Audit administrative actions
Add events for each state change:
// Declare events
event FeeReceiverChanged(address indexed oldReceiver, address indexed newReceiver);
event TradingFeeChanged(uint256 oldFee, uint256 newFee);
event PauseStateChanged(bool isPaused);
event MigrationFeeChanged(uint256 oldFee, uint256 newFee);
event PercentPerEpochChanged(uint256 oldPercent, uint256 newPercent);
event IncentivizerFeeChanged(uint256 oldFee, uint256 newFee);
event CreateFeeChanged(uint256 oldFee, uint256 newFee);
// Emit in functions
function changeFeeReceiver(address payable feeReceiver_) external onlyOwner {
address oldReceiver = feeReceiver;
feeReceiver = feeReceiver_;
emit FeeReceiverChanged(oldReceiver, feeReceiver_);
}
// Add similar event emissions to other state-changing functions
ACKNOWLEDGED: The Bellum Exchange team acknowledged this finding.
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 conducted a comprehensive review of all findings generated by the Slither static analysis tool. The risk of reentrancy in BellumFactory
was added to the report. The rest of the issues were either informational or false positives.
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