@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
615 lines (474 loc) • 16.1 kB
Markdown
# OP20S - Signature-Based Approvals
OP20S extends OP20 with signature-based approval mechanisms, enabling off-chain approvals and enhanced security through cryptographic signatures.
## Overview
OP20S adds:
- **Permit-style approvals** - Approve via signature instead of transaction
- **Nonce management** - Replay attack protection
- **Deadline enforcement** - Time-limited signatures
- **ML-DSA support** - Quantum-resistant signatures
```typescript
import {
OP20S,
OP20InitParameters,
Calldata,
BytesWriter,
ABIDataTypes,
} from '@btc-vision/btc-runtime/runtime';
import { u256 } from '@btc-vision/as-bignum/assembly';
@final
export class MyToken extends OP20S {
public constructor() {
super();
}
public override onDeployment(_calldata: Calldata): void {
this.instantiate(new OP20InitParameters(
u256.fromString('1000000000000000000000000'),
18,
'MyToken',
'MTK'
));
}
}
```
## ERC20Permit vs OP20S Comparison
| Feature | ERC20Permit (EIP-2612) | OP20S (OP_NET) |
|---------|------------------------|---------------|
| Language | Solidity | AssemblyScript |
| Signature Type | ECDSA (v, r, s) | Schnorr, ECDSA (deprecated), or ML-DSA |
| Domain Separator | EIP-712 | EIP-712 style |
| Quantum Resistance | No | Yes (ML-DSA option) |
| Signature Parameter | Three params (v, r, s) | Single bytes param |
| Nonce Type | `uint256` | `u256` |
## Why Signature-Based Approvals?
### Traditional Approval Flow
```
1. User signs APPROVE transaction
2. User submits APPROVE TX to blockchain
3. Contract updates allowance
4. User signs TRANSFER_FROM transaction (or protocol does)
5. Transfer executes
Total: 2 transactions required from user
```
### Signature-Based Flow
```
1. User signs approval MESSAGE (off-chain, no TX needed)
2. Protocol submits signature with action
3. Contract verifies signature and executes
Total: 1 transaction, user signs off-chain only
```
## Permit Flow
The following diagram shows the high-level permit flow:
```mermaid
config:
theme: dark
flowchart LR
A[👤 User signs off-chain] --> B[Send signature to relayer]
B --> C[Relayer submits permit TX]
C --> D{Deadline valid?}
D -->|No| E[Revert]
D -->|Yes| F{Nonce correct?}
F -->|No| G[Revert]
F -->|Yes| H{Signature valid?}
H -->|No| I[Revert]
H -->|Yes| J[Increment nonce]
J --> K[Set allowance]
K --> L[Emit ApprovalEvent]
```
## Detailed Permit Sequence
The following sequence diagram shows the complete permit process from off-chain signing to on-chain execution:
```mermaid
sequenceDiagram
participant User as 👤 User Wallet<br/>(Has Private Key)
participant OffChain as Off-Chain Signer<br/>(Browser/App)
participant Relayer as Relayer/Protocol<br/>(Submits TX)
participant Blockchain as Bitcoin L1
participant VM as WASM Runtime
participant OP20S as OP20S Contract
participant Storage as Storage Pointers
participant NonceMap as nonces Map<br/>(Prevents Replay)
participant AllowanceMap as allowance Map<br/>(Pointer 6)
participant DomainSep as Domain Separator<br/>(EIP-712)
participant EventLog as Event Log
Note over User,OffChain: Phase 1: Off-Chain Signing (No TX needed)
User->>OffChain: Request permit for spender
OffChain->>OP20S: Query nonce(owner) view call
Note over OffChain,OP20S: Read-only view call
OP20S->>NonceMap: get(owner)
NonceMap->>Storage: Read current nonce
Storage-->>NonceMap: nonce value
NonceMap-->>OP20S: currentNonce
OP20S-->>OffChain: Return nonce
OffChain->>DomainSep: Get DOMAIN_SEPARATOR()
DomainSep-->>OffChain: Domain hash
Note over DomainSep: Includes: name, version,<br/>chainId, contract address
OffChain->>OffChain: Build permit struct
Note over OffChain: owner, spender, value,<br/>nonce, deadline
OffChain->>OffChain: Hash permit data
Note over OffChain: permitHash = keccak256(<br/>abi.encode(typeHash, permit))
OffChain->>OffChain: Build EIP-712 message
Note over OffChain: message = keccak256(<br/>0x1901 + domainSep + permitHash)
User->>OffChain: Sign message with private key
Note over User: Schnorr or ML-DSA signature
OffChain-->>User: signature bytes
Note over User,Blockchain: Phase 2: On-Chain Submission (Relayer Pays)
User->>Relayer: Send signature + permit params
Note over User,Relayer: User sends NO on-chain TX
Relayer->>Blockchain: Submit permit() transaction
Note over Relayer: Relayer submits TX to chain
Blockchain->>VM: Execute transaction
VM->>OP20S: permit(owner, spender, value, deadline, sig)
activate OP20S
OP20S->>OP20S: Read calldata parameters
OP20S->>OP20S: Check Blockchain.block.medianTime
alt Deadline passed
OP20S->>VM: Revert('Permit expired')
VM->>Relayer: Transaction failed
Relayer->>User: Permit expired
else Valid deadline
OP20S->>NonceMap: get(owner)
NonceMap->>Storage: Read expected nonce
Storage-->>NonceMap: expectedNonce
NonceMap-->>OP20S: nonce value
OP20S->>DomainSep: getDomainSeparator()
DomainSep-->>OP20S: domain hash
OP20S->>OP20S: buildPermitMessage()
Note over OP20S: Reconstruct same message<br/>user signed off-chain
OP20S->>OP20S: verifySignature(owner, message, sig)
Note over OP20S: Recover signer from signature
alt Signature invalid OR signer != owner
OP20S->>VM: Revert('Invalid signature')
VM->>Relayer: Transaction failed
else Signature valid
OP20S->>OP20S: SafeMath.add(nonce, 1)
OP20S->>NonceMap: set(owner, nonce + 1)
NonceMap->>Storage: Write incremented nonce
Note over Storage: Replay protection:<br/>Old signatures now invalid
OP20S->>OP20S: _approve(owner, spender, value)
OP20S->>AllowanceMap: set(owner->spender, value)
AllowanceMap->>Storage: Write allowance
Note over Storage: Approval stored
OP20S->>OP20S: Create ApprovalEvent
OP20S->>EventLog: emit ApprovalEvent(owner, spender, value)
OP20S->>VM: Return success
deactivate OP20S
VM->>Blockchain: Commit state changes
Blockchain->>Relayer: Transaction success
Relayer->>User: Permit approved!
Note over User: Approved without<br/>submitting any TX!
end
end
```
## Message Construction
The following diagram shows how the permit message is constructed for signing:
```mermaid
config:
theme: dark
flowchart LR
A[Domain separator] --> B[Permit struct]
B --> C[Combine with EIP-712 prefix]
C --> D[Hash message]
D --> E[👤 User signs with private key]
E --> F[Submit signature on-chain]
F --> G[Verify signature]
G --> H{Valid?}
H -->|No| I[Revert]
H -->|Yes| J[Process permit]
```
## Nonce Management
Each address has a nonce that increments with each signature use to prevent replay attacks:
```mermaid
stateDiagram-v2
[*] --> NeverUsed: 👤 User account created
state NeverUsed {
[*] --> Nonce0
}
NeverUsed --> FirstPermit: permit() with nonce=0
state FirstPermit {
Nonce0 --> Nonce1: Signature verified<br/>Nonce incremented
}
FirstPermit --> SubsequentPermits
state SubsequentPermits {
Nonce1 --> Nonce2: permit() nonce=1
Nonce2 --> Nonce3: permit() nonce=2
Nonce3 --> NoncePlus: permit() nonce=3
NoncePlus --> NoncePlus: More permits
}
state "Replay Attack Blocked" as Blocked {
[*] --> AttemptOldNonce
AttemptOldNonce --> Reverted: Old signature submitted
}
SubsequentPermits --> Blocked: Try to reuse old signature
Blocked --> SubsequentPermits: Continue with correct nonce
```
## Permit Method
### Usage
```typescript
// Off-chain: User signs permit data
const permitData = {
owner: userAddress,
spender: protocolAddress,
value: amount,
nonce: await contract.nonces(userAddress),
deadline: Math.floor(Date.now() / 1000) + 3600 // 1 hour
};
const signature = signPermit(permitData, userPrivateKey);
// On-chain: Protocol submits permit
contract.permit(owner, spender, value, deadline, signature);
```
### Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `owner` | `Address` | Token owner granting approval |
| `spender` | `Address` | Address being approved |
| `value` | `u256` | Amount to approve |
| `deadline` | `u64` | Signature expiration timestamp |
| `signature` | `bytes` | Cryptographic signature |
### Signature Verification
The permit verifies:
1. **Signature validity** - Matches owner's public key
2. **Deadline** - Current time <= deadline
3. **Nonce** - Matches expected nonce for owner
4. **Domain** - Correct contract and chain
## Nonces
```typescript
// Get current nonce for address
const nonce: u256 = contract.nonces(address);
// Nonce auto-increments after successful permit
// This prevents signature replay
```
### Replay Protection
```typescript
// Signature for nonce 0 used
contract.permit(..., nonce=0, signature0); // Success, nonce now 1
// Same signature replayed
contract.permit(..., nonce=0, signature0); // FAILS - nonce mismatch
```
## Solidity Comparison (EIP-2612)
<table>
<tr>
<th>ERC20Permit (Solidity)</th>
<th>OP20S (OP_NET)</th>
</tr>
<tr>
<td>
```solidity
contract MyToken is ERC20Permit {
constructor()
ERC20("MyToken", "MTK")
ERC20Permit("MyToken")
{ }
}
// Usage
token.permit(
owner,
spender,
value,
deadline,
v, r, s // ECDSA signature components
);
```
</td>
<td>
```typescript
@final
export class MyToken extends OP20S {
constructor() {
super();
}
public override onDeployment(_: Calldata): void {
this.instantiate(new OP20InitParameters(
maxSupply, 18, 'MyToken', 'MTK'
));
}
}
// Usage
token.permit(
owner,
spender,
value,
deadline,
signature // Schnorr or ML-DSA
);
```
</td>
</tr>
</table>
## Domain Separator
OP20S uses EIP-712 style domain separation:
```typescript
// Domain includes:
// - Contract name
// - Contract version
// - Chain ID
// - Contract address
// This prevents cross-chain and cross-contract replay
```
## Quantum Resistance
OP20S supports ML-DSA (Dilithium) signatures for quantum resistance:
```typescript
// Traditional Schnorr signature
contract.permit(owner, spender, value, deadline, schnorrSignature);
// ML-DSA quantum-resistant signature
contract.permitQuantum(owner, spender, value, deadline, mldsaSignature);
```
### Extended Address
For quantum-safe operations, use `ExtendedAddress`:
```typescript
import { ExtendedAddress } from '@btc-vision/btc-runtime/runtime';
// Extended address with both key types
const extAddress = new ExtendedAddress(
traditionalPubKey, // Schnorr
mldsaPubKey // ML-DSA
);
```
See [Quantum Resistance](../advanced/quantum-resistance.md) for details.
## Implementation Details
### Permit Verification
```typescript
@method(
{ name: 'owner', type: ABIDataTypes.ADDRESS },
{ name: 'spender', type: ABIDataTypes.ADDRESS },
{ name: 'value', type: ABIDataTypes.UINT256 },
{ name: 'deadline', type: ABIDataTypes.UINT64 },
{ name: 'signature', type: ABIDataTypes.BYTES },
)
@returns({ name: 'success', type: ABIDataTypes.BOOL })
@emit('Approval')
public permit(calldata: Calldata): BytesWriter {
const owner = calldata.readAddress();
const spender = calldata.readAddress();
const value = calldata.readU256();
const deadline = calldata.readU64();
const signature = calldata.readBytes();
// Check deadline
if (Blockchain.block.medianTime > deadline) {
throw new Revert('Permit expired');
}
// Get and increment nonce
const nonce = this.nonces.get(owner);
this.nonces.set(owner, SafeMath.add(nonce, u256.One));
// Verify signature
const message = this.buildPermitMessage(owner, spender, value, nonce, deadline);
if (!this.verifySignature(owner, message, signature)) {
throw new Revert('Invalid signature');
}
// Set approval
this._approve(owner, spender, value);
return new BytesWriter(0);
}
```
### Message Format
```typescript
// Permit message structure
struct Permit {
address owner;
address spender;
uint256 value;
uint256 nonce;
uint256 deadline;
}
// Hashed with domain separator
messageHash = SHA256(domainSeparator || SHA256(permitTypeHash || encode(permit)))
```
## Additional Methods
OP20S adds these methods to OP20:
| Method | Description |
|--------|-------------|
| `permit(...)` | Approve via signature |
| `nonces(address)` | Get nonce for address |
| `DOMAIN_SEPARATOR()` | Get domain separator |
## Use Cases
### 1. Off-Chain Approvals
```typescript
// User signs permit off-chain
const sig = await user.signPermit(spender, amount, deadline);
// Protocol submits on user's behalf
await contract.permit(user, spender, amount, deadline, sig);
await contract.transferFrom(user, recipient, amount);
// User submitted no on-chain TX!
```
### 2. Single-Transaction Approve+Transfer
```typescript
// Protocol contract
@method(
{ name: 'owner', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
{ name: 'deadline', type: ABIDataTypes.UINT64 },
{ name: 'signature', type: ABIDataTypes.BYTES },
)
@returns({ name: 'success', type: ABIDataTypes.BOOL })
@emit('Deposit')
public depositWithPermit(calldata: Calldata): BytesWriter {
// Read permit params
const owner = calldata.readAddress();
const amount = calldata.readU256();
const deadline = calldata.readU64();
const signature = calldata.readBytes();
// Execute permit
token.permit(owner, this.address, amount, deadline, signature);
// Now transfer in same transaction
token.transferFrom(owner, this.address, amount);
return new BytesWriter(0);
}
```
### 3. Meta-Transactions
```typescript
// Relayer submits on behalf of user
@method(
{ name: 'user', type: ABIDataTypes.ADDRESS },
{ name: 'permitSig', type: ABIDataTypes.BYTES },
{ name: 'actionSig', type: ABIDataTypes.BYTES },
)
@returns({ name: 'success', type: ABIDataTypes.BOOL })
public executeMetaTx(calldata: Calldata): BytesWriter {
const user = calldata.readAddress();
const permitSig = calldata.readBytes();
const actionSig = calldata.readBytes();
// Verify permit
this.permit(user, ...permitSig);
// Execute action
this.executeAction(user, actionSig);
return new BytesWriter(0);
}
```
## Security Considerations
### Deadline Selection
```typescript
// Too short: User might not complete in time
const deadline = now + 60; // 1 minute - risky
// Reasonable: Enough time, limited exposure
const deadline = now + 3600; // 1 hour - good
// Too long: Extended attack window
const deadline = now + 86400 * 365; // 1 year - bad
```
### Signature Storage
```typescript
// NEVER store signatures on-chain unnecessarily
// They become public and could be analyzed
// DO process and discard
const sig = calldata.readBytes();
verifyAndProcess(sig);
// sig is gone after transaction
```
### Front-Running Protection
```typescript
// Permit followed by transfer in same tx = safe
permit(owner, spender, value, deadline, sig);
transferFrom(owner, recipient, value); // Same tx
// Separate transactions = front-running risk
// Attacker could see permit and race to use allowance
```
## Best Practices
1. **Set reasonable deadlines** - Not too short, not too long
2. **Process permits atomically** - Permit + action in same transaction
3. **Monitor nonces** - Track expected nonces off-chain
4. **Verify domains** - Ensure signatures are for correct contract
5. **Consider quantum safety** - Use ML-DSA for high-value applications
**Navigation:**
- Previous: [OP20 Token](./op20-token.md)
- Next: [OP721 NFT](./op721-nft.md)