Prepared by:
HALBORN
Last Updated 04/26/2024
Date of Engagement by: March 9th, 2022 - March 15th, 2022
100% of all REPORTED Findings have been addressed
All findings
4
Critical
0
High
0
Medium
0
Low
2
Informational
2
Stader Labs
engaged Halborn to conduct a security audit on their smart contracts beginning on March 9th, 2022 and ending on March 15th, 2022. The security assessment was scoped to the smart contracts provided to the Halborn team.
The team at Halborn was provided a week for the engagement and assigned two full-time 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 risks, which were partially addressed by the Stader Labs team
. The main ones are the following:
Implement manager transfer functionalities for lock and rewards-pool contracts.
Protect configuration parameters against abnormally low / high values by setting proper bounds.
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
)
Code repository:
terra-tokenomics
\begin{enumerate} \item CosmWasm Smart Contracts \begin{enumerate} \item Commit ID: \href{https://github.com/stader-labs/terra-tokenomics/tree/e5fb44cecaedad4ad78bed93351ac697e94adc7c}{e5fb44cecaedad4ad78bed93351ac697e94adc7c} \item Contracts in scope: \begin{enumerate} \item lock \item rewards-pool \item staking \end{enumerate} \end{enumerate} \end{enumerate}
Out-of-scope:
External libraries and financial related attacks.
Critical
0
High
0
Medium
0
Low
2
Informational
2
Impact x Likelihood
HAL-01
HAL-03
HAL-04
HAL-02
Security analysis | Risk level | Remediation Date |
---|---|---|
LACK OF MANAGER TRANSFER FUNCTIONALITY | Low | Risk Accepted |
MISSING BOUNDS ON CONFIGURATION VARIABLES | Low | Solved - 03/21/2022 |
LACK OF ADDRESS NORMALIZATION | Informational | Solved - 03/21/2022 |
CONFIGURATION PARAMETER NOT SET UPON INSTANTIATION | Informational | Acknowledged |
// Low
The rewards-pool and lock contracts do not implement any functionality to change the manager
address, which is set at contract instantiation.
If the manager
account keys are suspected to be compromised or if the development team needs to change the address for an operational reason, some functionality of the contract could be rendered unusable.
It is worth mentioning that this issue has a very limited impact, as there is only one manager-only functionality that can be used once.
pub fn update_configs(
deps: DepsMut,
info: MessageInfo,
_env: Env,
staking_contract: String,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
let sending_manager = info.sender.to_string();
if sending_manager.ne(&config.manager) {
return Err(ContractError::Unauthorized {});
}
if config.staking_contract.eq(&Addr::unchecked("0")) {
config.staking_contract = deps
.api
.addr_validate(staking_contract.to_lowercase().as_str())?;
}
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("method", "update_configs")
.add_attribute("staking_contract", staking_contract))
}
pub fn update_configs(
deps: DepsMut,
info: MessageInfo,
_env: Env,
staking_contract: String,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
let sending_manager = info.sender;
if sending_manager.ne(&config.manager) {
return Err(ContractError::Unauthorized {});
}
if config.staking_contract.eq(&Addr::unchecked("0")) {
config.staking_contract = deps
.api
.addr_validate(staking_contract.to_lowercase().as_str())?;
}
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("method", "update_configs"))
}
RISK ACCEPTED: The risk for this issue was accepted by the Stader Labs team
. They also mentioned that there is no use case for changing the manager in the future.
// Low
The staking contract has missing bounds on the emission_rate
and unbonding_period
variables. This can lead to unexpected contract behavior.
The bounds of these parameters should be enforced to avoid potential errors in the current or future uses of these variables.
pub fn instantiate(
deps: DepsMut,
env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
let state = State {
emission_rate: msg.emission_rate,
last_exchange_rate_updated_block: env.block.height,
next_undelegate_id: 0,
};
STATE.save(deps.storage, &state)?;
let config = Config {
manager: deps
.api
.addr_validate(msg.manager.to_lowercase().as_str())?,
xsd_cw20_contract: Addr::unchecked("0"),
sd_cw20_contract: deps
.api
.addr_validate(msg.sd_cw20_contract.to_lowercase().as_str())?,
rewards_pool_contract: deps
.api
.addr_validate(msg.rewards_pool_contract.to_lowercase().as_str())?,
lock_contract: deps
.api
.addr_validate(msg.lock_contract.to_lowercase().as_str())?,
unbonding_period: msg.unbonding_period,
};
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("method", "instantiate"))
}
pub fn update_config(
deps: DepsMut,
info: MessageInfo,
_env: Env,
update_config_request: UpdateConfigRequest,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
let mut state = STATE.load(deps.storage)?;
let sending_manager = info.sender.to_string().to_lowercase();
if sending_manager.ne(&config.manager) {
return Err(ContractError::Unauthorized {});
}
if let Some(sd_cw20) = update_config_request.sd_cw20_contract {
config.sd_cw20_contract = deps.api.addr_validate(sd_cw20.to_lowercase().as_str())?;
}
if let Some(xsd_cw20) = update_config_request.xsd_cw20_contract {
if config.xsd_cw20_contract.eq(&Addr::unchecked("0")) {
config.xsd_cw20_contract = deps.api.addr_validate(xsd_cw20.to_lowercase().as_str())?;
}
}
if let Some(lock) = update_config_request.lock_contract {
config.lock_contract = deps.api.addr_validate(lock.to_lowercase().as_str())?;
}
if let Some(rpc) = update_config_request.rewards_pool_contract {
config.rewards_pool_contract = deps.api.addr_validate(rpc.to_lowercase().as_str())?;
}
if let Some(er) = update_config_request.emission_rate {
state.emission_rate = er;
}
if let Some(up) = update_config_request.unbonding_period {
config.unbonding_period = up;
}
CONFIG.save(deps.storage, &config)?;
STATE.save(deps.storage, &state)?;
Ok(Response::new().add_attribute("method", "update_config"))
}
SOLVED: The issue was fixed with the above recommendation in commits:
// Informational
The set_manager
function of the staking contract takes user input directly as an address without performing any validation. This could lead to failed messages or the storage of incorrect information.
However, the impact in this particular case is very limited since the address submitted in the set_manager
function must execute the accept_manager
function to finish the process, protecting against potential mistakes such as those mentioned.
pub fn set_manager(
deps: DepsMut,
info: MessageInfo,
_env: Env,
manager: String,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if info.sender.ne(&config.manager) {
return Err(ContractError::Unauthorized {});
}
TMP_MANAGER_STORE.save(
deps.storage,
&TmpManagerStore {
manager: manager.clone(),
},
)?;
Ok(Response::new()
.add_attribute("method", "set_manager")
.add_attribute("new_manager", manager))
SOLVED: The issue was fixed with the above recommendation in commit 0f3fbb9237c43f381987e113fa418d4cd1afbb57.
// Informational
The instantiate function did not set the staking_contract
address, as it did for other required contract addresses in the configuration. Instead, it relied on update_config
being called post initialization. If the stacking address is not set by calling update_config
before executing transfer_rewards
, an attempt to transfer to address "0" will end the panic! macro and unnecessary consumption of gas fee per transaction.
It is worth noting that the update_config
function only allowed setting the staking address if it contained the initial value Addr::unchecked("0")
. This effectively forbids any future change after the first update.
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let config = Config {
staking_contract: Addr::unchecked("0"),
sd_cw20_contract: deps
.api
.addr_validate(msg.sd_cw20_contract.to_lowercase().as_str())?,
manager: deps
.api
.addr_validate(msg.manager.to_lowercase().as_str())?,
};
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let config = Config {
manager: deps
.api
.addr_validate(msg.manager.to_lowercase().as_str())?,
sd_cw20_contract: deps
.api
.addr_validate(msg.sd_cw20_contract.to_lowercase().as_str())?,
staking_contract: Addr::unchecked("0"),
};
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
CONFIG.save(deps.storage, &config)?;
ACKNOWLEDGED: The Stader Labs team
acknowledged this finding.
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. All vulnerabilities shown here were already disclosed in the above report. However, 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