Prepared by:
HALBORN
Last Updated 04/26/2024
Date of Engagement by: January 17th, 2022 - February 7th, 2022
100% of all REPORTED Findings have been addressed
All findings
7
Critical
0
High
0
Medium
1
Low
1
Informational
5
Stader Labs
engaged Halborn to conduct a security audit on their smart contracts beginning on January 17th, 2022 and ending on February 7th, 2022. The security assessment was scoped to the LunaX related smart contracts provided to the Halborn team.
The team at Halborn was provided three weeks for the engagement and assigned two security engineers to audit the security of the smart contract. The security engineers 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:
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 multiple risks, which has been addressed by Stader Labs
team. The main ones are the following:
Add manager transfer capabilities to allow changes on the privileged account of the contracts.
Check the "Protocol Inactive" state on all functionalities related to fund updates, stake tracking or swap contract interaction.
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
Code repository:
https://github.com/stader-labs/stader-liquid-token
\begin{enumerate} \item CosmWasm Smart Contracts - LunaX \begin{enumerate} \item Commit ID: \href{https://github.com/stader-labs/stader-liquid-token/tree/e2561633859cc846bea024854b61248dd6f28665}{e2561633859cc846bea024854b61248dd6f28665} \item Contracts in scope: \begin{enumerate} \item airdrops-registry \item reward \item staking \end{enumerate} \end{enumerate} \end{enumerate}
Out-of-scope:
External libraries and financial related attacks.
Critical
0
High
0
Medium
1
Low
1
Informational
5
Impact x Likelihood
HAL-01
HAL-02
HAL-03
HAL-05
HAL-06
HAL-07
HAL-04
Security analysis | Risk level | Remediation Date |
---|---|---|
MANAGER ADDRESS CANNOT BE TRANSFERRED | Medium | Solved - 02/08/2022 |
UNDERUSED PROTOCOL INACTIVE STATE | Low | Solved - 02/08/2022 |
CONFIGURATION PARAMETER COULD NOT BE UPDATED | Informational | Acknowledged |
OUTDATED INFORMATION DISPLAYED TO USERS | Informational | Solved - 02/08/2022 |
MISUSE OF HELPER METHODS | Informational | Solved - 02/08/2022 |
MULTIPLE INSTANCES OF UNCHECKED MATH | Informational | Acknowledged |
UNFINISHED DEVELOPMENT COMMENTS | Informational | Solved - 02/08/2022 |
// Medium
The functions to update the configuration in the contracts within scope lacked the option of setting a new manager
as a privileged address.
If the keys of the manager
account were suspected to be compromised, or the development team needed to change the address for an operational reason, a sizable portion of the contract's functionality will be rendered unusable.
contracts/airdrops-registry/src/contract.rs:59:pub fn update_airdrop_registry(
contracts/reward/src/contract.rs:204:pub fn update_config(
contracts/staking/src/contract.rs:143:pub fn update_config(
SOLVED: The issue was fixed with the above recommendation in commit 849a43b9c96cb6b70b67a0bb60aaa5c0be221d1e.
// Low
The staking contract implemented a switch to go into a pause-like state called “Protocol Inactive” by using the config.active
setting. This check was not implemented consistently across all the functions accessible for unprivileged users, but only on the deposit
one.
This state could be desirable for some reasons, where external users' interference would like to be kept to the minimum among all features of the contract.
contracts/staking/src/contract.rs:486 redeem_rewards function
contracts/staking/src/contract.rs:523 swap_rewards function
contracts/staking/src/contract.rs:486 reinvest function
contracts/staking/src/contract.rs:615 reimburse_slashing function
contracts/staking/src/contract.rs:715 undelegate_stake function
contracts/staking/src/contract.rs:810 reconcile_funds function
contracts/staking/src/contract.rs:888 withdraw_funds_to_wallet function
contracts/staking/src/contract.rs:976 claim_airdrops function
SOLVED: A new "operations control" mechanism has been implemented to have a more granular control over paused operations, covering all listed functionalities.
This issue was fixed in commit 368f9a665af2fd6d512d2687e40f8699b7a6e164.
// Informational
The instantiate function did not set the cw20_token_contract
address, as done with other contract addresses required in the configuration. Instead, it relied on update_config
being called post initialization, which could cause undesirable situations if this address is not set right after deployment.
It is worth noting that the update_config
function only allowed to set the CW20 address if it contained the initial value Addr::unchecked("0")
. This effectively forbade any future change after the first update.
airdrop_registry_contract: deps
.api
.addr_validate(msg.airdrops_registry_contract.as_str())?,
airdrop_withdrawal_contract: deps
.api
.addr_validate(msg.airdrop_withdrawal_contract.as_str())?,
reward_contract: deps.api.addr_validate(msg.reward_contract.as_str())?,
cw20_token_contract: Addr::unchecked("0"),
protocol_fee_contract: deps.api.addr_validate(msg.protocol_fee_contract.as_str())?,
pub fn update_config(
deps: DepsMut,
info: MessageInfo,
env: Env,
update_config: ConfigUpdateRequest,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
validate(&config, &info, &env, vec![Verify::SenderManager])?;
if let Some(cw20_contract) = update_config.cw20_token_contract {
if config.cw20_token_contract == Addr::unchecked("0") {
config.cw20_token_contract = deps.api.addr_validate(cw20_contract.as_str())?;
}
}
ACKNOWLEDGED: Stader Labs
acknowledged this finding.
// Informational
The reimburse_slashing
function of the staking contract does not update the total_staked
and exchange_rate
state variables. As detailed in the comment of line 635, most functionalities perform a check_slashing
at the beginning or update these values. However, this is not the case of query_user_info
and query_compute_deposit_breakdown
.
If a user queries those functions after a reimburse_slashing
has been performed, they would receive erroneous information due to the lack of update.
contracts/staking/src/contract.rs:615 reimburse_slashing function
SOLVED: Added a RedeemRewards
message from the staking contract as a result of the reimburse_slashing
function, which effectively updates the exchange rate at the end of the transaction.
This issue was fixed on commit 24ec25b51e52e98c1128cd77c379d94b3e9dab58s.
// Informational
The use of the unwrap
and expect
function is very useful for testing environments because a value is forcibly demanded to get an error (aka panic!
) if the "Option" does not have "Some" value or "Result". Nevertheless, leaving unwrap
or expect
functions in production environments is a bad practice because not only will this cause the program to crash out, or panic!, but also (in case of unwrap
) no helpful messages are shown to help the user solve, or understand the reason of the error.
contracts/reward/src/contract.rs: #L93, 168, 171, 217
contracts/staking/src/contract.rs: #L266, 361, 371, 400, 433, 579, 632, 696, 701, 743, 773, 785, 830, 836, 867, 880, 946, 956, 1020, 1108, 1121, 1137
contracts/staking/src/helpers.rs: #L90, 98, 122, 163
SOLVED: Most of the instances highlighted below has been fixed using the recommendations or deemed secure due to previous checks. Instances related to the result of checked_add
operations have not been modified, as the risk of overflow in those cases is minimal, since the entire supply of Terra
supply will not cause the value of uint128
to overflow.
This issue was fixed in commit 7478e89ee9a0f72b89690573fd2dee956d2408fa.
// Informational
Some mathematical operations that could cause unexpected behavior under specific circumstances were found on the codebase. Although no effective arithmetic over/underflow were found and the overflow-checks = true
flag was set on Cargo.toml
, it is still recommended to avoid unchecked math as much as possible to follow best-practices and limit the risk of future updates introducing an actual vulnerability.
packages/stader-utils/src/coin_utils.rs:173: let c_u256: Decimal = (b_u256 * a_u256).into();
packages/stader-utils/src/coin_utils.rs:181: let c_u256: Decimal = (b_u256 + a_u256).into();
packages/stader-utils/src/coin_utils.rs:189: let c_u256: Decimal = (a_u256 - b_u256).into();
packages/stader-utils/src/coin_utils.rs:243: Uint128::new(coin.amount.u128() + existing_coin.u128()),
packages/stader-utils/src/coin_utils.rs:278: Uint128::new(existing_coin.u128() - coin.amount.u128()),
packages/stader-utils/src/coin_utils.rs:432: coin.amount.u128() * ratio.numerator() / ratio.denominator(),
packages/stader-utils/src/coin_utils.rs:437: (num * dec.numerator() / dec.denominator()) as u128
ACKNOWLEDGED: Since the affected instances were not exploitable and therefore did not pose a direct risk to in-scope contracts, Stader Labs
acknowledged the potential risks outlined above.
// Informational
Multiple “ToDo” comments and commented code instances were found on the codebase. Although incomplete code does not directly cause a security vulnerability or affect the audit’s outcome as far as it is functional, having development comments could simplify the process for an attacker to find a valid attack surface within the contract.
In addition, it shows that the audited code will be different from the released one, which could cause that new vulnerabilities were to be introduced in the code after the audit.
staking/src/helpers.rs:29:// TODO: bchain99 - write unit-tests to validate.
staking/src/contract.rs:1092:// TODO - GM. Test this
staking/src/contract.rs:1101: // TODO - GM. Will converting u64 to string for batch id start work?
staking/src/contract.rs:521:// TODO - GM. Does swap have a fixed cost or a linear cost?
reward/src/contract.rs:77:// TODO - GM. Does swap have a fixed cost or a linear cost? Useful to make this permissionless.
reward/src/contract.rs:94: // let denoms: Vec<String> = total_rewards
reward/src/contract.rs:95: // .iter()
reward/src/contract.rs:96: // .map(|item| item.denom.clone())
reward/src/contract.rs:97: // .collect();
SOLVED: Suggested mitigation steps have been implemented by the Stader Labs team
in commit 07878e2eefa1e66a9a25ad3dc234340a355b5ad0.
Halborn used automated security scanners to assist with detection of well-known security issues and vulnerabilities. Among the tools used was cargo audit
, a security scanner for vulnerabilities reported to the RustSec Advisory Database. All vulnerabilities published in https://crates.io
are stored in a repository named The RustSec Advisory Database. cargo audit
is a human-readable version of the advisory database which performs a scanning on Cargo.lock. Security Detections are only in scope. To better assist the developers maintaining this code, the auditors are including the output with the dependencies tree, and this is included in the cargo audit output to better know the dependencies affected by unmaintained and vulnerable crates.
\begin{center} \begin{tabular}{|l|p{2cm}|p{9cm}|} \hline \textbf{ID} & \textbf{package} & \textbf{Short Description} \ \hline \href{https://rustsec.org/advisories/RUSTSEC-2020-0025}{RUSTSEC-2020-0025} & bigint & biginit is unmaintained, use uint instead \ \hline \end{tabular} \end{center}
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