Skip to main content

Gasless Token Approvals? Let’s Talk ERC-2612 permit()

· 4 min read

“Web3 and blockchain run on protocols — every improvement is a protocol upgrade, not just a feature update.”
— 0x_scater


Introduction

When building in Web3, understanding protocol-level innovations is critical. One such innovation is ERC-2612, which upgrades the standard ERC-20 token approval mechanism to support gasless approvals.

This blog explores:

  • Why ERC-2612 exists
  • How it improves the user experience
  • How it works under the hood using EIP-712 signatures
  • Why it’s important for onboarding and adoption

Let’s compare the traditional ERC-20 flow with ERC-2612.


Traditional ERC-20 Flow

ERC-20 token transfers with smart contracts (like staking, DEXs, etc.) require two transactions:

Old Flow (ERC-20):

  1. approve(spender, amount)
    • Sent by token owner X
    • Requires ETH to pay for gas
  2. transferFrom(owner, recipient, amount)
    • Called by the contract Y

participant X as User
participant T as Token Contract
participant Y as Protocol Contract

X->>T: approve(Y, amount)
Note right of X: Pays gas
Y->>T: transferFrom(X, Y, amount)

Drawbacks:

Requires two on-chain transactions

1.Users must own ETH to approve

2.Onboarding becomes harder (especially for new users who don’t have ETH yet)

Introduce ERC-2612: The permit()

ERC-2612 introduces a new function:

function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v, bytes32 r, bytes32 s
) external;

This allows a user (owner) to sign a gasless approval off-chain and any relayer (spender or someone else) to submit it on-chain.

Flow (ERC-2612):

1.X signs an EIP-712 message off-chain

2.Y (or any relayer) calls permit() + transferFrom()

Single transaction and No ETH required from X

    participant X as User
participant Y as Relayer
participant T as Token Contract
X->>X: Sign permit message (EIP-712)
Y->>T: permit(X, Y, amount, ...)
Y->>T: transferFrom(X, Y, amount)

Query raises:How does it work?

The permit() function uses the EIP-712 standard to verify typed data signatures.

EIP-712 Typed Data Signature:

EIP-712 allows for secure and human-readable signing by hashing a structured payload. This avoids generic sign() messages, enabling wallet providers (like MetaMask) to show readable content.

Example of payload:

{
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Permit": [
{"name": "owner", "type": "address"},
{"name": "spender", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "deadline", "type": "uint256"}
]
},
"domain": {
"name": "MyToken",
"version": "1",
"chainId": 1,
"verifyingContract": "0xYourTokenAddress"
},
"message": {
"owner": "0xX...",
"spender": "0xY...",
"value": "1000000000000000000",
"nonce": 0,
"deadline": 1742680400
}
}

Real Permit() code:

/ Token contract includes this:
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v, bytes32 r, bytes32 s
) external {
require(block.timestamp <= deadline, "Permit: expired deadline");

bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(
PERMIT_TYPEHASH,
owner,
spender,
value,
nonces[owner]++,
deadline
))
)
);

address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress == owner, "Invalid signature");

_approve(owner, spender, value);
}

Why need ERC-2612?

  1. Gasless UX: New users can approve tokens without ETH — just sign a message.

  2. One-Tx Flow: Only one transaction to complete an approval and usage, improving developer and user experience.

  3. Relayer-Friendly :Projects can sponsor gas fees for onboarding using relayers.

  4. Protocol-Level Upgrade: It’s not just a frontend trick — it changes the way ERC-20 tokens behave at the contract level.

Real-World Applications

  1. DEXs (like Uniswap) use permit() to enable one-click swaps

  2. DAOs allow gasless voting setups

  3. DeFi protocols onboard users without forcing them to buy ETH

Building With Protocols In Mind

ERC-2612 is a great example of how protocol-level upgrades make real impact.

If you're building in Web3, always ask:

Can I make this process more user-friendly at the protocol level?

Final Thoughts — SCATERLABs

Gasless approvals aren't a feature; they’re a shift in the ERC-20 interaction model.

They show what’s possible when we treat protocols as products.

Let’s keep building with protocols in mind.

— 0x_scater, Founder @ SCATERLABs

Resources i used:

  1. EIP-2612: https://eips.ethereum.org/EIPS/eip-2612

  2. EIP-712: https://eips.ethereum.org/EIPS/eip-712

  3. I use chatgpt for understanding the protocols

MultisigWallet

· 8 min read

purpose:

This contract implements a Multi-signature Wallet that allows a group of owners to collectively manage and authorize transactions. A transaction can only be executed when a specified number of owners approve it.

Functionalities of MultisigContract:

1.Owners: Only designated owners can submit, confirm, execute, or revoke transactions.

2.Submit Transaction: An owner proposes a transaction.

3.Confirm Transaction: Other owners must confirm the transaction.

4.Execute Transaction: Once the required number of confirmations is met, the transaction can be executed.

5.Revoke Confirmation: An owner can revoke their confirmation before execution.

6.Deposit: Ether can be deposited into the wallet.

About the Contract::

This is a simple Multi-signature contract that allows a set of designated owners to approve transactions before they can be executed.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MultisigWallet {
address[] public owners; // Owners of the multisig wallet who can approve transactions

mapping(address => bool) public isOwner;
uint public required;

struct Transaction {
address to;
uint value;
bytes data;
bool executed;
uint numConfirmations;
}

Transaction[] public transactions;

mapping(uint => mapping(address => bool)) public isConfirmed;

// --- Events ---
event Deposit(address indexed sender, uint amount);
event Submit(
uint indexed txIndex,
address indexed to,
uint value,
bytes data
);
event Confirm(uint indexed txIndex, address indexed owner);
event Execute(uint indexed txIndex);
event Revoke(uint indexed txIndex, address indexed owner);

// --- Modifiers ---
modifier onlyOwner() {
require(isOwner[msg.sender], "Not owner");
_;
}

modifier txExists(uint _txIndex) {
require(_txIndex < transactions.length, "Tx doesn't exist");
_;
}

modifier notExecuted(uint _txIndex) {
require(!transactions[_txIndex].executed, "Already executed");
_;
}

modifier notConfirmed(uint _txIndex) {
require(!isConfirmed[_txIndex][msg.sender], "Already confirmed");
_;
}

// --- Constructor ---
constructor(address[] memory _owners, uint _required) {
require(_owners.length > 0, "Owners required");
require(
_required > 0 && _required <= _owners.length,
"Invalid required"
); // Sets the number of required confirmations for executing a transaction


for (uint i; i < _owners.length; i++) {
address owner = _owners[i];
//@nkdoubt://what is the use of zero address in ethereum and significance of zero address?
require(owner != address(0), "Invalid owner");
require(!isOwner[owner], "Owner not unique");
isOwner[owner] = true;
owners.push(owner);
}

required = _required;
}

// --- Functions ---

receive() external payable {
emit Deposit(msg.sender, msg.value); // When anyone deposits Ether into the contract, it will be recorded in the event log

}

function submitTransaction(
address _to,
uint _value,
bytes memory _data
) public onlyOwner {
uint txIndex = transactions.length;

transactions.push(
Transaction({
to: _to,
value: _value,
data: _data,
executed: false,
numConfirmations: 0
})
);

emit Submit(txIndex, _to, _value, _data);
}

function confirmTransaction(
uint _txIndex
)
public
onlyOwner
txExists(_txIndex)
notExecuted(_txIndex)
notConfirmed(_txIndex)
{
Transaction storage transaction = transactions[_txIndex];
transaction.numConfirmations += 1;
isConfirmed[_txIndex][msg.sender] = true;

emit Confirm(_txIndex, msg.sender);
}

function executeTransaction(
uint _txIndex
) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
Transaction storage transaction = transactions[_txIndex];
require(
transaction.numConfirmations >= required,
"Not enough confirmations"
);

transaction.executed = true;

(bool success, ) = transaction.to.call{value: transaction.value}(
transaction.data
);
require(success, "Tx failed");

emit Execute(_txIndex);
}

function revokeConfirmation(
uint _txIndex
) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
require(isConfirmed[_txIndex][msg.sender], "Tx not confirmed");

Transaction storage transaction = transactions[_txIndex];
transaction.numConfirmations -= 1;
isConfirmed[_txIndex][msg.sender] = false;

emit Revoke(_txIndex, msg.sender);
}

// Utility functions
function getOwners() public view returns (address[] memory) {
return owners;
}

function getTransactionCount() public view returns (uint) {
return transactions.length;
}

function getTransaction(
uint _txIndex
)
public
view
returns (
address to,
uint value,
bytes memory data,
bool executed,
uint numConfirmations
)
{
Transaction storage transaction = transactions[_txIndex];
return (
transaction.to,
transaction.value,
transaction.data,
transaction.executed,
transaction.numConfirmations
);
}
}

Vulnerability Scenario:

If an attacker is one of the owners, they may try to:

1.Submit malicious transactions (e.g., to their own wallet).

2.Revoke confirmations repeatedly to prevent quorum.

3.Spam the contract with transactions to exhaust gas or memory.

4.Trigger reentrancy attacks if executeTransaction() is not protected.

5.Abuse revokeConfirmation() to manipulate the confirmation state rapidly.

Fix the contract:

nonReentrant Modifier: Prevents reentrancy attacks during transaction execution.

require(_to != address(this)): Prevents self-calls that could manipulate internal state or reenter functions.

revokeCooldown using lastRevokeTime : Imposes a slowdown (e.g., 30 seconds) between consecutive revokes by the same owner.

max pending transactions : Prevents spamming of the contract by limiting the total number of unexecuted transactions.

require(unique owners) : Enforces that all owners are valid and unique, rejecting zero-address or duplicates.

Updated contract:

After all these udpates ,the contract looks like:

//SPDX-Licnese-Identifier:MIT
pragma solidity ^0.8.20;

contract MultisigWallet {
address[] public owners;
mapping(address => bool) public isOwner;
uint public required;

struct Transaction {
address to;
uint value;
bytes data;
bool executed;
uint numConfirmations;
uint timestamp;
}

Transaction[] public transactions;
mapping(uint => mapping(address => bool)) public isConfirmed;
mapping(address => uint) public lastRevokeTime;

bool private locked;// Used to prevent reentrancy attacks


// --- Events ---
event Deposit(address indexed sender, uint amount);
event Submit(
uint indexed txIndex,
address indexed to,
uint value,
bytes data
);
event Confirm(uint indexed txIndex, address indexed owner);
event Execute(uint indexed txIndex);
event Revoke(uint indexed txIndex, address indexed owner);

// --- Modifiers ---
modifier onlyOwner() {
require(isOwner[msg.sender], "Not owner");
_;
}

modifier txExists(uint _txIndex) {
require(_txIndex < transactions.length, "Tx doesn't exist");
_;
}

modifier notExecuted(uint _txIndex) {
require(!transactions[_txIndex].executed, "Already executed");
_;
}

modifier notConfirmed(uint _txIndex) {
require(!isConfirmed[_txIndex][msg.sender], "Already confirmed");
_;
}
//added for reentrancy
modifier nonReentrant() {
require(!locked, "Reentrancy detected");
locked = true;
_;
locked = false;
}

// --- Constructor ---
constructor(address[] memory _owners, uint _required) {
require(_owners.length > 0, "Owners required");
require(
_required > 0 && _required <= _owners.length,
"Invalid required"
);

for (uint i; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0), "Invalid owner");// Prevents zero addresses or duplicates during deploymen
require(!isOwner[owner], "Owner not unique");
isOwner[owner] = true;
owners.push(owner);
}

required = _required;
}

// --- Receive Ether ---
receive() external payable {
emit Deposit(msg.sender, msg.value);
}

// --- Core Functions ---
function submitTransaction(
address _to,
uint _value,
bytes memory _data
) public onlyOwner {
require(_to != address(this), "Can't call self");// Prevents internal calls to the contract itself.
require(transactions.length < 1000, "Too many pending transactions");// Limit the number of pending transactions to prevent spamming

uint txIndex = transactions.length;

transactions.push(
Transaction({
to: _to,
value: _value,
data: _data,
executed: false,
numConfirmations: 0,
timestamp: block.timestamp
})
);

emit Submit(txIndex, _to, _value, _data);
}

function confirmTransaction(
uint _txIndex
)
public
onlyOwner
txExists(_txIndex)
notExecuted(_txIndex)
notConfirmed(_txIndex)
{
Transaction storage transaction = transactions[_txIndex];
transaction.numConfirmations += 1;
isConfirmed[_txIndex][msg.sender] = true;

emit Confirm(_txIndex, msg.sender);
}

function executeTransaction(
uint _txIndex
) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) nonReentrant {
Transaction storage transaction = transactions[_txIndex];

require(
transaction.numConfirmations >= required,
"Not enough confirmations"
);

transaction.executed = true;

(bool success, ) = transaction.to.call{value: transaction.value}(
transaction.data
);
require(success, "Tx failed");

emit Execute(_txIndex);
}

function revokeConfirmation(
uint _txIndex
) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
require(isConfirmed[_txIndex][msg.sender], "Tx not confirmed");

//Adds a 30-second delay before the same owner can revoke
require(
block.timestamp > lastRevokeTime[msg.sender] + 30,
"Revoke cooldown"
);
lastRevokeTime[msg.sender] = block.timestamp;

Transaction storage transaction = transactions[_txIndex];
transaction.numConfirmations -= 1;
isConfirmed[_txIndex][msg.sender] = false;

emit Revoke(_txIndex, msg.sender);
}

// --- View Functions ---
function getOwners() public view returns (address[] memory) {
return owners;
}

function getTransactionCount() public view returns (uint) {
return transactions.length;
}

function getTransaction(
uint _txIndex
)
public
view
returns (
address to,
uint value,
bytes memory data,
bool executed,
uint numConfirmations
)
{
Transaction storage transaction = transactions[_txIndex];
return (
transaction.to,
transaction.value,
transaction.data,
transaction.executed,
transaction.numConfirmations
);
}
}

How it works:

Let’s say the company wants to send 10 ETH to a freelancer for development work. But to ensure security and agreement, at least 2 directors must approve before the money is sent.

steps:

1.Deployment: Alice deploys the MultisigWallet contract with:

Owners: [Nithin, uday, rocky]

Required confirmations: 2

MultisigWallet([ Nithin, uday, rocky], 2)

2.Fund the Wallet: Anyone (e.g., the company) sends ETH to the wallet.

It emits a Deposit event.

send 20 ETH to MultisigWallet address

  1. Submit a Transaction: Nithin wants to pay a freelancer.

he submits a transaction to send 10 ETH to Dev.

submitTransaction(to: Dev, value: 10 ETH, data: "")

This is recorded in the contract as a Transaction object:

Not yet executed.

Has 0 confirmations.

Submit event is emitted with txIndex = 0.

4.Confirm the Transaction: Uday agrees with the transaction and confirms it.

confirmTransaction(0)

The number of confirmations increases to 1.

Confirm event is emitted.

Rocky also confirms:

confirmTransaction(0)

Now it has 2 confirmations — which meets the required threshold.

Confirm event is emitted again.

5.Execute the Transaction: Anyone of the owners (say NITHIN again) can now call:

executeTransaction(0)

Contract checks:

Is confirmed by 2 owners?

Is it not already executed?

If checks pass, the 10 ETH is sent to the freelancer.

Execute event is emitted.

Summary:

Why use a Multisig Wallet?

Trustless collaboration: No single person has full control.

Security: Reduces the risk of fund theft even if one owner is compromised.

Transparency: On-chain confirmations and log

Understanding ECDSA Signatures in Blockchain: From Math to Wallets

· 7 min read

The backbone of blockchain is cryptography because it ensures data security, integrity, and trust without needing a central authority.

One key innovation is ECDSA,which is used to Proof of ownership in Ethereum is achieved using public and private key pairs that are used to create digital signatures. Signatures are analogous to having to provide personal ID when withdrawing from the bank - the person needs to verify that they are the owner of the account.

What is ECDSA

The Elliptic Curve Digital Signature Algorithm (ECDSA) is based on Elliptic Curve Cryptography (ECC) and is used to generate keys, authenticate, sign, and verify messages

Signatures provide a means for authentication in blockchain technology, allowing operations, such as sending transactions, to be verified that they have originated from the intended signer

Basics Of Elliptici curve cryptography:

ECC is based on algebraic structures of elliptic curves over finite fields. Ethereum uses:

Curve: secp256k1

Equation: y² = x³ + 7 over a finite field F_p where p is a 256-bit prime.

It’s a smooth, symmetric curve (about the x-axis). Every operation is modulo a large prime, so the curve becomes a finite set of points

Graph Concept:

ECDSA

Points on the curve can be added together geometrically.

Multiplying a point (e.g. G) repeatedly is called scalar multiplication and is used for key generation

Visula Representation:

ECDSA

KeyGeneration:

Private Key (p): A random number in [1, n-1], where n is the curve's order.

Public Key (Q): A point on the curve computed by: Q = p * G where G is the generator point (fixed for secp256k1).

Security Basis: It’s computationally infeasible to reverse:

Given Q and G, it’s almost impossible to find p This is called the Elliptic Curve Discrete Logarithm Problem (ECDLP)

Signing Mechanism(r,s,and v are formed):

Steps to create Signature Creation:

msg: message (usually hashed using keccak256)

p: private key

n: order of the curve


steps :

1.Hash the message:h = keccak256(msg)

2.Generate a random nonce k (must be secret and unique every time!)

3.Compute random point R: R = k * G, take its x coordinate → r = R.x mod n (Think of r like a fingerprint for that unique signature instance.)

4.Compute s: s = k⁻¹ * (h + r * p) mod n (It ties your private key to the specific message hash securely.)

5.Compute v: The recovery ID, to tell which of the two possible public keys to recover.( helps Ethereum get the signer's address using ecrecover(). )
v = 27 + recovery_id → 27 or 28

**Final Signature = (r, s, v)**

Signature Verification:

Use (r, s, v) to verify that the signer owns the private key.

In Ethereum:

1.Recompute h = keccak256(msg)

2.Compute:
u1 = s⁻¹ * h mod n
u2 = s⁻¹ * r mod n

3.Compute point:
R' = u1 * G + u2 * Q

4.If R'.x mod n == r, the signature is valid

On-chain in ethereum:

address signer = ecrecover(hash, v, r, s);

Why ECDSA is Better (for now)

1.Smaller keys/signatures than RSA

2.Efficient on-chain operations (especially in Ethereum)

3.Widely adopted (Bitcoin, Ethereum, etc.)

4.But: vulnerable to quantum computing in the long term

Why ECDSA needed in Blockchain

ECDSA is used in blockchain to securely sign transactions so that only the person with the private key can authorize actions, and everyone else can verify it using the public key — ensuring authenticity, integrity, and non-repudiation of data without a central authority.

In simple terms:

ECDSA proves "I am the owner of this wallet" without revealing your private key, which is essential for trust and security in a decentralized network.

How it works :

let us take an example to understand the ECDSA in ethereum:

Nithinkumar wants to send 1 ETH to Uday. He uses an Ethereum wallet (e.g., MetaMask) to sign the transaction.

1.Message to be signed (Transaction Data):

{
"to": "0xBobAddress",
"value": 1 ETH,
"nonce": 0,
"gasPrice": 20 Gwei,
"gasLimit": 21000
}

2.The wallet hashes the transaction and signs it using Alice’s private key with ECDSA, producing a signature:

Signature = (r, s, v)

3.This signed transaction is broadcasted to the Ethereum network. 4.Ethereum nodes receive the transaction, use the (r, s, v) values to:

     Every Ethereum transaction includes:

to address
value
nonce
gasPrice
gasLimit
data
And the signature (r, s, v)

The node takes all of the transaction fields except the signature, and computes a Keccak256 hash of this transaction — let’s call it msgHash.

Using msgHash and the (r, s, v) values, the node uses the ECDSA recovery algorithm to reconstruct the public key that created the signature.

publicKey = ecrecover(msgHash, v, r, s)//This is a standard algorithm available in Ethereum and cryptography libraries.

derive the ethereum address using public key Once the public key is recovered:

Ethereum Address: Last 20 bytes of keccak256(public key)

Use the secp256k1 curve and multiply the private key with the generator point G:

publicKey = privateKey * G

Public key is a 128-character hexadecimal (512 bits) if uncompressed (starts with 0x04 + x + y).

Hash only the x and y part (not the 0x04 prefix).

keccak256(publicKey[1:]) // public key without 0x04 prefix

ethereum address = last_20_bytes(keccak256(pubkey))

This produces a 20-byte (40-hex-character) Ethereum address.

5.Verify that the recovered address matches the sender's address in the transaction.

If the signature is valid and Nithinkumar has enough ETH, the transaction is accepted and mined into a block.

Security Concerns :

Replay Attack:

Why Malleability Happens:

Due to curve symmetry, two values of s can produce valid signatures:
s
n - s

Both are valid, allowing attackers to create a second signature without the private key.

This allows replay attacks across networks (Ethereum ↔ BSC) or in contracts that don't check strict signature formats.

How to Prevent :

1.Enforce low s values: s ≤ n/2

2.Normalize signatures before verifying

3.Use EIP-155 to include chain ID in transactions

How Private Keys Can Be Stolen(if u neglect the nonce) :

ECDSA is secure only if:

⚠️ Never reuse the same nonce k — this will leak your private key!

Private Key Leaks Happen If: Same k reused → Attacker can compute p directly!

Predictable k (bad RNG) → s becomes solvable

Weak entropy during key generation

Exploit case:

If two signatures have the same r, attacker can solve for k, then back-calculate p. given:

s1 = k⁻¹ * (h1 + r * p)
s2 = k⁻¹ * (h2 + r * p)

Subtract:

s1 - s2 = k⁻¹ * (h1 - h2)
⇒ k = (h1 - h2) / (s1 - s2)
⇒ p = ((s * k - h) / r) mod n

Attacker got the private Key game over

Conclusion:

ECDSA keeps blockchain transactions safe and verified.

It proves who owns a wallet without showing the private key.

In this blog, we saw how ECDSA works and why it’s used in blockchains like Bitcoin and Ethereum. It’s what makes sure only the real owner can send crypto.

It’s a small part of crypto, but very important for trust.

Understanding Rust Ownership: What Makes Rust Unique

· 4 min read

Rust’s ownership model is what makes it stand out from other languages. Unlike languages that use garbage collection (like Java or Go), Rust enforces memory safety at compile time without needing a runtime.


Rust Ownership: The Heart of Its Memory Model

Rust’s ownership rules ensure memory is managed safely and efficiently, eliminating common bugs like dangling pointers, data races, or double frees.

The Three Ownership Rules

  1. Each value in Rust has a single owner.

  2. When the owner goes out of scope, the value is automatically dropped.

  3. A value can be:

    • moved,
    • borrowed immutably (any number of times),
    • or borrowed mutably (only once at a time).

Moving Ownership

fn main() {
let s1 = String::from("hello"); // s1 owns the String
let s2 = s1; // ownership moves to s2

// println!("{}", s1); // Error! s1 no longer owns the String
println!("{}", s2); // OK
}

Explanation:

  1. s1 owns the string initially.
  2. When assigned to s2, ownership is moved, not copied.
  3. s1 is now invalid; using it will cause a compile-time error.

🤁 Borrowing with References

If you want to access data without taking ownership, borrow it using references.

fn main() {
let s1 = String::from("hello");
print_length(&s1); // Pass a reference
println!("{}", s1); // Still valid!
}

fn print_length(s: &String) {
println!("Length is: {}", s.len());
}

Explanation:

  1. &s1 is an immutable reference.
  2. You can have multiple immutable references at the same time.
  3. s1 retains ownership.

Mutable References (Only One at a Time)

fn main() {
let mut s = String::from("hello");
change(&mut s); // Mutable borrow
println!("{}", s);
}

fn change(s: &mut String) {
s.push_str(", world");
}

Explanation:

  1. Only one mutable reference is allowed at a time.
  2. This rule prevents data races at compile time.

What Happens If You Try Two Mutable Borrows?

fn main() {
let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s; // Error: cannot borrow `s` as mutable more than once at a time

println!("{}, {}", r1, r2);
}

Rust will stop this code from compiling, ensuring safe access.


Lifetimes in Rust

Lifetimes are how Rust ensures that references are always valid. They don’t change how long data lives — they simply tell the compiler how long a reference is guaranteed to be valid.


Why Are Lifetimes Needed?

Here’s an example that doesn’t compile:

fn get_str() -> &String {
let s = String::from("hello");
&s // Error!
}

Error:

error[E0515]: cannot return reference to local variable `s`

This fails because s is dropped when the function ends — returning a reference to it would be a dangling pointer.


Example Using Lifetimes

fn main() {
let s1 = String::from("apple");
let s2 = String::from("banana");

let result = longest(&s1, &s2);
println!("Longest: {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

Explanation:

  • 'a is a lifetime annotation that says:

    • The returned reference will be valid as long as both input references are.
  • You must write lifetimes when the compiler can’t infer them — especially when returning references from a function.


Lifetimes

  1. Lifetimes help Rust verify reference safety at compile time.

  2. Most of the time, Rust infers lifetimes for you using lifetime elision rules.

  3. You need to explicitly annotate lifetimes when:

    • Returning references from functions.
    • Working with structs that store references.

Summary

  • Rust's ownership and borrowing system manages memory without a garbage collector.
  • You can move, borrow, or mutably borrow values, but under strict rules.
  • Only one mutable reference allowed at a time to avoid data races.
  • Lifetimes ensure references stay valid, especially when returning them from functions.
  • Most of the time, you don’t need to write lifetimes manually — but when you do, they make your code safe and predictable.

Layer 3 Security in Blockchain

· 2 min read

Layer 3 (L3) solutions are built on top of Layer 2 (L2) to enhance scalability, privacy, and customization for specific applications. While they bring powerful benefits, they also introduce unique security challenges.

Understanding Layer 3

Layer 3 primarily focuses on:

  • Application-specific scaling: Tailored solutions for DApps.
  • Interoperability: Enabling seamless communication between different chains.
  • Enhanced privacy: Utilizing zk-rollups and other cryptographic techniques.

Security Challenges in Layer 3

  1. Trust Assumptions

    • Many L3 solutions rely on centralized sequencers or validators.
    • The security of L3 is often dependent on L2, which itself depends on L1.
  2. Cross-Layer Dependencies

    • Bugs in L2 can cascade into L3.
    • Bridging between L2 and L3 increases attack surfaces.
  3. Smart Contract Risks

    • Custom L3 implementations may introduce new vulnerabilities.
    • Lack of standardization can lead to inconsistencies in security best practices.
  4. Data Availability Issues

    • If an L3 solution relies on off-chain data, availability failures can impact security.

Mitigation Strategies

  • Use well-audited smart contracts to minimize vulnerabilities.
  • Adopt decentralized sequencers to reduce centralization risks.
  • Ensure robust bridge security between L2 and L3.
  • Implement zk-proofs for trustless validation mechanisms.

Conclusion

Layer 3 solutions bring immense potential for blockchain scalability, but they must be designed with security at the core. Addressing trust assumptions, ensuring interoperability safety, and reinforcing smart contract security will be crucial for L3 adoption.