Cairo Security Flaws
Intro
StarkNet is a scalable Layer 2 solution for Ethereum, built on zk-STARK technology. It enables fast, secure, and low-cost transactions through the use of validity rollups. StarkNet operates on the Cairo VM, allowing for the development of smart contracts in the Cairo language.
While Cairo is a relatively new language, it is rapidly evolving and gaining popularity. However, as with any new technology, security concerns require careful examination.
Cairo Basics
Let’s start our dive into the world of Cairo by exploring its fundamentals. This will allow us to more effectively absorb subsequent information.
Primary Data Type
The primary data type in Cairo is a felt. It represents an integer whose value lies in the range from 0 to P-1, where P is a fixed large prime number equal to 2^251 + 17 * 2^192 + 1
bits.
Memory
Cairo employs a read-only, non-deterministic memory model. Each memory cell can be written to only once and its value cannot be modified during program execution. The syntax [x]
represents the value at memory address x
. This model implies that if a value is set at the beginning of a program, it remains constant throughout its execution.
Modifiers
Cairo employs the keyword fn to define functions. While this language supports parameter modifiers (such as mut
for mutable and ref
for reference parameters), it does not provide a mechanism for adding modifiers to the function itself. Unlike Solidity, which uses modifiers like external
or view
to influence function visibility and behavior, Cairo lacks such modifiers.
Want to dive even deeper into Cairo? The official documentation is your go-to guide.
Security Practices
Having explored the fundamental elements of Cairo, we will now turn our attention to analyzing the most common attack vectors and vulnerabilities inherent to this language. We will also discuss methods to mitigate these issues.
L1 → L2 Type Conversion
Let’s illustrate this problem using the example of addresses. As previously mentioned, Starknet addresses are of type felt, which has a range of 0 < x < P
(2^251 + 17 * 2^192 + 1
bits). In Ethereum L1, addresses are of type uint160, with a range of 0 <= x < 2^160
.
When transferring addresses between layers from L1 to L2, the address is typically represented as uint256. However, this can lead to issues where a valid L1 address is mapped to a null address or an unexpected address on L2.
For instance, consider an L1-to-L2 bridge contract that allows depositing tokens from L1 to L2. If the to
address parameter is not properly validated, it could result in tokens being sent to an unintended address on L2.
To mitigate this, it is crucial to validate parameters, especially those provided by users, when sending messages from L1 to L2. It is important to remember that the range of the felt type in Cairo is smaller than the range of the uint256
type used in Solidity.
Remember that this issue is relevant to all uint256
values when creating and verifying code.
Access Control
An incorrect implementation of access control can lead to serious consequences. The compromise of a single account as a result of an attack can lead to the compromise of the entire smart contract and, consequently, the loss of funds stored within.
A standard security mechanism is to verify the address of the calling function. In Solidity, the require
operator is typically used for this, causing the transaction to revert if the conditions are not met. In Cairo, the assert
operator plays a similar role.
Let’s consider a withdrawAssets
function that includes a check of the caller’s address to execute the function.
fn withdrawAssets() {
let caller = get_caller_address();
assert(caller == whitelistedAddress, 'caller not whitelisted');
}
To effectively implement access control in StarkNet contracts, it is recommended to use the OpenZeppelin
library. It provides pre-built contracts, such as Ownable
and AccessControl
, that allow for flexible definition of roles and management of access to contract functions.
However, when designing an access control system, the principle of least privilege should be adhered to. This means that each role should be granted only those permissions that are strictly necessary for its functions. Excessive permissions can create vulnerabilities and increase the risk of contract compromise.
Reentrancy
While reentrancy attacks have become less frequent due to the introduction of various protection methods and increased developer awareness, they are still possible in StarkNet.
OpenZeppelin developers have implemented a reentrancy guard
mechanism here as well, which protects critical contract functions from unauthorized re-entries.
Unlike Solidity, Cairo does not support function modifiers. For this reason, the OpenZeppelin library offers two methods: _start
and _end
, which must be called at the beginning and end of the protected function, respectively.
func _start{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() {
let (has_entered) = ReentrancyGuard_entered.read();
with_attr error_message("ReentrancyGuard: reentrant call") {
assert has_entered = FALSE;
}
ReentrancyGuard_entered.write(TRUE);
return ();
}
func _end{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() {
ReentrancyGuard_entered.write(FALSE);
return ();
}
The protected function should call ReentrancyGuard._start
before the first statement of the function and ReentrancyGuard._end
before the return statement. If the function is reentered, ReentrancyGuard._start
will be called again, and upon subsequent access to storage, it will see that has_entered
is now TRUE
, causing the assertion to fail.
Furthermore, developers should always remember to adhere to the checks-effects-interaction pattern, which also helps prevent this type of attack.
Repeat Signature Replay
The key takeaway from this attack is the critical importance of adhering to the principle of nonce
uniqueness. For every transaction, especially those involving signatures, the nonce must be strictly incremented. This implies that the signature infrastructure must request the current nonce value before generating each new signature.
Let’s imagine you’ve staked your funds in a protocol and have accumulated rewards. To withdraw these rewards, you need to confirm the transaction with a valid signature. The signature verification function takes the amount
, r
, and s
values of the signature as input. If the signature is valid, it will retrieve the address of the ecosystem token and transfer the amount to the calling subscriber.
However, an attacker could replay the transaction and call this function as many times as desired, as long as the amount parameter matches the original signed message. To prevent this, we add a nonce
value to the function.
@external
func swap_game_currency_safe{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr,
ecdsa_ptr : SignatureBuiltin*
}(
r: felt,
s: felt,
amount: felt
):
alloc_locals
let (local caller_address) = get_caller_address()
let (local nonce) = nonces.read(caller_address)
let (_signer) = signer.read()
# update nonce
nonces.write(caller_address, value=nonce+1)
let (message) = hash2{hash_ptr=pedersen_ptr}(amount, caller_address)
let (message_part_2) = hash2{hash_ptr=pedersen_ptr}(message, nonce)
verify_ecdsa_signature(
message=message_part_2, public_key=_signer, signature_r=r, signature_s=s
)
let (token_address_) = token_address.read()
let (contract_address) = get_contract_address()
IERC20.transferFrom(contract_address=token_address_, sender=contract_address, recipient=caller_address, amount=Uint256(amount, 0))
return ()
end
Now, for each function call, the user’s current nonce
is retrieved from the storage and incremented by 1
, so that the next call uses the updated value.
Upgradable Contracts
Unlike Ethereum, StarkNet distinguishes between contract classes and contract instances. A contract class encapsulates immutable bytecode representing the contract’s logic, while a contract instance is a specific deployment of a contract class with its own unique state.
A common pitfall in StarkNet development is the inadvertent direct deployment of an implementation contract, circumventing the proxy contract. To correctly implement the proxy pattern in StarkNet, the following steps should be followed:
- Define a contract class containing the core logic of the smart contract.
- Deploy a proxy contract that will forward all calls to the implementation contract.
- Initialize the implementation contract through the proxy contract, passing the necessary parameters.
Deviation from this pattern undermines the core purpose of the proxy pattern: providing abstraction and centralized control over contract state access. Bypassing the proxy allows for arbitrary modifications to the implementation contract’s state, potentially leading to unforeseen consequences and compromising system security.
To simplify working with contracts that have initialization functions, it is also recommended to use the OpenZeppelin’s Initializable
library. It provides convenient tools for the safe and reliable management of contract lifecycles.
Conclusion
Ensuring security is a paramount concern for both developers and auditors. Cairo, as a relatively new programming language, also necessitates a thorough analysis in terms of reliability and the identification of potential vulnerabilities.
Contents
YOU MAY ALSO LIKE
Oxor.io: Your Trusted Partner for Comprehensive Smart Contract Auditing
Expert Insights
Oxor.io offers expert smart contract auditing to ensure security and reliability in blockchain projects. Trust our experienced team to safeguard your decentralized applications.
Boost Investor Confidence with Certified Smart Contract Audits
Expert Insights
Discover how certified smart contract audits boost investor confidence and enhance fundraising for blockchain projects. Learn about Oxor.io's comprehensive audit process that sets your project apart
Overflow and Underflow Vulnerabilities in Cairo
Expert Insights
Explore the critical security considerations of Cairo language in our latest article on Overflow and Underflow vulnerabilities. Discover how Cairo's evolution from version 0.x to 1.0 has enhanced handling of these risks, ensuring more secure programming practices
Oxorio 2023 Security Report
Expert Insights
Explore OXORIO's Security Digest for 2023: a deep dive into our smart contract audit achievements, including issue severity, response rates, and types. See how we're shaping a secure blockchain walking into 2024!
Have a question?
Stay Connected with OXORIO
We're here to help and guide you through any inquiries you might have about blockchain security and audits.