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.

242 lines (211 loc) 8.33 kB
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); } }