Prepared by:
HALBORN
Last Updated 04/26/2024
Date of Engagement by: March 15th, 2022 - March 28th, 2022
100% of all REPORTED Findings have been addressed
All findings
8
Critical
0
High
0
Medium
0
Low
3
Informational
5
\client engaged Halborn to conduct a security audit on their smart contracts beginning on 2022-03-15 and ending on 2022-03-28. The security assessment was scoped to the smart contracts provided to the Halborn team.
The team at Halborn was provided two weeks for the engagement and assigned a full-time security engineer to audit the security of the smart contract. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.
The purpose of this audit is to:
Ensure that smart contract functions operate as intended
Identify potential security issues with the smart contracts
In summary, Halborn identified some security risks that were mostly addressed by the \client team.
Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy regarding the scope of the smart contract audit. 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 audit:
Research into architecture and purpose.
Smart Contract manual code review and walkthrough.
Graphing out functionality and contract logic/connectivity/functions
Manual Assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes.
Dynamic Analysis (ganache-cli
, brownie
, hardhat
)
Static Analysis(slither
)
\begin{enumerate} \item \href{https://github.com/bcnmy/SM-smart-contract/tree/master/contracts}{Safety Module} \begin{enumerate} \item AaveDistributionManager.sol \item BicoProtocolEcosystemReserve.sol \item DistributionTypes.sol \item InitializableAdminUpgradeabilityProxy.sol \item StakedTokenBptRev2.sol \item StakedTokenV3.sol \item lib/GovernancePowerDelegationERC20.sol \item lib/GovernancePowerWithSnapshot.sol \item lib/VersionedInitializable.sol \end{enumerate} \item Out-of-Scope Contracts \begin{enumerate} \item helper/.sol \item interface/.sol \item lib/.sol \item mock/.sol \end{enumerate} \end{enumerate}
Critical
0
High
0
Medium
0
Low
3
Informational
5
Impact x Likelihood
HAL-02
HAL-03
HAL-05
HAL-06
HAL-07
HAL-08
HAL-04
HAL-01
Security analysis | Risk level | Remediation Date |
---|---|---|
IGNORED RETURN VALUES | Low | Solved - 04/08/2022 |
MISSING ZERO ADDRESS CHECKS | Low | Solved - 04/08/2022 |
MISSING REENTRANCY GUARD | Low | Solved - 04/08/2022 |
EXPERIMENTAL KEYWORD USAGE | Informational | Acknowledged |
FLOATING PRAGMA | Informational | Solved - 04/08/2022 |
USE 1E18 CONSTANT FOR GAS OPTIMIZATION | Informational | Solved - 04/08/2022 |
MISUSE OF PUBLIC FUNCTIONS | Informational | Solved - 04/08/2022 |
USE ++I INSTEAD OF I++ IN LOOPS FOR GAS OPTIMIZATION | Informational | Acknowledged |
// Low
The return value of an external call is not stored in a local or state variable. In the BicoProtocolEcosystemReserve.sol
contract, there is a function that ignores the return value.
function transfer(
IERC20 token,
address recipient,
uint256 amount
) external onlyFundsAdmin {
token.transfer(recipient, amount);
}
SOLVED: The issue was solved in commit ID: 20611133e6edd306f15840a2e5735577f1f08050
// Low
Safety Module contracts have address fields in multiple functions. These functions are missing address validations. Each address should be validated and checked to be non-zero. This is also considered as a best practice.
During testing, it has been found that some of these inputs are not protected against using address(0)
as the target address.
constructor(address emissionManager, uint256 distributionDuration) public {
DISTRIBUTION_END = block.timestamp.add(distributionDuration);
EMISSION_MANAGER = emissionManager;
}
function _setFundsAdmin(address admin) internal {
_fundsAdmin = admin;
emit NewFundsAdmin(admin);
}
constructor(
IERC20 stakedToken,
IERC20 rewardToken,
uint256 cooldownSeconds,
uint256 unstakeWindow,
address rewardsVault,
address emissionManager,
uint128 distributionDuration,
string memory name,
string memory symbol,
uint8 decimals,
address governance
) public ERC20(name, symbol) AaveDistributionManager(emissionManager, distributionDuration) {
STAKED_TOKEN = stakedToken;
REWARD_TOKEN = rewardToken;
COOLDOWN_SECONDS = cooldownSeconds;
UNSTAKE_WINDOW = unstakeWindow;
REWARDS_VAULT = rewardsVault;
_aaveGovernance = ITransferHook(governance);
ERC20._setupDecimals(decimals);
}
constructor(
IERC20 stakedToken,
IERC20 rewardToken,
uint256 cooldownSeconds,
uint256 unstakeWindow,
address rewardsVault,
address emissionManager,
uint128 distributionDuration,
string memory name,
string memory symbol,
uint8 decimals,
address governance
) public ERC20(name, symbol) AaveDistributionManager(emissionManager, distributionDuration) {
STAKED_TOKEN = stakedToken;
REWARD_TOKEN = rewardToken;
COOLDOWN_SECONDS = cooldownSeconds;
UNSTAKE_WINDOW = unstakeWindow;
REWARDS_VAULT = rewardsVault;
_aaveGovernance = ITransferHook(governance);
ERC20._setupDecimals(decimals);
}
SOLVED: The issue was solved in commit ID: 20611133e6edd306f15840a2e5735577f1f08050
// Low
To protect against cross-function re-entrancy attacks, it may be necessary to use a mutex. By using this lock, an attacker can no longer exploit the withdrawal function with a recursive call. OpenZeppelin has its own mutex implementation called ReentrancyGuard
which provides a modifier to any function called nonReentrant
that protects the function with a mutex against re-entrancy attacks.
StakedTokenBptRev2::redeem()
StakedTokenBptRev2::claimRewards()
StakedTokenV3::redeem()
StakedTokenV3::claimRewards()
SOLVED: The issue was solved in commit ID: 20611133e6edd306f15840a2e5735577f1f08050
// Informational
ABIEncoderV2 is enabled and using experimental features could be dangerous in live deployments. The experimental ABI encoder does not handle non-integer values shorter than 32 bytes properly. This applies to bytesNN types, bool, enum and other types when they are part of an array or a struct and encoded directly from storage. This means these storage references have to be used directly inside abi.encode(...)
as arguments in external function calls or in event data without prior assignment to a local variable. The types bytesNN and bool will result in corrupted data, while enum might lead to an invalid revert.
pragma experimental ABIEncoderV2;
pragma experimental ABIEncoderV2;
pragma experimental ABIEncoderV2;
ACKNOWLEDGED: The Biconomy team
acknowledged this issue.
// Informational
The project contains many instances of floating pragma. Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, either an outdated compiler version that might introduce bugs that affect the contract system negatively or a pragma version too recent which has not been extensively tested.
AaveDistributionManager.sol::pragma solidity ^0.7.5;
DistributionTypes.sol::pragma solidity ^0.7.5;
StakedTokenBptRev2.sol::pragma solidity ^0.7.5;
StakedTokenV3.sol::pragma solidity ^0.7.5;
GovernancePowerDelegationERC20.sol::pragma solidity ^0.7.5;
GovernancePowerWithSnapshot.sol::pragma solidity ^0.7.5;
VersionedInitializable.sol::pragma solidity ^0.7.5;
SOLVED: The issue was solved in commit ID: 20611133e6edd306f15840a2e5735577f1f08050
// Informational
In Solidiy, the exponentiation operation (**
) costs up to 10 gas. It is possible to consume less gas to calculate the prices of the tokens if DECIMAL variable is fixed.
function _getRewards(
uint256 principalUserBalance,
uint256 reserveIndex,
uint256 userIndex
) internal pure returns (uint256) {
return principalUserBalance.mul(reserveIndex.sub(userIndex)).div(10**uint256(PRECISION));
}
uint256 currentTimestamp =
block.timestamp > DISTRIBUTION_END ? DISTRIBUTION_END : block.timestamp;
uint256 timeDelta = currentTimestamp.sub(lastUpdateTimestamp);
return
emissionPerSecond.mul(timeDelta).mul(10**uint256(PRECISION)).div(totalBalance).add(
currentIndex
);
}
SOLVED: The issue was solved in commit ID: 20611133e6edd306f15840a2e5735577f1f08050
// Informational
In public functions, the array arguments are immediately copied into memory, while external functions can read directly from the calldata. Reading calldata is cheaper than allocating memory.
Public functions need to write arguments to memory because public functions can be called internally. Internal calls are passed internally via pointers to memory. Therefore, a function expects its arguments to be located in memory when the compiler generates the code for an internal function.
AaveDistributionManager.getUserAssetData(address,address)
BicoProtocolEcosystemReserve.setFundsAdmin(address)
StakedTokenBptRev2.delegateByTypeBySig(address,IGovernancePowerDelegationToken.DelegationType,uint256,uint256,uint8,bytes32,bytes32)
StakedTokenBptRev2.delegateBySig(address,uint256,uint256,uint8,bytes32,bytes32)
SOLVED: The issue was solved in commit ID: 20611133e6edd306f15840a2e5735577f1f08050
// Informational
In all the loops, the variable i
is incremented using i++
. It is known that, in loops, using ++i
costs less gas per iteration than i++
. This also affects variables incremented inside the loop code block.
for (uint256 i = 0; i < assetsConfigInput.length; i++) {
AssetData storage assetConfig = assets[assetsConfigInput[i].underlyingAsset];
for (uint256 i = 0; i < stakes.length; i++) {
accruedRewards = accruedRewards.add(
_updateUserAssetInternal(
for (uint256 i = 0; i < stakes.length; i++) {
AssetData storage assetConfig = assets[stakes[i].underlyingAsset];
ACKNOWLEDGED: The Biconomy team
acknowledged this issue.
Halborn used automated testing techniques to enhance coverage of certain areas of the scoped contract. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified all the contracts in the repository and was able to compile them correctly into their ABI and binary formats. 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.
As a result of the tests carried out with the Slither tool, some results were obtained and these results were reviewed by Halborn
. Based on the results reviewed, some vulnerabilities were determined to be false positives and these results were not included in the report. The actual vulnerabilities found by Slither are already included in the report findings.
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