UNPKG

@btc-vision/btc-runtime

Version:

Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.

277 lines (235 loc) 9.04 kB
import { u256 } from '@btc-vision/as-bignum/assembly'; import { Blockchain } from '../env'; import { Plugin } from './Plugin'; 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 { encodeSelector, Selector } from '../math/abi'; import { ADDRESS_BYTE_LENGTH } from '../utils'; import { Calldata } from '../types'; import { EMPTY_POINTER } from '../math/bytes'; import { UpdateAppliedEvent, UpdateCancelledEvent, UpdateSubmittedEvent, } from '../events/updatable/UpdatableEvents'; /** * UpdatablePlugin - Plugin for updatable contracts with timelock protection. * * This plugin provides a secure update mechanism with a configurable delay period. * Unlike extending the Updatable base class, this plugin can be added to any contract. * * 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 * * @example * ```typescript * @final * export class MyContract extends OP_NET { * public constructor() { * super(); * // 144 blocks = ~24 hours * this.registerPlugin(new UpdatablePlugin(144)); * } * * // No need to modify execute() - the plugin handles update methods automatically! * } * ``` */ export class UpdatablePlugin extends Plugin { private readonly _pendingUpdateAddress: StoredAddress; private readonly _pendingUpdateBlock: StoredU256; private readonly _updateDelay: u64; /** * Creates a new UpdatablePlugin. * * @param updateDelay - Number of blocks to wait before update can be applied. * Default: 144 blocks (~24 hours) * Common values: * - 6 blocks = ~1 hour * - 144 blocks = ~24 hours * - 1008 blocks = ~1 week * @param addressPointer - Storage pointer for pending update address * @param blockPointer - Storage pointer for pending update block */ public constructor( updateDelay: u64 = 144, addressPointer: u16 = Blockchain.nextPointer, blockPointer: u16 = Blockchain.nextPointer, ) { super(); this._updateDelay = updateDelay; this._pendingUpdateAddress = new StoredAddress(addressPointer); this._pendingUpdateBlock = new StoredU256(blockPointer, EMPTY_POINTER); } // Method selectors public static get SUBMIT_UPDATE_SELECTOR(): Selector { return encodeSelector('submitUpdate(address)'); } public static get APPLY_UPDATE_SELECTOR(): Selector { return encodeSelector('applyUpdate(address,bytes)'); } public static get CANCEL_UPDATE_SELECTOR(): Selector { return encodeSelector('cancelUpdate()'); } public static get PENDING_UPDATE_SELECTOR(): Selector { return encodeSelector('pendingUpdate()'); } public static get UPDATE_DELAY_SELECTOR(): Selector { return encodeSelector('updateDelay()'); } /** * Returns the pending update source address. */ public get pendingUpdateAddress(): Address { return this._pendingUpdateAddress.value; } /** * Returns the block number when the pending update was submitted. */ public get pendingUpdateBlock(): u64 { return this._pendingUpdateBlock.value.lo1; } /** * Returns the configured update delay in blocks. */ public get updateDelay(): u64 { return this._updateDelay; } /** * Returns the block number when the pending update can be applied. */ 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; } /** * Attempts to execute an update-related method. * Returns the response if the method was handled, or null if not. * * @param method - The method selector * @param calldata - The calldata * @returns BytesWriter response if handled, null otherwise */ public override execute(method: Selector, calldata: Calldata): BytesWriter | null { switch (method) { case UpdatablePlugin.SUBMIT_UPDATE_SELECTOR: return this.submitUpdate(calldata); case UpdatablePlugin.APPLY_UPDATE_SELECTOR: return this.applyUpdate(calldata); case UpdatablePlugin.CANCEL_UPDATE_SELECTOR: return this.cancelUpdate(); case UpdatablePlugin.PENDING_UPDATE_SELECTOR: return this.getPendingUpdate(); case UpdatablePlugin.UPDATE_DELAY_SELECTOR: return this.getUpdateDelay(); default: return null; } } /** * Submits an update for timelock. */ private submitUpdate(calldata: Calldata): BytesWriter { this.onlyDeployer(); if (this.hasPendingUpdate) { throw new Revert('Update already pending. Cancel first.'); } const sourceAddress = calldata.readAddress(); if (!Blockchain.isContract(sourceAddress)) { throw new Revert('Source must be a deployed contract'); } const currentBlock = Blockchain.block.number; this._pendingUpdateAddress.value = sourceAddress; this._pendingUpdateBlock.value = u256.fromU64(currentBlock); 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. * Any remaining calldata after the source address is passed to onUpdate. */ private applyUpdate(calldata: Calldata): BytesWriter { this.onlyDeployer(); if (!this.hasPendingUpdate) { throw new Revert('No pending update'); } if (!this.canApplyUpdate) { throw new Revert('Update delay not elapsed'); } const sourceAddress = calldata.readAddress(); const pendingAddress = this._pendingUpdateAddress.value; if (!sourceAddress.equals(pendingAddress)) { throw new Revert('Address does not match pending update'); } // Clear pending state before update this._pendingUpdateAddress.value = Address.zero(); this._pendingUpdateBlock.value = u256.Zero; Blockchain.emit(new UpdateAppliedEvent(sourceAddress, Blockchain.block.number)); const updateCalldata = calldata.readBytesWithLength(); const writer = new BytesWriter(updateCalldata.byteLength) writer.writeBytes(updateCalldata); // Perform update - new bytecode takes effect next block Blockchain.updateContractFromExisting(sourceAddress, writer); return new BytesWriter(0); } /** * Cancels a pending update. */ private cancelUpdate(): BytesWriter { this.onlyDeployer(); if (!this.hasPendingUpdate) { throw new Revert('No pending update'); } const pendingAddress = this._pendingUpdateAddress.value; this._pendingUpdateAddress.value = Address.zero(); this._pendingUpdateBlock.value = u256.Zero; Blockchain.emit(new UpdateCancelledEvent(pendingAddress, Blockchain.block.number)); return new BytesWriter(0); } /** * Returns the pending update info. */ private getPendingUpdate(): BytesWriter { const response = new BytesWriter(ADDRESS_BYTE_LENGTH + 16); response.writeAddress(this._pendingUpdateAddress.value); response.writeU64(this.pendingUpdateBlock); response.writeU64(this.updateEffectiveBlock); return response; } /** * Returns the update delay. */ private getUpdateDelay(): BytesWriter { const response = new BytesWriter(8); response.writeU64(this._updateDelay); return response; } /** * Validates that the caller is the contract deployer. */ private onlyDeployer(): void { if (Blockchain.contractDeployer !== Blockchain.tx.sender) { throw new Revert('Only deployer can call this method'); } } }