Prepared by:
HALBORN
Last Updated 04/26/2024
Date of Engagement by: March 7th, 2022 - March 23rd, 2022
90% of all REPORTED Findings have been addressed
All findings
10
Critical
0
High
3
Medium
2
Low
3
Informational
2
Brokkr
engaged Halborn to conduct a security assessment on CosmWasm smart contracts beginning on March 7th, 2022 and ending on March 23rd, 2022.
The security engineers involved on the audit are blockchain and smart-contract security experts with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.
The purpose of this audit is to achieve the following:
Ensure that smart contract functions work as intended.
Identify potential security issues with the smart contracts.
In summary, Halborn identified some improvements to reduce the likelihood and impacts of the risks, which were mostly addressed by the Brokkr team
. The main ones are the following:
Transfer the ownership of contracts to a Governance contract to
control changes through voting. Otherwise, enable additional security measures for them.
When unstaking or claiming BRO rewards, remove stakers' info only if, additionally to the already existing conditions, pending bBRO reward is also zero.
Consider the block height of each stake when calculating the bBRO rewards.
Apply one of the proposed on-chain oracle strategies to avoid that
TWAP prices become stale.
Split owner address transfer functionality to allow transfer to be
completed by recipient.
External threats, such as financial related attacks, oracle attacks, and inter-contract functions and calls should be validated for expected logic and state.
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 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, purpose, and use of the platform.
Manual code read and walkthrough.
Manual assessment of use and safety for the critical Rust variables and functions in scope to identify any contracts logic related vulnerability.
Fuzz testing (Halborn custom fuzzing tool
)
Checking the test coverage (cargo tarpaulin
)
Scanning of Rust files for vulnerabilities (cargo audit
) \newline
\begin{enumerate} \item CosmWasm Smart Contracts \begin{enumerate} \item Repository: \href{https://github.com/block42-blockchain-company/brotocol-token-contracts/tree/main}{brotocol-token-contracts} \item Commit ID: \href{https://github.com/block42-blockchain-company/brotocol-token-contracts/tree/6e5b287382d1c3c29d568851ce3038ffff7407a3}{6e5b287382d1c3c29d568851ce3038ffff7407a3} \item Contracts in scope: \begin{enumerate} \item airdrop \item bonding-v1 \item oracle \item staking-v1 \item vesting \item whitelist-sale \end{enumerate} \end{enumerate} \end{enumerate}
Out-of-scope:
External libraries and financial related attacks
Critical
0
High
3
Medium
2
Low
3
Informational
2
Impact x Likelihood
HAL-01
HAL-05
HAL-06
HAL-07
HAL-08
HAL-04
HAL-02
HAL-03
HAL-09
HAL-10
Security analysis | Risk level | Remediation Date |
---|---|---|
POSSIBILITY TO TRANSFER AN ARBITRARY AMOUNT OF TOKENS OUT OF SOME CONTRACTS | High | Future Release |
BBRO TOKENS ARE LOST WHEN UNSTAKING OR CLAIMING REWARDS | High | Solved - 03/26/2022 |
BBRO REWARDS SCHEMA COULD PRODUCE UNFAIR ADVANTAGES | High | Partially Solved |
PRICES FROM TWAP ORACLE CAN BECOME STALE | Medium | Solved - 03/23/2022 |
PRIVILEGED ADDRESS CAN BE TRANSFERRED WITHOUT CONFIRMATION | Medium | Solved - 03/21/2022 |
MISSING VALIDATION FOR BASE RATE AND GROWTH PARAMETERS | Low | Solved - 03/26/2022 |
MISSING VALIDATION FOR BONDING DISCOUNTS AND REWARD RATIO | Low | Solved - 03/26/2022 |
MISSING VALIDATION FOR MIN AND MAX VALUES OF LOCKUP PERIOD | Low | Solved - 03/26/2022 |
VESTING SCHEDULES COULD BE UNCLAIMABLE | Informational | Acknowledged |
SLIGHT ROUNDING ISSUES WHEN PROVIDING LP TOKENS | Informational | Acknowledged |
// High
The bonding-v1 and mvp-treasury contracts allow an admin (Brokkr team) to transfer an arbitrary amount of tokens
out of them. Two possible attack scenarios will be described below.
\textbf{\underline{Attack scenario 1}:}
A malicious (or compromised) admin calls the update_config
function in the bonding-v1 contract to change the value of treasury_contract
to an address controlled by him.
As a consequence of Step 1, every time users provide LP tokens or UST to bonding-v1 contract, they will be transferred to the malicious address.
\textbf{\underline{Attack scenario 2}:}
A malicious (or compromised) admin calls the spend
function in mvp-treasury contract with recipient
=
As a consequence of Step 1, the aforementioned function could fully drain the mvp-treasury contract.
A malicious (or compromised) admin could change the value of treasury_contract
in bonding-v1 to an address controlled by him:
pub fn update_config(
deps: DepsMut,
owner: Option<String>,
lp_token: Option<String>,
rewards_pool_contract: Option<String>,
treasury_contract: Option<String>,
if let Some(rewards_pool_contract) = rewards_pool_contract {
config.rewards_pool_contract = deps.api.addr_canonicalize(&rewards_pool_contract)?;
}
if let Some(treasury_contract) = treasury_contract {
config.treasury_contract = deps.api.addr_canonicalize(&treasury_contract)?;
}
\color{black}\color{white}spend
function allows a malicious (or compromised) admin to fully drain the mvp-treasury contract:
pub fn spend(
deps: DepsMut,
env: Env,
asset_info: AssetInfo,
recipient: String,
) -> Result<Response, ContractError> {
let balance = queries::query_asset_balance(deps.as_ref(), env, asset_info.clone())?.amount;
if balance.is_zero() {
return Err(ContractError::InsufficientFunds {});
}
let asset = Asset {
info: asset_info,
amount: balance,
};
Ok(Response::new()
.add_messages(vec![
asset.into_msg(&deps.querier, deps.api.addr_validate(&recipient)?)?
])
.add_attributes(vec![("action", "spend")]))
}
PENDING: The Brokkr team
stated that they plan to transfer the ownership of the contracts to a full-fledged governance contract setup
in the midterm, which will severely reduce the likelihood of liquidity loss exposure attacks. Meanwhile, they will handle the Brokkr admin by a multisig wallet
, as suggested in the recommendation.
// High
When a user unstake
or claim BRO rewards
in staking-v1 contract, his information is totally removed from STAKERS
item if the following conditions are true:
Under the mentioned circumstances, the user won't be able to claim his bBRO rewards, even if the amount is greater than zero, i.e.: he totally loses all his bBRO tokens.
User's information is totally removed from STAKERS
item when he unstakes:
// decrease stake amount
state.total_stake_amount = state.total_stake_amount.checked_sub(amount)?;
staker_info.unlocked_stake_amount = staker_info.unlocked_stake_amount.checked_sub(amount)?;
if staker_info.pending_bro_reward.is_zero() && staker_info.total_staked()?.is_zero() {
remove_staker_info(deps.storage, &sender_addr_raw);
} else {
store_staker_info(deps.storage, &sender_addr_raw, &staker_info)?;
}
\color{black}\color{white}User's information is totally removed from STAKERS
item when he claims his BRO rewards:
staker_info.pending_bro_reward = Uint128::zero();
staker_info.unlock_expired_lockups(&env.block)?;
if staker_info.total_staked()?.is_zero() {
remove_staker_info(deps.storage, &sender_addr_raw);
} else {
store_staker_info(deps.storage, &sender_addr_raw, &staker_info)?;
}
SOLVED: The issue was fixed in commit f9bbc72a85ff872b36691c4992d7a86439a4bba2.
// High
When users stake
, the compute_normal_bbro_reward
function is called to calculate the bBRO rewards accrued. Because of the calculation of bbro_reward
value depends on the subtraction between last_distribution_block
and last_balance_update
, it is possible the following attack scenario:
\textbf{\underline{Attack scenario}:}
Just after a reward distribution, an attacker stakes 10_000000 BRO
(Block height: 200
).
Attacker waits until there is another reward distribution and he front-runs the transaction to additionally stake 80_000000 BRO
before the distribution of BRO tokens (Block height: 700
).
The attacker unstakes 80_000000 BRO
just after the reward distribution (could be even in the same block!).
The calculation of his bBRO reward will consider that 90_000000 BRO
has been staked for 500
blocks (700 - 200). However, only 10_000000
BRO
of the total has been staked for 500
blocks. This situation gives the attacker an unfair advantage.
let stake_amount = self.total_staked()?;
if stake_amount.is_zero() || state.last_distribution_block < self.last_balance_update {
return Ok(());
}
let epoch_info = query_epoch_info(querier, epoch_manager_contract)?;
let epochs_staked = Uint128::from(state.last_distribution_block - self.last_balance_update)
.checked_div(Uint128::from(epoch_info.epoch))?;
let bbro_per_epoch_reward =
stake_amount.checked_div(epoch_info.epochs_per_year())? * epoch_info.bbro_emission_rate;
let bbro_reward = bbro_per_epoch_reward.checked_mul(epochs_staked)?;
self.pending_bbro_reward = self.pending_bbro_reward.checked_add(bbro_reward)?;
self.last_balance_update = current_block;
Ok(())
PARTIALLY SOLVED: The Brokkr team
claimed that contracts will be launched to mainnet with the following parameters:
epoch
= 17,280 blocks (1 day)unstake_period_blocks
= 241,920 blocks (14 days)Because of this setup, the attack vector mentioned above is not profitable unless the rewards are distributed after 14 days, which is highly unlikely, especially with the use of an off-chain trigger (out-of-scope of this audit).
If the above-mentioned parameters are subsequently incorrectly changed with the update_config
function, this security issue could arise again, so it has been marked as "Partially Solved"
.
// Medium
consult_price
function in contracts/oracle/src/queries.rs calculates the return value using the price_average
stored in oracle contract's storage (PRICE_LAST
). However, the function does not previously validate if the price has been updated within a reasonable timeframe.
As a consequence, prices
calculated in this TWAP oracle can rapidly become
stale
if users do not bond tokens frequently enough or if Brokkr's off-chain trigger does not work correctly (out-of-scope for this audit), which could affect negatively users' operations or protocol funds.
price_average
is extracted from oracle contract's storage (PRICE_LAST
) without validating if the price has been updated within a reasonable timeframe:
let config = load_config(deps.storage)?;
let price_last = load_price_cumulative_last(deps.storage)?;
let price_average = if config.asset_infos[0].equal(&asset) {
price_last.price_0_average
} else if config.asset_infos[1].equal(&asset) {
price_last.price_1_average
} else {
return Err(StdError::generic_err("Invalid asset info"));
};
\color{black}\color{white}Return value of consult_price
function is calculated using price_average
, even if this value is stale:
} else {
let price_precision = Uint256::from(10_u128.pow(TWAP_PRECISION.into()));
Uint256::from(amount) * price_average / Decimal256::from_uint256(price_precision)
};
Ok(ConsultPriceResponse {
amount: consult_price.into(),
})
SOLVED: The issue was fixed in commit 44a54bddf48a4e28b711449ddcb13d9bf31afdb9.
// Medium
An incorrect use of the update_config
function in contracts can set owner to an invalid address and inadvertently lose control of the contracts, which cannot be undone in any way. Currently, the owner of the contracts can change owner address using the aforementioned function in a single transaction
and without confirmation
from the new address.
The affected smart contracts are the following:
pub fn update_config(deps: DepsMut, owner: Option<String>) -> Result<Response, ContractError> {
let mut config = load_config(deps.storage)?;
if let Some(owner) = owner {
config.owner = deps.api.addr_canonicalize(&owner)?;
}
pub fn update_config(
deps: DepsMut,
owner: Option<String>,
lp_token: Option<String>,
rewards_pool_contract: Option<String>,
treasury_contract: Option<String>,
astroport_factory: Option<String>,
oracle_contract: Option<String>,
ust_bonding_reward_ratio: Option<Decimal>,
ust_bonding_discount: Option<Decimal>,
lp_bonding_discount: Option<Decimal>,
min_bro_payout: Option<Uint128>,
vesting_period_blocks: Option<u64>,
lp_bonding_enabled: Option<bool>,
) -> Result<Response, ContractError> {
let mut config = load_config(deps.storage)?;
if let Some(owner) = owner {
config.owner = deps.api.addr_canonicalize(&owner)?;
}
pub fn update_config(
deps: DepsMut,
owner: Option<String>,
price_update_interval: Option<u64>,
) -> Result<Response, ContractError> {
let mut config = load_config(deps.storage)?;
if let Some(owner) = owner {
config.owner = deps.api.addr_canonicalize(&owner)?;
}
pub fn update_config(
deps: DepsMut,
owner: Option<String>,
paused: Option<bool>,
unstake_period_blocks: Option<u64>,
min_staking_amount: Option<Uint128>,
min_lockup_period_epochs: Option<u64>,
max_lockup_period_epochs: Option<u64>,
base_rate: Option<Decimal>,
linear_growth: Option<Decimal>,
exponential_growth: Option<Decimal>,
) -> Result<Response, ContractError> {
let mut config = load_config(deps.storage)?;
if let Some(owner) = owner {
config.owner = deps.api.addr_canonicalize(&owner)?;
}
pub fn update_config(
deps: DepsMut,
owner: Option<String>,
genesis_time: Option<u64>,
) -> Result<Response, ContractError> {
let mut config = load_config(deps.storage)?;
if let Some(owner) = owner {
config.owner = deps.api.addr_canonicalize(&owner)?;
}
SOLVED: The issue was fixed in commit 79549c38936e99a89a1fa7aa7e38456032f47389.
// Low
instantiate
and update_config
functions in staking-v1 contract do not validate that values of base_rate
, linear_growth
or exponential_growth
are less or equal than a threshold (e.g.: 0.1) predefined in the contract.
The aforementioned values are used to calculate premium bBRO rewards. If those values are not correctly set, the premium bBRO rewards for users could be much higher than expected.
instantiate
function does not validate that base_rate
, linear_growth
or exponential_growth
are less or equal than a predefined threshold:
store_config(
deps.storage,
&Config {
owner: deps.api.addr_canonicalize(&msg.owner)?,
paused: false,
bro_token: deps.api.addr_canonicalize(&msg.bro_token)?,
rewards_pool_contract: deps.api.addr_canonicalize(&msg.rewards_pool_contract)?,
bbro_minter_contract: deps.api.addr_canonicalize(&msg.bbro_minter_contract)?,
epoch_manager_contract: deps.api.addr_canonicalize(&msg.epoch_manager_contract)?,
unstake_period_blocks: msg.unstake_period_blocks,
min_staking_amount: msg.min_staking_amount,
lockup_config: LockupConfig {
min_lockup_period_epochs: msg.min_lockup_period_epochs,
max_lockup_period_epochs: msg.max_lockup_period_epochs,
base_rate: msg.base_rate,
linear_growth: msg.linear_growth,
exponential_growth: msg.exponential_growth,
},
},
)?;
\color{black}\color{white}update_config
function does not validate that base_rate
, linear_growth
or exponential_growth
are less or equal than a predefined threshold:
if let Some(base_rate) = base_rate {
config.lockup_config.base_rate = base_rate;
}
if let Some(linear_growth) = linear_growth {
config.lockup_config.linear_growth = linear_growth;
}
if let Some(exponential_growth) = exponential_growth {
config.lockup_config.exponential_growth = exponential_growth;
}
SOLVED: The issue was fixed in commit 032d729b4cddd49c990fdb5d9e78c608d21f0d25.
// Low
instantiate
and update_config
functions in bonding-v1 contract do not validate that values of ust_bonding_reward_ratio
, ust_bonding_discount
or lp_bonding_discount
are less or equal than 1.
The aforementioned values are used to calculate rewards distribution and amounts of BRO tokens to claim. If those values are not correctly set, operations will throw error messages and won't allow legitimate users to claim their rewards, thus generating a denial of service (DoS) in Brokkr protocol.
instantiate
function does not validate that ust_bonding_discount
or lp_bonding_discount
are less or equal than 1:
if msg.ust_bonding_reward_ratio > Decimal::from_str("1.0")?
|| msg.ust_bonding_reward_ratio <= Decimal::zero()
{
return Err(ContractError::InvalidUstBondRatio {});
}
store_config(
deps.storage,
&Config {
owner: deps.api.addr_canonicalize(&msg.owner)?,
bro_token: deps.api.addr_canonicalize(&msg.bro_token)?,
lp_token: deps.api.addr_canonicalize(&msg.lp_token)?,
rewards_pool_contract: deps.api.addr_canonicalize(&msg.rewards_pool_contract)?,
treasury_contract: deps.api.addr_canonicalize(&msg.treasury_contract)?,
astroport_factory: deps.api.addr_canonicalize(&msg.astroport_factory)?,
oracle_contract: deps.api.addr_canonicalize(&msg.oracle_contract)?,
ust_bonding_reward_ratio: msg.ust_bonding_reward_ratio,
ust_bonding_discount: msg.ust_bonding_discount,
lp_bonding_discount: msg.lp_bonding_discount,
min_bro_payout: msg.min_bro_payout,
vesting_period_blocks: msg.vesting_period_blocks,
lp_bonding_enabled: msg.lp_bonding_enabled,
},
)?;
\color{black}\color{white}update_config
function does not validate that ust_bonding_reward_ratio
, ust_bonding_discount
or lp_bonding_discount
are less or equal than 1:
if let Some(ust_bonding_reward_ratio) = ust_bonding_reward_ratio {
config.ust_bonding_reward_ratio = ust_bonding_reward_ratio;
}
if let Some(ust_bonding_discount) = ust_bonding_discount {
config.ust_bonding_discount = ust_bonding_discount;
}
if let Some(lp_bonding_discount) = lp_bonding_discount {
config.lp_bonding_discount = lp_bonding_discount;
}
SOLVED: The issue was fixed in commit e80b7dc97ff20b683cd27d7a4cdaa6d7d60c1076.
// Low
instantiate
and update_config
functions in staking-v1 contract do not validate that min_lockup_period_epochs
is less than max_lockup_peri
od_epochs
.
The aforementioned values are used to validate the lockup period when staking locked BRO tokens or locking a previous staked amount. If those values are not correctly set, operations will throw error messages and won't allow legitimate users to stake or lock BRO tokens, thus generating a denial of service (DoS) in Brokkr protocol.
instantiate
function does not validate that min_lockup_period_epochs
is less than max_lockup_period_epochs
:
store_config(
deps.storage,
&Config {
owner: deps.api.addr_canonicalize(&msg.owner)?,
paused: false,
bro_token: deps.api.addr_canonicalize(&msg.bro_token)?,
rewards_pool_contract: deps.api.addr_canonicalize(&msg.rewards_pool_contract)?,
bbro_minter_contract: deps.api.addr_canonicalize(&msg.bbro_minter_contract)?,
epoch_manager_contract: deps.api.addr_canonicalize(&msg.epoch_manager_contract)?,
unstake_period_blocks: msg.unstake_period_blocks,
min_staking_amount: msg.min_staking_amount,
lockup_config: LockupConfig {
min_lockup_period_epochs: msg.min_lockup_period_epochs,
max_lockup_period_epochs: msg.max_lockup_period_epochs,
base_rate: msg.base_rate,
linear_growth: msg.linear_growth,
exponential_growth: msg.exponential_growth,
},
},
)?;
\color{black}\color{white}update_config
function does not validate that min_lockup_period_epochs
is less than max_lockup_period_epochs
:
if let Some(min_lockup_period_epochs) = min_lockup_period_epochs {
config.lockup_config.min_lockup_period_epochs = min_lockup_period_epochs;
}
if let Some(max_lockup_period_epochs) = max_lockup_period_epochs {
config.lockup_config.max_lockup_period_epochs = max_lockup_period_epochs;
}
SOLVED: The issue was fixed in commit b9c1e4ad60fa79e030737e5374a8b027c147d091.
// Informational
When registering new vesting accounts, the vesting contract sets the last claimed time to the configured genesis time. That parameter could be greater than the expiration date of some vesting schedules, such that vesting_info.last_claim_time > vesting_info.end_time
, which makes it unclaimable. However, it is possible for the administrator to update genesis time and set new vesting schedules.
if current_time > schedule.end_time && self.last_claim_time < schedule.end_time {
claimable_amount += schedule.bro_amount;
}
\textbf{ACKNOWLEDGED:} The Brokkr team
acknowledged this finding.
// Informational
When calculating the return values in get_share_in_assets
function every time users provide LP tokens to bonding-v1 contract, the "multiply before divide" principle is not followed, which could generate slight rounding issues in some edge scenarios.
fn get_share_in_assets(
querier: &QuerierWrapper,
bro_pool: &Asset,
ust_pool: &Asset,
lp_amount: Uint128,
lp_token_addr: Addr,
) -> StdResult<(Uint128, Uint128)> {
let total_share = query_supply(querier, lp_token_addr)?;
let share_ratio = Decimal::from_ratio(lp_amount, total_share);
Ok((bro_pool.amount * share_ratio, ust_pool.amount * share_ratio))
}
\textbf{ACKNOWLEDGED:} The Brokkr team
acknowledged this finding.
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