@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
242 lines (211 loc) • 8.33 kB
text/typescript
import { u256 } from '@btc-vision/as-bignum/assembly';
import { Blockchain } from '../env';
import { OP_NET } from './OP_NET';
import { StoredAddress } from '../storage/StoredAddress';
import { StoredU256 } from '../storage/StoredU256';
import { Address } from '../types/Address';
import { Revert } from '../types/Revert';
import { BytesWriter } from '../buffer/BytesWriter';
import { EMPTY_POINTER } from '../math/bytes';
import {
UpdateAppliedEvent,
UpdateCancelledEvent,
UpdateSubmittedEvent,
} from '../events/updatable/UpdatableEvents';
const pendingUpdateAddressPointer: u16 = Blockchain.nextPointer;
const pendingUpdateBlockPointer: u16 = Blockchain.nextPointer;
/**
* Updatable - Base contract for updatable contracts with timelock protection.
*
* This contract provides a secure update mechanism with a configurable delay period.
* The pattern prevents instant malicious updates by requiring:
* 1. submitUpdate() - Submit the source contract address, starts the timelock
* 2. Wait for the delay period to pass
* 3. applyUpdate() - Apply the update after the delay
*
* Users can monitor for UpdateSubmitted events and exit if they distrust pending changes.
*
* @example
* ```typescript
* @final
* export class MyUpdatableContract extends Updatable {
* // Set a 24-hour delay (144 blocks at 10 min/block)
* protected readonly updateDelay: u64 = 144;
*
* public override execute(method: Selector, calldata: Calldata): BytesWriter {
* switch (method) {
* case encodeSelector('submitUpdate'):
* return this.submitUpdate(calldata.readAddress());
* case encodeSelector('applyUpdate'):
* const sourceAddress = calldata.readAddress();
* const updateCalldata = calldata.readBytesWithLength();
* return this.applyUpdate(sourceAddress, updateCalldata);
* case encodeSelector('cancelUpdate'):
* return this.cancelUpdate();
* default:
* return super.execute(method, calldata);
* }
* }
* }
* ```
*/
export class Updatable extends OP_NET {
/**
* The pending update source address.
* Zero address means no pending update.
*/
protected readonly _pendingUpdateAddress: StoredAddress;
/**
* The block number when the update was submitted.
* Stored as u256, used as u64.
*/
protected readonly _pendingUpdateBlock: StoredU256;
/**
* The number of blocks to wait before an update can be applied.
* Override this in derived contracts to set the delay.
*
* Common values:
* - 6 blocks = ~1 hour
* - 144 blocks = ~24 hours
* - 1008 blocks = ~1 week
*/
protected readonly updateDelay: u64 = 144; // ~24 hours default
protected constructor() {
super();
this._pendingUpdateAddress = new StoredAddress(pendingUpdateAddressPointer);
this._pendingUpdateBlock = new StoredU256(pendingUpdateBlockPointer, EMPTY_POINTER);
}
/**
* Returns the pending update source address.
* Returns zero address if no update is pending.
*/
public get pendingUpdateAddress(): Address {
return this._pendingUpdateAddress.value;
}
/**
* Returns the block number when the pending update was submitted.
* Returns 0 if no update is pending.
*/
public get pendingUpdateBlock(): u64 {
return this._pendingUpdateBlock.value.lo1;
}
/**
* Returns the block number when the pending update can be applied.
* Returns 0 if no update is pending.
*/
public get updateEffectiveBlock(): u64 {
const submitBlock = this.pendingUpdateBlock;
if (submitBlock === 0) return 0;
return submitBlock + this.updateDelay;
}
/**
* Returns true if there is a pending update.
*/
public get hasPendingUpdate(): bool {
return this.pendingUpdateBlock !== 0;
}
/**
* Returns true if the pending update can be applied (delay has passed).
*/
public get canApplyUpdate(): bool {
if (!this.hasPendingUpdate) return false;
return Blockchain.block.number >= this.updateEffectiveBlock;
}
/**
* Submits an update for timelock.
*
* The source address must be a deployed contract containing the new bytecode.
* After submission, the update can only be applied after updateDelay blocks.
*
* Emits UpdateSubmitted event.
*
* @param sourceAddress - The source contract address containing new bytecode
* @returns Empty response
* @throws If caller is not deployer
* @throws If source is not a deployed contract
* @throws If an update is already pending
*/
protected submitUpdate(sourceAddress: Address): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
// Check no pending update
if (this.hasPendingUpdate) {
throw new Revert('Update already pending. Cancel first.');
}
// Validate source is a deployed contract
if (!Blockchain.isContract(sourceAddress)) {
throw new Revert('Source must be a deployed contract');
}
// Store pending update
const currentBlock = Blockchain.block.number;
this._pendingUpdateAddress.value = sourceAddress;
this._pendingUpdateBlock.value = u256.fromU64(currentBlock);
// Emit event
const effectiveBlock = currentBlock + this.updateDelay;
Blockchain.emit(new UpdateSubmittedEvent(sourceAddress, currentBlock, effectiveBlock));
return new BytesWriter(0);
}
/**
* Applies a pending update after the timelock period has passed.
*
* The provided address must match the pending update address as an
* additional security measure against front-running attacks.
*
* Emits UpdateApplied event before the update (new bytecode takes effect next block).
*
* @param sourceAddress - The source contract address (must match pending)
* @param calldata - The calldata to pass to onUpdate method of the new contract
* @returns Empty response
* @throws If caller is not deployer
* @throws If no update is pending
* @throws If delay has not passed
* @throws If provided address does not match pending
*/
protected applyUpdate(sourceAddress: Address, calldata: BytesWriter): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
// Check pending update exists
if (!this.hasPendingUpdate) {
throw new Revert('No pending update');
}
// Check delay has passed
if (!this.canApplyUpdate) {
throw new Revert('Update delay not elapsed');
}
// Verify address matches pending
if (!sourceAddress.equals(this._pendingUpdateAddress.value)) {
throw new Revert('Address does not match pending update');
}
// Clear pending state before update
this._pendingUpdateAddress.value = Address.zero();
this._pendingUpdateBlock.value = u256.Zero;
// Emit event
Blockchain.emit(new UpdateAppliedEvent(sourceAddress, Blockchain.block.number));
// Perform update - new bytecode takes effect next block
Blockchain.updateContractFromExisting(sourceAddress, calldata);
return new BytesWriter(0);
}
/**
* Cancels a pending update.
*
* Can only be called by the deployer. Clears the pending update state.
*
* Emits UpdateCancelled event.
*
* @returns Empty response
* @throws If caller is not deployer
* @throws If no update is pending
*/
protected cancelUpdate(): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
// Check pending update exists
if (!this.hasPendingUpdate) {
throw new Revert('No pending update');
}
const pendingAddress = this._pendingUpdateAddress.value;
// Clear pending state
this._pendingUpdateAddress.value = Address.zero();
this._pendingUpdateBlock.value = u256.Zero;
// Emit event
Blockchain.emit(new UpdateCancelledEvent(pendingAddress, Blockchain.block.number));
return new BytesWriter(0);
}
}