Prepared by:
HALBORN
Last Updated Unknown date
Date of Engagement: June 17th, 2024 - July 17th, 2024
100% of all REPORTED Findings have been addressed
All findings
9
Critical
0
High
0
Medium
0
Low
6
Informational
3
Allbridge
engaged Halborn to conduct a security assessment of the Bridge and ERC20 contracts, beginning on June 17th, 2024 and ending on July 17th, 2024. This security assessment was scoped to the smart contracts in the bridge-casper-contract GitHub repository.
Allbridge is a simple and reliable tool for moving assets between different blockchain networks. It contributes to a more connected blockchain world by enabling easy and fast transfers, allowing assets to move quickly, similar to normal transactions on any blockchain. Allbridge also offers the flexibility to choose how to send and receive tokens, whether they are native to the blockchain or wrapped versions.
The team at Halborn assigned a full-time security engineer to verify the security of the smart contracts. 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 acknowledged by the Allbridge team
. The main ones are the following:
The status of the token should be checked before performing any operations.
The access permission of the constructor entry point should be changed to Groups since a user group is created.
The fee rate should be checked to ensure it is lower than 100% or under a maximum threshold to avoid unintended errors.
The ERC20 contract should provide a mechanism for upgrades if needed.
The lock_id should be validated in conjunction with the transaction sender's address to avoid forged lock requests.
Ownership should be transferred in a two-step process to avoid lossing the control of the contract.
Halborn performed a combination of the manual view of the code and automated security testing to balance efficiency, timeliness, practicality, and accuracy regarding the scope of the smart contract assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation, automated testing techniques help enhance the coverage of smart contracts. They 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, purpose, and use of the platform.
Manual code read and walk through.
Manual Assessment of use and safety for the critical Rust variables and functions in scope to identify any arithmetic related vulnerability classes.
Cross contract call controls.
Architecture related logical controls.
Scanning of Rust files for vulnerabilities (cargo audit
)
Integration testing using the Casper Engine Test Support.
Tesnet deployment and comprehensive review of transactions through the CPSR live explorer.
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
0
Medium
0
Low
6
Informational
3
Security analysis | Risk level | Remediation Date |
---|---|---|
Unlock operation could be done on disabled tokens | Low | Risk Accepted |
Incorrect access permissions in constructor entry point | Low | Risk Accepted |
Fee rate not validated | Low | Risk Accepted |
Lack of contract upgrade capability | Low | Risk Accepted |
Potential lock ID forgery leading to DoS | Low | Risk Accepted |
Owneship transfer in one step | Low | Risk Accepted |
Compliance with Casper Fungible Token Standard (CEP-18) | Informational | Acknowledged |
Redundant code | Informational | Acknowledged |
Missing argument on entry point | Informational | Acknowledged |
//
The unlock
function allows unlocking the funds that have been locked on the origin chain, transferring or minting the corresponding token on the Casper chain.
However, this function does not verify if the token to be unlocked is already set as disabled in the token_info.token_status
variable.
If the lock
operation is not disabled on the other side of the bridge, this would allow continuing to work with a disabled token, but without the option of recovering the initial tokens on the origin chain, since the lock/burn operation on this chain does not allow the operation if the token is disabled.
Code snippet of unlock
function from contracts/bridge/src/bridge_unlock.rs file:
pub fn unlock(unlock_args: UnlockArgs) -> ContractResult<()> {
Self::assert_active()?;
assert_lock_id_version(unlock_args.lock_id.as_u128(), Self::get_version()?)?;
Self::verify_signature(&unlock_args)?;
let (token_info, token_address) = get_assert_token_info_by_source(
&unlock_args.token_source,
&unlock_args.token_source_address,
)?;
let amount_total = from_system_precision(unlock_args.amount, token_info.precision);
let fee = if runtime::get_caller() == Self::get_unlock_signer()? {
token_info.min_fee
} else {
U256::zero()
};
It is recommended to validate the token_info.token_status
parameter at the beginning of the unlock
function, before unlocking/mining any tokens.
RISK ACCEPTED: The AllBridge team has accepted the risk of this finding by stating that they need the ability to unlock transactions that might have been left incomplete at the time of token deactivation.
//
The endpoint
function in the common/bridge_utils/src/utils.rs
file is used to create each EntryPoint
object during contract deployment. The problem is that this function automatically sets the access permissions of the entry point to Public
by default.
In the deploy
function, a user group is created to prevent the constructor entry point from being called from an external source. The constructor
entry point initializes all the main parameters of the contract, so it should be executed only once.
However, because the endpoint
function sets the EntryPointAccess
parameter for the constructor entry point to Public
, the user group creation becomes ineffective.
Thankfully, this issue is mitigated by the init
functions that create the dictionaries during the constructor
execution, as they revert execution if the dictionary already exists.
Code snippet of endpoint
function from common/bridge_utils/src/utils.rs file for EntryPoint object creation:
pub fn endpoint(name: &str, param: Vec<Parameter>) -> EntryPoint {
EntryPoint::new(
String::from(name),
param,
CLType::Unit,
EntryPointAccess::Public,
EntryPointType::Contract,
)
}
Code snippet of get_entry_points
function from contracts/bridge/src/bridge_contract.rs file for constructor entry point creation:
entry_points.add_entry_point(endpoint(
"constructor",
vec![
Parameter::new("version", u8::cl_type()),
Parameter::new("role_manager", AccountHash::cl_type()),
Parameter::new("oracle", Oracle::cl_type()),
Parameter::new("unlock_signer", AccountHash::cl_type()),
Parameter::new("fee_collector", AccountHash::cl_type()),
Parameter::new("base_fee_rate_bp", U256::cl_type()),
],
));
It is recommended to modify the EntryPointAccess
parameter of the constructor
entry point and set it to Groups
.
Alternatively, since the dictionary initialization already prevents a second execution, the user group can be removed from the deploy
function, which would save some gas during deployment.
RISK ACCEPTED: The AllBridge team has accepted the risk of this finding.
//
The base_fee_rate_bp
parameter is a fee rate used to calculate the amount of fee collected during the lock operation.
As a general best practice in such operations, any rate corresponding to a fee should be checked to ensure it is less than 100% or below a fixed maximum threshold (e.g., 20%).
In this case, the base_fee_rate_bp
value is not validated in either the set_base_fee_rate
or the init
functions. Although any value over 100% would result in the BridgeError::AmountTooSmall
error, a 100% value would be allowed, which could leave the amount_to_lock
equal to zero, for example.
Code snippet of set_base_fee_rate
function from contract/bridge/src/bridge_mics.rs file:
pub fn set_base_fee_rate(base_fee_rate: U256) -> ContractResult<()> {
Manager::assert_role(Manager::Bridge)?;
Bridge::set_base_fee_rate_bp(base_fee_rate)
}
Code snippet of init
function from contract/bridge/src/bridge.rs file for contract initialization:
Self::set_status(true)?;
Self::set_version(version)?;
Self::set_oracle(oracle)?;
Self::set_fee_collector(fee_collector)?;
Self::set_unlock_signer(unlock_signer)?;
Self::set_package_hash(package_hash)?;
Self::set_base_fee_rate_bp(base_fee_rate_bp)?;
Ok(())
}
It is recommended to validate the base_rate_fee_bp
parameter before storing it, checking that it is less than 100% or another type of maximum threshold (also less than 100%).
RISK ACCEPTED: The AllBridge team has accepted the risk of this finding.
//
The current ERC20 token
contract lacks a mechanism for future upgrades, which is essential for adapting to changes and improvements.
Casper supports contract versioning, allowing developers to deploy new versions of contracts while maintaining compatibility with existing states. This feature is crucial for the long-term evolution and security of smart contracts, enabling seamless updates and continuous improvement without disrupting existing functionality.
It is recommended to implement an upgrade function to enable the contract to be updated to new versions, ensuring ongoing maintenance and flexibility for future enhancements.
RISK ACCEPTED: The AllBridge team has accepted the risk of this finding.
//
The lock
function allows a user to create a lock on the Casper blockchain by sending the LockArgs parameters and some funds.
The only checks performed by the contract are to validate if the lock_id
has already been registered and if the funds are sufficient to cover the fee. If a malicious transaction is made before the legitimate one with the same lock_id
and an amount sufficient to cover the fees, the legitimate lock would be rejected because the lock_id
already exists, resulting in a DoS attack.
The sender of the lock transaction is not verified at any point. Although this is a very unlikely scenario because the lock_id
is supposed to be a random identifier, it is still possible.
Code snippet of _create_lock
function from contract/bridge/src/validator.rs file:
let locks_dict = Locks::instance()?;
assert_lock_id_version(new_lock.lock_id.as_u128(), Self::get_version()?)?;
assert(
!locks_dict.has(&new_lock.lock_id),
BridgeError::LockAlreadyExists,
)?;
let sender = runtime::get_caller();
let lock = Lock {
sender,
recipient: new_lock.recipient,
amount: to_system_precision(amount_to_lock, token_info.precision),
destination: new_lock.destination,
token_source: token_info.token_source,
token_source_address: token_info.token_source_address,
};
It is recommended to store the lock_id
in conjunction with the sender's address as the key in the Locks dictionary to prevent any possibility of lock record falsification.
RISK ACCEPTED: The AllBridge team has accepted the risk of this finding.
//
The transfer_ownership
function from ERC20 contract transfers ownership in one step.
In case of an error in the minter input, the contract would be locked in another account without access.
Code snippet of transfer_ownership
function from contracts/erc20/src/lib.rs file:
/// Transfers ownership of the contract to a new account (`minter`).
/// Can only be called by the current minter.
pub fn transfer_ownership(&mut self, minter: Address) {
self.assert_minter();
let minter_uref = *self.minter_uref.get_or_init(minter::minter_uref);
minter::write_minter_to(minter_uref, minter)
}
It is recommended to perform this action in two steps: one to establish the new owner's address (propose_new_owner) and one to accept it (accept_ownership).
The latter function can only be executed by the new owner.
RISK ACCEPTED: The AllBridge team has accepted the risk of this finding.
//
The current ERC20 contract used for fungible tokens it is based on the erc20-guide-extraction repository from Casper Network GitHub, and it has not been updated for 2 years.
To ensure enhanced security and maintenance, it is recommended to transition to the Casper CEP-18 standard, which is actively maintained and audited by the Casper community, and serves as the standard for fungible tokens in Casper.
It is recommended to transition to the Casper CEP-18 standard, which serves as the standard for fungible tokens in Casper.
This update will align the token implementation with the latest industry standards and best practices.
ACKNOWLEDGED: The AllBridge team has acknowledged this finding.
//
The create_unlock
function contains code that is already present in the unlock
function, leading to redundancy and increasing gas usage in transactions and deployment.
Code snippet of create_unlock
function from contract/bridge/src/validator.rs file:
pub fn create_unlock(unlock_args: &UnlockArgs) -> ContractResult<()> {
assert_lock_id_version(unlock_args.lock_id.as_u128(), Self::get_version()?)?;
let unlock_id = compose_unlock_id(&unlock_args.lock_source, unlock_args.lock_id.as_u128());
assert_unlock_not_exists(&unlock_id)?;
Unlocks::instance()?.set(unlock_id);
Ok(())
}
Code snippet of unlock
function from contract/bridge/src/bridge_unlock.rs file:
pub fn unlock(unlock_args: UnlockArgs) -> ContractResult<()> {
Self::assert_active()?;
assert_lock_id_version(unlock_args.lock_id.as_u128(), Self::get_version()?)?;
Self::verify_signature(&unlock_args)?;
let (token_info, token_address) = get_assert_token_info_by_source(
&unlock_args.token_source,
&unlock_args.token_source_address,
)?;
let amount_total = from_system_precision(unlock_args.amount, token_info.precision);
let fee = if runtime::get_caller() == Self::get_unlock_signer()? {
token_info.min_fee
} else {
U256::zero()
};
let unlock_id = compose_unlock_id(&unlock_args.lock_source, unlock_args.lock_id.as_u128());
assert(
!Unlocks::instance()?.has(unlock_id),
BridgeError::AlreadyReceived,
)?;
To optimize gas consumption, it is recommended to incorporate the line Unlocks::instance()?.set(unlock_id);
within the unlock
function and to remove the create_unlock
one.
ACKNOWLEDGED: The AllBridge team has acknowledged this finding.
//
The constructor
EntryPoint object is not properly created because the corresponding function takes the input argument contract_package_hash
, which is not declared in the EntryPoint input parameters.
Code snippet of get_entry_points
function from contract/bridge/bin/bringe_contract.rs file:
entry_points.add_entry_point(endpoint(
"constructor",
vec![
Parameter::new("version", u8::cl_type()),
Parameter::new("role_manager", AccountHash::cl_type()),
Parameter::new("oracle", Oracle::cl_type()),
Parameter::new("unlock_signer", AccountHash::cl_type()),
Parameter::new("fee_collector", AccountHash::cl_type()),
Parameter::new("base_fee_rate_bp", U256::cl_type()),
],
));
Code snippet of constructor
function from contract/bridge/bin/bridge_contract.rs file:
fn constructor() {
Bridge::init(
runtime::get_named_arg("version"),
runtime::get_named_arg("role_manager"),
runtime::get_named_arg("oracle"),
runtime::get_named_arg("unlock_signer"),
runtime::get_named_arg("fee_collector"),
runtime::get_named_arg("contract_package_hash"),
runtime::get_named_arg("base_fee_rate_bp"),
)
.unwrap_or_revert();
}
It is recommended to rewrite the declaration of the EntryPoint object to include the contract_package_hash
variable. However, this is not critical as calls to entry points work with extra parameters in addition to those declared.
ACKNOWLEDGED: The AllBridge team has 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. 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.
ID | Package | Description |
---|---|---|
curve25519-dalek Version: 3.2.1 | Timing variability Upgrade to >=4.1.3 | |
ed25519-dalek Version: 1.0.1 | Double Public Key Signing Function Oracle Attack | |
h2 Version: 0.3.18 | Degradation of service in h2 servers with CONTINUATION Flood Upgrade to ^0.3.26 OR >=0.4.4 | |
h2 Version: 0.3.18 | Resource exhaustion vulnerability in h2 may lead to Denial of Service (DoS) Upgrade to ^0.3.24 OR >=0.4.2 | |
mio Version: 0.8.6 | Tokens for named pipes may be delivered after deregistration Upgrade to >=0.8.11 | |
openssl Version: 0.10.52 | openssl buffer over-read Upgrade to >=0.10.55 | |
time Version: 0.1.45 | Potential segfault in the time crate Upgrade to >=0.2.23 | |
tungstenite Version: 0.18.0 | Tungstenite allows remote attackers to cause a denial of service Upgrade to >=0.20.1 | |
ansi_term Version: 0.12.1 | Unmaintained | |
dotenv Version: 0.15.0 | Unmaintained | |
json Version: 0.12.4 | Unmaintained | |
lmdb Version: 0.8.0 | Unmaintained | |
mach Version: 0.3.2 | Unmaintained | |
parity-wasm Version: 0.41.0 | Unmaintained | |
wee_alloc Version: 0.4.5 | Unmaintained | |
atty Version: 0.2.14 | Potential unaligned read | |
crossbeam-utils Version: 0.7.2 | Unsoundness of AtomicCell<*64> arithmetics on 32-bit targets that support Atomic*64 | |
memoffset Version: 0.5.6 | memoffset allows reading uninitialized memory | |
openssl Version: 0.10.52 | openssl X509StoreRef::objects is unsound | |
rmp-serde Version: 0.14.4 | rmp-serde Raw and RawRef unsound | |
secp256k1 Version: 0.20.3 | Unsound API in secp256k1 allows use-after-free and invalid deallocation from safe code | |
- | bumpalo Version: 3.12.1 | Yanked |
- | hermit-abi Version: 0.3.1 | Yanked |
- | rustix Version: 0.37.14 | Yanked |
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
Bridge Contracts
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed