StarkNet Smart Contract Common Pitfalls

StarkNet Contract Common Pitfalls

What is StarkNet

StarkNet is a permissionless decentralized ZK-Rollup operating as an L2 network over Ethereum, where any dApp can achieve unlimited scale for its computation, without compromising Ethereum’s composability and security. — STARK.

Cairo Basic

Before going through the common pitfalls, let’s start with the Cairo basic syntax. StarkNet uses the Cairo programming language for writing StarkNet contracts. We have wrapped up some basics that you might need to understand first.

Field Element (felt)

There is only one data type in Cairo called field element (felt). The felt is a signed integer in the range from -P/2 < x < P/2 where P is a very large (prime) number (252-bit).

Variable Declaration

In Cairo, a variable can be declared by using the let, const, local or tempvar keywords.

let a = 1
let (bal) = balance.read()
const ONE = 1
const hello_string = 'hello' # 448378203247
alloc_locals
local a = b
tempvar a = 1

Storage Variable

The @storage_var decorator declares a variable that will be kept in the storage. In this case, this variable consists of a single felt, called balance.

# Define a storage variable.
@storage_var
func balance() -> (res : felt):
end
# Read a storage variable.
let (bal) = balance.read()
# Write a storage variable.
balance.write(initial_balance)

Storage Maps

Adding the argument to the storage variable will change the balance storage variable to a map from the public key (user) to balance (act like Solidity keyword mapping)

# A map from user (represented by account contract address)
# to their balance.
@storage_var
func balance(user : felt) -> (res : felt):
end
# Read a storage variable.
let (bal) = balance.read(user)
# Write a storage variable.
balance.write(user, initial_balance)

Function Visibility

Normally, all functions in StarkNet are internal functions that can only be called by the contract itself. Using @view or @external decorators will expose the function to be called by other contracts.

# Returns the current balance.
@view
func get_balance{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr,
}() -> (res : felt):
let (bal) = balance.read()
return (res=bal)
end
# Increases the balance by the given amount.
@external
func increase_balance{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr,
}(amount : felt):
let (bal) = balance.read()
balance.write(bal + amount)
# Emit the event.
update_balance.emit(new_balance=bal + amount)
return ()
end

Getting the Caller’s Address

In order to obtain the address of the account contract that is invoking our function, we can use the get_caller_address() library function:

from starkware.starknet.common.syscalls import get_caller_address# ...
let (caller_address) = get_caller_address()

Cross-Contract Call

Define an interface by using the @contract_interface decorator with the namespace keyword and declare the external functions in order to call this contract from another contract:

@contract_interface
namespace ICounterContract:
func increase_balance(amount : felt):
end
func get_balance() -> (res : felt):
end
end
@external
func call_increase_balance{syscall_ptr : felt*, range_check_ptr}(
contract_address : felt, amount : felt
):
ICounterContract.increase_balance(
contract_address=contract_address, amount=amount
)
return ()
end
@view
func call_get_balance{syscall_ptr : felt*, range_check_ptr}(
contract_address : felt
) -> (res : felt):
let (res) = ICounterContract.get_balance(
contract_address=contract_address
)
return (res=res)
end

Common Pitfalls in StarkNet

Let’s move to the main topic, the following examples show the common issues that might be encountered in the StarkNet contract development.

Arithmetic Issue

Input Validation

Since, the felt variable has range from -P/2 < x < P/2 where P is a very large (prime) number (252-bit), the contract must be implemented carefully as the variable can store a negative value.

@external
func transfer{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr
}(from_addr : felt, to_addr : felt, amount : felt):
alloc_locals
let (from_bal) = balance.read(from_addr)
let (to_bal) = balance.read(to_addr)
balance.write(from_addr, from_bal - amount)
balance.write(to_addr, to_bal + amount)
return ()
end

Integer Overflow

Performing mathematical operation in Cairo might cause an unexpected result. The above-mentioned transfer function is also vulnerable to the integer overflow issue. When the to_bal + amount is larger than the maximum integer value (P/2), the result will overflow and wrapped around to a very large negative value.

balance.write(to_addr, to_bal + amount)
@external
func transfer{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr
}(from_addr : felt, to_addr : felt, amount : felt):
alloc_locals
# check the amount is non negative value
assert_nn(amount)
let (from_bal) = balance.read(from_addr)
let (to_bal) = balance.read(to_addr)
# check overflow/underflow
assert_nn(from_bal - amount)
assert_nn(to_bal + amount)
balance.write(from_addr, from_bal - amount)
balance.write(to_addr, to_bal + amount)
return ()
end

Authentication & Authorization

The user authorization in StarkNet can be checked by using the get_caller_address() system call function which returns the caller of the function call.

@external
func transfer{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr
}(from_addr : felt, to_addr : felt, amount : felt):
alloc_locals
# check the amount is non negative value
assert_nn(amount)
let (from_bal) = balance.read(from_addr)
let (to_bal) = balance.read(to_addr)
# check overflow/underflow
assert_nn(from_bal - amount)
assert_nn(to_bal + amount)
balance.write(from_addr, from_bal - amount)
balance.write(to_addr, to_bal + amount)
return ()
end
@external
func transfer{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr
}(to_addr : felt, amount : felt):
alloc_locals
# check the amount is non negative value
assert_nn(amount)
# get caller address
let (from_addr) = get_caller_address()
let (from_bal) = balance.read(from_addr)
let (to_bal) = balance.read(to_addr)
# check overflow/underflow
assert_nn(from_bal - amount)
assert_nn(to_bal + amount)
balance.write(from_addr, from_bal - amount)
balance.write(to_addr, to_bal + amount)
return ()
end

Reentrancy Attack

Even if there is no native token callback in StarkNet, poor implementation or callbacks in various standards (ERC721.onerc721received, ERC777.tokensReceived, etc.) may still be vulnerable to reentrancy attacks.

@external
func withdrawAll{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr
}():
alloc_locals
let (caller) = get_caller_address()
let (bal) = balance.read(caller)
assert_not_zero(bal)
let (token) = token.read()
IERC777.transfer(token, caller, bal)

balance.write(caller, 0)
return ()
end
@external
func withdrawAll{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr
}():
alloc_locals
# check
let (caller) = get_caller_address()
let (bal) = balance.read(caller)
assert_not_zero(bal)

# effect
balance.write(caller, 0)
# interact
let (token) = token.read()
IERC777.transfer(token, caller, bal)
return ()
end

Cross Chain Attack

In StarkNet the cross-chain message is free to be created by any address both for L1 to L2 and L2 to L1. The source of the cross-chain message must be checked to prevent a message spoofing attack.

@l1_handler
func deposit{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr
}(
from_address : felt,
l2_recipient : felt,
amount : felt,
sender_address : felt,
):
alloc_locals
let (token) = _token.read()
IToken.mint(token, l2_recipient, amount)
deposit.emit(l2_recipient, amount)
return ()
end
@l1_handler
func deposit{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr
}(
from_address : felt,
l2_recipient : felt,
amount : felt,
sender_address : felt,
):
alloc_locals
# check the from_address
let (l1_bridge) = _l1_bridge.read()
assert from_address = l1_bridge
let (token) = _token.read()
IToken.mint(token, l2_recipient, amount)
deposit.emit(l2_recipient, amount)
return ()
end

Conclusion

These typical StarkNet pitfalls are merely examples. Other weaknesses, like the business logic problems, may be hidden in the contract. New technology also brings new attack vectors, which both the platform developers and users must be aware of. We hope that these example vulnerabilities can help increase the awareness and improve the security level of the decentralized world.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Inspex

Inspex

Cybersecurity professional service, specialized in blockchain and smart contract auditing https://twitter.com/InspexCo