Download audit report
This document presents the results of a security review conducted by Nethermind Security for a system of smart contracts referred to as the Token Bridge developed by Karnot. The audited contracts are designed to facilitate the secure transfer of assets between the Starknet Layer 2 (L2) network and bespoke Layer 3 (L3) networks, known as Appchains.
This bridge is a crucial infrastructure component within the Piltover ecosystem, a system developed to extend Starknet by enabling the creation and operation of L3 solutions. By providing a reliable mechanism for liquidity to flow from L2 to L3, the Token Bridge is a foundational component for Appchains built with frameworks like Madara.
The bridge allows users to lock tokens in the L2 contract, which then uses the Piltover messaging protocol to communicate with a corresponding contract on an L3 Appchain to mint an equivalent amount of wrapped tokens. The reverse process, where tokens are burned on L3, initiates a message to unlock the original assets on L2. The architectural design is inspired by the official Starkgate L1-L2 bridge, adapting its proven concepts for the unique requirements of the L2-L3 environment.
The audit comprises 1964 lines of Cairo code. The audit was performed using (a) manual analysis of the codebase, (b) automated analysis tools, and (c) creation of test cases.
| Audit assessment | |
| Audit Type | Security Review |
| Initial Report | June 23, 2025 |
| Final Report | September 3, 2025 |
| Repository | karnotxyz/starknet_bridge |
| Initial Commit | 65a441c951 |
| Final Commit | 7804c983c1 |
| Documentation | README |
| Documentation Assessment | High |
| Test Suite Assessment | High |
Findings by severity
| # | Contract | LoC | Comments | Ratio | Blank | Total |
| 1 | src/constants.cairo | 15 | 10 | 66.7% | 10 | 35 |
| 2 | src/lib.cairo | 57 | 9 | 15.8% | 15 | 81 |
| 3 | src/withdrawal_limit/component.cairo | 125 | 31 | 24.8% | 19 | 175 |
| 4 | src/withdrawal_limit/interface.cairo | 6 | 1 | 16.7% | 1 | 8 |
| 5 | src/access_control/component.cairo | 151 | 9 | 6.0% | 27 | 187 |
| 6 | src/access_control/roles.cairo | 8 | 8 | 100.0% | 1 | 17 |
| 7 | src/timelock/timelock.cairo | 77 | 16 | 20.8% | 17 | 110 |
| 8 | src/erc20/roles_interface.cairo | 115 | 36 | 31.3% | 26 | 177 |
| 9 | src/erc20/err_msg.cairo | 34 | 0 | 0.0% | 2 | 36 |
| 10 | src/erc20/interface.cairo | 9 | 2 | 22.2% | 2 | 13 |
| 11 | src/erc20/erc20.cairo | 471 | 158 | 33.5% | 83 | 712 |
| 12 | src/erc20/replaceability_interface.cairo | 37 | 17 | 45.9% | 13 | 67 |
| 13 | src/erc20/access_control_interface.cairo | 21 | 15 | 71.4% | 6 | 42 |
| 14 | src/bridge/types.cairo | 20 | 3 | 15.0% | 5 | 28 |
| 15 | src/bridge/interface.cairo | 80 | 2 | 2.5% | 12 | 94 |
| 16 | src/bridge/token_bridge.cairo | 738 | 181 | 24.5% | 138 | 1057 |
| Total | 1964 | 498 | 25.4% | 377 | 2839 |
| # | Finding | Severity | Update |
| 1 | Missing token balance checks on ERC20 transfer functions | Low | Fixed |
| 2 | Inconsistent ERC20 Standard Usage | Info | Acknowledged |
| 3 | Incorrect max balance exceeded assertion in the accept_deposit(...) function | Info | Fixed |
| 4 | System does not support ERC20 tokens that lack the optional decimals function | Info | Acknowledged |
| 5 | The initiate_token_withdraw_with_id function usage is not enforced and ID uniqueness is not validated | Info | Acknowledged |
| 6 | Token activation can fail due to a race condition | Info | Fixed |
| 7 | Token settings are not cleared upon unsuccessful token activation | Info | Fixed |
| 8 | Tokens in the pending state cannot be blocked or deactivated | Info | Acknowledged |
| 9 | Incorrect error message in the decrease_withdrawal_limit(...) function | Best Practices | Fixed |
| 10 | Missing event emission during unsuccessful token activation | Best Practices | Fixed |
The Token Bridge is a set of smart contracts designed to facilitate the transfer of assets between the Starknet Layer 2 (L2) network and a Starknet-based application-specific chain (Appchain, or L3). The primary objective of the bridge is to provide a secure and reliable mechanism for users to lock tokens on L2 and receive corresponding wrapped tokens on the L3 Appchain, and vice-versa.
The architecture and design of the Token Bridge are heavily inspired by the official Starknet (Starkgate) bridge, which bridges assets between Ethereum (L1) and Starknet (L2). The reference implementation, specifically the StarknetTokenBridge.sol contract, is written in Solidity and deployed on Ethereum.
In contrast, the Token Bridge contracts are intended for deployment on Starknet (L2). This L2 bridge communicates with a corresponding Cairo contract on the L3 Appchain via the Piltover messaging protocol, which is the Cairo implementation of Starknet's core messaging contracts.
The system is centered around the token_bridge.cairo contract, which manages all core bridging functionalities on Starknet L2.
Key components and features include:
TokenStatus enum, which can be one of Unknown, Pending, Active, Deactivated, or Blocked.deposit(...) & deposit_with_message(...): Users lock ERC20 tokens on L2 by calling these functions. This action sends a message to the L3 Appchain to mint an equivalent amount of the corresponding token.withdraw(...): To withdraw assets, a user first burns the tokens on L3, which sends a message back to the L2 bridge. The user then calls withdraw(...) on the L2 contract to consume this message and unlock the original tokens.deposit_cancel_request(...) and deposit_reclaim(...) to allow users to reclaim their assets if a deposit message fails to be consumed on L3 within a specified time frame.APP_GOVERNOR can set a max_total_balance for each token. This acts as a global deposit limit, preventing the total value locked (TVL) for a specific token from exceeding a predefined cap, thereby controlling the overall risk exposure of the bridge.WithdrawalLimitComponent provides functionality to enforce daily withdrawal quotas on a per-token basis, mitigating the potential impact of a security incident.SECURITY_AGENT role in case of an emergency, halting all major operations like deposits and withdrawals.ReentrancyGuardComponent to prevent reentrancy attacks on state-changing functions.The Token Bridge implements a granular, hierarchical role-based access control (RBAC) system to manage administrative and security-sensitive functions. This system is more elaborate than the one found in the reference Starkgate implementation. The roles and their administrative hierarchy are defined as follows:
| Role | Role Admin |
UPGRADE_GOVERNOR | DEFAULT_ADMIN (Timelock Contract) |
GOVERNANCE_ADMIN | GOVERNANCE_ADMIN (Self-managed) |
APP_GOVERNOR | GOVERNANCE_ADMIN |
SECURITY_ADMIN | GOVERNANCE_ADMIN |
SECURITY_AGENT | SECURITY_ADMIN |
TOKEN_ADMIN | APP_GOVERNOR |
A key security feature is the integration of a TimelockController contract. The UPGRADE_GOVERNOR role, which has the sole authority to upgrade the bridge contract, is assigned exclusively to the Timelock contract. This ensures that any contract upgrade is subject to a mandatory time delay, providing an opportunity for users and guardians to review the proposed changes.
While based on the Starkgate bridge, the Token Bridge introduces several notable architectural and functional differences.
permissioned_enroll flag. When disabled (the default), token enrollment is permissionless. When enabled by the APP_GOVERNOR, enrollment is restricted to the TOKEN_ADMIN role.WithdrawalLimit.sol, the withdrawal limit is a single percentage applied globally to all tokens. Token Bridge's WithdrawalLimitComponent improves upon this by allowing for per-token daily withdrawal percentage limits. Furthermore, the ability to modify these limits is split: SECURITY_ADMIN can only increase or disable limits, while the lower-privileged SECURITY_AGENT can only decrease them, creating a safer operational hierarchy.The risk rating methodology used by Nethermind Security follows the principles established by the OWASP Foundation. The severity of each finding is determined by two factors: Likelihood and Impact.
Likelihood measures how likely the finding is to be uncovered and exploited by an attacker. This factor will be one of the following values:
When defining the likelihood of a finding, other factors are also considered. These can include but are not limited to motive, opportunity, exploit accessibility, ease of discovery, and ease of exploit.
Impact is a measure of the damage that may be caused if an attacker exploits the finding. This factor will be one of the following values:
When defining the impact of a finding, other factors are also considered. These can include but are not limited to data and state integrity, loss of availability, financial loss, and reputation damage. After defining the likelihood and impact of an issue, the severity is determined according to the table below.
| Impact / Likelihood | Low | Medium | High |
| High | Medium | High | Critical |
| Medium | Low | Medium | High |
| Low | Info / Best Practices | Low | Medium |
| Undetermined | Undetermined | Undetermined | Undetermined |
To address issues that do not fit a High, Medium or Low severity, Nethermind Security also uses three more finding severities: Informational, Best Practices, and Undetermined.
ERC20 transfer functions| File | Fix commit | Type | Status |
src/bridge/token_bridge.cairo | PR #34 | Low | Fixed |
The token_bridge contract allows users to withdraw tokens through several functions: withdraw(...), deposit_with_message_reclaim(...), and deposit_reclaim(...). In all these cases, the function first consumes a message to authorize the transfer and then proceeds to send the tokens to the recipient.
The issue is that these functions do not verify whether the token transfer was successful. The message is consumed optimistically before the transfer(...) call is made. Some ERC20 tokens may not revert on a failed transfer (e.g., if the token is paused or the recipient is blacklisted) but instead return a boolean status. Since this return value is not checked, the transaction can succeed even if the tokens were not actually moved.
This can lead to a scenario where a user's withdrawal message is consumed, but they never receive their tokens due to a failed transfer. Because the message is marked as used, the user cannot retry the operation, resulting in their funds being permanently stuck in the bridge contract. It is worth noting that the accept_deposit(...) function correctly checks for transfer success by comparing balances before and after the call, but this pattern was not applied to the withdrawal and reclaim flows.
For brevity, only the withdraw(...) function is shown, as the reclaim functions follow the same flawed logic.
fn withdraw(
ref self: ContractState,
token: ContractAddress,
amount: u256,
recipient: ContractAddress,
) {
// ...
// @audit The message is consumed before the transfer is attempted.
self.consume_message(token, amount, recipient);
let tokenDispatcher = IERC20Dispatcher { contract_address: token };
// @audit-issue The success of this transfer is not verified.
// If it fails, the message is already consumed and funds are lost.
tokenDispatcher.transfer(recipient, amount);
// ...
}Recommendation
Consider validating that the token transfer was successful in the withdraw(...), deposit_with_message_reclaim(...), and deposit_reclaim(...) functions. This can be achieved similarly to the implementation in the accept_deposit(...) function.
| File | Fix commit | Type | Status |
src/bridge/token_bridge.cairo | — | Info | Acknowledged |
The accept_deposit(...) function is responsible for handling user deposits of ERC20 tokens into the contract. It verifies that the token is supported, checks against a maximum balance, and then performs a transfer_from(...) operation to move tokens from the caller to the contract's address. Finally, it asserts that the transfer was successful by comparing the contract's balance before and after the transfer.
The issue lies in the fact that Starknet ERC20 tokens can have different function signatures, specifically camelCase (e.g., transferFrom(...), balanceOf(...)) or snake_case (e.g., transfer_from(...), balance_of(...)). While the latest standard recommends using snake_case, older tokens might still adhere to camelCase. The current implementation exclusively uses snake_case for balance_of(...) and transfer_from(...) calls.
fn accept_deposit(self: @ContractState, token: ContractAddress, amount: u256) {
// ...
let dispatcher = IERC20Dispatcher { contract_address: token };
// @audit Using snake_case
let current_balance: u256 = dispatcher.balance_of(get_contract_address());
let max_total_balance = self.get_max_total_balance(token);
assert(current_balance + amount < max_total_balance, Errors::MAX_BALANCE_EXCEEDED);
let this_address = get_contract_address();
// @audit Using snake_case
let initial_balance = dispatcher.balance_of(this_address);
// @audit-issue Only snake_case `transfer_from(...)` is used.
dispatcher.transfer_from(caller, this_address, amount);
assert(
// @audit Using snake_case
dispatcher.balance_of(this_address) == initial_balance + amount,
Errors::TOKENS_NOT_TRANSFERRED,
);
}If a user attempts to deposit an ERC20 token that uses camelCase function signatures, the transfer_from(...) call will fail. This is because the dispatcher is configured to call the snake_case version of the function, which does not exist on camelCase standard tokens.
Recommendation
Consider implementing a mechanism to support both camelCase and snake_case ERC20 token standards. This would ensure compatibility with a wider range of ERC20 tokens on Starknet.
Resolution
We acknowledge this issue. We feel this should be fine as most of the relevant tokens on Starknet mainnet have already migrated, and going for forward the new standard says the token must have snake_case. On a side note madara currently supports cairo version 2.9.2 which does not have try/catch. This can be revisited in the next sprint when we migrate to higher cairo version.
accept_deposit(...) function| File | Fix commit | Type | Status |
src/bridge/token_bridge.cairo | PR #34 | Info | Fixed |
In the accept_deposit(...) function, the contract checks if a new deposit would exceed the max_total_balance for a token. The check is performed using a strict less-than ( <) comparison.
This prevents a deposit that would make the contract's balance exactly equal to max_total_balance. This behavior is inconsistent with the reference Starkgate implementation, which uses a less-than-or-equal-to ( <=) check, allowing the balance to reach the maximum limit.
fn accept_deposit(self: @ContractState, token: ContractAddress, amount: u256) {
// ...
let current_balance = IERC20Dispatcher { contract_address: token }.balance_of(self.own_address);
let max_total_balance = self.token_settings.read(token).max_total_balance;
// @audit-issue The use of `<` prevents the total balance from ever reaching max_total_balance.
assert(current_balance + amount < max_total_balance, Errors::MAX_BALANCE_EXCEEDED);
// ...
}Recommendation
Consider changing the strict less-than ( <) comparison to less-than-or-equal-to ( <=) to correctly allow deposits up to the maximum configured balance.
decimals function| File | Fix commit | Type | Status |
src/bridge/token_bridge.cairo | — | Info | Acknowledged |
The deployment_message_payload(...) function constructs a calldata payload containing metadata about a given token. This metadata includes the token's name, symbol, and the number of decimals. The function makes a direct external call to the decimals() function on the token's contract address to fetch this information.
pub fn deployment_message_payload(token: ContractAddress) -> Span<felt252> {
// ...
let dispatcher = IERC20MetadataDispatcher { contract_address: token };
token.serialize(ref calldata);
// ...
let symbol_selector = selector!("symbol");
let mut symbol = call_contract_syscall(token, symbol_selector, array![].span())
.unwrap_syscall();
calldata = deserialize_and_append(symbol, calldata);
// @audit-issue The decimals() function is called directly, but it is optional in the ERC20 standard.
dispatcher.decimals().serialize(ref calldata);
calldata.span()
}The problem arises because the ERC20 token standard specifies that the decimals() function is OPTIONAL. While many tokens implement it for usability, its presence is not guaranteed.
Consequently, any valid ERC20-compliant token that does not implement the decimals() function will be incompatible with the protocol. When the deployment_message_payload(...) function is called with such a token, the direct call to dispatcher.decimals() will fail, causing the transaction to revert. This restricts the variety of tokens the platform can support and may lead to unexpected failures for users.
Recommendation
Consider whether the decimals() value is strictly necessary for the deployment_message_payload(...) function's intended purpose. If the decimals() information is not critical for the core functionality, consider removing the call to dispatcher.decimals() to ensure broader compatibility with all ERC20 tokens, including those that do not implement the optional decimals() function.
Resolution
Acknowledged. We decide on to support tokens that support decimals().
initiate_token_withdraw_with_id function usage is not enforced and ID uniqueness is not validated| File | Fix commit | Type | Status |
src/cairo/token_bridge.cairo | — | Info | Acknowledged |
The TokenBridge contract facilitates bridging tokens from L3 to the Starknet L2. A new function, initiate_token_withdraw_with_id(...), was introduced to help off-chain services track withdrawal transactions. This function takes a user-supplied id, calls the original initiate_token_withdraw(...) function, and then emits a WithdrawInitiatedWithId event that includes this id.
However, the original initiate_token_withdraw(...) function remains publicly callable. This allows users to initiate a withdrawal by calling it directly, which will only emit the WithdrawInitiated event, bypassing the new event with the unique id. Additionally, the id parameter in the new initiate_token_withdraw_with_id(...) function is provided by the user, and the contract does not enforce its uniqueness.
This design has two implications for off-chain systems. First, any indexing service that relies exclusively on the WithdrawInitiatedWithId event will fail to track withdrawals made through direct calls to initiate_token_withdraw(...). Second, since the uniqueness of the id is not guaranteed on-chain, different withdrawal transactions could be emitted with the same ID, which the off-chain tracking logic must be designed to handle correctly.
The following code snippets show the two functions:
fn initiate_token_withdraw(
ref self: ContractState,
l1_token: ContractAddress,
l1_recipient: ContractAddress,
amount: u256,
) {
// ...
// @audit This function is still callable directly.
self.emit(WithdrawInitiated { l1_token, l1_recipient, amount, caller_address });
}The newly added initiate_token_withdraw_with_id(...) function calls the original initiate_token_withdraw(...) function and emits an additional event.
fn initiate_token_withdraw_with_id(
ref self: ContractState,
l1_token: ContractAddress,
l1_recipient: ContractAddress,
amount: u256,
id: u256, // @audit-issue The uniqueness of this ID is not enforced.
) {
self
.initiate_token_withdraw(
l1_token: l1_token, l1_recipient: l1_recipient, amount: amount,
);
self
.emit(
WithdrawInitiatedWithId {
l1_token, l1_recipient, amount, caller_address: get_caller_address(), id,
},
);
}Recommendation
Consider if the consistency and uniqueness of withdrawal tracking are critical enough to be enforced on-chain. If correctness is crucial, changes could be implemented to ensure all withdrawals utilize the ID-based event mechanism and that the provided IDs are unique. Otherwise, the off-chain logic can be designed to handle both withdrawal paths and potential duplicate IDs to enforce correctness.
Resolution
Acknowledged. The off-chain logic will manage the correctness.
| File | Fix commit | Type | Status |
src/bridge/token_bridge.cairo | PR #34 PR #34 | Info | Fixed |
To enable deposits for a new token, it must first be deployed on the Appchain. This process is initiated on Starknet, and the token's status in the token_bridge contract is set to Pending. The check_deployment_status(...) function is then called within deposit-related functions to check if the deployment message has been successfully consumed on the Appchain and verified on Starknet. If so, the message is marked as SEALED and the token's status is updated to Active, enabling deposits.
The check_deployment_status(...) function includes a timeout mechanism. If a Pending token's deployment message is not SEALED within the pending_deployment_expiration period (currently 5 days), its status is reverted to Unknown. A race condition can occur if an Appchain's sequencer is inactive for more than 5 days and then comes back online to process the deployment message.
In this scenario, the token will be successfully deployed on the Appchain, but its status on the Starknet token_bridge will have already reverted to Unknown. Any subsequent attempts to re-register the token will fail on the Appchain with a TOKEN_ALREADY_EXISTS error. Since there is no mechanism to remove the token on the Appchain, this leads to a permanent state inconsistency, effectively blocking all future deposits for that token through the bridge. While this is unlikely for the main Starknet L1 -> L2 bridge, it poses a risk for newer Appchains that may experience sequencer downtime.
fn check_deployment_status(ref self: ContractState, token: ContractAddress) {
self.pausable.assert_not_paused();
let settings = self.token_settings.read(token);
if (settings.token_status != TokenStatus::Pending) {
return;
}
let message_status = self
.messaging_contract
.read()
.sn_to_appchain_messages(settings.deployment_message_hash);
if (message_status == MessageToAppchainStatus::Sealed) {
let new_settings = TokenSettings { token_status: TokenStatus::Active, ..settings };
self.token_settings.write(token, new_settings);
self.emit(TokenActivated { token });
} else if (get_block_timestamp() > settings.pending_deployment_expiration) {
let new_settings = TokenSettings { token_status: TokenStatus::Unknown, ..settings };
// @audit-issue If the Appchain processes the deployment after this timeout, the token
// will be deployed on the Appchain but remain `Unknown` on Starknet, blocking it permanently.
self.token_settings.write(token, new_settings);
}
}Recommendation
Consider revisiting the token activation mechanism to handle this edge case. One approach could be to utilize the Piltover's message cancellation mechanism to ensure that the token wasn't activated at the smart contract level.
Update from Nethermind Security
The latest changes, contain issues that need to be addressed:
message_status != MessageToAppchainStatus::Cancelling && get_block_timestamp() > settings.pending_deployment_expiration is problematic. This logic allows the code to enter this branch even if the message has already been cancelled, preventing it from reaching the MessageToAppchainStatus::Cancelled handling. The start_message_cancellation function should only be invoked when the message status is Pending;start_message_cancellation is called and the cancellation delay has passed, there should be a call to the cancel_message function. Without this, the message cancellation process remains incomplete;| File | Fix commit | Type | Status |
src/bridge/token_bridge.cairo | PR #34 | Info | Fixed |
The token_bridge contract manages the enrollment of new tokens. When a user interacts with a token, the check_deployment_status(...) function is called to verify if the token has been successfully activated on the Appchain. Activation is confirmed if the corresponding deployment message from Starknet has been sealed.
If the deployment message is not sealed within the pending_deployment_expiration window (currently 5 days), the token activation is considered to have failed. In this scenario, the function correctly reverts the token_status to TokenStatus::Unknown. However, it fails to clear the deployment_message_hash and pending_deployment_expiration from the failed enrollment attempt.
While other settings like max_total_balance may be intended to persist as they can be set by the governor independently of the token status, the leftover message hash and expiration time represent stale data. This "dirty" state could be misleading for off-chain tooling that queries these values and could introduce subtle bugs in future contract iterations if new logic does not account for an Unknown token having leftover deployment data.
fn check_deployment_status(ref self: ContractState, token: ContractAddress) {
// ...
if (message_status == MessageToAppchainStatus::Sealed) {
// ...
} else if (get_block_timestamp() > settings.pending_deployment_expiration) {
// @audit-issue Only the token_status is updated to Unknown. Other settings from the failed
// deployment, like deployment_message_hash and pending_deployment_expiration,
// persist, leaving stale data in storage.
let new_settings = TokenSettings { token_status: TokenStatus::Unknown, ..settings };
self.token_settings.write(token, new_settings);
}
}Recommendation
Consider clearing all relevant settings for a token when its deployment expires and it is marked as Unknown.
| File | Fix commit | Type | Status |
src/bridge/token_bridge.cairo | — | Info | Acknowledged |
The token_bridge contract provides several security roles to manage the lifecycle of tokens. The token_admin can move a token from an Active state to Deactivated or from an Unknown state to Blocked, preventing further deposits in both cases.
However, a gap exists in this security model for tokens that are in the Pending state - the period after enrollment but before successful activation. During this window, there is no direct mechanism for the token_admin to block the token. Instead, they must wait for the pending period to resolve. This requires actively monitoring the token's status until it either becomes Active (if deployment succeeds) or reverts to Unknown (if it fails), at which point it can finally be blocked or deactivated. This reactive approach is inefficient and introduces a risk that users might interact with a problematic token before it can be contained.
The only pre-emptive action available is for a higher-privileged app_governor to set the token's max_total_balance to 1 (lowest possible value). This creates an asymmetry in privileges, requiring escalation for a situation that a security-focused role like token_admin should be able to handle.
Recommendation
Consider introducing a mechanism to allow a privileged role, such as the token_admin, to block a token while it is in the Pending state. This could be achieved by adding a new token status to prevent the token from becoming active and to halt any associated actions.
Resolution
Fixing this causes unnecssary complication in the design. set_max_total_balance seems fine to be used in case a situation arises.
decrease_withdrawal_limit(...) function| File | Fix commit | Type | Status |
src/bridge/token_bridge.cairo | PR #34 | Best Practices | Fixed |
The decrease_withdrawal_limit(...) function is an administrative function that can be called by the security agent to lower the daily withdrawal limit percentage for a given token.
The function correctly asserts that the new limit is strictly less than the current one. However, the error message provided in the assert statement is Errors::NEW_LIMIT_MUST_BE_GREATER, which is the opposite of the required condition.
This incorrect error message can be misleading for the caller, when attempting to adjust the withdrawal limits.
fn decrease_withdrawal_limit(
ref self: ContractState, token: ContractAddress, daily_withdrawal_limit_pct: u8,
) {
self.bridge_access_control.assert_only_security_agent();
let current_pct = self.withdrawal.get_daily_withdrawal_limit_pct(token);
// @audit-issue The error message is incorrect. The new limit must be smaller.
assert(daily_withdrawal_limit_pct < current_pct, Errors::NEW_LIMIT_MUST_BE_GREATER);
self.withdrawal.write_daily_withdrawal_limit_pct(token, daily_withdrawal_limit_pct);
// ...
}Recommendation
Consider changing the error message to accurately reflect that the new daily withdrawal limit percentage must be smaller than the current one.
| File | Fix commit | Type | Status |
src/bridge/token_bridge.cairo | PR #34 | Best Practices | Fixed |
The check_deployment_status(...) function emits a TokenActivated event when a token is successfully activated. However, when activation fails due to an expired deployment, the token status is reverted to Unknown without emitting a corresponding event. The absence of an event for this state change makes it difficult for off-chain services to track unsuccessful token activations.
fn check_deployment_status(ref self: ContractState, token: ContractAddress) {
// ...
if (message_status == MessageToAppchainStatus::Sealed) {
// @audit Event is emitted on successful activation.
self.emit(TokenActivated { token });
// ...
} else if (get_block_timestamp() > settings.pending_deployment_expiration) {
// @audit-issue No event is emitted when token activation fails and its status is reverted.
let new_settings = TokenSettings { token_status: TokenStatus::Unknown, ..settings };
self.token_settings.write(token, new_settings);
}
}Recommendation
Consider emitting a dedicated event when a token's deployment expires and its status is reverted to Unknown.
Software documentation refers to the written or visual information that describes the functionality, architecture, design, and implementation of software. It provides a comprehensive overview of the software system and helps users, developers, and stakeholders understand how the software works, how to use it, and how to maintain it. Software documentation can take different forms, such as user manuals, system manuals, technical specifications, requirements documents, design documents, and code comments. Software documentation is critical in software development, enabling effective communication between developers, testers, users, and other stakeholders. It helps to ensure that everyone involved in the development process has a shared understanding of the software system and its functionality. Moreover, software documentation can improve software maintenance by providing a clear and complete understanding of the software system, making it easier for developers to maintain, modify, and update the software over time. Smart contracts can use various types of software documentation. Some of the most common types include:
These types of documentation are essential for smart contract development and maintenance. They help ensure that the contract is properly designed, implemented, and tested, and they provide a reference for developers who need to modify or maintain the contract in the future.
The Token Bridge team has provided a comprehensive walkthrough of the project in the kick-off call, and the README includes a detailed explanation of the intended functionalities. Moreover, the team addressed all questions and concerns raised by the Nethermind Security team, providing valuable insights and a comprehensive understanding of the project's technical aspects.
| 101Tests passed | 0Failed |
The complete test output is in the full report.
As auditors, we recommend that any changes or updates made to the audited codebase undergo a re-audit or security review to address potential vulnerabilities or risks introduced by the modifications. By conducting a re-audit or security review of the modified codebase, you can significantly enhance the overall security of your system and reduce the likelihood of exploitation. However, we do not possess the authority or right to impose obligations or restrictions on our clients regarding codebase updates, modifications, or subsequent audits. Accordingly, the decision to seek a re-audit or security review lies solely with you.
This report is based on the scope of materials and documentation provided by you to Nethermind in order that Nethermind could conduct the security review outlined in 1. Executive Summary and 2. Audited Files. The results set out in this report may not be complete nor inclusive of all vulnerabilities. Nethermind has provided the review and this report on an as-is, where-is, and as-available basis. You agree that your access and/or use, including but not limited to any associated services, products, protocols, platforms, content, and materials, will be at your sole risk. Blockchain technology remains under development and is subject to unknown risks and flaws. The review does not extend to the compiler layer, or any other areas beyond the programming language, or other programming aspects that could present security risks. This report does not indicate the endorsement of any particular project or team, nor guarantee its security. No third party should rely on this report in any way, including for the purpose of making any decisions to buy or sell a product, service or any other asset. To the fullest extent permitted by law, Nethermind disclaims any liability in connection with this report, its content, and any related services and products and your use thereof, including, without limitation, the implied warranties of merchantability, fitness for a particular purpose, and non-infringement. Nethermind does not warrant, endorse, guarantee, or assume responsibility for any product or service advertised or offered by a third party through the product, any open source or third-party software, code, libraries, materials, or information linked to, called by, referenced by or accessible through the report, its content, and the related services and products, any hyperlinked websites, any websites or mobile applications appearing on any advertising, and Nethermind will not be a party to or in any way be responsible for monitoring any transaction between you and any third-party providers of products or services. As with the purchase or use of a product or service through any medium or in any environment, you should use your best judgment and exercise caution where appropriate. FOR AVOIDANCE OF DOUBT, THE REPORT, ITS CONTENT, ACCESS, AND/OR USAGE THEREOF, INCLUDING ANY ASSOCIATED SERVICES OR MATERIALS, SHALL NOT BE CONSIDERED OR RELIED UPON AS ANY FORM OF FINANCIAL, INVESTMENT, TAX, LEGAL, REGULATORY, OR OTHER ADVICE.
The Leading Engineers of Blockchain Infrastructure