> For the complete documentation index, see [llms.txt](https://docs.astralbeam.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.astralbeam.io/smart-contracts/evm-locker.md).

# EVM MultiSigLocker

The MultiSigERC20Locker contract manages token locking and unlocking on EVM chains for cross-chain bridging to Casper Network.

## Overview

| Property         | Value                  |
| ---------------- | ---------------------- |
| Solidity Version | ^0.8.24                |
| License          | MIT                    |
| Upgradeability   | UUPS Proxy             |
| Security         | M-of-N Multi-signature |

## Core Functions

### lock

Lock ERC-20 tokens for bridging to Casper.

```solidity
function lock(
    address token,
    uint256 amount,
    bytes32 casperRecipient
) external payable returns (uint256 nonce);
```

**Parameters:**

| Name              | Type    | Description                    |
| ----------------- | ------- | ------------------------------ |
| `token`           | address | ERC-20 token address           |
| `amount`          | uint256 | Amount to lock                 |
| `casperRecipient` | bytes32 | Recipient public key on Casper |

**Returns:**

* `nonce` - Unique identifier for this lock operation

**Requirements:**

* `msg.value >= bridgeFee()` - Must pay bridge fee
* **When `bridgeFee()` is 0, `msg.value` must be exactly 0** — attached value reverts with `UnexpectedNativeValue` instead of being trapped (V15+)
* `amount > 0` — zero-amount locks always revert `AmountTooLow`, even when `minLockAmount` is configured to 0 (V15+)
* Token must be registered
* User must have approved tokens
* Contract must not be paused

**Example:**

```typescript
// Approve tokens first
await token.approve(locker.address, amount);

// Lock with bridge fee
const tx = await locker.lock(
    tokenAddress,
    ethers.parseUnits("100", 6), // 100 USDC
    casperPubKeyBytes32,
    { value: ethers.parseEther("0.0001") }
);

const receipt = await tx.wait();
// Parse Locked event for nonce
```

**Events Emitted:**

```solidity
event Locked(
    address indexed token,
    address indexed sender,
    uint256 amount,
    bytes32 indexed casperRecipient,
    uint256 nonce
);
```

***

### unlock

Unlock ERC-20 tokens based on M-of-N relayer signatures.

```solidity
function unlock(
    address token,
    uint256 amount,
    address recipient,
    bytes32 casperEventKey,
    bytes32 casperStateRoot,
    bytes[] calldata signatures
) external;
```

**Parameters:**

| Name              | Type     | Description                       |
| ----------------- | -------- | --------------------------------- |
| `token`           | address  | ERC-20 token address              |
| `amount`          | uint256  | Amount to unlock                  |
| `recipient`       | address  | Ethereum recipient                |
| `casperEventKey`  | bytes32  | Unique key from Casper burn event |
| `casperStateRoot` | bytes32  | Casper state root at burn         |
| `signatures`      | bytes\[] | Array of relayer signatures       |

**Requirements:**

* At least `threshold` valid signatures
* Event not already processed
* Sufficient locked balance
* Contract not paused

**Events Emitted:**

```solidity
event Unlocked(
    address indexed token,
    address indexed recipient,
    uint256 amount,
    bytes32 indexed casperStateRoot,
    bytes32 eventKey
);
```

***

### mintWrapped

Mint wrapped tokens for Casper-native assets.

```solidity
function mintWrapped(
    bytes32 casperTokenHash,
    uint256 amount,
    address recipient,
    bytes32 casperEventKey,
    bytes32 casperStateRoot,
    bytes[] calldata signatures
) external;
```

**Parameters:**

| Name              | Type     | Description                       |
| ----------------- | -------- | --------------------------------- |
| `casperTokenHash` | bytes32  | Casper CEP-18 contract hash       |
| `amount`          | uint256  | Amount to mint                    |
| `recipient`       | address  | Ethereum recipient                |
| `casperEventKey`  | bytes32  | Unique key from Casper lock event |
| `casperStateRoot` | bytes32  | Casper state root                 |
| `signatures`      | bytes\[] | Relayer signatures                |

**Use Case:** When CSPR or other Casper-native tokens are locked on Casper, wrapped versions (wCSPR) are minted on Ethereum.

***

### burnWrapped

Burn wrapped tokens to unlock on Casper.

```solidity
function burnWrapped(
    address wrappedToken,
    uint256 amount,
    bytes32 casperRecipient
) external payable returns (uint256 nonce);
```

**Parameters:**

| Name              | Type    | Description           |
| ----------------- | ------- | --------------------- |
| `wrappedToken`    | address | Wrapped token address |
| `amount`          | uint256 | Amount to burn        |
| `casperRecipient` | bytes32 | Recipient on Casper   |

**Returns:**

* `nonce` - Unique identifier for burn operation

**Events Emitted:**

```solidity
event WrappedBurned(
    address indexed wrappedToken,
    bytes32 indexed casperTokenHash,
    address indexed sender,
    uint256 amount,
    bytes32 casperRecipient,
    uint256 nonce
);
```

***

## Admin Functions

### registerToken

Register a new ERC-20 token for bridging.

```solidity
function registerToken(
    address token,
    bytes32 casperContractHash,
    uint8 casperDecimals
) external;
```

**Access:** Admin only

**Reverts** with `ZeroCasperHash` if `casperContractHash` is zero (it collides with the "unregistered" mapping sentinel — V15+; also enforced on `updateTokenMapping`), and with `TokenDecimalsOutOfRange` if the token reports `decimals() > 36` (`MAX_TOKEN_DECIMALS`, V15+).

**Events:**

```solidity
event TokenRegistered(
    address indexed token,
    bytes32 indexed casperContractHash,
    string name,
    string symbol,
    uint8 decimals
);
```

***

### unregisterToken

Remove an ERC-20 token from the bridge registry. Added in V6.

```solidity
function unregisterToken(address token) external;
```

**Access:** Admin only

**Parameters:**

| Name    | Type    | Description                        |
| ------- | ------- | ---------------------------------- |
| `token` | address | ERC-20 token address to unregister |

**Behavior:**

* Clears `_tokenToCasper` and `_casperToToken` mappings
* Removes token from `_registeredTokens` array
* Clears per-token minimum lock amount if set
* Uses swap-and-pop for gas-efficient array removal

**Use Case:** Remove incorrectly registered tokens or deprecated tokens from the bridge.

**Events:**

```solidity
event TokenUnregistered(
    address indexed token,
    bytes32 indexed casperContractHash
);
```

***

### updateTokenMapping

Update the Casper contract hash for an already registered token.

```solidity
function updateTokenMapping(
    address token,
    bytes32 newCasperContractHash
) external;
```

**Access:** Admin only

**Parameters:**

| Name                    | Type    | Description                               |
| ----------------------- | ------- | ----------------------------------------- |
| `token`                 | address | ERC-20 token address (must be registered) |
| `newCasperContractHash` | bytes32 | New Casper CEP-18 contract hash           |

**Use Case:** Update the Casper-side contract mapping if a token migrates to a new CEP-18 contract.

***

### updateRelayerSet

Update the relayer set and threshold.

```solidity
function updateRelayerSet(
    address[] calldata relayers,
    uint256 threshold
) external;
```

**Parameters:**

| Name        | Type       | Description             |
| ----------- | ---------- | ----------------------- |
| `relayers`  | address\[] | New relayer addresses   |
| `threshold` | uint256    | Required signatures (M) |

**Access:** Admin only

**Validation:** `threshold >= 2`, `threshold <= relayers.length`, and `threshold * 2 > relayers.length` (strict majority) — a minority quorum such as 2-of-100 is rejected with `InvalidThreshold`.

**Events:**

```solidity
event RelayerSetUpdated(
    address[] relayers,
    uint256 threshold
);
```

***

### deployWrappedToken

Deploy a wrapped ERC-20 for Casper-native tokens.

```solidity
function deployWrappedToken(
    bytes32 casperTokenHash,
    string calldata name,
    string calldata symbol,
    uint8 decimals
) external returns (address wrappedToken);
```

**Returns:**

* `wrappedToken` - Address of deployed wrapped token

**Events:**

```solidity
event WrappedTokenDeployed(
    address indexed wrappedToken,
    bytes32 indexed casperTokenHash,
    string name,
    string symbol,
    uint8 decimals
);
```

***

### replaceWrappedToken

V13 provides a one-step mapping rotation for zero-supply Casper-native wrappers deployed before the bridge-only-burn hardening. It is admin-only and requires the locker to be fully paused. The function rejects a zero or unknown Casper hash, inconsistent wrapper mappings/immutables/enumeration, and any non-zero old-token total supply. It copies token metadata and the configured minimum burn amount; there is no holder snapshot or balance migration.

```solidity
function replaceWrappedToken(
    bytes32 casperTokenHash
) external returns (address newWrappedToken);
```

On success, the forward and reverse mappings and the existing enumeration slot point to the new wrapper atomically. The old reverse mapping is deleted, so the old wrapper is no longer recognized by `isWrappedToken` or `burnWrapped`.

```solidity
event WrappedTokenReplaced(
    address indexed oldWrappedToken,
    address indexed newWrappedToken,
    bytes32 indexed casperTokenHash
);
```

New V13 wrappers do not expose holder-callable `burn(uint256)`. Their only burn entry point is `burnFrom(account, amount)`, restricted to the locker and consuming the allowance granted to the locker. Users must call `burnWrapped` so every supply reduction emits `WrappedBurned` for the Casper unlock path.

***

## Minimum Amount Functions

The contract supports configurable minimum lock amounts to prevent dust attacks and DoS vectors. Both a global default and per-token overrides are available.

### setMinLockAmount

Set the global default minimum lock amount (in token units).

```solidity
function setMinLockAmount(uint256 _minLockAmount) external;
```

**Access:** Admin only

**Parameters:**

| Name             | Type    | Description                                |
| ---------------- | ------- | ------------------------------------------ |
| `_minLockAmount` | uint256 | Minimum in token units (e.g., 1 = 1 token) |

**Behavior:**

* Value is scaled by token decimals when checking (e.g., 1 becomes 1e6 for USDC, 1e18 for WETH)
* Applies to all tokens without a per-token override
* Set to 0 to disable minimum check

***

### setTokenMinLockAmount

Set minimum lock amount for a specific token.

```solidity
function setTokenMinLockAmount(
    address token,
    uint256 minAmount
) external;
```

**Access:** Admin only

**Parameters:**

| Name        | Type    | Description                            |
| ----------- | ------- | -------------------------------------- |
| `token`     | address | ERC-20 token address                   |
| `minAmount` | uint256 | Minimum in token's smallest unit (wei) |

**Behavior:**

* Overrides the global default for this specific token
* Set to 0 to use the global default instead
* Value is NOT scaled - specify the exact amount in wei

**Example:**

```typescript
// Set minimum for USDC (6 decimals) to 1 USDC
await locker.setTokenMinLockAmount(
    usdcAddress,
    1_000_000n  // 1 USDC in wei (1e6)
);

// Set minimum for WETH (18 decimals) to 0.01 WETH
await locker.setTokenMinLockAmount(
    wethAddress,
    10_000_000_000_000_000n  // 0.01 WETH in wei (1e16)
);
```

**Events:**

```solidity
event TokenMinLockAmountUpdated(address indexed token, uint256 minAmount);
```

***

### getMinLockAmountForToken

Get the effective minimum lock amount for a token.

```solidity
function getMinLockAmountForToken(address token) external view returns (uint256);
```

**Returns:**

* Per-token minimum if set, otherwise global minimum scaled by token decimals
* Returns 0 if no minimum is configured

***

### getTokenMinLockAmount

Get the raw per-token minimum (without global fallback).

```solidity
function getTokenMinLockAmount(address token) external view returns (uint256);
```

**Returns:**

* The per-token minimum, or 0 if not explicitly set

***

### minLockAmount

Get the global default minimum lock amount.

```solidity
function minLockAmount() external view returns (uint256);
```

**Returns:**

* Global minimum in token units (before decimal scaling)

***

## View Functions

### getLockedBalance

Get the locked balance for a token.

```solidity
function getLockedBalance(address token) external view returns (uint256);
```

### isEventProcessed

Check if an event has been processed.

```solidity
function isEventProcessed(bytes32 eventKey) external view returns (bool);
```

### getRelayerSet

Get current relayer configuration.

```solidity
function getRelayerSet() external view returns (
    address[] memory relayers,
    uint256 threshold
);
```

### isRelayer

Check if address is a relayer.

```solidity
function isRelayer(address account) external view returns (bool);
```

### bridgeFee

Get current bridge fee.

```solidity
function bridgeFee() external view returns (uint256 fee);
```

### getWrappedToken

Get wrapped token for Casper token.

```solidity
function getWrappedToken(bytes32 casperTokenHash) external view returns (address);
```

***

## Events Reference

### Token Operations

```solidity
// V10: Locked and WrappedBurned carry a trailing chainId (changes topic0 vs legacy deployments)
event Locked(address indexed token, address indexed sender, uint256 amount, bytes32 indexed casperRecipient, uint256 nonce, uint256 chainId);
event Unlocked(address indexed token, address indexed recipient, uint256 amount, bytes32 indexed casperStateRoot, bytes32 eventKey);
event WrappedMinted(address indexed wrappedToken, bytes32 indexed casperTokenHash, address indexed recipient, uint256 amount, bytes32 casperEventKey);
event WrappedBurned(address indexed wrappedToken, bytes32 indexed casperTokenHash, address indexed sender, uint256 amount, bytes32 casperRecipient, uint256 nonce, uint256 chainId);
```

> **Relayer compatibility:** because the trailing `chainId` changes the event signature hash, relayers subscribe to both the V10 and legacy topic0 values and decode the data section per generation. A scanner pinned to only one generation goes silently deaf on the other.

### Administration

```solidity
event TokenRegistered(address indexed token, bytes32 indexed casperContractHash, string name, string symbol, uint8 decimals);
event TokenUnregistered(address indexed token, bytes32 indexed casperContractHash);
event RelayerSetUpdated(address[] relayers, uint256 threshold);
event WrappedTokenDeployed(address indexed wrappedToken, bytes32 indexed casperTokenHash, string name, string symbol, uint8 decimals);
event FeeParametersUpdated(uint256 bridgeFee, address feeRecipient);
```

***

## Error Codes

| Error                           | Description                         |
| ------------------------------- | ----------------------------------- |
| `InsufficientFee`               | Bridge fee not paid                 |
| `TokenNotRegistered`            | Token not registered for bridging   |
| `AmountTooLow(amount, minimum)` | Lock amount below minimum threshold |
| `InsufficientSignatures`        | Not enough valid signatures         |
| `EventAlreadyProcessed`         | Event key already processed         |
| `InvalidSignature`              | Signature verification failed       |
| `InsufficientLockedBalance`     | Not enough tokens locked            |
| `Paused`                        | Contract is paused                  |

***

## Integration Examples

### JavaScript/TypeScript

```typescript
import { ethers } from 'ethers';

// Connect to contract
const locker = new ethers.Contract(
    lockerAddress,
    MultiSigERC20LockerABI,
    signer
);

// Check if token is registered
const isRegistered = await locker.isTokenRegistered(usdcAddress);

// Get bridge fee
const fee = await locker.bridgeFee();

// Lock tokens
async function lockTokens(token: string, amount: bigint, recipient: string) {
    // Convert Casper public key to bytes32
    const recipientBytes = ethers.zeroPadValue(recipient, 32);

    // Approve first
    const tokenContract = new ethers.Contract(token, ERC20ABI, signer);
    await tokenContract.approve(locker.address, amount);

    // Lock with fee
    const tx = await locker.lock(token, amount, recipientBytes, {
        value: fee
    });

    return tx.wait();
}
```

### Web3.js

```javascript
const locker = new web3.eth.Contract(abi, lockerAddress);

// Get locked balance
const balance = await locker.methods.getLockedBalance(tokenAddress).call();

// Get relayer info
const { relayers, threshold } = await locker.methods.getRelayerSet().call();
console.log(`Threshold: ${threshold} of ${relayers.length}`);
```

***

## Security Considerations

1. **Signature Verification**: All unlock operations require M-of-N valid signatures
2. **Replay Protection**: Event keys are tracked to prevent double-processing
3. **Fee Protection**: Bridge fees must be paid to prevent spam
4. **Pause Mechanism**: Admin can pause in emergencies
5. **Token Registration**: Only registered tokens can be bridged
6. **Minimum Lock Amounts**: Configurable minimums prevent dust attacks and DoS vectors (see [Security Model](/security/security.md#anti-dusting-protection))

***

## Planned Enhancements

> **Coming Soon:** Maximum lock amount limits (per-token and global) will be added to limit exposure and provide additional security controls.

### initializeV7

Initialize V7 storage: EIP-712 domain separation.

```solidity
function initializeV7() external;
```

**Access:** `onlyAdmin`. Must be called once after upgrading to V7 implementation. On fresh deployments the deploy scripts (`DeployMultiSigLockerUpgradeable.s.sol` / `DeployMultiSigLockerCREATE2.s.sol`) call `initializeV7()` through `initializeV10()` automatically in the deploy broadcast and verify the resulting domain separator. See [EIP-712 Typed Attestations](/security/verification.md#eip-712-typed-attestations-find-001) for implementation details.

**Purpose:** Sets up the EIP-712 domain separator (`CasperBridgeLocker`, version `1`, current chain ID, contract address) for signature verification in `unlock()` and `mintWrapped()`.

### domainSeparator

Get the EIP-712 domain separator for off-chain signature construction.

```solidity
function domainSeparator() external view returns (bytes32);
```

> **Signature Format (V7+):** Signatures must be EIP-712 typed data signatures using domain `CasperBridgeLocker` version `1`. Legacy personal-sign format signatures are rejected.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.astralbeam.io/smart-contracts/evm-locker.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
